DEV Community

Erick Engelhardt
Erick Engelhardt

Posted on

2

Demystifying the Adapter Pattern

Ever tried plugging a USB-C into a micro-USB port? That’s exactly what the Adapter Pattern is here for. Not literally, but conceptually.

The Adapter Pattern is all about making incompatible interfaces work together. It acts like a bridge between two systems that weren’t designed to cooperate. You wrap one object with another that translates method calls and properties into a compatible format.


When Should You Use It?

  • You have legacy code that doesn't match your current interface.
  • You want to integrate third-party libraries with your own abstraction.
  • You're refactoring a codebase gradually and need backward compatibility.
  • You're dealing with multiple interfaces that perform similar but not identical functions.

This is especially useful in enterprise-scale projects where changing one system has ripple effects across many modules.


Real-World Example: Payments

Imagine you're switching payment providers. Your app was built using an older LegacyPayment class, but you want to move toward a new unified interface.

Step 1 - Legacy system and desired abstraction

export class LegacyPaymentSdk {
  sendPayment(amount: number) {
    console.log(`Paid $${amount} via LegacyPayment`);
  }
}

export class StripeSdk {
  charge(value: number, metadata: Record<string, any>) {
    console.log(`Charged $${value} via Stripe with metadata:`, metadata);
  }
}


export interface PaymentPayload {
  amount: number;
  payerName: string;
  payerEmail: string;
}

export interface PaymentProvider {
  pay(data: PaymentPayload): PaymentResult;
}

export interface PaymentResult {
  transactionId: string;
  success: boolean;
}
Enter fullscreen mode Exit fullscreen mode

Step 2 - Adapters for each implementation

export class LegacyPaymentAdapter implements PaymentProvider {
  constructor(private legacy: LegacyPaymentSdk) {}

  pay(data: PaymentPayload): PaymentResult {
    console.log('Adapter translating pay() to sendPayment()');
    // Only amount is used; legacy gateway does not support metadata
    this.legacy.sendPayment(data.amount);
    return { transactionId: 'legacy-123', success: true }; // Simulated response
  }
}

export class StripeAdapter implements PaymentProvider {
  constructor(private stripe: StripeSdk) {}

  pay(data: PaymentPayload): PaymentResult {
    console.log('Adapter translating pay() to charge()');
    const metadata = {
      name: data.payerName,
      email: data.payerEmail
    };
    this.stripe.charge(data.amount, metadata);
    return { transactionId: 'stripe-456', success: true }; // Simulated response
  }
}
Enter fullscreen mode Exit fullscreen mode

Step 3 - Service layer using the interface

export class PaymentService {
  constructor(private provider: PaymentProvider) {}

  handleCheckout(data: PaymentPayload): PaymentResult {
    console.log('Service initiating payment...');
    const result = this.provider.pay(data);
    console.log(`Service completed payment. Transaction ID: ${result.transactionId}, Success: ${result.success}`);
    return result;
  }
}
Enter fullscreen mode Exit fullscreen mode

Step 4 - Usage with different adapters

const legacyAdapter = new LegacyPaymentAdapter(new LegacyPaymentSdk());
const stripeAdapter = new StripeAdapter(new StripeSdk());

const legacyService = new PaymentService(legacyAdapter);
const stripeService = new PaymentService(stripeAdapter);

legacyService.handleCheckout({
  amount: 100,
  payerName: 'Alice',
  payerEmail: 'alice@example.com'
});

stripeService.handleCheckout({
  amount: 200,
  payerName: 'Bob',
  payerEmail: 'bob@example.com'
});
Enter fullscreen mode Exit fullscreen mode

Run the Example

yarn start
Enter fullscreen mode Exit fullscreen mode

Full Output

1) Service initiating payment...
2) Adapter translating pay() to sendPayment()
3) Paid $100 via LegacyPayment
4) Service completed payment.

