Your application has a clean PaymentGateway interface used everywhere. Now you need to integrate Stripe, whose SDK uses cents instead of dollars, different method names, and its own result objects — none of which match your interface. You can’t modify Stripe’s SDK, and you don’t want to scatter Stripe-specific code throughout your application. The Adapter pattern is the precise solution: write a thin wrapper that translates one interface into the other, and the rest of your code never knows Stripe exists.
This guide builds the pattern from first principles. We’ll look at why incompatible interfaces are inevitable in real projects, implement a full Object Adapter (the variant you’ll use 95% of the time), compare it to the Class Adapter form, and then map the pattern onto real JDK examples you already use every day.
All code compiles and runs with Java 17. No external dependencies required.
The Problem: Interfaces That Don’t Match
In a medium-sized application you will regularly encounter three sources of interface mismatch: third-party libraries (Stripe, AWS SDK, Twilio), legacy internal code written before your current standards existed, and domain models from different bounded contexts that use different vocabulary for the same concept. You can’t change any of them safely. Your choices are: litter your business logic with conversion code, fork and modify the dependency (a maintenance nightmare), or write a dedicated translator — the Adapter.
The Adapter pattern has a clean definition from Gamma et al.: convert the interface of a class into another interface that clients expect. The client knows only the interface it wants; the adapter satisfies that interface by delegating to the class it’s wrapping. Neither side is changed.
Structure: The Four Participants
Before writing any code, it’s worth fixing the vocabulary because the GoF names are used everywhere in literature and code reviews.

