bigonotes
CS ConceptsDesign Patterns

Adapter Pattern

Wrap a legacy class or third-party SDK so it fits the interface your code expects, without changing either side.

7 min readLast updated Design Patterns

You have a clean interface your code depends on. Then you must integrate a legacy system or a third-party SDK whose method names, parameter types and data formats are all different, and you can't change it. Adapter puts a small translator class in between, so neither side has to change.

Intent#

Convert the interface of a class into another interface clients expect. Adapter lets classes work together that couldn't otherwise because of incompatible interfaces. (GoF)

Analogy: a travel power adapter. Your US charger doesn't fit a European socket. The adapter changes neither the charger nor the socket; it just makes them fit.

The problem: SDK details leak into business logic#

Without an adapter, the service has to know every logger's API:

class StandardLogger:
    def log(self, message: str) -> None:
        print(f"[LOG] {message}")


# Third-party SDK: you cannot change this code
class ExternalDatadogSDK:
    def send_event(self, payload: dict, priority: int) -> None:
        print(f"Datadog received: {payload['msg']} with priority {priority}")


class OrderService:
    def __init__(self, logger):
        self.logger = logger

    def create_order(self) -> None:
        # Business logic now knows about every logging vendor
        if isinstance(self.logger, StandardLogger):
            self.logger.log("Order created")
        elif isinstance(self.logger, ExternalDatadogSDK):
            self.logger.send_event({"msg": "Order created"}, priority=1)
  • Violates Open/Closed: switching to another vendor means another elif in OrderService and every other service.
  • Violates Dependency Inversion: your core logic should set the rules, not reshape dictionaries for an external tool.

Structure#

An adapter sits between the client and the class it can't call directly.

Adapter: the client calls the target interface; the adapter translates each call into the adaptee's API
Participant Role
Target The interface your client expects
Adaptee The existing class with a useful but incompatible interface
Adapter Implements Target, holds an Adaptee, translates each call "under the hood"
Client Uses only Target; doesn't know an adapter is involved

Two flavors:

  • Object adapter (composition): the adapter has an adaptee. The usual choice: it works with any subclass of the adaptee.
  • Class adapter (inheritance): the adapter is an adaptee and also implements the target. In Python this is multiple inheritance (class Adapter(Adaptee, Target)); languages with single class inheritance only allow it when the target is an interface.

Example 1: logging SDK#

The service logs through one interface; the adapter translates each call for the vendor SDK.

from abc import ABC, abstractmethod


# Target: what your system expects
class LoggerInterface(ABC):
    @abstractmethod
    def log(self, message: str) -> None: ...


class StandardLogger(LoggerInterface):
    def log(self, message: str) -> None:
        print(f"[LOG] {message}")


# Adaptee: the external SDK, unchanged
class ExternalDatadogSDK:
    def send_event(self, payload: dict, priority: int) -> None:
        print(f"Datadog received: {payload['msg']} with priority {priority}")


# Adapter: implements YOUR interface, translates to THEIR API
class DatadogAdapter(LoggerInterface):
    def __init__(self, sdk: ExternalDatadogSDK):
        self._sdk = sdk

    def log(self, message: str) -> None:
        self._sdk.send_event(payload={"msg": message}, priority=1)


class OrderService:
    # Depends only on the abstraction
    def __init__(self, logger: LoggerInterface):
        self.logger = logger

    def create_order(self) -> None:
        self.logger.log("Order created successfully")


OrderService(StandardLogger()).create_order()
# [LOG] Order created successfully
OrderService(DatadogAdapter(ExternalDatadogSDK())).create_order()
# Datadog received: Order created successfully with priority 1

The adapter acts as an anti-corruption layer. If the vendor renames send_event() next year, you change one adapter file and nothing else notices.

Example 2: payment gateways#

Adapters often convert types and return values, not just method names.

from abc import ABC, abstractmethod


# Target
class PaymentGateway(ABC):
    @abstractmethod
    def process_payment(self, amount: float) -> None: ...

    @abstractmethod
    def refund(self, amount: float, transaction_id: str) -> bool: ...

    @abstractmethod
    def get_payment_status(self, transaction_id: str) -> str: ...


