Adapter Pattern: Making Incompatible Interfaces Work Together
After finishing the creational patterns, we now enter the world of structural patterns — which are all about how classes and objects are composed to form larger structures. First up: the Adapter Pattern, which helps incompatible interfaces work together.
What Is the Adapter Pattern?
The Adapter Pattern allows objects with incompatible interfaces to work together by acting as a translator between them. It's like a plug converter: the socket expects a certain shape, and you use an adapter to make your device compatible.
It’s especially useful when:
- You integrate third-party libraries or legacy code that can't be changed.
- You want to conform different systems to a common interface in your application.
- You want to write clean, decoupled code despite external inconsistencies.
Real-World Analogy: Travel Power Adapter
Imagine you're traveling from the US to Europe. Your laptop charger has a US plug, but European outlets are shaped differently. Instead of buying a new charger, you use a travel adapter to connect your existing charger to the European socket.
You're not changing the charger or the outlet — you're just translating between them. That’s exactly what the Adapter Pattern does in code.
Real Backend Example: Adapting a Third-Party Payment API
Suppose your backend system defines a generic
Step 1: Define the Target Interface
# payment_processor.py from abc import ABC, abstractmethod class PaymentProcessor(ABC): @abstractmethod def pay(self, amount: float): pass
Step 2: Assume a Third-Party Class (Incompatible Interface)
# third_party/paypal_sdk.py class PayPalAPI: def __init__(self, user_email): self.email = user_email def send_payment(self, money): print(f"[PayPal] Sent ${money:.2f} from {self.email}")
This
Step 3: Create the Adapter
# adapters/paypal_adapter.py from payment_processor import PaymentProcessor from third_party.paypal_sdk import PayPalAPI class PayPalAdapter(PaymentProcessor): def __init__(self, email): self.paypal = PayPalAPI(email) def pay(self, amount: float): self.paypal.send_payment(amount)
Now the
Step 4: Use in the Application
# main.py from adapters.paypal_adapter import PayPalAdapter def checkout(payment_processor: PaymentProcessor, total: float): payment_processor.pay(total) paypal = PayPalAdapter("client@example.com") checkout(paypal, 99.99)
Output:
[PayPal] Sent $99.99 from client@example.com
Your application remains decoupled from the third-party API. Future changes to PayPal (or switching to Stripe) won’t affect your core logic — only the adapter needs updating.
Where Adapter Pattern Shines in Backend Systems
- Integrating different database drivers with a common repository interface
- Wrapping legacy classes that don’t follow your system’s interface
- Connecting to external services (e.g., email, SMS, payment, auth)
- Creating compatibility layers for microservices with differing contracts
- Standardizing data format conversions (e.g., XML ↔ JSON)
Pitfalls and Considerations
- Overusing adapters can introduce unnecessary layers if not needed.
- Don’t confuse this with the Decorator Pattern — which adds behavior, while Adapter changes interface.
- Make sure the adapter hides complexity, not just redirects calls.
Final Thoughts
The Adapter Pattern is one of the most practical tools in a backend engineer’s toolbox — especially when dealing with systems you don’t control. It helps you embrace the messiness of the real world without letting it leak into your clean architecture.
Think of it as a translator at a meeting: everyone speaks their native language, but still understands each other thanks to the adapter.