- Target — the interface your client code depends on. In our example:
PaymentGateway. - Adaptee — the existing class with the incompatible interface. In our example:
StripeClient(a third-party SDK). - Adapter — the class that implements Target and wraps Adaptee, translating calls between the two. In our example:
StripePaymentAdapter. - Client — uses only the Target interface, completely unaware of the Adaptee. In our example:
OrderService.
Step 1 — Define the Target Interface
This is the contract your application owns. It should be designed around your domain language, not around any vendor’s API. Notice it uses decimal dollars and simple string status values — the way your domain thinks about payments.
package adapter;
/**
* The Target interface — what YOUR application's code expects.
* Every payment gateway in your system must implement this.
*/
public interface PaymentGateway {
boolean charge(String customerId, double amount, String currency);
boolean refund(String transactionId, double amount);
String getStatus(String transactionId);
}
This interface is stable. Every part of your application — the order service, the subscription engine, the refund controller — depends only on PaymentGateway. Switching payment providers means writing a new adapter, not touching any of those callers.
Step 2 — The Adaptee (What You’re Wrapping)
This is Stripe’s SDK. You cannot modify it. Notice the key differences from your interface: it works in cents (not dollars), uses createCharge instead of charge, issueRefund instead of refund, and returns its own result object rather than a plain boolean.
package adapter;
/**
* The Adaptee — a third-party payment SDK with a completely different interface.
* Imagine this is Stripe's actual SDK: you cannot modify this class,
* and it doesn't implement PaymentGateway.
*/
public class StripeClient {
private final String apiKey;
public StripeClient(String apiKey) {
this.apiKey = apiKey;
System.out.println("[Stripe] Initialized with key: " + apiKey.substring(0, 8) + "...");
}
// Stripe uses cents, not decimal amounts
public StripeChargeResult createCharge(String customerId, long amountInCents, String currency) {
System.out.printf("[Stripe] Charging customer=%s, amount=%d cents, currency=%s%n",
customerId, amountInCents, currency);
return new StripeChargeResult("ch_" + System.currentTimeMillis(), true, null);
}
// Stripe's refund method takes a charge ID and uses different naming
public boolean issueRefund(String chargeId, long amountInCents) {
System.out.printf("[Stripe] Refunding charge=%s, amount=%d cents%n", chargeId, amountInCents);
return true;
}
// Stripe uses 'retrieve' not 'getStatus', and returns an object
public StripeChargeResult retrieveCharge(String chargeId) {
System.out.printf("[Stripe] Retrieving charge=%s%n", chargeId);
return new StripeChargeResult(chargeId, true, "succeeded");
}
public static class StripeChargeResult {
public final String chargeId;
public final boolean success;
public final String status;
public StripeChargeResult(String chargeId, boolean success, String status) {
this.chargeId = chargeId;
this.success = success;
this.status = status;
}
}
}
Three incompatibilities are happening at once: naming (createCharge vs charge), data representation (cents vs dollars), and return types (StripeChargeResult vs boolean/String). The adapter must handle all three translations without leaking any Stripe concept to the caller.
Step 3 — The Object Adapter
The adapter implements PaymentGateway (the Target) and wraps a StripeClient (the Adaptee) via composition. This is called the Object Adapter because it holds an object of the Adaptee rather than subclassing it. Each method translates the incoming call into whatever Stripe needs.
package adapter;
/**
* The Adapter — bridges StripeClient to PaymentGateway.
*
* Object Adapter variant: holds a StripeClient instance via composition.
* This means it can wrap any StripeClient including subclasses.
*/
public class StripePaymentAdapter implements PaymentGateway {
private final StripeClient stripe;
public StripePaymentAdapter(StripeClient stripe) {
this.stripe = stripe;
}
@Override
public boolean charge(String customerId, double amount, String currency) {
// Translation 1: dollars → cents
long amountInCents = Math.round(amount * 100);
// Translation 2: charge() → createCharge(), lowercase currency
StripeClient.StripeChargeResult result =
stripe.createCharge(customerId, amountInCents, currency.toLowerCase());
// Translation 3: StripeChargeResult → boolean
return result.success;
}
@Override
public boolean refund(String transactionId, double amount) {
long amountInCents = Math.round(amount * 100);
// Translation: refund() → issueRefund(), your "transactionId" is Stripe's "chargeId"
return stripe.issueRefund(transactionId, amountInCents);
}
@Override
public String getStatus(String transactionId) {
// Translation: getStatus() → retrieveCharge(), StripeChargeResult → String
StripeClient.StripeChargeResult result = stripe.retrieveCharge(transactionId);
if (!result.success) return "FAILED";
return result.status != null ? result.status.toUpperCase() : "UNKNOWN";
}
}
Every translation happens in one place. When Stripe changes something — say, they rename a method or change the status values — you fix it in the adapter and nowhere else. All the callers continue working unchanged.
💡 Composition over Inheritance: The Object Adapter holds a reference to the Adaptee rather than extending it. This is almost always the right choice in Java because it works even when the Adaptee class is final, and it lets you swap out or decorate the underlying instance. It also avoids the fragile base-class problem that can arise with inheritance-based adapters.
Step 4 — The Client (Knows Nothing About Stripe)
The OrderService receives a PaymentGateway via constructor injection. It calls charge() and getStatus() without any idea that Stripe is on the other side. You could swap the adapter for a PayPal adapter tomorrow and this class would not change by a single character.
package adapter;
/**
* The Client — uses only the PaymentGateway interface.
* It has no idea whether it's talking to Stripe, PayPal, or Braintree.
*/
public class OrderService {
private final PaymentGateway gateway;
public OrderService(PaymentGateway gateway) {
this.gateway = gateway;
}
public void processOrder(String orderId, String customerId, double total) {
System.out.printf("%nProcessing order %s for customer %s, total: $%.2f%n",
orderId, customerId, total);
boolean charged = gateway.charge(customerId, total, "USD");
if (charged) {
System.out.println("Payment accepted. Order confirmed.");
String status = gateway.getStatus("ch_" + orderId);
System.out.println("Transaction status: " + status);
} else {
System.out.println("Payment failed. Order rejected.");
}
}
}
Putting It Together: The Main Demo
package adapter;
public class Main {
public static void main(String[] args) {
System.out.println("=== Adapter Design Pattern Demo ===\n");
// Construct the adapter: wrap StripeClient behind PaymentGateway
StripeClient stripeClient = new StripeClient("sk_test_4eC39HqLyjWDarjtT1zdp7dc");
PaymentGateway stripeGateway = new StripePaymentAdapter(stripeClient);
// Client code sees only PaymentGateway — no Stripe anywhere
OrderService orderService = new OrderService(stripeGateway);
orderService.processOrder("ORD-001", "cus_abc123", 99.99);
System.out.println("\n-- Testing refund --");
boolean refunded = stripeGateway.refund("ch_ORD-001", 99.99);
System.out.println("Refund issued: " + refunded);
// JDK example: InputStreamReader is an Adapter
System.out.println("\n-- JDK Adapter: InputStreamReader --");
System.out.println("InputStreamReader wraps System.in (InputStream) as a Reader.");
System.out.println("Your code reads chars; the adapter handles byte-to-char conversion.");
System.out.println("\n=== Demo complete ===");
}
}
Console Output
Compiled and run with Java 17 (Eclipse Temurin). The key thing to observe: OrderService.processOrder() works in dollars and domain vocabulary; the Stripe-specific translation (cents, lower-case currency, createCharge) is invisible to it.
— Using Stripe via Adapter —
[Stripe] Initialized with key: sk_test_…
Processing order ORD-001 for customer cus_abc123, total: $99.99
[Stripe] Charging customer=cus_abc123, amount=9999 cents, currency=usd
Payment accepted. Order confirmed.
[Stripe] Retrieving charge=ch_ORD-001
Transaction status: SUCCEEDED
— Testing refund —
[Stripe] Refunding charge=ch_ORD-001, amount=9999 cents
Refund issued: true
— JDK Adapter: InputStreamReader —
InputStreamReader wraps System.in (InputStream) as a Reader.
Your code reads chars; the adapter handles byte-to-char conversion.
=== Demo complete ===
Object Adapter vs Class Adapter
The implementation above is an Object Adapter — it wraps the Adaptee via composition. There is also a Class Adapter form that uses multiple inheritance to directly inherit from both the Target and the Adaptee. Java does not support multiple class inheritance, so the Class Adapter in Java can only be used when the Target is an interface (which it almost always is), and it extends the Adaptee class while implementing the Target interface.
// Class Adapter: extends Adaptee, implements Target
// Can only be used when you CAN extend the Adaptee (it's not final)
public class StripePaymentClassAdapter extends StripeClient implements PaymentGateway {
public StripePaymentClassAdapter(String apiKey) {
super(apiKey);
}
@Override
public boolean charge(String customerId, double amount, String currency) {
long cents = Math.round(amount * 100);
return createCharge(customerId, cents, currency.toLowerCase()).success;
// Note: calls inherited method directly — no 'stripe.' prefix needed
}
@Override
public boolean refund(String transactionId, double amount) {
return issueRefund(transactionId, Math.round(amount * 100));
}
@Override
public String getStatus(String transactionId) {
StripeChargeResult r = retrieveCharge(transactionId);
return r.success ? (r.status != null ? r.status.toUpperCase() : "UNKNOWN") : "FAILED";
}
}
The Class Adapter is simpler to write but has real limitations: it cannot wrap a final class, it cannot wrap a subclass of StripeClient (it hardcodes the concrete type), and it exposes all public methods of StripeClient to the client (polluting the API). Prefer the Object Adapter in nearly every case.
⚠️ Class Adapter Pitfall: When you extend StripeClient, every Stripe-specific method becomes visible on your adapter object. Code that holds a reference typed as the adapter (not as PaymentGateway) can accidentally call createCharge() directly, bypassing your translation layer. The Object Adapter prevents this entirely because the StripeClient instance is a private final field.
Adapter in the JDK: You’ve Already Used This Pattern
The JDK uses the Adapter pattern pervasively. Once you see the structure, you’ll recognize it everywhere.
InputStreamReader — the most commonly cited JDK adapter. BufferedReader (the client) works with the Reader interface (the Target). InputStream (the Adaptee) provides byte-based I/O. InputStreamReader is the Adapter that wraps an InputStream and translates byte reads into character reads, handling charset decoding in the process.
// InputStreamReader adapts InputStream (Adaptee) to Reader (Target)
// BufferedReader is the client — it only knows about Reader
BufferedReader reader = new BufferedReader(
new InputStreamReader(System.in, StandardCharsets.UTF_8)
);
// Same pattern as our payment adapter: client uses Target, adapter translates to Adaptee
Arrays.asList() — adapts a plain array (Adaptee) to the List interface (Target). The returned list is a fixed-size view backed by the array, not a copy — it’s a classic adapter wrapping something with a different API behind the interface everyone expects.
Collections.enumeration() — adapts a Collection to the old Enumeration interface, for compatibility with legacy code that predates the Collections framework. Pure adapter: a modern object behind an old interface.
Spring’s HandlerAdapter — the Spring MVC dispatcher servlet doesn’t know whether a controller is a @Controller, a Servlet, or an HttpRequestHandler. Each controller type gets a dedicated HandlerAdapter that translates the dispatcher’s single handle() call into whatever that controller type actually needs. The dispatcher is the client; HandlerAdapter is the adapter; the controller implementations are the adaptees.
When to Use the Adapter Pattern
Reach for Adapter when: you need to integrate a third-party library that has an incompatible interface with your domain model. You’re working with legacy code that was written against a different API standard. You want to insulate your application from a vendor so you can swap providers without changing application code. You’re connecting two subsystems that were developed independently and happen to model the same concept differently.
Avoid it when: the interfaces are only slightly different and you could redesign the Target interface to be a better fit for both sides (sometimes adapting is a symptom of premature interface design). You find yourself writing adapters-of-adapters — chaining two translators together is a sign the fundamental design needs attention. The Adaptee changes frequently and the translation layer becomes expensive to maintain; at some point a redesign is cheaper than accumulating adapter complexity.
✅ Best Practice: Define your Target interface in your domain language first, before looking at any third-party SDK. Write your application against that interface. Then, when you bring in an external dependency, you write the adapter to map from the outside world into your domain vocabulary — not the other way around. This keeps your domain model clean and every adapter thin.
Adapter vs Decorator vs Facade
These three structural patterns look similar because they all wrap something — but they have distinct purposes. The Adapter changes the interface a class presents; it’s a translation layer. The Decorator keeps the same interface but adds behavior on top — a logging decorator still implements PaymentGateway but records each call before delegating. The Facade provides a simpler interface over a subsystem of multiple classes, reducing complexity rather than bridging incompatibility. If you’re changing interfaces, it’s Adapter. If you’re adding behavior without changing the interface, it’s Decorator. If you’re hiding a cluster of classes behind a single entry point, it’s Facade.
Runnable Code on GitHub
The complete, runnable code for this article is at ankurm.com/git.app/asmhatre/design-patterns under 02-structural/adapter/. Clone the repo, navigate to that folder, and run:
javac adapter/*.java -d out/adapter
java -cp out/adapter adapter.Main
See Also
- Bridge Design Pattern in Java — separates abstraction from implementation so both can vary independently; often confused with Adapter
- Decorator Design Pattern in Java — adds behavior without changing the interface; the complement to Adapter
- Facade Design Pattern in Java — simplifies a complex subsystem behind a single interface
Further Reading
- Adapter — Refactoring.Guru
- The Adapter Pattern in Java — Baeldung
- Design Patterns: Elements of Reusable Object-Oriented Software — Gamma, Helm, Johnson, Vlissides, Chapter 4
- Head First Design Patterns — Freeman & Robson, Chapter 7