5) Service initiating payment...
6) Adapter translating pay() to charge()
7) Charged $200 via Stripe with metadata: { name: 'Bob', email: 'bob@example.com' }
8) Service completed payment.
Enter fullscreen mode Exit fullscreen mode

Output Breakdown

LegacyPayment flow:

  • 1) The PaymentService starts the legacy checkout process.
  • 2) The adapter translates the call to match the legacy interface.
  • 3) Payment is processed using the legacy gateway.
  • 4) The service completes the first transaction.

Stripe flow:

  • 5) Begins the second checkout using the Stripe adapter.
  • 6) Adapter maps the unified method to Stripe’s method.
  • 7) Payment succeeds, including user metadata.
  • 8) Finalizes the second transaction.

Testing the Adapter Integration

The repository includes unit tests to ensure your service logic remains decoupled from any specific SDK or gateway.

The file payment.service.spec.ts uses a mocked version of the PaymentProvider interface to test the PaymentService independently.

This allows you to:

  • Fully isolate service behavior from real implementations
  • Guarantee contract compliance
  • Swap adapters without affecting test coverage

Run the Tests

yarn test
Enter fullscreen mode Exit fullscreen mode

Test File: payment.service.spec.ts

This testing approach ensures your PaymentService can be reused anywhere, as long as a compatible adapter is injected.


This structure lets you plug and play with multiple providers without changing your business logic. Each adapter can include gateway-specific logic, such as formatting, validation, metadata conversion, retries, and so on.

If tomorrow you adopt another provider (like PayPal or Adyen), all you need is another adapter.


Benefits of the Adapter Pattern

  • Decoupling: Your core app logic is isolated from external or legacy systems.
  • Gradual migration: Swap internals behind the scenes without breaking public interfaces.
  • Consistency: Enforce a common API across different providers or modules.
  • Testability: Mock adapters in tests instead of actual implementations.
  • Extendability: Add logic to adapt not just method names but also data formatting, validation, or retries.

Common Pitfalls

  • Overuse: Don't use adapters as an excuse to skip proper refactoring.
  • Performance hit: Wrapping layers can introduce slight overhead.
  • Too much abstraction: Over-adapting might confuse new developers.

Use it where there's a real gap in compatibility, not as a blanket solution for all mismatches.


Pro Tips

  • Pair with Dependency Injection for even better separation of concerns.
  • Combine with Factory Pattern to automate the adapter selection logic.
  • Favor interfaces over concrete classes to increase flexibility.
  • Avoid adapting things that should just be rewritten. Sometimes adapters hide tech debt instead of solving it.

Adapters aren't glamorous, but they solve real-world messes. And let's be honest, clean architecture often comes down to choosing the right boring solution at the right time.

Check out a full working repo: https://github.com/erickne/ts-adapter-pattern

Image of Timescale

Timescale – the developer's data platform for modern apps, built on PostgreSQL

Timescale Cloud is PostgreSQL optimized for speed, scale, and performance. Over 3 million IoT, AI, crypto, and dev tool apps are powered by Timescale. Try it free today! No credit card required.

Try free

Top comments (1)

Collapse
 
paulo_victordesouzatel profile image
Paulo Victor De Souza Telles

Loved this! The adapter pattern finally makes sense. 🙌

AWS Q Developer image

Build your favorite retro game with Amazon Q Developer CLI in the Challenge & win a T-shirt!

Feeling nostalgic? Build Games Challenge is your chance to recreate your favorite retro arcade style game using Amazon Q Developer’s agentic coding experience in the command line interface, Q Developer CLI.

Participate Now

👋 Kindness is contagious

Explore this insightful write-up embraced by the inclusive DEV Community. Tech enthusiasts of all skill levels can contribute insights and expand our shared knowledge.

Spreading a simple "thank you" uplifts creators—let them know your thoughts in the discussion below!

At DEV, collaborative learning fuels growth and forges stronger connections. If this piece resonated with you, a brief note of thanks goes a long way.

Okay