This repository accompanies the lecture on the Adapter Pattern, focusing on integrating incompatible systems and building flexible backend architectures.
The Adapter Pattern is a structural design pattern that allows incompatible interfaces to work together.
It acts as a translator between:
- the structure expected by the application and
- an external or incompatible system
Instead of rewriting existing systems, the adapter converts one interface into another compatible interface.
This pattern is extremely common in:
- backend engineering
- API integrations
- payment gateways
- external SDK integrations
- legacy system migration
Convert one interface into another interface the client expects.
Or more simply:
Make incompatible systems communicate safely.
Imagine a company receiving invoice files from different suppliers.
Each supplier sends files in different formats:
- Excel
- XML
- CSV
- JSON
But the accounting system only understands one standard structure.
Instead of rewriting the accounting system every time:
- supplier sends external format
- converter transforms the structure
- accounting system receives compatible data
The converter behaves exactly like:
the Adapter Pattern.
Use Adapter Pattern when:
- integrating third-party APIs
- consuming external SDKs
- working with legacy systems
- unifying different providers
- standardizing communication structures
- migrating systems gradually
- avoiding modification of external code
Without Adapter Pattern, systems directly depend on external implementations.
Example:
legacyBankAPI.makeTransaction(amount);Problems with this approach:
- Tight coupling
- Harder maintenance
- Difficult provider replacement
- Scattered conversion logic
- Fragile architecture
- Reduced scalability
As systems grow, direct integrations become increasingly difficult to manage.
The Adapter Pattern separates responsibilities into four main components.
- Defines the structure expected by the backend
- Standardizes communication
- Creates abstraction between backend and providers
Example:
- PaymentProcessor
- Represents the incompatible external system
- Existing SDK or third-party service
- Cannot directly communicate with backend structure
Example:
- LegacyBankAPI
- Converts interfaces
- Wraps external systems internally
- Translates calls between structures
Example:
- BankAdapter
- Uses only the standard interface
- Remains independent from provider details
- Depends on abstraction instead of implementation
Example:
- Main class / backend service
<<interface>>
PaymentProcessor
----------------
+ pay(amount)
β²
β
BankAdapter
----------------
- LegacyBankAPI
+ pay(amount)
β
βΌ
LegacyBankAPI
----------------
+ makeTransaction()
β²
β
Main
(Client)
Imagine an E-Commerce backend system.
The backend expects all payment providers to expose:
pay(double amount)However, an old banking SDK already exists and exposes:
makeTransaction(double value)The backend should:
- use one standard payment interface
- remain independent from provider implementation
- support future payment providers easily
- avoid modifying external SDKs
Important condition:
the backend should NOT directly depend on external provider structures.
When the system runs:
- The backend communicates only with the standard interface
- The adapter wraps the external SDK
- The adapter translates requests internally
- The backend simply calls:
paymentProcessor.pay(250);Inside the adapter:
pay() β makeTransaction()This creates:
- loose coupling
- scalability
- maintainability
- provider flexibility
A very important mental model:
- Client = uses abstraction
- Adapter = translates requests
- Adaptee = external incompatible system
Or:
The backend decides what it wants The adapter decides how to translate it
The same backend can support completely different payment providers simply by changing adapters.
Example flow:
1. Create PayPalAdapter
2. Create StripeAdapter
3. Create BankAdapter
4. Backend uses PaymentProcessor only
No modification is required inside backend business logic.
The Adapter Pattern is widely used in real backend systems.
Examples include:
- payment gateways
- REST API integrations
- cloud storage systems
- SMS providers
- email services
- database drivers
- authentication providers
- legacy ERP systems
- Kafka/RabbitMQ wrappers
This pattern strongly supports:
The adapter wraps external systems instead of inheriting from them.
New adapters can be added without modifying backend logic.
Each adapter handles one compatibility translation responsibility.
| Advantage | Explanation |
|---|---|
| Reusability | Reuse external services safely |
| Flexibility | Integrate multiple providers |
| Maintainability | Cleaner architecture |
| Scalability | Easy provider expansion |
| Decoupling | Backend depends on abstraction |
| Isolation | External changes remain isolated |
| Disadvantage | Explanation |
|---|---|
| More Classes | Adds adapter layers |
| Extra Abstraction | Slightly increases complexity |
| Additional Translation Logic | Requires conversion handling |
π solutions/
All solutions are available in the /solutions folder.
Each solution includes:
- UML + design explanation
- Clean Java implementation
- Adapter Pattern usage for integrating external systems
- Separation between client, adapters, and adaptees
- Encapsulation of integration logic inside dedicated adapter classes
- Notes on design decisions and extensibility
- Example usage via
Main.java
- π§ Activity: Multi Payment Gateway Integration System
Students should first implement the system independently, then compare their design with the provided solution.
After studying this pattern, you should be able to:
- Identify incompatible system integrations
- Design adapter-based architectures
- Reduce coupling between services
- Integrate external SDKs safely
- Build scalable backend integrations
- Apply composition correctly
- Standardize communication between providers
The Adapter Pattern becomes powerful when:
external systems can change freely while the backend remains stable through standardized abstractions.