Skip to content

Adapter Pattern

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

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.