Adapter Pattern
Principle Link
Adapter Pattern is a pattern that implements Dependency Inversion. It supports Interface Segregation Principle. It supports Open-Closed Principle.
A core module depends on a narrow contract. An adapter translates external APIs or external data shapes into that contract. Core policy code stays stable.
Core Idea
An adapter is a boundary translator. It translates one interface into another interface. It can translate one data shape into another data shape. The target side should match domain language used by the core module.
Adapter keeps vendor details at the system edge. Core logic does not depend on provider SDK classes, payload formats, or protocol quirks. This gives clear decoupling between policy and integration details.
Conceptual Overview
All examples in this page follow the shared context: Hypothetical Scenario.
The handbook scenario has recommendation, marketplace, and ownership-cost features. Data comes from many providers. One provider sends JSON with snake case keys. Another provider sends CSV rows. Another provider sends SOAP XML.
Without adapters, use cases absorb format conversion and protocol conversion. That mixes policy logic with integration logic. One provider change can force edits across many modules.
With adapters, each provider has one adapter. Each adapter maps external shape to one internal contract. Use cases work with stable interfaces and stable domain objects.
Computing History
The Gang of Four defined Adapter in 1994 as a structural pattern. The pattern converts the interface of one class into another interface clients expect. Teams use it to reuse existing components under new contracts.
Sources: Gamma et al. (1994)
The Problem It Solves
Adapter solves boundary mismatch problems.
- interface mismatch: method names and signatures differ
- protocol mismatch: REST, SOAP, file, and queue interfaces differ
- payload mismatch: external fields and types do not match internal models
- semantics mismatch: one source reports monthly cost, another source reports yearly cost
- evolution mismatch: vendor API versions change at different speed than core modules
If a system skips adapters, core modules import external SDK types. That creates high coupling and wider change impact.
When to Use Adapter
Use Adapter Pattern in these cases.
- core module needs one clean port and external provider API does not fit that port
- ETL pipeline needs one canonical record built from many source formats
- legacy client cannot change and new module needs a new contract
- multiple providers expose the same business fact in different shapes
- test design needs fakes that satisfy the same contract as production adapters
Do not use adapter for pure business policy. Policy belongs in domain services. Adapter belongs at integration boundaries.
Generic Example: Interface Adapter in Python
from dataclasses import dataclass
from typing import Protocol
@dataclass(frozen=True)
class MarketValue:
vin: str
amount_usd: int
class MarketValuePort(Protocol):
def get_market_value(self, vin: str) -> MarketValue:
raise NotImplementedError
class BluebookClient:
def fetch_vehicle_price(self, vehicle_id: str) -> dict:
return {
"vehicle_id": vehicle_id,
"price": 21950,
"currency": "USD",
}
class BluebookAdapter(MarketValuePort):
def __init__(self, client: BluebookClient):
self.client = client
def get_market_value(self, vin: str) -> MarketValue:
payload = self.client.fetch_vehicle_price(vehicle_id=vin)
return MarketValue(
vin=payload["vehicle_id"],
amount_usd=int(payload["price"]),
)
BluebookClient exposes one external interface.
MarketValuePort defines the interface the core needs.
BluebookAdapter performs translation.
Generic Example: ETL Data Shape Adapter in Python
In ETL pipelines, adapter maps records into one canonical schema. This step is data adaptation. The same pattern applies.
from dataclasses import dataclass
from datetime import datetime, timezone
@dataclass(frozen=True)
class CanonicalComplaintRecord:
complaint_id: str
vin: str
issue_category: str
incident_date: str
severity: int
source_system: str
ingested_at_utc: str
class ComplaintFeedRowAdapter(Protocol):
def to_canonical(self, row: dict) -> CanonicalComplaintRecord:
raise NotImplementedError
class CsvComplaintRowAdapter(ComplaintFeedRowAdapter):
def to_canonical(self, row: dict) -> CanonicalComplaintRecord:
return CanonicalComplaintRecord(
complaint_id=str(row["complaint_number"]),
vin=str(row["vin"]).upper(),
issue_category=str(row["component"]).strip(),
incident_date=str(row["incident_date"]),
severity=int(row.get("severity_level", 1)),
source_system="csv_feed",
ingested_at_utc=datetime.now(timezone.utc).isoformat(),
)
class JsonComplaintRowAdapter(ComplaintFeedRowAdapter):
def to_canonical(self, row: dict) -> CanonicalComplaintRecord:
return CanonicalComplaintRecord(
complaint_id=str(row["id"]),
vin=str(row["vehicle"]["vin"]).upper(),
issue_category=str(row["issue"]["category"]),
incident_date=str(row["issue"]["date"]),
severity=int(row["issue"].get("priority", 1)),
source_system="json_api",
ingested_at_utc=datetime.now(timezone.utc).isoformat(),
)
ETL pipeline can process any source adapter through one contract.
class ComplaintIngestionPipeline:
def __init__(self, adapter: ComplaintFeedRowAdapter):
self.adapter = adapter
def transform(self, rows: list[dict]) -> list[CanonicalComplaintRecord]:
return [self.adapter.to_canonical(row) for row in rows]
The pipeline does not know source format. Adapter isolates that concern.
Generic Example: TypeScript Adapter
type ReliabilityRecord = {
vin: string
score: number
source: string
}
interface ReliabilityPort {
getReliability(vin: string): Promise<ReliabilityRecord>
}
class LegacyReliabilityClient {
async getVehicle(vin: string): Promise<{ vehicle_code: string; rating_value: string }> {
return { vehicle_code: vin, rating_value: "88" }
}
}
class LegacyReliabilityAdapter implements ReliabilityPort {
constructor(private readonly client: LegacyReliabilityClient) {}
async getReliability(vin: string): Promise<ReliabilityRecord> {
const payload = await this.client.getVehicle(vin)
return {
vin: payload.vehicle_code,
score: Number(payload.rating_value),
source: "legacy_reliability",
}
}
}
Dependency Injection and Substitution
Adapter works best with dependency injection. Composition root chooses the adapter implementation. Core use case receives only the target port.
class BuildOwnershipEstimateUseCase:
def __init__(self, market_value_port: MarketValuePort):
self.market_value_port = market_value_port
def run(self, vin: str) -> int:
value = self.market_value_port.get_market_value(vin)
return int(value.amount_usd * 0.08)
class FakeMarketValueAdapter(MarketValuePort):
def get_market_value(self, vin: str) -> MarketValue:
return MarketValue(vin=vin, amount_usd=20000)
def test_ownership_estimate() -> None:
use_case = BuildOwnershipEstimateUseCase(market_value_port=FakeMarketValueAdapter())
assert use_case.run("VIN-0001") == 1600
FakeMarketValueAdapter substitutes production adapter through the same contract.
This follows Liskov substitution.
Trade-offs and Failure Modes
Adapter adds one layer. That layer has runtime and maintenance cost. The cost is small in most systems. The value is high at unstable boundaries.
Common failure modes:
- adapter contains business rules that belong in domain services
- one adapter class handles many providers and becomes hard to maintain
- mapping drops fields with no validation and data quality degrades
- adapter returns external DTOs instead of canonical models
Control tactics:
- keep one adapter per provider and per target contract
- validate mappings with contract tests
- add schema checks in ETL transform stage
- keep mapping logs for rejected or malformed rows
Foundational Principle Links
This pattern links to Abstraction and Boundaries. Adapter defines a strict boundary between core policy and integration details.
This pattern links to Modularity and Composition. Adapters compose with ports and use cases through explicit contracts.
This pattern links to Simplicity First. Core modules read one clean contract instead of many provider formats.
This pattern links to State and Data Modeling. ETL adapters map many source schemas into one canonical model.
Practice Checklist
- define a narrow target contract in domain language
- place adapter at boundary layer, not in domain core
- map external fields to canonical types in one place
- keep adapter code free of domain policy branches
- wire adapters with dependency injection in composition root
- create fake adapters for fast unit tests
- add mapping validation for ETL adapters
- monitor adapter failures and rejected records
Written by: Pedro Guzmán
See References for complete APA-style bibliographic entries used on this page.