Adapter Pattern
Make incompatible interfaces work together. Object adapter vs class adapter, and the legacy-integration use case.
What it is#
The Adapter pattern converts the interface of one class into another interface clients expect. It lets classes work together that otherwise could not, because their interfaces are incompatible. The Gang of Four also calls it Wrapper — and the everyday metaphor is the travel power adapter that lets a UK plug fit a US socket without rewiring either end.
The pattern has three roles. The Target is the interface the client already calls. The Adaptee is the existing, incompatible class — usually a legacy library, a vendor SDK, or a third-party API we cannot modify. The Adapter is the thin class in the middle that implements the Target interface by delegating to (or extending) the Adaptee, translating method names, argument shapes, and error conventions along the way.
The pattern is about interface translation, not behaviour change. An Adapter should not add new business logic; if it does, you have probably reached for the wrong pattern (look at Facade or Decorator instead).
Class structure#
┌────────────────────┐ Client ────────► │ Target │ ├────────────────────┤ │ request() │ └─────────▲──────────┘ │ │ implements │ ┌─────────┴──────────┐ ┌──────────────────┐ │ Adapter │◇───────►│ Adaptee │ ├────────────────────┤ ├──────────────────┤ │ request() │ │ specificRequest()│ │ { adaptee │ └──────────────────┘ │ .specific...() │ │ } │ └────────────────────┘The diamond is composition — the object adapter holds an Adaptee reference. A class adapter would replace the composition arrow with an inheritance arrow up to Adaptee, but Java’s single inheritance makes that variant the less common of the two.
When to use it#
Reach for Adapter when all of these line up:
- You have an existing client that expects some Target interface.
- You have an existing class with the right behaviour but the wrong shape.
- You cannot — or do not want to — change either side.
- The mismatch is interface-level: method names, argument order, return shape, checked-vs-unchecked exceptions. Not “the behaviour is fundamentally different.”
Typical scenarios:
- Legacy integration. A new code path needs to call a 2008 SOAP client that throws checked exceptions and returns DOM
Nodetrees; the rest of the app speaksOptional<T>and unchecked failures. - Vendor SDK switch. You ship a
PaymentGatewayinterface; today it adapts Stripe, tomorrow a second adapter handles Adyen, with no change to call sites. - Standards bridging. A
Readerover abyte[]source, or aListview over a primitive array — converting one shape to another that downstream APIs already understand. - Test seams. Wrap a static, hard-to-mock library in an interface you control, so tests can substitute a fake.
When not to use it:
- When you control both sides. Just align the interfaces.
- When the wrapper would need to add features, not just translate. That is Decorator.
- When the wrapper would front many classes behind one simplified API. That is Facade.
How it works#
The classic interview example: a MediaPlayer that the application already understands, and two third-party classes (Mp3Player, Mp4Player) with their own incompatible signatures. We adapt them so the application sees a single uniform interface.
// --- Target: what the client (application) expects ---public interface MediaPlayer { void play(String filename);}
// --- Adaptees: existing, incompatible classes ---public final class Mp3Player { public void playMp3(String path) { System.out.println("mp3 stream from " + path); }}
public final class Mp4Player { public void streamMp4(String url, int bitrateKbps) { System.out.println("mp4 @ " + bitrateKbps + "kbps from " + url); }}
// --- Object adapter: composition, the common Java form ---public final class Mp3Adapter implements MediaPlayer { private final Mp3Player adaptee; public Mp3Adapter(Mp3Player adaptee) { this.adaptee = adaptee; }
@Override public void play(String filename) { if (!filename.endsWith(".mp3")) { throw new IllegalArgumentException("Mp3Adapter cannot play " + filename); } adaptee.playMp3(filename); }}
public final class Mp4Adapter implements MediaPlayer { private static final int DEFAULT_BITRATE = 1500; private final Mp4Player adaptee; public Mp4Adapter(Mp4Player adaptee) { this.adaptee = adaptee; }
@Override public void play(String filename) { if (!filename.endsWith(".mp4")) { throw new IllegalArgumentException("Mp4Adapter cannot play " + filename); } adaptee.streamMp4(filename, DEFAULT_BITRATE); }}
// --- Client code: depends only on the Target ---public final class Jukebox { private final MediaPlayer player; public Jukebox(MediaPlayer player) { this.player = player; } public void queue(String filename) { player.play(filename); }}
// Wiring:// MediaPlayer mp3 = new Mp3Adapter(new Mp3Player());// MediaPlayer mp4 = new Mp4Adapter(new Mp4Player());// new Jukebox(mp3).queue("song.mp3");// new Jukebox(mp4).queue("trailer.mp4");A few non-obvious details earn their keep:
- The adapter validates inputs at the boundary.
Mp3Adapterrejects non-mp3 filenames up front rather than passing junk into the adaptee — adapters are a natural place to enforce the contract of the Target interface. - Sensible defaults stay in the adapter.
Mp4Player.streamMp4needs a bitrate; the Target does not expose one. The adapter picks a default. If the choice ever needs to be configurable, you add it as a constructor parameter on the adapter, not the Target. - The Adaptee is held by reference, not constructed. That keeps the adapter testable and lets the Adaptee be a mock, a stub, or a shared singleton.
- No business logic. The adapter only translates. The day it starts deciding whether to play, the design has drifted toward Facade or Decorator.
Variants#
| Variant | Mechanism | When it fits |
|---|---|---|
| Object adapter | Adapter holds an Adaptee reference and delegates. | The default in Java. Works with final adaptees, can adapt subclasses too. |
| Class adapter | Adapter inherits from both Target (interface) and Adaptee (class). | Rare in Java because of single-inheritance — only works when Target is an interface and Adaptee is a class that is not already extended. |
| Two-way adapter | Adapter implements both Target and Adaptee interfaces, so it can stand in for either side. | Bridging two coexisting subsystems that each expect their own interface. |
| Default / pluggable adapter | The adapter is a no-op base class; clients override only the methods they care about. | Wide interfaces with many optional methods (Swing’s MouseAdapter is the canonical case). |
The object adapter is the Java default because the JDK is single-inheritance: a class adapter cannot extend Adaptee if the adapter already extends something else, and most adaptees worth adapting are concrete classes you do not want to subclass.
Object adapter
Composition. Adapter has-a Adaptee.
Works even when Adaptee is final.
Can adapt a whole subclass family with one adapter.
Slightly more boilerplate (delegate methods).
Class adapter
Inheritance. Adapter is-a Adaptee.
Cannot adapt final classes, cannot mix multiple adaptees.
Locked to one concrete adaptee class.
Less boilerplate — inherited methods are already there.
Example systems#
The JDK is full of adapters in plain sight:
Arrays.asList(T[])— adapts aT[]so it satisfies theList<T>interface. The returned list is a view, not a copy.Collections.list(Enumeration<T>)— adapts the legacyEnumeration(Java 1.0) into the modernListAPI.InputStreamReader— adapts a byte-orientedInputStreaminto the character-orientedReaderinterface, with charset translation as the only added behaviour.java.io.OutputStreamWriter— the symmetric counterpart for writing.- Swing’s
MouseAdapter— the default / pluggable variant; it implementsMouseListenerwith empty methods so subclasses override only the events they care about.
Beyond the JDK, the pattern is the workhorse of integration code:
- Payment-gateway abstractions. A house
PaymentGatewayinterface, thenStripeAdapter,AdyenAdapter,BraintreeAdaptertranslating to each vendor SDK. Switching vendors becomes a wiring change, not a rewrite. - Logging facades. SLF4J’s strength is that it adapts to whichever concrete backend (
logback,log4j2,java.util.logging) the host application chose. - ORM repositories. A
UserRepositoryinterface adapted byJpaUserRepository,MongoUserRepository,InMemoryUserRepositoryfor tests.
Trade-offs#
What you gain:
- Reuse without modification. Legacy or third-party code keeps its existing shape; clients keep theirs; the adapter is the only thing that knows both worlds.
- Open-closed in practice. Supporting a new vendor or legacy API means a new adapter class — no edits to call sites.
- Test seams for free. Once a vendor SDK is behind an adapter interface, tests can substitute a fake.
- Single translation point. Argument reshaping, exception translation, units conversion all live in one obvious place.
What you pay:
- A layer to maintain. Every Target-method signature change ripples through every adapter. With ten vendors behind one interface, that is ten files to edit.
- Indirection in stack traces. A failure in
Mp4Player.streamMp4now reports throughMp4Adapter.play. Useful most days, noisy when debugging hot paths. - Leaky abstractions. If the Target interface cannot express a vendor-only feature, the adapter either silently drops it or exposes a “vendor-specific options” map that defeats the abstraction.
- Performance overhead is usually negligible — one extra method call — but allocation in tight loops (e.g. wrapping each element of a stream) can show up in profiles.
Related patterns#
- Decorator Pattern — same shape (wrap an object, delegate to it), different intent. Decorator keeps the interface and adds behaviour; Adapter changes the interface and adds none.
- Facade Pattern — Facade simplifies a subsystem behind one entry point; Adapter translates one class to a different shape. Facade is “one for many”; Adapter is “one for one.”
- Dependency Inversion Principle (DIP) — Adapter is the concrete-class-side answer to DIP: high-level code depends on the Target interface, the adapter binds it to a specific low-level provider.
- Interface Segregation Principle (ISP) — wide third-party interfaces are a recurring driver for adapters that expose only the slice the client needs.
- Open Closed Principle (OCP) — adding a new vendor without touching call sites is the OCP win this pattern delivers.