Adapter Pattern
The Adapter Pattern is a structural design pattern that allows incompatible classes, interfaces, or systems to work together. It acts as a bridge between code that expects one interface and another class that provides a different interface.
In real software projects, developers often need to connect old code with new code, integrate external APIs, replace third-party libraries, or make different services follow the same structure. The Adapter Pattern helps solve these problems without changing the existing code directly.
Introduction
In previous articles of this Design Patterns series, we discussed creational patterns such as Singleton, Factory, Abstract Factory, Builder, and Prototype. These patterns focus mainly on object creation.
The Adapter Pattern belongs to the structural design patterns category. Structural patterns focus on how classes and objects are connected and organized to form larger systems.
Adapter is one of the most practical design patterns because real applications rarely work with perfectly matching interfaces. External packages, legacy systems, APIs, payment gateways, notification providers, and storage services often use different method names, parameters, and response formats. Adapter helps unify these differences.
What Is the Adapter Pattern?
The Adapter Pattern is a design pattern that converts the interface of one class into another interface expected by the client code. It allows classes with incompatible interfaces to work together without modifying their source code.
In simple terms, an adapter wraps an existing class and exposes a new interface that the application can understand.
For example, imagine your application expects a payment service with a method called pay. However, a third-party payment library provides a method called makeTransaction. Instead of changing your application code everywhere, you can create an adapter that provides pay and internally calls makeTransaction.
Real-Life Example of Adapter
A simple real-life example is a power plug adapter. If your laptop charger has a plug that does not fit the wall socket, you do not change the charger or the wall. You use an adapter that connects both sides.
In software, the same idea applies. If one class does not match the interface expected by another class, an adapter can translate between them.
The client code continues using the interface it understands, while the adapter handles communication with the incompatible class.
Why the Adapter Pattern Is Important
The Adapter Pattern is important because it helps developers integrate different systems while keeping code clean and maintainable. Without an adapter, the main application code may become full of conditions, conversions, and provider-specific details.
For example, if an application integrates multiple SMS providers, each provider may have a different method name and payload format. Without adapters, the notification service would need to know the details of every provider.
With Adapter Pattern, each provider has its own adapter that follows a common interface. The notification service depends only on that interface and remains clean.
Problem Without Adapter Pattern
Imagine an application that sends notifications. The application expects every notification sender to have a method called send.
But an external SMS library has a different method:
class ExternalSmsService
{
public function sendSmsMessage(string $phoneNumber, string $text): bool
{
// Send SMS using external provider
return true;
}
}If the application directly uses this external class, the notification code becomes tied to this specific provider. If the provider changes, the application code may need many updates.
Adapter Pattern solves this by wrapping the external service inside a class that matches the application's expected interface.
Basic Adapter Pattern Example in PHP
First, define the interface expected by the application:
interface NotificationSender
{
public function send(string $recipient, string $message): bool;
}The external service has a different method name and parameter style:
class ExternalSmsService
{
public function sendSmsMessage(string $phoneNumber, string $text): bool
{
// External SMS sending logic
return true;
}
}Now create an adapter:
class SmsServiceAdapter implements NotificationSender
{
public function __construct(
private ExternalSmsService $smsService
) {
}
public function send(string $recipient, string $message): bool
{
return $this->smsService->sendSmsMessage($recipient, $message);
}
}The adapter implements the interface expected by the application and internally calls the external service method.
Using the Adapter
The application can now use the adapter through the NotificationSender interface:
$externalSmsService = new ExternalSmsService();
$sender = new SmsServiceAdapter($externalSmsService);
$sender->send('+905000000000', 'Your verification code is 123456.');The application does not need to know that the external service uses sendSmsMessage internally. It only calls send.
This keeps the main application code independent from the external provider details.
Main Parts of the Adapter Pattern
The Adapter Pattern usually includes these main parts:
Client: The code that needs to use a specific interface.
Target interface: The interface expected by the client.
Adaptee: The existing class with an incompatible interface.
Adapter: The class that converts the adaptee interface into the target interface.
This structure allows the client to work with incompatible classes indirectly through a stable interface.
Object Adapter vs Class Adapter
There are two common forms of Adapter Pattern: object adapter and class adapter.
An object adapter uses composition. It receives an instance of the incompatible class and calls its methods internally. This is the most common approach in PHP and modern object-oriented design.
A class adapter uses inheritance. It extends the incompatible class and adapts its behavior through inherited methods. This approach is less flexible and is not always possible, especially when a language does not support multiple inheritance.
In PHP projects, object adapters are usually preferred because composition is more flexible than inheritance.
Object Adapter Example
The previous SMS example is an object adapter because SmsServiceAdapter contains an ExternalSmsService object.
class SmsServiceAdapter implements NotificationSender
{
public function __construct(
private ExternalSmsService $smsService
) {
}
public function send(string $recipient, string $message): bool
{
return $this->smsService->sendSmsMessage($recipient, $message);
}
}This design is flexible because the adapter does not need to inherit from the external service. It simply uses it internally.
Class Adapter Example
A class adapter may extend the external class and implement the expected interface:
class SmsClassAdapter extends ExternalSmsService implements NotificationSender
{
public function send(string $recipient, string $message): bool
{
return $this->sendSmsMessage($recipient, $message);
}
}This works in simple cases, but it is less flexible because the adapter is tightly connected to the external class through inheritance.
Object adapters are usually better for maintainable and testable software.
Real-World Example: Payment Gateway Adapter
Payment systems are a common use case for the Adapter Pattern. Different payment providers often have different APIs, method names, request formats, and response structures.
Your application may define a common payment interface:
interface PaymentGateway
{
public function charge(float $amount, string $currency): bool;
}A third-party payment provider may use a different method:
class ThirdPartyPaymentApi
{
public function makePayment(array $payload): array
{
return [
'success' => true,
'transaction_id' => 'TX123',
];
}
}An adapter can convert your application's expected call into the provider's format:
class ThirdPartyPaymentAdapter implements PaymentGateway
{
public function __construct(
private ThirdPartyPaymentApi $api
) {
}
public function charge(float $amount, string $currency): bool
{
$response = $this->api->makePayment([
'amount' => $amount,
'currency' => $currency,
]);
return $response['success'] ?? false;
}
}The application can now use ThirdPartyPaymentAdapter as a PaymentGateway without depending on the provider-specific API.
Real-World Example: File Storage Adapter
Another practical example is file storage. An application may support local storage, Amazon S3, Google Cloud Storage, or another storage provider. Each provider may have a different API.
The application can define a common interface:
interface FileStorage
{
public function upload(string $path, string $content): bool;
public function delete(string $path): bool;
}A cloud provider may use different methods:
class CloudStorageClient
{
public function putObject(string $key, string $body): bool
{
return true;
}
public function removeObject(string $key): bool
{
return true;
}
}The adapter can translate between the application interface and the cloud provider interface:
class CloudStorageAdapter implements FileStorage
{
public function __construct(
private CloudStorageClient $client
) {
}
public function upload(string $path, string $content): bool
{
return $this->client->putObject($path, $content);
}
public function delete(string $path): bool
{
return $this->client->removeObject($path);
}
}This design allows the application to replace storage providers more easily in the future.
Adapter Pattern with Legacy Code
Legacy code is one of the most common reasons to use the Adapter Pattern. Many projects contain old classes that still work but do not match the structure of the newer application.
Instead of rewriting old code immediately, developers can create adapters around legacy classes. This allows new code to interact with old code through clean interfaces.
This approach is useful during refactoring because it reduces risk. The legacy code remains unchanged, while the adapter provides a modern interface for the rest of the application.
Legacy Code Adapter Example
Suppose an old user system has this class:
class OldUserSystem
{
public function findUserByEmailAddress(string $email): array
{
return [
'full_name' => 'John Doe',
'email_address' => $email,
];
}
}The new application expects this interface:
interface UserProvider
{
public function findByEmail(string $email): UserDto;
}An adapter can connect the old class with the new interface:
class OldUserSystemAdapter implements UserProvider
{
public function __construct(
private OldUserSystem $oldSystem
) {
}
public function findByEmail(string $email): UserDto
{
$data = $this->oldSystem->findUserByEmailAddress($email);
return new UserDto(
name: $data['full_name'],
email: $data['email_address']
);
}
}This allows the new application to use the old system without depending directly on its old data format.
Adapter Pattern and External APIs
External APIs often change or use structures that do not match the application's internal design. Adapter Pattern helps isolate these differences.
For example, one API may return user names as full_name, another may return name, and another may return first_name and last_name separately. Instead of spreading these differences throughout the application, adapters can convert external responses into internal DTOs.
This keeps the core application clean and protects it from external API changes.
Adapter Pattern and DTOs
DTOs, or Data Transfer Objects, work well with Adapter Pattern. An adapter can receive data from an external system and convert it into a DTO used by the application.
This is useful because the application can work with stable internal objects instead of raw external arrays or provider-specific response formats.
For example, a payment adapter may convert an external API response into a PaymentResultDto. A user adapter may convert an old database record into a UserDto. This improves consistency and reduces duplicated mapping logic.
Adapter Pattern in Laravel
In Laravel applications, Adapter Pattern is useful for integrating payment gateways, SMS providers, email providers, file storage services, third-party APIs, and legacy systems.
A Laravel service can depend on an interface such as PaymentGateway or SmsProvider. The service container can bind that interface to a specific adapter implementation.
$this->app->bind(
PaymentGateway::class,
ThirdPartyPaymentAdapter::class
);This allows controllers and services to depend on the interface instead of a specific provider. If the provider changes later, the binding can be updated without changing the business logic.
Adapter Pattern in Symfony
Symfony applications can also use adapters through services and dependency injection. Developers can define interfaces for external systems and create adapter services for each provider.
This is especially useful in large Symfony projects where integrations should be isolated from the domain logic. The domain layer can depend on stable interfaces, while infrastructure adapters handle external details.
This approach supports clean architecture and improves testability.
Adapter Pattern and Dependency Injection
Adapter Pattern works best when combined with dependency injection. Instead of creating adapters manually everywhere, the application can inject the required adapter through an interface.
class NotificationService
{
public function __construct(
private NotificationSender $sender
) {
}
public function notify(string $recipient, string $message): bool
{
return $this->sender->send($recipient, $message);
}
}The NotificationService does not know whether the sender is an email adapter, SMS adapter, push notification adapter, or a fake test adapter. It only depends on the NotificationSender interface.
Adapter Pattern and Clean Architecture
The Adapter Pattern is strongly related to clean architecture. Clean architecture encourages keeping the core business logic independent from external systems such as databases, APIs, frameworks, and third-party services.
Adapters can be placed at the boundaries of the application. They translate between the external world and the internal application model.
For example, a payment adapter translates between an external payment API and the internal PaymentGateway interface. A repository adapter translates between database queries and domain objects. A storage adapter translates between cloud storage APIs and the internal FileStorage interface.
Adapter Pattern vs Facade Pattern
Adapter Pattern and Facade Pattern are both structural design patterns, but they solve different problems.
The Adapter Pattern makes an incompatible interface compatible with what the client expects. It changes the interface of an existing class through a wrapper.
The Facade Pattern provides a simplified interface to a complex subsystem. It does not necessarily adapt an incompatible interface. Instead, it hides complexity behind a simpler API.
In short, Adapter focuses on compatibility, while Facade focuses on simplification.
Adapter Pattern vs Decorator Pattern
Adapter Pattern and Decorator Pattern also use wrapping, but their goals are different.
The Adapter Pattern wraps an object to change its interface. The Decorator Pattern wraps an object to add new behavior without changing its interface.
For example, an adapter may convert makePayment into charge. A decorator may add logging, caching, or validation around an existing charge method while keeping the same interface.
In short, Adapter changes how an object is accessed, while Decorator adds behavior to an object.
Adapter Pattern vs Proxy Pattern
The Proxy Pattern controls access to another object, while the Adapter Pattern converts one interface into another.
A proxy may add lazy loading, access control, caching, or remote communication while keeping the same interface. An adapter usually changes the interface so incompatible classes can work together.
Both patterns can wrap another object, but their purpose is different.
Benefits of Adapter Pattern
The Adapter Pattern provides several important benefits in object-oriented software design.
Main benefits include:
Allows incompatible interfaces to work together.
Protects application code from external API changes.
Improves separation between business logic and integration logic.
Supports interface-based design and dependency injection.
Makes legacy code easier to use in modern systems.
Reduces duplicated conversion and mapping logic.
Improves maintainability when replacing third-party providers.
These benefits make Adapter Pattern very useful in real-world projects that depend on external systems.
Drawbacks of Adapter Pattern
Although Adapter Pattern is useful, it can add extra classes to the project. If the interface mismatch is very small or the integration is used only once, creating a full adapter may be unnecessary.
Another drawback is that too many adapters can make the system harder to navigate if they are not organized clearly. Developers should use meaningful names and place adapters in logical folders such as Infrastructure, Integrations, or Adapters.
Also, an adapter should not hide important errors or remove necessary provider-specific behavior without a clear reason.
When to Use Adapter Pattern
Use Adapter Pattern when an existing class, external library, legacy system, or API does not match the interface your application expects.
Adapter Pattern is useful when:
You need to integrate a third-party service with a different interface.
You want to protect your code from external provider details.
You need to use legacy code in a modern application.
You want multiple providers to follow the same internal interface.
You need to convert external data formats into internal DTOs.
You want to replace a dependency later without changing business logic.
If these conditions exist, Adapter Pattern can make the design cleaner and more flexible.
When Not to Use Adapter Pattern
Do not use Adapter Pattern when there is no real interface mismatch. If the existing class already matches what your application needs, an adapter may only add unnecessary complexity.
Avoid Adapter Pattern when:
The class already has the correct interface.
The integration is extremely simple and unlikely to change.
The adapter only forwards calls without improving design.
The pattern makes the code harder to understand.
A direct dependency is acceptable for a small script or simple feature.
Design patterns should solve real design problems, not create extra structure without value.
Common Mistakes with Adapter Pattern
One common mistake is putting too much business logic inside the adapter. The adapter should mainly translate interfaces, parameters, responses, and formats. Business rules should usually stay in services, domain classes, or use cases.
Another mistake is leaking external provider details into the application. If the adapter returns raw external responses everywhere, the application is still coupled to the provider.
A third mistake is creating one large adapter for many unrelated services. It is usually better to create focused adapters for specific integrations or responsibilities.
A fourth mistake is not handling errors properly. External APIs may fail, and the adapter should convert provider-specific errors into meaningful application-level exceptions or results.
Best Practices for Adapter Pattern
To use Adapter Pattern correctly, developers should keep adapters focused, clear, and consistent.
Useful best practices include:
Define a stable internal interface first.
Make the adapter implement the internal interface.
Keep external API details inside the adapter.
Convert external responses into internal DTOs when needed.
Use dependency injection to provide adapters to services.
Keep adapters focused on translation, not business workflows.
Use clear names such as StripePaymentAdapter or TwilioSmsAdapter.
Handle provider errors consistently.
Organize adapters in a clear project structure.
These practices help keep adapter-based designs clean and maintainable.
Practical Checklist Before Using Adapter Pattern
Before using the Adapter Pattern, developers can ask these questions:
Does an existing class have an interface that does not match my application?
Am I integrating an external API or third-party library?
Do I need to protect business logic from provider-specific details?
Will I need to replace this provider later?
Can I define a stable internal interface?
Will an adapter reduce duplicated conversion logic?
Will the adapter make the code easier to test?
If the answer is yes to several of these questions, Adapter Pattern may be a good design choice.
Conclusion
The Adapter Pattern is a structural design pattern that allows incompatible classes, APIs, libraries, or systems to work together through a common interface. It acts as a bridge between what the application expects and what an existing class provides.
Adapter Pattern is especially useful for third-party integrations, payment gateways, SMS providers, file storage services, legacy systems, external APIs, and clean architecture boundaries. It keeps business logic independent from provider-specific details and makes the application easier to maintain and test.
However, Adapter should be used only when there is a real interface mismatch or integration problem. When applied correctly, the Adapter Pattern is one of the most practical patterns for building flexible, clean, and maintainable object-oriented software.