# Adaptee 1: legacy system with different names and types
class LegacyPaymentSystem:
    def make_payment(self, amount: str) -> None:
        print(f"Legacy: paying ${amount}")

    def cancel_transaction(self, txn_id: str, amount: str) -> None:
        print(f"Legacy: cancelling {txn_id} for ${amount}")

    def check_status(self, txn_id: str) -> int:
        return 1  # 1 = success, 0 = pending, -1 = failed


# Adaptee 2: third-party library that works in integer cents and int IDs
class ThirdPartyPayPal:
    def pay(self, cents: int) -> None:
        print(f"PayPal: charging {cents} cents")

    def do_refund(self, cents: int, trans_id: int) -> str:
        return "REFUNDED"

    def get_transaction_details(self, trans_id: int) -> str:
        return "COMPLETED"


class LegacyPaymentAdapter(PaymentGateway):
    def __init__(self, legacy: LegacyPaymentSystem):
        self._legacy = legacy

    def process_payment(self, amount: float) -> None:
        self._legacy.make_payment(str(amount))  # float → str

    def refund(self, amount: float, transaction_id: str) -> bool:
        self._legacy.cancel_transaction(transaction_id, str(amount))
        return True

    def get_payment_status(self, transaction_id: str) -> str:
        # int code → status string
        match self._legacy.check_status(transaction_id):
            case 1:
                return "SUCCESS"
            case 0:
                return "PENDING"
            case -1:
                return "FAILED"
            case _:
                return "UNKNOWN"


class PayPalAdapter(PaymentGateway):
    def __init__(self, paypal: ThirdPartyPayPal):
        self._paypal = paypal

    def process_payment(self, amount: float) -> None:
        self._paypal.pay(round(amount * 100))  # dollars → int cents

    def refund(self, amount: float, transaction_id: str) -> bool:
        try:
            txn = int(transaction_id)  # str ID → int ID
        except ValueError:
            return False
        result = self._paypal.do_refund(round(amount * 100), txn)
        return result == "REFUNDED"

    def get_payment_status(self, transaction_id: str) -> str:
        try:
            txn = int(transaction_id)
        except ValueError:
            return "INVALID_ID"
        return self._paypal.get_transaction_details(txn)


# Client: works with any gateway, unaware of adapters
class PaymentProcessor:
    def __init__(self, gateway: PaymentGateway):
        self._gateway = gateway

    def process_order(self, amount: float, order_id: str) -> None:
        self._gateway.process_payment(amount)
        if self._gateway.get_payment_status(order_id) == "FAILED":
            self._gateway.refund(amount, order_id)


legacy = LegacyPaymentAdapter(LegacyPaymentSystem())
PaymentProcessor(legacy).process_order(100.50, "ORDER001")
paypal = PayPalAdapter(ThirdPartyPayPal())
PaymentProcessor(paypal).process_order(250.75, "12345")

Adapters can also convert units and semantics. A legacy rectangle whose scale() takes a percentage can sit behind a Shape.resize(factor) interface:

from abc import ABC, abstractmethod


class Shape(ABC):
    @abstractmethod
    def draw(self) -> None: ...

    @abstractmethod
    def resize(self, factor: float) -> None: ...

    @abstractmethod
    def get_area(self) -> float: ...


class LegacyRectangle:
    def __init__(self, w: float, h: float):
        self.w, self.h = w, h

    def display(self) -> None:
        print(f"Rectangle {self.w} x {self.h}")

    def scale(self, percent: float) -> None:
        self.w *= percent / 100
        self.h *= percent / 100

    def calculate_area(self) -> float:
        return self.w * self.h


class RectangleAdapter(Shape):
    def __init__(self, rect: LegacyRectangle):
        self._rect = rect

    def draw(self) -> None:
        self._rect.display()  # display() → draw()

    def resize(self, factor: float) -> None:
        self._rect.scale(factor * 100)  # factor → percentage

    def get_area(self) -> float:
        return self._rect.calculate_area()


shape = RectangleAdapter(LegacyRectangle(2, 3))
shape.resize(2)
print(shape.get_area())  # 24.0

Example 3: class adapter and two-way adapter#

A class adapter inherits from the adaptee instead of wrapping it:

from abc import ABC, abstractmethod


