Adapter lets your application speak in its own language while wrapping external APIs that were never designed for your internal model. This is one of the most practical patterns in backend engineering.


Problem

Our checkout code wants this contract:

public interface PaymentProcessor {
    PaymentResult charge(PaymentRequest request);
}

But the providers expose incompatible APIs.


UML

classDiagram
    class PaymentProcessor {
      <<interface>>
      +charge(PaymentRequest) PaymentResult
    }
    class StripeSdkClient
    class PayPalSdkClient
    class StripePaymentAdapter
    class PayPalPaymentAdapter
    PaymentProcessor <|.. StripePaymentAdapter
    PaymentProcessor <|.. PayPalPaymentAdapter
    StripePaymentAdapter --> StripeSdkClient
    PayPalPaymentAdapter --> PayPalSdkClient

Implementation Walkthrough

public final class PaymentRequest {
    private final String orderId;
    private final long amountInCents;

    public PaymentRequest(String orderId, long amountInCents) {
        this.orderId = orderId;
        this.amountInCents = amountInCents;
    }

    public String getOrderId() { return orderId; }
    public long getAmountInCents() { return amountInCents; }
}

public final class PaymentResult {
    private final boolean success;
    private final String providerReference;

    private PaymentResult(boolean success, String providerReference) {
        this.success = success;
        this.providerReference = providerReference;
    }

    public static PaymentResult success(String ref) {
        return new PaymentResult(true, ref);
    }
}

public interface PaymentProcessor {
    PaymentResult charge(PaymentRequest request);
}

public final class StripeSdkClient {
    public String createCharge(long cents, String externalId) {
        return "stripe-" + externalId + "-" + cents;
    }
}

public final class StripePaymentAdapter implements PaymentProcessor {
    private final StripeSdkClient stripeSdkClient;

    public StripePaymentAdapter(StripeSdkClient stripeSdkClient) {
        this.stripeSdkClient = stripeSdkClient;
    }

    @Override
    public PaymentResult charge(PaymentRequest request) {
        String ref = stripeSdkClient.createCharge(request.getAmountInCents(), request.getOrderId());
        return PaymentResult.success(ref);
    }
}

Application code remains stable:

public final class CheckoutService {
    private final PaymentProcessor paymentProcessor;

    public CheckoutService(PaymentProcessor paymentProcessor) {
        this.paymentProcessor = paymentProcessor;
    }

    public PaymentResult checkout(String orderId, long amount) {
        return paymentProcessor.charge(new PaymentRequest(orderId, amount));
    }
}

The design benefit is not just easier provider swapping. It is that the checkout flow can now speak in business terms such as PaymentRequest and PaymentResult instead of leaking vendor-specific request models into core application code.


Why Adapter Matters

Without Adapter, third-party SDK details spread across the codebase:

  • request shape
  • exception mapping
  • provider-specific terminology
  • response parsing

With Adapter, those concerns stay at the integration boundary.

That boundary is one of the most valuable habits in backend architecture because it prevents third-party SDK choices from dictating the shape of your internal model.


Practical Advice

An adapter should translate, not invent business logic. If retry policy, idempotency, or fallback logic is added there, document it explicitly because the adapter is now becoming part integration layer and part policy layer.