How to make incompatible APIs work together without rewriting existing code
You need to integrate a legacy payment processor, but:
processPayment(amount, cardNumber)charge(PaymentDetails): PaymentResultFrom the Gang of Four (1994):
"Converts the interface of one class into an interface expected by clients, allowing classes with incompatible interfaces to collaborate."
It's a structural pattern that acts as a translation layer.
Think: power plug adapter for international travel.
The adapter implements what the client wants while wrapping what already exists.
// Legacy payment processor (can't modify this)
class LegacyPaymentProcessor {
processPayment(amount: number, cardNumber: string): boolean {
// Old implementation
return true;
}
}
// Your code - tightly coupled to legacy API
class CheckoutService {
processOrder(amount: number, card: string) {
const legacy = new LegacyPaymentProcessor();
const success = legacy.processPayment(amount, card);
// Can't test, can't swap, can't modernize
}
}
Hardcoded dependency. Can't test. Can't migrate.
// The modern interface your application wants
interface PaymentGateway {
charge(paymentDetails: PaymentDetails): PaymentResult;
}
interface PaymentDetails {
amount: number;
currency: string;
paymentMethod: PaymentMethod;
}
interface PaymentResult {
success: boolean;
transactionId: string;
message: string;
}
This is what your code SHOULD work with.
class LegacyPaymentAdapter implements PaymentGateway {
constructor(private legacyProcessor: LegacyPaymentProcessor) {}
charge(paymentDetails: PaymentDetails): PaymentResult {
// Translate modern format to legacy format
const amount = paymentDetails.amount;
const cardNumber = paymentDetails.paymentMethod.token;
// Delegate to legacy system
const success = this.legacyProcessor.processPayment(amount, cardNumber);
// Translate legacy response to modern format
return {
success,
transactionId: `TXN-${Date.now()}`,
message: success ? 'Payment processed' : 'Payment failed'
};
}
}
class CheckoutService {
constructor(private paymentGateway: PaymentGateway) {}
processOrder(paymentDetails: PaymentDetails) {
const result = this.paymentGateway.charge(paymentDetails);
if (result.success) {
console.log(`Order completed: ${result.transactionId}`);
}
}
}
// Production usage
const legacyProcessor = new LegacyPaymentProcessor();
const adapter = new LegacyPaymentAdapter(legacyProcessor);
const checkout = new CheckoutService(adapter);
CheckoutService has no idea the legacy system exists.
As Michael Feathers describes in Working Effectively with Legacy Code:
Adapters are "seams" - places where you can alter behavior without editing in that place.
// Production: real adapter
const prodService = new CheckoutService(
new LegacyPaymentAdapter(new LegacyPaymentProcessor())
);
// Testing: mock adapter
class MockPaymentGateway implements PaymentGateway {
charge(): PaymentResult {
return { success: true, transactionId: 'TEST', message: 'ok' };
}
}
const testService = new CheckoutService(new MockPaymentGateway());
Now you can test without touching the legacy system.
From Apify's 2025 blog post on migrating Braintree to Stripe:
Challenge: Migrate 10,000+ users from Braintree to Stripe without disruption
Solution: Implemented adapters for both payment processors
PaymentProcessor interfaceOutcome: Zero downtime, no failed payments, seamless transition
interface PaymentProcessor {
processPayment(amount: number, currency: string): Promise<PaymentResult>;
}
class StripeAdapter implements PaymentProcessor {
constructor(private stripe: StripeSDK) {}
async processPayment(amount: number, currency: string) {
// Stripe expects cents
const charge = await this.stripe.createCharge(amount * 100, currency);
return { success: charge.status === 'succeeded', transactionId: charge.id };
}
}
class PayPalAdapter implements PaymentProcessor {
constructor(private paypal: PayPalSDK) {}
async processPayment(amount: number, currency: string) {
const payment = await this.paypal.execute_payment({ amount, currency });
return {
success: payment.state === 'approved',
transactionId: payment.paymentID
};
}
}
Same interface, different implementations. Swap anytime.
class Adapter implements Target {
constructor(private adaptee: Adaptee) {} // Composition
method() { return this.adaptee.legacyMethod(); }
}
class Adapter extends Adaptee implements Target { // Inheritance
method() { return this.legacyMethod(); }
}
Use object adapters. More flexible, runtime composition, avoids inheriting unwanted methods.
Martin Fowler's Strangler Fig pattern for incremental modernization:
Adapters are the bridge layer during transition.
Changes incompatible interface → compatible interface
interface Target { charge(); }
class Adapter implements Target {
charge() { adaptee.processPayment(); }
}
Simplifies complex interface → simpler interface
class Facade {
doEverything() {
system.step1(); system.step2(); system.step3();
}
}
Adds behavior, keeps same interface
class Decorator implements Target {
charge() { log(); return target.charge(); }
}
Too many adapters obscure system design
Adapter does too much beyond translation
Adapters hide fundamental problems
Extra layer adds latency
Adapters enable modernization without risky rewrites. They're transition tools, not permanent architecture.
Start small. Test thoroughly. Migrate incrementally.