Technology Sep 13, 2026 · 20 min read

Adapter Pattern in Java: How to Integrate Payment Gateways Without Tight Coupling”

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 star...

DE
DEV Community
by Lucas Carlos
Adapter Pattern in Java: How to Integrate Payment Gateways Without Tight Coupling”

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:

PaymentService does 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?

DE
Source

This article was originally published by DEV Community and written by Lucas Carlos.

Read original article on DEV Community
Back to Discover

Reading List