class ModernPrinter(ABC):
    @abstractmethod
    def print_text(self, text: str) -> None: ...


class OldPrinter:
    def print_black_and_white(self, content: str) -> None:
        print(f"B&W: {content}")


# Class adapter: inherits the adaptee, implements the target
class PrinterClassAdapter(OldPrinter, ModernPrinter):
    def print_text(self, text: str) -> None:
        # Inherited method, no wrapped field
        self.print_black_and_white(text)


PrinterClassAdapter().print_text("hello")  # B&W: hello

A two-way adapter implements both interfaces so it can be used from either side:

from abc import ABC, abstractmethod


class MediaPlayer(ABC):
    @abstractmethod
    def play(self, filename: str) -> None: ...

    @abstractmethod
    def stop(self) -> None: ...


class AudioDevice(ABC):
    @abstractmethod
    def start_playback(self, file: str) -> None: ...

    @abstractmethod
    def stop_playback(self) -> None: ...

    @abstractmethod
    def pause(self) -> None: ...


class TwoWayAdapter(MediaPlayer, AudioDevice):
    # MediaPlayer side: delegates to the AudioDevice methods
    def play(self, filename: str) -> None:
        self.start_playback(filename)

    def stop(self) -> None:
        self.stop_playback()

    # AudioDevice side
    def start_playback(self, file: str) -> None:
        print(f"Playing: {file}")

    def stop_playback(self) -> None:
        print("Stopped")

    def pause(self) -> None:
        print("Paused")


TwoWayAdapter().play("song.mp3")  # Playing: song.mp3

Template#

This is the bare object-adapter shape to start from:

from abc import ABC, abstractmethod


class Target(ABC):
    @abstractmethod
    def request(self) -> None: ...


class Adaptee:
    def specific_request(self) -> None:
        """Useful, but the wrong shape."""


class Adapter(Target):
    def __init__(self, adaptee: Adaptee):
        self._adaptee = adaptee

    def request(self) -> None:
        self._adaptee.specific_request()  # translate, then delegate

Real-world examples#

You already use adapters in the standard library and in most integrations:

  • io.TextIOWrapper adapts a binary stream into a text stream.
  • Python DB-API drivers (sqlite3, psycopg) adapt each database's native protocol to one common connection-and-cursor interface.
  • Payment processors (Stripe, PayPal, a legacy bank API) behind one gateway interface.
  • SMS providers like Twilio behind a notifier interface.
  • Import/export of CSV, JSON and XML behind one reader interface.

When to use it#

Adapter fits when the mismatch is in the interface, not the behaviour:

  • You want to use an existing class whose interface doesn't match.
  • You're integrating third-party or legacy code you can't modify.
  • You want several implementations behind one common interface.
  • You're migrating systems or supporting old and new API versions side by side.

When not to use it#

Skip it in these cases:

  • You own the source and can simply change the class.
  • The interfaces are fundamentally different in what they do, not just how they're named. No thin translator will fix that.
  • You'd need adapters everywhere, which signals a redesign is due.

Pitfalls#

Keep adapters thin and watch for these problems:

  • Lossy translation. Converting dollars to integer cents or string IDs to int can lose precision or fail; handle it explicitly, as the PayPal adapter does.
  • Business logic creeping into adapters. Keep them to translation and delegation.
  • Extra layer to debug, and a small cost for each extra call.
  • Adapter sprawl if every integration grows its own slightly different adapter.

Adapter vs similar patterns#

Several structural patterns wrap an object. They differ in what they do to its interface:

Pattern Interface Purpose
Adapter Changes it Make an existing class fit
Decorator Keeps it Add behavior
Proxy Keeps it Control access
Facade New, simpler one Simplify a whole subsystem
Bridge Designed up front Let abstraction and implementation vary independently

Adapter is applied after the fact to make existing things fit; Bridge is designed up front.

Key takeaways#

  • Adapter implements the interface you want and translates calls to the class you have.
  • Prefer the object adapter (composition); a class adapter needs inheritance from the adaptee.
  • It's your anti-corruption layer: vendor changes stay inside one class.
  • Adapters translate names, types, units and return codes. Keep business logic out of them.

Found a mistake? Tell me on LinkedIn ↗

Further reading#