Software systems rarely remain isolated.
Even applications that start out simple eventually need to communicate with banks, payment gateways, email services, ERPs, shipping providers, and third-party APIs.
The problem begins when each integration has a different contract and those differences start leaking directly into the application's business logic.
This is exactly the kind of situation where the Adapter Pattern becomes useful.
In this article, we will explore how to apply the Adapter Pattern in Java to a real-world scenario involving multiple payment gateways. We will analyze not only the implementation itself, but also the architectural problem that motivates the pattern, its structure, advantages, limitations, and impact on system evolution.
1. Fundamental Concepts
What Are Design Patterns?
Before discussing Adapter specifically, it is important to understand what a Design Pattern actually is.
A Design Pattern is not ready-made code that should simply be copied into any application.
Instead, it represents a reusable solution to a recurring software design problem.
Throughout the development of different systems, certain architectural problems tend to appear repeatedly.
For example:
- How can we create complex objects without tightly coupling the code to concrete classes?
- How can incompatible objects work together?
- How can behavior be changed at runtime?
- How can a class avoid depending directly on several concrete implementations?
- How can multiple components be notified when an event occurs?
Design Patterns provide proven ways to organize solutions to these kinds of problems.
The classic patterns documented by the Gang of Four, or GoF, are usually grouped into three main categories.
Creational Patterns
These patterns deal with object creation.
Examples include:
- Factory Method;
- Abstract Factory;
- Builder;
- Singleton;
- Prototype.
Structural Patterns
These patterns deal with how classes and objects are composed and connected.
Examples include:
- Adapter;
- Facade;
- Decorator;
- Composite;
- Proxy;
- Bridge.
Behavioral Patterns
These patterns focus on communication and responsibility distribution between objects.
Examples include:
- Strategy;
- Observer;
- Command;
- State;
- Template Method.
The pattern discussed in this article belongs to the second category.
What Is the Adapter Pattern?
The Adapter Pattern is a structural design pattern used when two components have incompatible interfaces but still need to work together.
The Adapter acts as a translation layer.
Instead of modifying the entire system to understand an external component, we create an intermediate object that converts the external interface into the format expected by the application.
The basic idea can be represented like this:
Application
|
v
Internal Interface
|
v
Adapter
|
v
External Component
The application does not need to understand how the external component works internally.
It only needs to understand its own contract.
The Adapter is responsible for translating calls, parameters, and responses.
A Simple Analogy
A common analogy is a power outlet adapter.
Imagine buying a laptop in another country.
The charger works perfectly, and the electrical outlet also works perfectly.
The problem is that their physical formats are incompatible.
There are three possible solutions.
The first would be replacing the entire electrical installation.
The second would be modifying the laptop charger.
The third would be placing an adapter between them.
The third option is usually the simplest.
The same idea can be applied to software:
Application
|
v
Adapter
|
v
External API
Neither the application nor the external library necessarily needs to be rewritten.
We simply create a layer capable of translating between them.
What Problem Does the Adapter Pattern Solve?
Consider an e-commerce platform.
Initially, the company works with only one payment gateway called LegacyBank.
The code could look something like this:
public class PaymentService {
private final LegacyBankClient client;
public PaymentService(LegacyBankClient client) {
this.client = client;
}
public void processPayment(String orderId, double value) {
client.executeCharge(orderId, value);
}
}
In a very small system, this may seem completely acceptable.
The problem becomes clearer when we analyze the dependency:
PaymentService
|
v
LegacyBankClient
The business logic is directly tied to an external provider.
If LegacyBankClient changes, PaymentService may also need to change.
If the company stops working with LegacyBank, PaymentService will need to be modified.
If a second provider is introduced, the code may start growing quickly.
For example:
if (provider.equals("LEGACY_BANK")) {
legacyBank.executeCharge(
orderId,
amount.doubleValue()
);
} else if (provider.equals("MODERN_PAY")) {
modernPay.createPayment(
customerId,
amount,
orderId
);
}
Then a third gateway is added:
else if (provider.equals("FAST_PAY")) {
// New integration
}
Then a fourth:
else if (provider.equals("GLOBAL_PAY")) {
// Another integration
}
At this point, PaymentService starts accumulating responsibilities.
It is no longer responsible only for coordinating payments.
It now also understands:
• external contracts;
• parameter formats;
• authentication details;
• response handling;
• return codes;
• provider-specific naming;
• type conversions;
• implementation details of each vendor.
This significantly increases coupling.
The Coupling Problem
Imagine that our service knows all of this:
PaymentService
|
+-- knows how LegacyBank works
|
+-- knows how ModernPay works
|
+-- knows how FastPay works
|
+-- knows how GlobalPay works
This means external changes can directly affect internal business logic. For example, if ModernPay changes its API from:
createPayment(...)
to:
authorizeTransaction(...)
that change could directly affect our business layer.
That is not ideal.
A healthier architecture would look like this:
PaymentService
|
v
PaymentGateway
The application defines the contract it wants to use. External implementation details remain outside the business logic.
Participants in the Adapter Pattern
The classic Adapter Pattern includes several important participants.
Client
The Client is the code that wants to use a particular service.
In our case:
PaymentService
The service wants to process payments
Target
The Target is the interface the Client expects to use.
In our project:
PaymentGateway
It represents the internal contract expected by the application.
Adaptee
The Adaptee is the existing class whose interface is incompatible with the system.
In our case:
LegacyBankClient
ModernPayClient
These classes represent external libraries or APIs.
Adapter
The Adapter is responsible for translating between the Target and the Adaptee.
We will have:
LegacyBankAdapter
ModernPayAdapter
The Adapter receives a call in the format used by our domain and converts it to the format expected by the external provider.
The Complete Flow
The overall behavior can be represented like this:
PaymentService
|
| calls pay()
v
PaymentGateway
|
v
ModernPayAdapter
|
| translates PaymentRequest
v
ModernPayClient
|
| calls the external API
v
ModernPayResponse
|
| translates the response
v
PaymentResult
|
v
PaymentService
Notice that translation happens in both directions.
First:
Internal model -> External model
Then:
External model -> Internal model
This is one of the Adapter's main responsibilities.
2. Development
Real-World Scenario
Imagine an e-commerce platform that processes thousands of orders. During its first few years, all payments were handled by a single provider:
LegacyBank
The initial architecture looked like this:
Customer
|
v
Checkout
|
v
PaymentService
|
v
LegacyBank
As long as there was only one provider, the solution worked. As the company grew, new requirements emerged.
The business started demanding:
• a second payment gateway;
• the ability to compare fees;
• redundancy in case one provider became unavailable;
• support for new payment methods;
• easier provider negotiation;
• the ability to replace a provider in the future.
The company then decided to integrate ModernPay. The problem was that both APIs had completely different contracts.
LegacyBank API
The legacy gateway exposes an operation similar to:
executeCharge(
String reference,
double value
)
Its response is:
LegacyBankResponse
containing:
status
transactionCode
ModernPay API
The new provider uses:
createPayment(
String customerId,
BigDecimal amount,
String orderId
)
And returns:
ModernPayResponse
containing:
successful
paymentId
Both APIs serve the same business purpose:
process a payment.
But their contracts are completely different.
The Naive Solution
One possible solution would be to let PaymentService understand every gateway directly.
PaymentService
|
+-- LegacyBankClient
|
+-- ModernPayClient
However, this creates strong coupling. Every new gateway would require modifications to the same class. In addition, conversion logic would also end up inside it:
PaymentService
+-- convert PaymentRequest to LegacyBank format
+-- convert LegacyBank response
+-- convert PaymentRequest to ModernPay format
+-- convert ModernPay response
+-- execute payment business rules
Different responsibilities are clearly being mixed together.
Architecture Using Adapter
The proposed solution creates an internal contract called:
PaymentGateway
Every payment provider used by the application will be exposed through this interface.
The architecture becomes:
Customer
|
v
Checkout API
|
v
PaymentService
|
v
PaymentGateway
|
+----------------+
| |
v v
LegacyBankAdapter ModernPayAdapter
| |
v v
LegacyBank API ModernPay API
The most important point is:
PaymentServicedoes not know the external providers.
It only knows:
PaymentGateway
Visual Representation 1 — UML Class Diagram
The first diagram required by the assignment represents the structure of the classes.
In this diagram, we can clearly identify:
Target -> PaymentGateway
Client -> PaymentService
Adapters -> LegacyBankAdapter and ModernPayAdapter
Adaptees -> LegacyBankClient and ModernPayClient
The UML diagram should not be merely decorative.
It must accurately reflect the code presented in the article.
Visual Representation 2 — Software Architecture
Now we can view the pattern within a broader software ecosystem.
The two diagrams serve different purposes.The UML diagram explains:
• How are the classes organized?
The architecture diagram explains:
• Where does the Adapter fit within the overall system?
That distinction is important because they represent two different perspectives.
Java Implementation
Now let's implement exactly what was shown in the UML diagram.
1. Creating the Internal Contract
First, we define the interface representing any payment gateway accepted by our application.
public interface PaymentGateway {
PaymentResult pay(PaymentRequest request);
}
This interface has a simple responsibility:
receive a payment request and return its result.
Notice that we are not using any LegacyBank or ModernPay-specific types.
This is intentional.
Our domain should not depend on external provider models.
2. Defining the Payment Request
We can use a record, available in modern versions of Java.
import java.math.BigDecimal;
public record PaymentRequest(
String orderId,
BigDecimal amount,
String customerId
) {
}
Using BigDecimal for monetary values is preferable to floating-point types such as double inside the application's domain.
Our system keeps its preferred internal model.
If a specific external API requires a double, the Adapter is responsible for performing that conversion.
This detail matters.
The domain should not be weakened simply because an external integration uses a particular representation.
3. Standardizing the Result
We also create our own response type:
public record PaymentResult(
boolean approved,
String transactionId,
String message
) {
}
Regardless of which provider is used, the rest of the application always receives a PaymentResult.
This standardizes how responses are consumed.
4. The Legacy Client
Imagine that we have no control over this class.
It could come from a third-party library.
public class LegacyBankClient {
public LegacyBankResponse executeCharge(
String reference,
double value
) {
System.out.println(
"Processing payment through LegacyBank..."
);
return new LegacyBankResponse(
"OK",
"LEGACY-" + System.currentTimeMillis()
);
}
}
Its response also belongs to the external provider:
public record LegacyBankResponse(
String status,
String transactionCode
) {
}
The problem is clear.
Our internal contract provides:
pay(PaymentRequest request)
LegacyBank provides:
executeCharge(
String reference,
double value
)
The interfaces are incompatible.
5. Creating LegacyBankAdapter
Now we create the bridge between both contracts.
public class LegacyBankAdapter
implements PaymentGateway {
private final LegacyBankClient client;
public LegacyBankAdapter(
LegacyBankClient client
) {
this.client = client;
}
@Override
public PaymentResult pay(
PaymentRequest request
) {
LegacyBankResponse response =
client.executeCharge(
request.orderId(),
request.amount().doubleValue()
);
boolean approved =
"OK".equalsIgnoreCase(
response.status()
);
String message = approved
? "Payment approved"
: "Payment declined";
return new PaymentResult(
approved,
response.transactionCode(),
message
);
}
}
This is the core of the pattern.
Notice the two conversions.
First:
request.orderId()
request.amount().doubleValue()
convert our internal object into the parameters expected by LegacyBank.
Then:
response.status()
response.transactionCode()
are converted into:
PaymentResult
As a result, the rest of the system never needs to know that LegacyBank uses "OK" as a status value.
6. Implementing ModernPay
Now consider another provider.
import java.math.BigDecimal;
public class ModernPayClient {
public ModernPayResponse createPayment(
String customerId,
BigDecimal amount,
String orderId
) {
System.out.println(
"Processing payment through ModernPay..."
);
return new ModernPayResponse(
true,
"MODERN-" + System.currentTimeMillis()
);
}
}
Response:
public record ModernPayResponse(
boolean successful,
String paymentId
) {
}
The incompatibility still exists.
But now we do not need to modify PaymentService.
We simply create another Adapter.
7. ModernPayAdapter
public class ModernPayAdapter
implements PaymentGateway {
private final ModernPayClient client;
public ModernPayAdapter(
ModernPayClient client
) {
this.client = client;
}
@Override
public PaymentResult pay(
PaymentRequest request
) {
ModernPayResponse response =
client.createPayment(
request.customerId(),
request.amount(),
request.orderId()
);
String message =
response.successful()
? "Payment approved"
: "Payment declined";
return new PaymentResult(
response.successful(),
response.paymentId(),
message
);
}
}
Again, provider-specific details remain isolated.
ModernPay works with:
successful
paymentId
Our system works with:
approved
transactionId
message
Who translates between them?
ModernPayAdapter
8. Identifying the Providers
We create an enum:
public enum PaymentProvider {
LEGACY_BANK,
MODERN_PAY
}
This avoids directly working with Strings such as:
"LEGACY"
or:
"MODERN"
and reduces the chance of typing errors.
9. Creating PaymentService
Now we can write the main payment service.
import java.util.Map;
public class PaymentService {
private final Map<
PaymentProvider,
PaymentGateway
> gateways;
public PaymentService(
Map<PaymentProvider, PaymentGateway> gateways
) {
this.gateways = gateways;
}
public PaymentResult process(
PaymentProvider provider,
PaymentRequest request
) {
PaymentGateway gateway =
gateways.get(provider);
if (gateway == null) {
throw new IllegalArgumentException(
"Gateway not configured: "
+ provider
);
}
return gateway.pay(request);
}
}
This code demonstrates an important architectural change.
The service does not contain:
LegacyBankClient
or:
ModernPayClient
It contains:
PaymentGateway
This means that PaymentService depends on an abstraction.
10. Configuring the Application
Now we create the objects.
import java.math.BigDecimal;
import java.util.Map;
public class Main {
public static void main(String[] args) {
PaymentGateway legacyGateway =
new LegacyBankAdapter(
new LegacyBankClient()
);
PaymentGateway modernGateway =
new ModernPayAdapter(
new ModernPayClient()
);
Map<PaymentProvider, PaymentGateway>
gateways = Map.of(
PaymentProvider.LEGACY_BANK,
legacyGateway,
PaymentProvider.MODERN_PAY,
modernGateway
);
PaymentService paymentService =
new PaymentService(gateways);
PaymentRequest request =
new PaymentRequest(
"ORDER-1001",
new BigDecimal("299.90"),
"CUSTOMER-500"
);
PaymentResult result =
paymentService.process(
PaymentProvider.MODERN_PAY,
request
);
System.out.println(
"Payment approved: "
+ result.approved()
);
System.out.println(
"Transaction: "
+ result.transactionId()
);
System.out.println(
"Message: "
+ result.message()
);
}
}
What Happens During Execution?
Suppose we select:
PaymentProvider.MODERN_PAY
The execution flow will be:
Main
|
v
PaymentService.process()
|
v
Map looks up MODERN_PAY
|
v
ModernPayAdapter
|
v
ModernPayClient
|
v
ModernPay API
|
v
ModernPayResponse
|
v
ModernPayAdapter converts it
|
v
PaymentResult
|
v
PaymentService
If we select:
PaymentProvider.LEGACY_BANK
the flow only changes after the provider selection:
PaymentService
|
v
LegacyBankAdapter
|
v
LegacyBankClient
The core business logic remains unchanged.
Adding a Third Gateway
Imagine that the company decides to integrate another provider:
FastPay
Without Adapter, we might need to modify PaymentService.
With the current architecture, we can simply create:
public class FastPayAdapter
implements PaymentGateway {
private final FastPayClient client;
public FastPayAdapter(
FastPayClient client
) {
this.client = client;
}
@Override
public PaymentResult pay(
PaymentRequest request
) {
// Conversion for FastPay
return new PaymentResult(
true,
"FAST-123",
"Payment approved"
);
}
}
Then we add:
FAST_PAY
to the enum and configure the implementation.
The internal logic of PaymentService remains almost unchanged.
Relationship With SOLID Principles
Although Adapter is a Design Pattern and SOLID is a separate set of principles, there is an interesting relationship between them.
Dependency Inversion Principle
One of the biggest improvements in this architecture can be seen here:
PaymentService
|
v
PaymentGateway
instead of:
PaymentService
|
v
ModernPayClient
or:
PaymentService
|
v
LegacyBankClient
The business layer now depends on an abstraction.
Single Responsibility Principle
Before:
PaymentService
+-- payment business logic
+-- LegacyBank conversion
+-- ModernPay conversion
+-- external API handling
After:
PaymentService
+-- coordinates payment processing
LegacyBankAdapter
+-- handles LegacyBank integration
ModernPayAdapter
+-- handles ModernPay integration
Responsibilities are more clearly separated.
Open/Closed Principle
Ideally, we want the system to remain open for extension while avoiding unnecessary modifications to existing code.
Adding another gateway mainly means creating another implementation of:
PaymentGateway
instead of inserting more provider-specific details into the core payment logic.
Testability
Another important benefit is the ability to test PaymentService without calling real payment gateways.
We can create a fake gateway:
public class FakePaymentGateway
implements PaymentGateway {
@Override
public PaymentResult pay(
PaymentRequest request
) {
return new PaymentResult(
true,
"TEST-123",
"Test payment approved"
);
}
}
In a test:
import java.math.BigDecimal;
import java.util.Map;
public class PaymentServiceTest {
public static void main(String[] args) {
PaymentGateway fakeGateway =
new FakePaymentGateway();
PaymentService service =
new PaymentService(
Map.of(
PaymentProvider.MODERN_PAY,
fakeGateway
)
);
PaymentResult result =
service.process(
PaymentProvider.MODERN_PAY,
new PaymentRequest(
"ORDER-1",
new BigDecimal("100.00"),
"CUSTOMER-1"
)
);
assert result.approved();
}
}
This means our test does not depend on:
- internet access;
- real APIs;
- credentials;
- gateway availability;
- actual financial transactions.
The abstraction makes automated testing significantly easier.
Before and After
We can summarize the architectural change.
◘ Before
PaymentService
|
+-- LegacyBankClient
|
+-- ModernPayClient
|
+-- LegacyBank conversion
|
+-- ModernPay conversion
|
+-- provider-specific integration rules
◘ After
PaymentService
|
v
PaymentGateway
|
+-- LegacyBankAdapter
| |
| v
| LegacyBankClient
|
+-- ModernPayAdapter
|
v
ModernPayClient
The total number of classes may increase. However, the responsibilities become clearer and better organized.That leads to an important point.
Design Patterns Do Not Necessarily Reduce the Amount of Code
There is a common misconception that applying a Design Pattern will always result in less code.
That is not necessarily true.
In this example, we could place everything in a single class using a few if statements.
We would probably have fewer files.
But code size is not the only relevant metric.
We also need to consider:
• maintainability;
• readability;
• evolution;
• testability;
• coupling;
• impact of changes;
• separation of responsibilities.
In small projects, creating several adapters may be unnecessary.In systems with multiple integrations and constant evolution, this separation becomes much more valuable.
Advantages of the Adapter Pattern
1. Reduced Coupling
The main business logic no longer depends directly on external APIs. This prevents third-party details from spreading throughout the system.
2. Change Isolation
Imagine that ModernPay changes:
paymentId
to:
transactionReference
Most of the impact can remain restricted to:
ModernPayAdapter
The domain does not necessarily need to change.
3. Legacy System Integration
We do not always have the ability to modify older systems. An Adapter allows us to use a legacy system without changing its original implementation.
4. Easier Provider Replacement
If LegacyBank is no longer used, its Adapter can be removed without rewriting the entire payment flow.
5. Better Testability
Real implementations can be replaced with fakes, mocks, or stubs.
6. Domain Protection
External models do not need to spread across the rest of the application.
Instead of using:
ModernPayResponse
throughout the system, we immediately convert it into:
PaymentResult
Disadvantages and Trade-Offs
Every Design Pattern has a cost.
Using Adapter also introduces additional complexity.
1. More Classes
For each integration, we may end up with:
Client
Adapter
DTOs
Mapper
Configuration
In a small project, this may feel excessive.
2. More Levels of Indirection
◘ Before:
PaymentService
|
v
API
◘ After:
PaymentService
|
v
PaymentGateway
|
v
Adapter
|
v
Client
|
v
API
The architecture becomes more flexible, but also introduces more layers.
3. Adapters Still Require Maintenance
Adapter does not eliminate external changes.
If ModernPay changes its API, we still need to update code.
The difference is that we try to contain that change within the Adapter.
4. Overly Generic Interfaces Can Become Problematic
Imagine that LegacyBank supports:
refunds
installments
Pix
bank slips
credit cards
while ModernPay supports only:
credit cards
Creating one huge interface that attempts to represent every feature from every provider may create another architectural problem.
The abstraction must represent meaningful concepts within the domain.
5. Adapter Does Not Solve Availability Problems
It is important not to assign responsibilities to the pattern that it does not have.
Adapter does not automatically solve:
• timeouts;
• service outages;
• circuit breaking;
• retries;
• load balancing;
• high availability;
• network failures;
• duplicate requests;
• idempotency.
These concerns require additional architectural mechanisms.
Adapter Is Not Strategy
Adapter and Strategy can appear together, but they solve different problems.
Adapter
The question is:
How can components with different interfaces work together?
Example:
PaymentGateway
|
v
ModernPayAdapter
Strategy
The question is:
How can I select between different algorithms or behaviors?
For example:
PaymentStrategy
+-- CheapestGatewayStrategy
+-- FastestGatewayStrategy
+-- FallbackGatewayStrategy
A more advanced architecture could use both.
Strategy could decide:
• Which gateway should be used?
Adapter would handle:
• How do we communicate with the selected gateway?
This distinction matters because Design Patterns are not necessarily competitors.
They can be combined.
Adapter Is Not Facade Either
Facade has a different purpose.Its goal is to provide a simplified interface to a complex subsystem.
Adapter has another primary concern:
• compatibility between interfaces.
An Adapter may simplify an integration as a side effect, but its main intention is to adapt incompatible contracts.
When Should You Use Adapter?
Adapter is particularly useful when:
• an external library does not match the application's expected contract;
• multiple external APIs need to be integrated;
• legacy systems need to be incorporated;
• the domain should be protected from third-party models;
• different providers perform similar functions through different interfaces;
• an existing class cannot be modified directly.
When Might Adapter Be Unnecessary?
Design Patterns should not be applied simply because they exist.
Adapter may be overengineering when:
• there is only one extremely simple integration;
• the external interface already matches the application's needs;
• there is no realistic expectation of evolution;
• the extra layer does not provide meaningful maintainability benefits.
The context should justify the abstraction.
A Broader Architectural View
In a real-world application, Adapter would only be one part of the overall architecture.
We could have:
+---------------+
| Front-end |
+-------+-------+
|
v
+---------------+
| Checkout API |
+-------+-------+
|
v
+---------------+
| Order Service |
+-------+-------+
|
v
+---------------+
|PaymentService |
+-------+-------+
|
v
+---------------+
|PaymentGateway |
+-------+-------+
|
+------------+-------------+
| |
v v
+-----------------+ +-----------------+
|LegacyBankAdapter| |ModernPayAdapter |
+--------+--------+ +--------+--------+
| |
v v
+-----------------+ +-----------------+
| LegacyBank API | | ModernPay API |
+-----------------+ +-----------------+
Around these integrations, we could also add:
Retry
Timeout
Circuit Breaker
Logging
Metrics
Tracing
Authentication
This demonstrates an important architectural lesson:
The Adapter solves a specific problem.
It is not the entire architecture.
It is one component within it.
3. Conclusion
In this article, we analyzed the Adapter Pattern in a scenario involving a Java application integrating multiple payment gateways.
The original problem was not simply that two APIs were different.
The real issue was the coupling that could emerge if the business layer started depending directly on each provider's specific contract.
The solution was to introduce an abstraction:
PaymentGateway
and create adapters responsible for translating each external API into that common contract.
We moved from an architecture like this:
PaymentService
|
v
Specific providers
to:
PaymentService
|
v
PaymentGateway
|
v
Adapters
|
v
External APIs
The main benefit of the pattern is not reducing the number of lines of code.
Its real value is controlling dependencies.
By isolating external integrations, we can improve important system characteristics such as:
• maintainability;
• testability;
• readability;
• extensibility;
• change isolation;
• separation of responsibilities.
At the same time, there is a cost.
We introduce more classes, more interfaces, and more levels of indirection.
For that reason, just like any other Design Pattern, Adapter should not be applied automatically.
It should be used when the architectural problem genuinely justifies it.
My Perspective on the Pattern
After studying Adapter in more depth, I realized that the implementation itself is not particularly complex. Creating a class that implements an interface and delegates calls to another class is relatively straightforward. The most interesting part is the architectural decsion behind it.
The real value appears when we understand that we are creating a boundary between:
our system
and:
an external system
That boundary prevents external implementation details from spreading throughout the application.It also made it clearer to me that Design Patterns are not mandatory recipes.The most important step is identifying the problem first. Only then should we evaluate whether a particular pattern is the right solution.
In this payment gateway scenario, Adapter makes sense because we have different providers performing essentially the same business function while exposing incompatible contracts.
References
• Gamma, Erich; Helm, Richard; Johnson, Ralph; Vlissides, John.
Design Patterns: Elements of Reusable Object-Oriented Software.
Addison-Wesley, 1994.
• Freeman, Eric; Robson, Elisabeth.
Head First Design Patterns.
O'Reilly Media.
• Oracle Java Documentation.
• Refactoring.Guru — Adapter Design Pattern.
What About You?
If you were building a platform that needed to integrate five or six different payment gateways, how would you structure those integrations? Would you use the Adapter Pattern to create a common interface, or would you choose a different approach?


Top comments (0)