Adapter Pattern
The Adapter pattern lets two incompatible interfaces work together by wrapping one of them in a translator object. Use it when you need to integrate a third-party library, a legacy class, or any API whose signature does not match what your code expects.
Overview
The Adapter pattern (also called Wrapper) bridges two interfaces that were never meant to talk to each other. Instead of rewriting the original class — which may be third-party, legacy, or simply expensive to change — you build a thin object that exposes the interface your code wants and delegates internally to the one that already exists. It is the textbook way to plug a square peg into a round hole without modifying either side.
When to use it
- You need to use an existing class but its interface doesn't match the one your code expects.
- You're integrating a third-party SDK or legacy module that you can't (or shouldn't) modify.
- You want to isolate the rest of the system from a volatile external API behind a stable internal contract.
Example
interface Logger {
log(message: string): void;
}
class ThirdPartyLogger {
writeMessage(level: string, payload: { msg: string }) {
console.log(`[${level}] ${payload.msg}`);
}
}
class ThirdPartyLoggerAdapter implements Logger {
constructor(private readonly external: ThirdPartyLogger) {}
log(message: string): void {
this.external.writeMessage("info", { msg: message });
}
}
const logger: Logger = new ThirdPartyLoggerAdapter(new ThirdPartyLogger());
logger.log("App started");Pros
- Lets you reuse existing code without modifying it.
- Decouples your domain from third-party or legacy APIs.
- Single responsibility: the translation lives in one well-named class.
Cons
- Adds an extra layer of indirection that has to be maintained alongside the original.
- A bidirectional adapter can become a complex compromise.
- Easy to abuse — sometimes refactoring the caller is cleaner than wrapping the callee.