Adapter

Allow objects with incompatible interfaces to work together. Wrap an existing class with a new interface so legacy or third-party code fits into your system without modification.

The Problem

Your app uses a RoundHole class that accepts RoundPeg objects. A vendor gives you SquarePeg objects. You can’t modify either. They’re incompatible — but geometrically a square peg can fit in a round hole if the square’s diagonal is no larger than the circle’s diameter. You need a translator between the two interfaces.

This comes up constantly with third-party libraries, legacy systems, and external APIs that return data in a format your code doesn’t expect.


The Solution

Create an Adapter class that implements the interface your code expects, but internally delegates to the incompatible class. The client never knows the translation is happening.

Client uses → «interface» Target + request(): string Adapter + request(): string Adaptee (incompatible) + specificRequest() wraps/calls
Adapter — wraps the incompatible Adaptee so Client sees only the Target interface

Two variants:

  • Object Adapter — wraps the adaptee via composition (shown above, more flexible)
  • Class Adapter — extends both Target and Adaptee via multiple inheritance (TypeScript mixins)

Real-World Analogy

A power plug adapter. Your laptop has a 3-pin US plug; the wall socket is a 2-pin European outlet. The adapter converts the physical interface — the laptop and wall socket never need to change. The same principle applies everywhere: CSV-to-JSON converters, XML-to-REST gateways, imperial-to-metric converters.


TypeScript Example

// Your system's expected interface
interface AnalyticsTracker {
  trackEvent(name: string, properties: Record<string, unknown>): void;
  trackPageView(url: string): void;
}

// A third-party analytics SDK with a completely different interface
class LegacyAnalyticsSDK {
  sendHit(category: string, action: string, label: string, value?: number): void {
    console.log(`Legacy hit: ${category}/${action}/${label}${value ? `/${value}` : ''}`);
  }

  logPageLoad(location: string, title: string): void {
    console.log(`Legacy page: ${location} - ${title}`);
  }
}

// Adapter — makes LegacyAnalyticsSDK look like AnalyticsTracker
class LegacyAnalyticsAdapter implements AnalyticsTracker {
  constructor(private readonly sdk: LegacyAnalyticsSDK) {}

  trackEvent(name: string, properties: Record<string, unknown>): void {
    const label = JSON.stringify(properties);
    const value  = typeof properties['value'] === 'number' ? properties['value'] : undefined;
    this.sdk.sendHit('event', name, label, value);
  }

  trackPageView(url: string): void {
    const title = url.split('/').filter(Boolean).pop() ?? 'home';
    this.sdk.logPageLoad(url, title);
  }
}

// Client code — only knows about AnalyticsTracker
function trackSignup(tracker: AnalyticsTracker, plan: string): void {
  tracker.trackEvent('signup', { plan, timestamp: Date.now() });
  tracker.trackPageView('/welcome');
}

// Wire it up
const legacySdk = new LegacyAnalyticsSDK();
const adapter   = new LegacyAnalyticsAdapter(legacySdk);

trackSignup(adapter, 'pro');
// → Legacy hit: event/signup/{"plan":"pro","timestamp":...}
// → Legacy page: /welcome - welcome

// Later, swap in a modern SDK with zero changes to trackSignup()
class ModernAnalytics implements AnalyticsTracker {
  trackEvent(name: string, properties: Record<string, unknown>): void {
    console.log('Modern event:', name, properties);
  }
  trackPageView(url: string): void {
    console.log('Modern pageview:', url);
  }
}

trackSignup(new ModernAnalytics(), 'enterprise'); // same call, different backend

When to Use

  • You want to use an existing class but its interface is incompatible with the rest of your code
  • You’re integrating a third-party library and don’t want to couple your code to its API directly
  • You need several existing subclasses to share common behaviour but can’t add it to every subclass

Pros

  • Single Responsibility — interface conversion is isolated in the adapter
  • Open/Closed — add new adapters without changing client code or the adaptee
  • Enables integrating incompatible legacy or third-party code cleanly

Cons

  • Adds an extra layer of indirection — debugging traces go through the adapter
  • Sometimes it’s simpler to just change the service class if you own it
  • Many adapters can become a maintenance burden if the adaptee API changes frequently

  • Facade simplifies a complex subsystem; Adapter makes an incompatible interface usable
  • Decorator adds behaviour while keeping the same interface; Adapter changes the interface
  • Proxy keeps the same interface but controls access; Adapter bridges two different interfaces