🧱 Structural

Adapter Pattern

Converts the interface of a class into another interface clients expect. Adapter lets classes work together that couldn't otherwise because of incompatible interfaces. It acts as a wrapper between two objects, catching calls for one object and transforming them to a format recognizable by the second object.

Problem

You have an existing class whose interface doesn't match what the rest of your code expects. You can't modify the existing class because it may be a third-party library or legacy code that many other parts of the system depend on. Creating an entirely new compatible implementation wastes effort and duplicates logic.

Solution

Create an adapter class that implements the target interface and wraps the adaptee. The adapter translates incoming calls from the target interface into calls on the adaptee's interface. This way, the client code works with the adapter as if it were a regular target object, while the adaptee remains untouched.

Participants

RoleResponsibility
TargetDefines the domain-specific interface the Client uses
AdapterAdapts the interface of Adaptee to the Target interface
AdapteeDefines an existing interface that needs adapting
ClientCollaborates with objects conforming to the Target interface

TypeScript Example

TypeScript
interface Logger {
  info(message: string): void;
  error(message: string): void;
}

class LegacyLogger {
  writeLog(level: number, msg: string) {
    const prefix = level === 0 ? "INFO" : "ERROR";
    console.log(`[${prefix}] ${msg}`);
  }
}

class LoggerAdapter implements Logger {
  constructor(private legacy: LegacyLogger) {}

  info(message: string): void {
    this.legacy.writeLog(0, message);
  }

  error(message: string): void {
    this.legacy.writeLog(1, message);
  }
}

function processRequest(logger: Logger) {
  logger.info("Request received");
  logger.error("Something went wrong");
}

const adapter = new LoggerAdapter(new LegacyLogger());
processRequest(adapter);

Real-World Example

Power adapters are the most intuitive real-world analogy—they let devices with one type of plug work with outlets of a different standard. In software, database drivers act as adapters between your application's data-access interface and the specific database protocol. API wrappers around REST services or converting XML responses to JSON are also common adapter use cases in modern development.

When to Use

  • You want to use an existing class but its interface is incompatible with the rest of your code
  • You need to reuse several existing subclasses that lack common functionality you can't add to the superclass
  • You are integrating a third-party library or legacy system with a different interface
  • You need to create a reusable class that cooperates with unrelated classes having incompatible interfaces

When Not to Use

  • The interfaces are already compatible or can be easily unified
  • You have control over the adaptee and can modify it directly
  • The overhead of an extra layer of indirection outweighs the benefits

Understanding the Adapter Pattern

The Adapter pattern is one of the most important structural design patterns in object-oriented software design. Originally cataloged by the Gang of Four (Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides) in their seminal 1994 book "Design Patterns: Elements of Reusable Object-Oriented Software," this pattern continues to be a cornerstone of modern software architecture.

Converts the interface of a class into another interface clients expect. Adapter lets classes work together that couldn't otherwise because of incompatible interfaces. It acts as a wrapper between two objects, catching calls for one object and transforming them to a format recognizable by the second object. The pattern involves the following key participants: Target, Adapter, Adaptee, Client. Each participant has a well-defined role, and the interactions between them create a flexible, maintainable structure that can evolve with changing requirements.

In TypeScript and modern JavaScript development, the Adapter pattern is particularly valuable because the language's type system and interface support allow you to express the pattern with compile-time safety. TypeScript generics can make Adapter implementations even more flexible and reusable while maintaining strong type checking throughout the pattern's structure.

Knowing when to apply the Adapter pattern is as important as knowing how. Common use cases include: You want to use an existing class but its interface is incompatible with the rest of your code; You need to reuse several existing subclasses that lack common functionality you can't add to the superclass; You are integrating a third-party library or legacy system with a different interface. However, overusing design patterns can lead to unnecessarily complex code. Always evaluate whether the pattern genuinely solves your problem or if a simpler approach would suffice.

The Adapter pattern is related to several other design patterns: Bridge, Decorator, Proxy, Facade. Understanding these relationships helps you choose the right pattern for your specific situation and combine patterns effectively when building complex systems.

Related Patterns

More Structural Patterns

Explore All Design Patterns

Browse our complete reference of 23 design patterns with TypeScript examples, UML participants, and practical guidance.