Adapter
The Adapter pattern wires together two incompatible interfaces. The adapter wraps the “adaptee” and exposes the interface the rest of the code expects. Use it for legacy code, third-party APIs, and gradual migrations.
Class + object + functional adapters
EXAMPLE
// 1) Problem — incompatible interfaces
interface ModernLogger {
log(level: 'info' | 'warn' | 'error', msg: string, context?: object): void;
}
// Legacy logger has a different API
class LegacyLogger {
write(message: string) { /* writes to file */ }
writeError(message: string) { /* writes errors to stderr */ }
}
// 2) Adapter — translates calls
class LegacyLoggerAdapter implements ModernLogger {
constructor(private legacy: LegacyLogger) {}
log(level: 'info' | 'warn' | 'error', msg: string, context?: object) {
const formatted = context ? `${msg} ${JSON.stringify(context)}` : msg;
if (level === 'error') this.legacy.writeError(formatted);
else this.legacy.write(`[${level.toUpperCase()}] ${formatted}`);
}
}
function process(logger: ModernLogger) {
logger.log('info', 'Processing', { userId: 42 });
}
process(new LegacyLoggerAdapter(new LegacyLogger()));
// 3) Third-party API adapter
interface PaymentProvider {
charge(amount: number, currency: string): Promise<{ ok: boolean; ref: string }>;
}
// Stripe SDK has a different shape
class StripeAdapter implements PaymentProvider {
constructor(private stripe: Stripe) {}
async charge(amount: number, currency: string) {
const intent = await this.stripe.paymentIntents.create({
amount: Math.round(amount * 100), // dollars → cents
currency: currency.toLowerCase(),
confirm: true,
});
return { ok: intent.status === 'succeeded', ref: intent.id };
}
}
// PayPal SDK — completely different API
class PayPalAdapter implements PaymentProvider {
constructor(private paypal: PayPalSdk) {}
async charge(amount: number, currency: string) {
const order = await this.paypal.orders.create({
intent: 'CAPTURE',
purchase_units: [{ amount: { value: amount.toFixed(2), currency_code: currency } }],
});
const captured = await this.paypal.orders.capture(order.id);
return { ok: captured.status === 'COMPLETED', ref: order.id };
}
}
// App code only knows the PaymentProvider interface
class Checkout {
constructor(private payments: PaymentProvider) {}
async pay(order: Order) {
return this.payments.charge(order.total, order.currency);
}
}
// Wire at composition root
const checkout = new Checkout(new StripeAdapter(new Stripe(STRIPE_KEY)));
// 4) Functional adapter — closure-based (no classes)
type ChargeFn = (amount: number, currency: string) => Promise<{ ok: boolean; ref: string }>;
const stripeChargeAdapter = (stripe: Stripe): ChargeFn =>
async (amount, currency) => {
const intent = await stripe.paymentIntents.create({ /* … */ });
return { ok: intent.status === 'succeeded', ref: intent.id };
};
const charge = stripeChargeAdapter(new Stripe(STRIPE_KEY));
await charge(99.99, 'USD');
// 5) Adapter as a translation layer between two systems
// Modern web app reads from a legacy SOAP API
class UserService {
constructor(private legacy: LegacySoapClient) {}
async getUser(id: number): Promise<User> {
const soap = await this.legacy.GetUserById({ userId: id.toString() });
return {
id: parseInt(soap.UserDto.Id),
name: soap.UserDto.FullName,
email: soap.UserDto.EmailAddress.toLowerCase(),
createdAt: new Date(soap.UserDto.CreationDate),
roles: soap.UserDto.RoleList.Role.map(r => r.RoleName),
};
}
}
// 6) Logging / metrics / tracing — adapter for cross-cutting concerns
interface MetricsClient {
increment(name: string, tags?: Record<string, string>): void;
timing(name: string, ms: number, tags?: Record<string, string>): void;
}
class DatadogAdapter implements MetricsClient {
constructor(private dd: DogStatsD) {}
increment(name: string, tags = {}) { this.dd.increment(name, 1, this.tagToArray(tags)); }
timing(name: string, ms: number, tags = {}) { this.dd.histogram(name, ms, this.tagToArray(tags)); }
private tagToArray(tags: Record<string, string>) {
return Object.entries(tags).map(([k, v]) => `${k}:${v}`);
}
}
class PrometheusAdapter implements MetricsClient {
constructor(private registry: Registry) {}
increment(name: string, tags = {}) {
const counter = new Counter({ name, help: name, labelNames: Object.keys(tags) });
counter.inc(tags as any);
}
timing(name: string, ms: number, tags = {}) { /* histogram */ }
}
// 7) Object adapter vs class adapter
// JavaScript / Python typically use OBJECT adapter — composition:
class Adapter {
constructor(private adaptee: Adaptee) {}
method() { return this.adaptee.differentMethod(); }
}
// Java / C# can use CLASS adapter — inheritance:
class Adapter2 extends Adaptee implements Target {
method() { return this.differentMethod(); }
}
// Object adapter is more flexible — wrap multiple adaptees, dynamic swap.
// 8) Two-way adapter — translates both directions
class TwoWayAdapter implements ModernApi, LegacyApi {
// Implement both interfaces; route to appropriate underlying calls
}
// 9) Adapter for testing — mock external systems
class FakePaymentProvider implements PaymentProvider {
private charges: { amount: number; currency: string }[] = [];
async charge(amount: number, currency: string) {
this.charges.push({ amount, currency });
return { ok: true, ref: 'fake-' + Math.random() };
}
getCharges() { return this.charges; }
}
const checkout = new Checkout(new FakePaymentProvider());
await checkout.pay({ total: 100, currency: 'USD' });
expect((checkout.payments as FakePaymentProvider).getCharges()).toHaveLength(1);
// 10) Gradual migration pattern
// Old API → adapter → new internal API
// Then move callers off the adapter as they migrate.
// Phase 1: introduce adapter
class NewApiAdapter {
constructor(private old: OldApi) {}
fetchUser(id: number) { return this.old.GetUserSync(id); }
}
// Phase 2: code uses NewApiAdapter
// Phase 3: replace internals with real new implementation
// Phase 4: delete the OldApi
// 11) When to use Adapter
// ✅ Working with legacy code you can't modify
// ✅ Integrating third-party libraries with different shapes
// ✅ Gradual migration between two APIs
// ✅ Testing — fake/stub external dependencies
// ✅ Switching between providers (multi-cloud, multi-payment)
// 12) When NOT to use Adapter
// ❌ The 'adaptee' already matches your interface
// ❌ Only one adapter — just use the underlying API directly
// ❌ When the gap requires too much logic — consider a Facade or refactor
// 13) Adapter vs Facade vs Bridge
// Adapter — makes existing class compatible with a different interface
// Facade — simplifies a complex subsystem (one class for many)
// Bridge — separates abstraction from implementation (two parallel hierarchies)
// 14) Common bugs
// • Adapter does TOO much (becomes a god class) — extract logic
// • Adapter exposes adaptee's bugs through translation — verify carefully
// • Wrong shape mapping (e.g. integer → string conversion off-by-one)
// • Async/sync mismatch — adapter may need to fake one
// • Different error semantics — wrap exceptions consistently
// 15) Real-world examples (in standard libraries)
// • Java: InputStreamReader (adapts byte stream to char stream)
// • .NET: AdoNetClient adapters (SqlClient, OracleClient, etc.)
// • Node: Streams interop with Web Streams via Readable.toWeb / Readable.fromWeb
// • React: useSyncExternalStore (adapts external state to React)
// 16) Best practices
// • Keep the adapter THIN — translate, don't add behaviour
// • Name clearly: SomethingAdapter or SomethingProvider
// • Test the adapter in isolation
// • Document the translation rules
// • Inject the adapter at the composition root — don't construct mid-flow
// • One adapter per provider — don't merge into a giant switch
Why it matters
Adapter wraps an existing API to fit a new interface; clients depend on the new interface, you swap providers freely. Pair with dependency injection — test fakes become trivial adapters of the same shape.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
// Wrap a class with one interface to make it look like another.
class StripeAdapter {
constructor(stripe) { this.stripe = stripe; }
charge(amount) { return this.stripe.createCharge({ amount }); }
}
Try it Yourself »
Discussion
Loading…