What is it? #
An Adapter wraps an object whose interface does not match what your code expects, and exposes the interface you want.
You reach for it when you cannot change either side. A third-party SDK has its method names; your application has its own interface; something has to translate between them.
The adapter is that translation, and it lives in exactly one class. Your application code never sees the vendor's vocabulary.
The second benefit is containment. When the vendor changes their API, or you replace them entirely, one file changes.
Think of it like this #
A travel plug adapter. The hotel socket and your charger both work perfectly; they simply do not fit. The adapter changes nothing electrically — it makes one shape acceptable to the other.
You would not rewire your charger for every country, and you should not rewrite your application for every vendor.
Simple example #
Your application expects a Notifier with a single notify(to, message) method. A third-party SMS library offers send_message(recipient_number, text_body, sender_id) and returns its own result object.
Code #
from abc import ABC, abstractmethod
# --- What your application wants ---
class Notifier(ABC):
@abstractmethod
def notify(self, to: str, message: str) -> bool: ...
# --- What the vendor gives you (you cannot change this) ---
class VendorSmsClient:
def __init__(self, api_key, sender_id):
self.api_key, self.sender_id = api_key, sender_id
def send_message(self, recipient_number: str, text_body: str) -> dict:
return {"status": "QUEUED", "msg_id": "abc123", "segments": 1}
class VendorEmailSdk:
def __init__(self, token): self.token = token
def dispatch(self, payload: dict) -> int:
return 202 # HTTP-style status code
# --- Adapters: one per vendor, translating both ways ---
class SmsAdapter(Notifier):
def __init__(self, client: VendorSmsClient):
self._client = client
def notify(self, to: str, message: str) -> bool:
if not to.startswith("+"):
to = f"+91{to}" # normalise to what the vendor needs
result = self._client.send_message(recipient_number=to, text_body=message[:160])
return result.get("status") in {"QUEUED", "SENT"}
class EmailAdapter(Notifier):
def __init__(self, sdk: VendorEmailSdk, from_address: str):
self._sdk, self._from = sdk, from_address
def notify(self, to: str, message: str) -> bool:
status = self._sdk.dispatch({
"from": self._from,
"to": [to],
"subject": message.split("\n")[0][:80],
"html": f"<p>{message}</p>",
})
return 200 <= status < 300
# --- Application code speaks only its own language ---
def alert_all(notifiers: list[Notifier], message: str, recipients: list[str]):
for notifier, recipient in zip(notifiers, recipients):
ok = notifier.notify(recipient, message)
print("sent" if ok else "failed")
alert_all(
[SmsAdapter(VendorSmsClient("key", "BURNJT")),
EmailAdapter(VendorEmailSdk("token"), "[email protected]")],
"Server disk is 92% full",
["9876543210", "[email protected]"],
)
When to use it
- integrating a third-party library whose interface you cannot change
- replacing a vendor without touching application code
- migrating gradually from an old interface to a new one
When NOT to use it
- you control both sides and can simply change one
- the adapter starts adding business logic instead of translating
- there is only one consumer and no prospect of switching
How it works #
Notifier is your interface, defined by what your application needs — not by what any vendor offers. That direction matters: an interface copied from one SDK cannot be satisfied by another.
SmsAdapter holds the vendor client and translates. It maps your argument names to theirs, normalises the phone number, truncates to the SMS length limit, and converts their dictionary result into the boolean your code expects.
EmailAdapter does a completely different translation — building a payload dictionary and interpreting an HTTP status code — while presenting the identical notify method.
alert_all is application code. It calls notify and knows nothing about statuses, segments or payload shapes. Vendor vocabulary stops at the adapter boundary.
Each adapter is easy to test in isolation with a stub vendor client, and the application logic can be tested with a simple fake notifier.
Replacing the SMS vendor means writing one new adapter class. Nothing else in the codebase changes, which is the containment benefit in concrete terms.
Real-world use #
Adapters appear at every integration point: payment gateways, cloud storage, search engines, email and SMS providers, analytics tools.
In larger systems this idea is called an anti-corruption layer: a boundary that stops another system's model leaking into yours. Without it, vendor concepts spread through your codebase and become impossible to remove.
Adapters are also useful during migrations. Wrap the old system in the new interface, move callers over one at a time, then write a second adapter for the new system and switch.
Legacy code benefits too. A twelve-year-old module with awkward method names can be wrapped once and used through a clean interface, without the risky rewrite.
Common mistakes #
- Designing the interface around the vendor, so no other vendor can implement it.
- Putting business rules in the adapter. It should translate, not decide.
- Leaking vendor types — exceptions, result objects, enums — through the interface.
- Writing one giant adapter for several vendors instead of one per vendor.
- Adapting something you control, when changing it directly would be simpler.
Practice #
Define a PaymentGateway interface with charge(amount, token) returning a transaction ID string. Write two adapters for imaginary SDKs with very different method signatures and return shapes. Then write a function that uses the interface and confirm it needs no changes when you switch adapters.