How the Adapter Pattern Seamlessly Integrates PayPal and Stripe Payments
This article explains the Adapter design pattern, demonstrates step‑by‑step how to wrap existing PayPal payment code with a Stripe adapter in TypeScript, and discusses best practices, common pitfalls, and extensions such as factories and nested adapters for flexible payment integration.
Background
The Adapter Pattern is a structural design pattern that provides a wrapper or "adapter" around existing classes so that incompatible interfaces can work together. In real‑world payment gateways or third‑party services, each often defines its own unique interface, making direct substitution impossible.
Step 1: Define the Target Interface
We create a PaymentProcessor interface that represents the service offered to the rest of the application.
<code>interface PaymentProcessor {
pay(amount: number): void;
}</code>Step 2: Existing PayPal Payment Processor
The current system uses PayPalPaymentProcessor , which implements PaymentProcessor and provides the pay() method.
<code>class PayPalPaymentProcessor implements PaymentProcessor {
public pay(amount: number): void {
console.log(`Paid $${amount} using PayPal.`);
}
}</code>Step 3: New Stripe Payment Processor
Stripe offers a different interface with a makePayment() method.
<code>class StripePaymentProcessor {
public makePayment(amountInCents: number): void {
console.log(`Paid $${amountInCents / 100} using Stripe.`);
}
}</code>Step 4: Create the Adapter
The adapter implements the target PaymentProcessor interface and internally delegates to StripePaymentProcessor , converting dollars to cents.
<code>class StripePaymentAdapter implements PaymentProcessor {
private stripePaymentProcessor: StripePaymentProcessor;
constructor(stripePaymentProcessor: StripePaymentProcessor) {
this.stripePaymentProcessor = stripePaymentProcessor;
}
public pay(amount: number): void {
// Convert dollars to cents and call Stripe's method
this.stripePaymentProcessor.makePayment(amount * 100);
}
}</code>Step 5: Use the Adapter in the Application
The application can now work with either PayPal or Stripe without changing existing code.
<code>// Application code
const amountToPay = 100; // $100
// Existing PayPal processor
const paypalProcessor: PaymentProcessor = new PayPalPaymentProcessor();
paypalProcessor.pay(amountToPay);
// New Stripe processor via adapter
const stripeProcessor: PaymentProcessor = new StripePaymentAdapter(new StripePaymentProcessor());
stripeProcessor.pay(amountToPay);
</code>Output:
<code>Paid $100 using PayPal.
Paid $100 using Stripe.
</code>Key Points
PaymentProcessor is the target interface shared by both processors.
PayPalPaymentProcessor already implements this interface.
StripePaymentProcessor has a different interface, so we create StripePaymentAdapter to adapt it to PaymentProcessor .
Richer Adapters
Adapters can also add validation, logging, error handling, or integrate configuration files. Examples include supporting multiple payment gateways, logging systems, storage services, or email providers.
Factory for Multiple Adapters
<code>interface PaymentProcessor {
pay(amount: number): void;
}
class PaymentAdapterFactory {
static getPaymentProcessor(type: string, version?: string): PaymentProcessor {
if (type === 'alipay') {
if (version === 'v1') {
return new AlipayV1Adapter(new AlipayV1Processor());
} else if (version === 'v2') {
return new AlipayV2Adapter(new AlipayV2Processor());
}
} else if (type === 'stripe') {
return new StripeAdapter(new StripeProcessor());
}
throw new Error(`Unsupported payment type: ${type}`);
}
}
const alipayProcessorV1 = PaymentAdapterFactory.getPaymentProcessor('alipay', 'v1');
alipayProcessorV1.pay(100);
const alipayProcessorV2 = PaymentAdapterFactory.getPaymentProcessor('alipay', 'v2');
alipayProcessorV2.pay(200);
</code>Nested Adapters
Adapters can be composed to form chains, each handling a specific transformation.
<code>class StripeAdapter implements PaymentProcessor {
private stripeProcessor: StripePaymentProcessor;
constructor(stripeProcessor: StripePaymentProcessor) {
this.stripeProcessor = stripeProcessor;
}
public pay(amount: number): void {
this.stripeProcessor.makePayment(amount * 100);
}
}
class CurrencyFormatterAdapter implements PaymentProcessor {
private paymentProcessor: PaymentProcessor;
constructor(paymentProcessor: PaymentProcessor) {
this.paymentProcessor = paymentProcessor;
}
public pay(amount: number): void {
const formattedAmount = parseFloat(amount.toFixed(2));
console.log(`Formatted amount: $${formattedAmount}`);
this.paymentProcessor.pay(formattedAmount);
}
}
const stripeProcessor = new StripeAdapter(new StripePaymentProcessor());
const formattedStripeProcessor = new CurrencyFormatterAdapter(stripeProcessor);
formattedStripeProcessor.pay(123.456);
// Output:
// Formatted amount: $123.46
// Paid $123.46 using Stripe.
</code>Common Misconceptions
Adapter pattern is not a universal solution; overusing it adds complexity.
It does more than simple encapsulation—it translates interfaces.
It cannot solve deep business‑logic incompatibilities.
Adapters require maintenance as third‑party APIs evolve.
Performance overhead is usually minimal but should be considered in high‑throughput scenarios.
Proper naming and documentation keep adapters readable.
Use adapters as a remediation strategy, not as a first‑choice design.
Conclusion
The essence of the Adapter Pattern is to resolve interface incompatibility, improving flexibility and reusability. Apply it when integrating legacy code, third‑party libraries, or multiple implementations without altering existing client code.
Code Mala Tang
Read source code together, write articles together, and enjoy spicy hot pot together.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.