With the Adapter pattern, we isolate the business rule from the concrete dependency.
Starting scenario
We have a backend with a simple user CRUD: create, edit, retrieve, and delete through API endpoints. Data goes to some database — say MySQL — and the structure starts as Controller → Service → Database. So far, the team is comfortable.
Until someone decides: next week we migrate from MySQL to PostgreSQL. From there, the house falls down on technology. Beyond restructuring the database, the team must hunt down MySQL references: inserts, connection, scattered queries.
Most of the time this delays delivery, reduces quality, skips tests, and introduces bugs. It would be better to reduce the dependency between the business rule and whoever performs the specific operation — here, the database. The Adapter helps build the system that way.
Adapter pattern
We have a third-party plugin, library, module, or service that does something we want in the business rule — here, persisting data. The path:
- Define an interface with the contract of what we need.
- Expose only methods that make sense in context (SOLID).
- Implement the interface in classes that adapt the third-party code.
We create CreateDatabaseCustomerProtocol with a create method that receives CustomerInputEntity and returns SuccessfulEntityCreation:
interface CustomerInputEntity {
name: string;
email: string;
birthDate: Date;
}
interface SuccessfulEntityCreation {
readonly id: number;
readonly name: string;
readonly email: string;
readonly birthDate: Date;
}
interface CreateDatabaseCustomerProtocol {
createCustomerOnDatabase(
customer: CustomerInputEntity,
): SuccessfulEntityCreation;
}
Instead of Controller → Service → Database, we move to Controller → Service → Protocols → Plugin.
The service loses knowledge of how the CRUD reaches the database. It is composed of protocols; the concrete implementation arrives at runtime — via dependency injection. While we use PostgreSQL, we implement the protocols in adapters (or connectors).
CreateDatabaseCustomerProtocol can be implemented by CreateDatabaseCustomerPostgresqlAdapter, CreateDatabaseCustomerMysqlAdapter, CreateDatabaseCustomerMongoDBAdapter, or CreateDatabaseCustomerMockedAdapter. The service looks like this:
class CustomerService {
constructor(
private readonly createCustomer: CreateDatabaseCustomerProtocol,
) {}
public register(customer: CustomerInputEntity): SuccessfulEntityCreation {
return this.createCustomer.createCustomerOnDatabase(customer);
}
}
For the service, it does not matter whether the database returns JSON, XML, or another format — the adapter translates to the contract the business rule expects.
Advantages
- Maintenance: any plugin can be replaced without rewriting the service.
- Tests: to test only the business rule, inject a mock adapter implementing the same protocol.
- Clean code: separated responsibilities; the business rule does not carry driver details.
Next step
Take a frontend with dozens of libraries and identify what you actually use. Pick one feature — converting BRL to USD, for example. Describe the contract (input and output) and implement an adapter on top of the library that does it today. Repeat where the dependency hurts.
Relation to other patterns
In the article Design Patterns: Strategy, the focus is swapping algorithms behind a contract. The Adapter isolates external dependencies behind an owned interface. The two complement each other: Strategy varies behavior; Adapter translates the outside world.
Originally published on LinkedIn on April 26, 2022.