Adapter
Converts the interface of a class into the interface the client expects, allowing classes with incompatible interfaces to collaborate without modifying their source code.
Intent
Make incompatible interfaces work together, wrapping an existing class (the adaptee) in a wrapper that exposes the interface expected by the client — without changing either the adaptee or the client.
The Adapter belongs to the structural patterns category in the GoF catalog (1994) and is one of the most used patterns in system integration: whenever you consume a third-party library, an external SDK, or a legacy API whose interfaces don't fit the contract your domain defines, the Adapter is the right tool.
Problem
Imagine your application defines a Logger interface with
the method log(level, message). You decide to adopt a
third-party logging library — say, ExternalLogger — that
exposes a completely different contract: separate methods
writeInfo(msg), writeWarning(msg) and
writeError(msg).
You can't modify ExternalLogger (it's third-party code,
updated by another team). You also don't want to modify your whole
application to call the new API directly — that creates coupling with
the vendor and turns a future library swap into a massive refactor.
The naive solution would be to scatter conditionals or direct calls to the library throughout the code. The Adapter solves this cleanly: a single intermediate object translates the calls.
Object Adapter vs Class Adapter
The GoF describes two variants:
- Object Adapter (composition): the adapter keeps a reference to the adaptee and delegates calls to it. It's the idiomatic form in TypeScript and PHP — it doesn't depend on multiple inheritance, works with subclasses of the adaptee, and is more flexible.
- Class Adapter (multiple inheritance): the adapter inherits from both the target interface and the adaptee. In TypeScript and PHP, multiple class inheritance doesn't exist — so this variant doesn't apply directly. In PHP, it can be partially simulated with traits, but the result tends to be more complex without real gain. Always prefer the Object Adapter.
In the examples below, we use exclusively the Object Adapter.
Solution
The Adapter organizes the code into four participants:
-
Target (target interface): the contract the client
expects. E.g.:
Loggerinterface withlog(level, message). -
Adaptee (existing/incompatible class): the class whose
interface needs to be adapted. E.g.:
ExternalLoggerwithwriteInfo(),writeWarning(),writeError(). -
Adapter: implements the Target interface and keeps a
reference to the Adaptee. Translates each Target call into the
Adaptee's API. E.g.:
ExternalLoggerAdapter implements Logger. - Client: uses only the Target interface — it never knows the Adaptee directly.
With the Adapter in place, swapping the logging library is a change to a single class — the rest of the system remains untouched.
Structure
«interface» Target
┌─────────────────────────────┐
│ + log(level, message): void │
└─────────────────────────────┘
▲ ▲
│ │ implements
Client code ExternalLoggerAdapter
uses Target (Adapter)
┌─────────────────────────────┐
│ - adaptee: ExternalLogger │
│ + log(level, message): void │
└─────────────────────────────┘
│ delegates to
▼
ExternalLogger
(Adaptee)
┌───────────────────────────┐
│ + writeInfo(msg): void │
│ + writeWarning(msg): void │
│ + writeError(msg): void │
└───────────────────────────┘
Call flow:
client.log("error", "Connection failed")
│
▼ (via the Logger interface)
ExternalLoggerAdapter.log("error", "Connection failed")
│
│ switch(level) → calls writeError()
▼
ExternalLogger.writeError("Connection failed")
Code examples
Example 1 — Third-party logger adapter
The client only knows the Logger interface. The Adapter
translates the calls to the incompatible API of the external library.
// ── Target — interface the application code knows ─────────────
type LogLevel = "info" | "warning" | "error";
interface Logger {
log(level: LogLevel, message: string): void;
}
// ── Adaptee — third-party library with an incompatible API ────
// (code we CANNOT modify)
class ExternalLogger {
writeInfo(msg: string): void {
console.log(`[INFO] ${msg}`);
}
writeWarning(msg: string): void {
console.warn(`[WARNING] ${msg}`);
}
writeError(msg: string): void {
console.error(`[ERROR] ${msg}`);
}
}
// ── Adapter (Object Adapter via composition) ───────────────────
class ExternalLoggerAdapter implements Logger {
// Keeps a reference to the adaptee — without inheriting from it.
constructor(private readonly adaptee: ExternalLogger) {}
log(level: LogLevel, message: string): void {
switch (level) {
case "info": this.adaptee.writeInfo(message); break;
case "warning": this.adaptee.writeWarning(message); break;
case "error": this.adaptee.writeError(message); break;
}
}
}
// ── Client code — uses only the Logger interface ──────────────
function processOrder(logger: Logger, orderId: string): void {
logger.log("info", `Starting processing for order ${orderId}`);
logger.log("warning", `Low stock for order ${orderId}`);
logger.log("error", `Payment failed for order ${orderId}`);
}
// Wiring — the client never knows an ExternalLogger exists:
const logger = new ExternalLoggerAdapter(new ExternalLogger());
processOrder(logger, "ORD-001");
// [INFO] Starting processing for order ORD-001
// [WARNING] Low stock for order ORD-001
// [ERROR] Payment failed for order ORD-001
<?php
// ── Target — interface the application code knows ─────────────
interface Logger
{
public function log(string $level, string $message): void;
}
// ── Adaptee — third-party library with an incompatible API ────
// (code we CANNOT modify)
class ExternalLogger
{
public function writeInfo(string $msg): void
{
echo "[INFO] {$msg}" . PHP_EOL;
}
public function writeWarning(string $msg): void
{
echo "[WARNING] {$msg}" . PHP_EOL;
}
public function writeError(string $msg): void
{
echo "[ERROR] {$msg}" . PHP_EOL;
}
}
// ── Adapter (Object Adapter via composition) ───────────────────
class ExternalLoggerAdapter implements Logger
{
// Keeps a reference to the adaptee — without inheriting from it.
public function __construct(
private readonly ExternalLogger $adaptee
) {}
public function log(string $level, string $message): void
{
match ($level) {
'info' => $this->adaptee->writeInfo($message),
'warning' => $this->adaptee->writeWarning($message),
'error' => $this->adaptee->writeError($message),
default => $this->adaptee->writeInfo($message),
};
}
}
// ── Client code — uses only the Logger interface ──────────────
function processOrder(Logger $logger, string $orderId): void
{
$logger->log('info', "Starting processing for order {$orderId}");
$logger->log('warning', "Low stock for order {$orderId}");
$logger->log('error', "Payment failed for order {$orderId}");
}
// Wiring — the client never knows an ExternalLogger exists:
$logger = new ExternalLoggerAdapter(new ExternalLogger());
processOrder($logger, 'ORD-001');
// [INFO] Starting processing for order ORD-001
// [WARNING] Low stock for order ORD-001
// [ERROR] Payment failed for order ORD-001
Example 2 — Payment gateway adapter
A very common case in practice: your application defines its own payment contract, and each provider (Stripe, PagSeguro, Mercado Pago) has its own SDK with a different API. One Adapter per provider keeps the domain isolated.
// ── Target — the application domain's payment contract ────────
interface PaymentGateway {
charge(amountCents: number, description: string): Promise<string>;
}
// ── Adaptee A — Stripe SDK (incompatible interface) ───────────
class StripeSdk {
async createCharge(amount: number, currency: string, desc: string): Promise<{ id: string }> {
// Simulation of the real call to Stripe
return { id: `stripe_ch_${Date.now()}` };
}
}
// ── Adaptee B — PagSeguro SDK (different interface) ───────────
class PagSeguroSdk {
async processCharge(params: {
amount: number;
currency: string;
reference: string;
}): Promise<{ transactionId: string }> {
return { transactionId: `pag_${Date.now()}` };
}
}
// ── Adapter A ─────────────────────────────────────────────────
class StripeAdapter implements PaymentGateway {
constructor(private readonly sdk: StripeSdk) {}
async charge(amountCents: number, description: string): Promise<string> {
const result = await this.sdk.createCharge(
amountCents,
"USD",
description
);
return result.id;
}
}
// ── Adapter B ─────────────────────────────────────────────────
class PagSeguroAdapter implements PaymentGateway {
constructor(private readonly sdk: PagSeguroSdk) {}
async charge(amountCents: number, description: string): Promise<string> {
const result = await this.sdk.processCharge({
amount: amountCents / 100, // PagSeguro uses whole units, not cents
currency: "USD",
reference: description,
});
return result.transactionId;
}
}
// ── Client code — decoupled from the provider ──────────────────
async function processPayment(
gateway: PaymentGateway,
amountCents: number
): Promise<void> {
const transactionId = await gateway.charge(amountCents, "Monthly subscription");
console.log(`Payment approved. Transaction: ${transactionId}`);
}
// Swapping providers = changing only the wiring line:
const gateway = new StripeAdapter(new StripeSdk());
await processPayment(gateway, 4990); // $49.90
<?php
// ── Target — the application domain's payment contract ────────
interface PaymentGateway
{
public function charge(int $amountCents, string $description): string;
}
// ── Adaptee A — Stripe SDK (incompatible interface) ───────────
class StripeSdk
{
public function createCharge(
int $amount,
string $currency,
string $desc
): array {
// Simulation of the real call to Stripe
return ['id' => 'stripe_ch_' . time()];
}
}
// ── Adaptee B — PagSeguro SDK (different interface) ───────────
class PagSeguroSdk
{
public function processCharge(array $params): array
{
return ['transactionId' => 'pag_' . time()];
}
}
// ── Adapter A ─────────────────────────────────────────────────
class StripeAdapter implements PaymentGateway
{
public function __construct(private readonly StripeSdk $sdk) {}
public function charge(int $amountCents, string $description): string
{
$result = $this->sdk->createCharge($amountCents, 'USD', $description);
return $result['id'];
}
}
// ── Adapter B ─────────────────────────────────────────────────
class PagSeguroAdapter implements PaymentGateway
{
public function __construct(private readonly PagSeguroSdk $sdk) {}
public function charge(int $amountCents, string $description): string
{
// PagSeguro uses whole units, not cents
$result = $this->sdk->processCharge([
'amount' => $amountCents / 100,
'currency' => 'USD',
'reference' => $description,
]);
return $result['transactionId'];
}
}
// ── Client code — decoupled from the provider ──────────────────
function processPayment(PaymentGateway $gateway, int $amountCents): void
{
$transactionId = $gateway->charge($amountCents, 'Monthly subscription');
echo "Payment approved. Transaction: {$transactionId}" . PHP_EOL;
}
// Swapping providers = changing only the wiring line:
$gateway = new StripeAdapter(new StripeSdk());
processPayment($gateway, 4990); // $49.90
When to use
- Integrating a third-party library or SDK whose interface doesn't match your domain's contract — and you can't modify the library.
- Isolating the domain from external providers: payment gateways, e-mail services, SMS providers, geolocation APIs. The Adapter creates an anti-corruption barrier: the domain evolves without coupling to the provider.
- Reusing legacy classes with interfaces incompatible with new code — without rewriting them.
- Supporting multiple implementations of a service transparently to the client (see the Stripe and PagSeguro example).
When to avoid
- When you control both interfaces: if you can modify the adaptee so it directly implements the target interface, there's no need for an Adapter — modify the source.
- When the interface difference is trivial: renaming a parameter doesn't justify creating a whole indirection layer.
- As a substitute for refactoring: Adapters over bad internal code are a stopgap. In poorly structured code you own, the solution is to refactor, not to adapt.
Pros and cons
Pros
- Single Responsibility Principle: interface conversion lives in a dedicated class.
- Open/Closed Principle: new providers require only a new Adapter, without touching existing code.
- Isolates business code from third-party SDKs and APIs — makes testing easier (just inject a fake that implements the target interface).
- Allows swapping the underlying implementation without changing the client.
Cons
- Adds an indirection layer — increases the number of classes and can make the flow less obvious on first read.
- If the difference between the interfaces is very large (completely different paradigms), the Adapter can become complex and hard to maintain.
- Adapters over Adapters (a long chain) are a sign that the design needs to be revisited.
Common pitfalls
1. Confusing Adapter with Facade
Adapter and Facade look superficially similar — both wrap existing code. The key difference: the Adapter converts an incompatible interface into another expected by the client (focus on interface compatibility). The Facade simplifies and unifies a complex subsystem into a high-level interface (focus on simplifying usage). An Adapter can adapt a single class; a Facade usually orchestrates multiple classes.
2. Leaking the adaptee's interface to the client
Warning: the goal of the Adapter is that the client
never references the adaptee directly. If the client code imports or
knows ExternalLogger, the isolation has been compromised.
The Adapter should be the only boundary — inject it via interface into
the client and keep the adaptee encapsulated.
3. Adapter with business logic
The Adapter should only translate calls — it shouldn't contain business logic. If you find yourself adding validation rules or complex data transformations inside the Adapter, extract those responsibilities into a separate service. An Adapter that does too much violates the Single Responsibility Principle and becomes hard to test.
4. Not creating tests with the real adaptee
Unit tests for the Adapter should use a mock of the adaptee (to isolate the translation logic). Integration tests with the real adaptee (e.g.: an actual call to Stripe in sandbox mode) are necessary, but separate. Don't mix the two testing levels in the same test suite.
Related patterns
The Adapter is frequently compared to and combined with other structural patterns:
The Decorator keeps the same interface as the object it decorates — its goal is to add responsibilities, not change the contract. The Adapter, on the contrary, has exactly the goal of changing the interface. The Facade simplifies a subsystem; it doesn't adapt an incompatible interface — but both create a single entry point for external code. The Proxy also wraps the original object, but keeps the same interface and adds access control, caching or lazy initialization — it doesn't change the contract. The Bridge separates an abstraction from its implementation from the design stage; the Adapter reconciles incompatible interfaces that already exist independently.