Loading learning content...
We've established the problem: clients expect one interface, but the available component provides another. Modification of either is undesirable or impossible. The solution must bridge this gap without touching existing code.
The answer is the Adapter Pattern — a structural design pattern that converts the interface of a class into another interface that clients expect. The adapter lets classes work together that couldn't otherwise because of incompatible interfaces.
The pattern's brilliance lies in its simplicity: create a wrapper object that implements the expected interface while delegating actual work to the incompatible component. The wrapper translates between the two interfaces, allowing the client to work through the expected interface while leveraging the existing functionality.
This pattern appears everywhere in the physical world. The electrical adapters you use when traveling internationally are literal examples — they convert one plug shape to another, allowing your devices to work with foreign outlets without modifying either the device or the outlet.
By the end of this page, you will understand the Adapter Pattern's structure, its participants and their roles, the intent behind the pattern, and how wrapping enables interface translation. You'll see concrete implementations and understand what makes the pattern elegant.
The Gang of Four define the Adapter Pattern as follows:
Convert the interface of a class into another interface clients expect. Adapter lets classes work together that couldn't otherwise because of incompatible interfaces.
Let's unpack this definition:
"Convert the interface" — The adapter performs interface transformation. It takes operations available in one form and presents them in another form. This isn't about adding new functionality; it's about reshaping existing functionality to fit expected contracts.
"Clients expect" — The pattern is driven by client expectations. We're not changing what the client expects; we're providing what the client expects using components that speak a different language.
"Work together" — The goal is collaboration. Components that would otherwise be isolated by interface incompatibility can now participate in the same system, contributing their functionality through translated interfaces.
"Couldn't otherwise" — This emphasizes that the pattern solves a real impedance mismatch. Without the adapter, direct collaboration is impossible because the interfaces simply don't align.
The Adapter Pattern is also known as Wrapper. This name directly describes the mechanism: the adapter wraps the adaptee, presenting a new interface while containing the original. The term 'wrapper' is particularly common in dynamically-typed languages and in contexts where the translation is simple.
The pattern's power lies in three key properties:
Single Responsibility — The adapter has exactly one job: translate between interfaces. It contains no business logic, no data storage, no side effects beyond delegation. This makes it easy to understand, test, and maintain.
Open/Closed Principle — Existing components (client and adaptee) remain unchanged. We extend the system's capabilities by adding a new adapter, not by modifying existing code.
Interface Segregation — The client depends only on the interface it needs. The adapter's internal dependency on the adaptee is an implementation detail hidden from the client.
The Adapter Pattern involves four key participants, each with a specific role in the collaboration.
Target — The interface that the client expects. This is the contract that the client code is written against. In a well-designed system, this interface represents domain concepts in domain language.
Client — The component that needs to use functionality through the Target interface. The client is unaware that adaptation is occurring; it works with what it believes is a native implementation of Target.
Adaptee — The existing component with useful functionality but an incompatible interface. This might be a third-party library, a legacy system, or any component whose interface doesn't match Target.
Adapter — The new component that implements Target and maintains a reference to an Adaptee. The adapter intercepts calls to Target methods and translates them into appropriate calls on the Adaptee.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364
/** * TARGET: The interface the client expects * This is defined in terms of the client's domain */interface Target { request(): string;} /** * ADAPTEE: Existing class with useful functionality * but incompatible interface */class Adaptee { public specificRequest(): string { return "Adaptee's useful behavior"; } // The adaptee may have additional methods // that aren't needed by the client public otherFunction(): void { console.log("Other functionality"); }} /** * ADAPTER: Implements Target, wraps Adaptee * Translates Target interface to Adaptee interface */class Adapter implements Target { private adaptee: Adaptee; constructor(adaptee: Adaptee) { this.adaptee = adaptee; } public request(): string { // Translate the call from Target's interface // to Adaptee's interface return this.adaptee.specificRequest(); }} /** * CLIENT: Works with Target interface * Completely unaware of the Adaptee */class Client { private target: Target; constructor(target: Target) { this.target = target; } public doWork(): void { const result = this.target.request(); console.log(`Client received: ${result}`); }} // Usage: Client works with Adaptee through Adapterconst adaptee = new Adaptee();const adapter = new Adapter(adaptee);const client = new Client(adapter);client.doWork(); // Works! Client uses Adaptee indirectly| Participant | Role | Characteristics |
|---|---|---|
| Target | Interface expected by client | Defines the domain-specific contract; stable; unaware of adaptation |
| Client | Consumer of Target interface | Works only with Target; unaware of Adaptee or Adapter implementation |
| Adaptee | Existing component to be adapted | Has useful functionality; incompatible interface; unchanged by adaptation |
| Adapter | Translator between interfaces | Implements Target; holds reference to Adaptee; performs translation |
The adapter's translation mechanism varies in complexity based on the incompatibility being bridged. Let's examine the spectrum from simple to sophisticated.
Simple Delegation — When interfaces are fundamentally compatible but use different names, the adapter simply delegates:
class SimpleAdapter implements Target {
request(): string {
return this.adaptee.specificRequest(); // Just rename
}
}
Parameter Translation — When method signatures differ in parameter types or order:
class ParameterAdapter implements PaymentProcessor {
processPayment(amount: Money, card: CardDetails): PaymentResult {
// Translate parameters to adaptee's expectations
const cents = amount.toCents();
const token = this.tokenize(card);
return this.adaptee.charge(cents, token);
}
}
Return Value Translation — When the adaptee returns data in a different format:
class ReturnAdapter implements CustomerRepository {
async find(id: string): Promise<Customer | null> {
const response = await this.adaptee.fetch(id);
return response.found ? this.mapToDomain(response.data) : null;
}
}
Full Bidirectional Translation — Complex adapters translate both inputs and outputs:
class FullAdapter implements Target {
request(domainInput: DomainType): DomainResult {
const adapteeInput = this.translateInputToAdaptee(domainInput);
const adapteeResult = this.adaptee.specificRequest(adapteeInput);
return this.translateResultToDomain(adapteeResult);
}
}
The adapter should contain ONLY translation logic — no business rules, no data persistence, no side effects beyond what the adaptee provides. If your adapter is growing complex, ask yourself: Am I adapting an interface, or am I building a new service? If the latter, you need a different architectural approach.
Let's implement a complete, realistic adapter scenario. We'll adapt a third-party notification library to work with our application's notification interface.
The Scenario: Our application defines a NotificationService interface for sending notifications. We want to integrate PushNotificationSDK, a third-party library with a completely different interface.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226
// ═══════════════════════════════════════════════════════════════// TARGET: Our application's notification interface// ═══════════════════════════════════════════════════════════════ interface NotificationService { sendNotification( userId: string, notification: NotificationPayload ): Promise<NotificationResult>; getNotificationStatus(notificationId: string): Promise<NotificationStatus>;} interface NotificationPayload { title: string; body: string; priority: 'low' | 'medium' | 'high'; metadata?: Record<string, string>;} interface NotificationResult { success: boolean; notificationId: string; deliveredAt?: Date; error?: string;} type NotificationStatus = 'pending' | 'delivered' | 'failed' | 'unknown'; // ═══════════════════════════════════════════════════════════════// ADAPTEE: Third-party SDK with different interface// ═══════════════════════════════════════════════════════════════ // This represents an external library we cannot modifyclass PushNotificationSDK { private apiKey: string; constructor(apiKey: string) { this.apiKey = apiKey; } // Different method name, different parameters async push(config: PushConfig): Promise<PushResponse> { // Actual SDK implementation would make API calls console.log(`Sending push via SDK: ${config.message}`); return { id: `push_${Date.now()}`, code: 200, timestamp: Date.now(), }; } // Different method for checking status async checkDelivery(pushId: string): Promise<DeliveryInfo> { return { pushId, state: 'DELIVERED', deviceToken: 'abc123', }; } // Helper method to get device token (required for pushing) async getDeviceToken(userId: string): Promise<string | null> { // In reality, this would look up the token return `device_token_for_${userId}`; }} // SDK's types are completely different from our application's typesinterface PushConfig { deviceToken: string; message: string; title: string; urgency: number; // 1-5 scale, not string enum customData: object;} interface PushResponse { id: string; code: number; timestamp: number;} interface DeliveryInfo { pushId: string; state: 'PENDING' | 'DELIVERED' | 'FAILED' | 'EXPIRED'; deviceToken: string;} // ═══════════════════════════════════════════════════════════════// ADAPTER: Bridges our interface to the SDK// ═══════════════════════════════════════════════════════════════ class PushNotificationAdapter implements NotificationService { private sdk: PushNotificationSDK; constructor(sdk: PushNotificationSDK) { this.sdk = sdk; } async sendNotification( userId: string, notification: NotificationPayload ): Promise<NotificationResult> { try { // Step 1: Get device token (SDK requirement our interface doesn't have) const deviceToken = await this.sdk.getDeviceToken(userId); if (!deviceToken) { return { success: false, notificationId: '', error: 'User has no registered device', }; } // Step 2: Translate our payload to SDK's config const pushConfig = this.translateToSDKConfig( notification, deviceToken ); // Step 3: Call the SDK const response = await this.sdk.push(pushConfig); // Step 4: Translate SDK response to our result format return this.translateFromSDKResponse(response); } catch (error) { return { success: false, notificationId: '', error: error instanceof Error ? error.message : 'Unknown error', }; } } async getNotificationStatus(notificationId: string): Promise<NotificationStatus> { try { const deliveryInfo = await this.sdk.checkDelivery(notificationId); return this.translateDeliveryState(deliveryInfo.state); } catch { return 'unknown'; } } // ═══════════════════════════════════════════════════════════ // Private translation methods — the core of the adapter // ═══════════════════════════════════════════════════════════ private translateToSDKConfig( notification: NotificationPayload, deviceToken: string ): PushConfig { return { deviceToken, title: notification.title, message: notification.body, urgency: this.translatePriorityToUrgency(notification.priority), customData: notification.metadata ?? {}, }; } private translatePriorityToUrgency(priority: 'low' | 'medium' | 'high'): number { const mapping: Record<typeof priority, number> = { low: 1, medium: 3, high: 5, }; return mapping[priority]; } private translateFromSDKResponse(response: PushResponse): NotificationResult { return { success: response.code === 200, notificationId: response.id, deliveredAt: new Date(response.timestamp), error: response.code !== 200 ? `SDK error: ${response.code}` : undefined, }; } private translateDeliveryState( state: 'PENDING' | 'DELIVERED' | 'FAILED' | 'EXPIRED' ): NotificationStatus { const mapping: Record<typeof state, NotificationStatus> = { PENDING: 'pending', DELIVERED: 'delivered', FAILED: 'failed', EXPIRED: 'failed', // We don't have 'expired' — map to closest }; return mapping[state]; }} // ═══════════════════════════════════════════════════════════════// CLIENT: Uses NotificationService without knowing about SDK// ═══════════════════════════════════════════════════════════════ class OrderService { constructor(private notificationService: NotificationService) {} async notifyOrderShipped(userId: string, orderId: string): Promise<void> { const result = await this.notificationService.sendNotification(userId, { title: 'Order Shipped!', body: `Your order ${orderId} is on its way.`, priority: 'medium', metadata: { orderId, type: 'shipping' }, }); if (!result.success) { console.error(`Failed to notify: ${result.error}`); } }} // ═══════════════════════════════════════════════════════════════// COMPOSITION ROOT: Wire up the adapter// ═══════════════════════════════════════════════════════════════ const sdk = new PushNotificationSDK('api-key-12345');const notificationService: NotificationService = new PushNotificationAdapter(sdk);const orderService = new OrderService(notificationService); // OrderService works with our clean interface// Completely unaware that PushNotificationSDK existsorderService.notifyOrderShipped('user-123', 'order-456');Key observations from this implementation:
The SDK's interface is completely different — Different method names, different parameter types, different return types, and the SDK requires a device token that our interface doesn't expose.
The adapter handles the complexity — It gets the device token internally, translates priority strings to numeric urgency, converts our payload to the SDK's config format, and translates the response back.
The client (OrderService) is clean — It works with NotificationService using domain language. It knows nothing about PushNotificationSDK, device tokens, or urgency numbers.
Vendor replacement is isolated — If we switch to a different notification provider, we write a new adapter. OrderService and all other clients remain unchanged.
Let's formalize the structural relationships between the Adapter Pattern's participants.
Structural Relationships:
Adapter implements/extends Target — The adapter conforms to the target interface, making it a valid substitute wherever Target is expected.
Adapter contains/references Adaptee — Through composition, the adapter holds an instance of the adaptee (or in the class adapter variant, inherits from it).
Client depends only on Target — The client has no compile-time or runtime knowledge of the adapter or adaptee implementation.
Adaptee is independent — The adaptee knows nothing about the adaptation. It's an existing class that happens to be useful but incompatible.
The Request Flow:
Client
│
│ request() ──────────────► Adapter
│ │
│ │ translates parameters
│ │
│ ▼
│ specificRequest() ──► Adaptee
│ │
│ │ executes
│ │
│ ◄────────────────┘
│ │
│ │ translates result
│ │
◄────────────────────────────┘
│ receives domain result
A well-designed adapter ensures that translation logic appears exactly once, in the adapter class. If you find yourself translating between interfaces elsewhere in your codebase, either the translation should move into the adapter, or you need additional adapters for additional interface pairs.
The Adapter Pattern's wrapper mechanism delivers significant engineering benefits that justify its use over ad-hoc translation approaches.
Single Point of Translation
All translation logic is centralized in the adapter class. This means:
Independence of Client and Adaptee
Neither the client nor the adaptee knows about each other:
Support for Incremental Migration
Adapters enable gradual system evolution:
Clean Domain Model
Domain objects and interfaces remain pure:
The Adapter Pattern is a textbook example of the Open/Closed Principle. The system is CLOSED for modification (we don't change existing clients or adaptees) but OPEN for extension (we can add new adapters to integrate new components). This principle is why the pattern scales well as systems grow.
The Adapter Pattern is applicable in specific situations. Knowing when to use it (and when not to) is as important as knowing how to implement it.
Apply the Adapter Pattern when:
You need to use an existing class whose interface doesn't match — The classic use case. The functionality exists but the shape is wrong.
You want to create a reusable class that cooperates with unrelated classes — Design for adaptation from the start when you know integrations are coming.
You need to use several existing subclasses but impractical to adapt all by subclassing — When you have many adaptees with a common base, an object adapter can work with all of them.
You want to isolate vendor-specific code — Keep third-party dependencies at the boundary of your system behind adapters.
You're building an Anti-Corruption Layer — In Domain-Driven Design, adapters form the core of anti-corruption layers that protect domain models from external system concepts.
Do NOT apply the Adapter Pattern when:
Interfaces are already compatible — If the adaptee already implements the target interface (or could trivially do so), an adapter adds unnecessary indirection.
The translation involves significant business logic — If you're not just translating interfaces but also transforming data semantically or orchestrating complex operations, you're building a service, not an adapter.
You can modify one of the interfaces — If you control both the client and the service and there's no external constraint, consider designing compatible interfaces from the start.
You need to add new behavior — Adapters translate; they don't add. If you need to extend functionality, consider the Decorator Pattern instead.
The incompatibility is fundamental, not just interface-level — If the adaptee's conceptual model is entirely different from what the client needs, an adapter alone won't bridge the gap. You may need richer domain translation.
| Situation | Use Adapter? | Reason |
|---|---|---|
| Third-party library with different interface | Yes | Clean boundary isolation |
| Legacy system integration | Yes | Protect modern code from legacy concerns |
| Multiple vendors for same capability | Yes | Swap vendors without client changes |
| Testing with different implementations | Often | Mock adapters are cleaner than SDK mocks |
| Interfaces already compatible | No | Unnecessary indirection |
| Need to add new capabilities | No | Use Decorator instead |
| Complex data transformation required | Maybe | Consider if Adapter is still appropriate |
| Fundamentally different conceptual models | No | Need Anti-Corruption Layer with richer translation |
We've explored the Adapter Pattern as the solution to interface incompatibility. Let's consolidate our understanding.
What's next:
We've covered the core mechanics of the Adapter Pattern. But there's a critical implementation decision we haven't fully explored: should the adapter use composition (object adapter) or inheritance (class adapter)? The next page examines both variations, their tradeoffs, and guidance on when to use each approach.
You now understand how the Adapter Pattern solves interface incompatibility through wrapping. You can identify the pattern's participants, understand the translation mechanism, implement real-world adapters, and recognize when the pattern is (and isn't) appropriate. Next, we'll explore the two primary ways to implement adapters.