Structural

Adapter

Convert the interface of a class into another interface that clients expect — make incompatible interfaces work together.

Beginner Pattern #06 of 23

What is the Adapter Pattern?

The Adapter pattern acts as a bridge between two incompatible interfaces. It wraps an existing class (the Adaptee) with a new interface (the Target) that the client expects — without modifying either the client or the adaptee.

Think of it as a translator: the client speaks one language, the adaptee speaks another, and the adapter translates between them in real time.

Real-world analogy: A power travel adapter. Your laptop charger has a US plug (Adaptee), the European wall socket expects a different shape (Target interface). The travel adapter sits in between — neither the charger nor the wall is modified.

The Problem It Solves

You're integrating a third-party analytics library into your app. Your code calls analytics.track(Event) but the library exposes logger.log(String, Map). You can't change the library, and you don't want to scatter conversion logic throughout your codebase.

You need a single place that translates your Event calls into the library's expected format — without coupling your business logic to the third-party API.

Participants

Target (interface)
The interface the client expects and works with. Defines the domain-specific interface.
Adaptee
The existing class with an incompatible interface — the thing you want to reuse but can't change.
Adapter
Implements the Target interface and wraps the Adaptee. Translates Target calls into Adaptee calls.
Client
Collaborates with the Target interface only — completely unaware of the Adaptee's existence.

Visual Flow Diagram

Client uses Target «interface» Target request() Adapter request() { adaptee.specificReq() Adaptee (3rd party / legacy) specificRequest() delegates to implements wraps

Java Code Example

Java — Object Adapter (composition) + Class Adapter (inheritance)
// ── OBJECT ADAPTER (preferred — uses composition) ──────────────────

// Target — what the client expects
public interface MediaPlayer {
    void play(String filename);
}

// Adaptee — existing class with incompatible interface
public class VLCPlayer {
    public void playVLC(String file) {
        System.out.println("VLC playing: " + file);
    }
}
public class MP4Player {
    public void playMP4(String file) {
        System.out.println("MP4 playing: " + file);
    }
}

// Adapter — wraps adaptee, exposes Target interface
public class MediaAdapter implements MediaPlayer {
    private VLCPlayer vlc;
    private MP4Player mp4;

    public MediaAdapter(String type) {
        if (type.equals("vlc"))  vlc = new VLCPlayer();
        if (type.equals("mp4"))  mp4 = new MP4Player();
    }

    public void play(String filename) {
        if (vlc != null) vlc.playVLC(filename);
        if (mp4 != null) mp4.playMP4(filename);
    }
}

// Client — only knows MediaPlayer
public class AudioPlayer implements MediaPlayer {
    public void play(String filename) {
        String ext = filename.substring(filename.lastIndexOf('.') + 1);
        if (ext.equals("mp3")) {
            System.out.println("Playing MP3: " + filename);
        } else if (ext.equals("vlc") || ext.equals("mp4")) {
            new MediaAdapter(ext).play(filename); // delegates via adapter
        } else {
            System.out.println("Unsupported format");
        }
    }
}

// ── CLASS ADAPTER (uses multiple inheritance — Java: extend + implement) ─
public class ClassAdapter extends VLCPlayer implements MediaPlayer {
    public void play(String filename) {
        playVLC(filename); // inherited from VLCPlayer directly
    }
}

// Client usage
AudioPlayer player = new AudioPlayer();
player.play("song.mp3");
player.play("movie.vlc");
player.play("video.mp4");

When to Use / Avoid

✓ Use When

  • You want to reuse an existing class but its interface doesn't match what you need
  • Integrating third-party or legacy code without modifying it
  • You want to create a reusable class that cooperates with unrelated classes
  • Multiple adaptees need to be unified behind one interface

✕ Avoid When

  • You can modify the source class — just align the interfaces directly
  • Too many adaptees — may signal a design that needs rethinking
  • The translation is so complex it introduces significant overhead or bugs

Real-World Examples

Pros & Cons

Pros

  • Single Responsibility — translation logic isolated in one class
  • Open/Closed — add new adapters without changing client or adaptee
  • Enables reuse of existing classes with incompatible interfaces
  • Object Adapter can adapt multiple adaptees at once via composition

Cons

  • Overall complexity increases — extra indirection layer
  • Class Adapter is inflexible — adapts only one specific class
  • If many methods need adapting, the adapter can become unwieldy

How Adapter Can Be Broken

⚠ Attack Vectors

  • Leaking the adaptee: Adapter exposes a getter for the wrapped Adaptee — clients obtain it directly and call its specific methods, bypassing the abstraction entirely
  • Incomplete translation: Adapter silently swallows some Target methods (empty body or no-op) because the Adaptee has no equivalent — clients get unexpected null/void behaviour with no error
  • Adapter chains: Adapter wraps another Adapter wraps another — deep nesting causes performance issues and makes debugging a nightmare
  • State mismatch: Adapter holds stale state if the Adaptee's internal state changes externally and the Adapter caches a translated copy
  • Class Adapter over-coupling: Using inheritance-based Class Adapter makes the Adapter tightly coupled to the specific Adaptee subclass — any change in Adaptee hierarchy breaks the Adapter

✓ Prevention

  • Never expose the adaptee: Keep the Adaptee reference private with no getter — the only way to interact is through the Target interface
  • Throw on unimplemented methods: If a Target method has no Adaptee equivalent, throw UnsupportedOperationException rather than silently doing nothing — fail fast
  • Prefer Object Adapter over Class Adapter: Composition over inheritance gives flexibility to swap the adaptee implementation and avoids fragile base class issues
  • Limit adapter nesting: If you need Adapter of Adapter, it's a signal to refactor — redesign the interfaces or create one direct adapter from source to final target
  • Delegate, don't cache: Always forward calls directly to the Adaptee in real time rather than caching translated values
Java — Break & Fix
// ❌ BREAKING — leaking the adaptee
public class LeakyAdapter implements MediaPlayer {
    private VLCPlayer vlc = new VLCPlayer();
    public VLCPlayer getVLC() { return vlc; } // ❌ exposes internals
    public void play(String f) { vlc.playVLC(f); }
}
// Client now calls getVLC().playVLC() — adapter is useless

// ✅ FIX — no getter, adaptee strictly private
public class SafeAdapter implements MediaPlayer {
    private final VLCPlayer vlc; // private, final, no getter
    public SafeAdapter(VLCPlayer vlc) { this.vlc = vlc; }
    public void play(String f) { vlc.playVLC(f); }
}

// ❌ BREAKING — silent no-op on unimplemented method
public class BrokenAdapter implements MediaPlayer {
    public void play(String f)   { /* TODO — nothing happens! */ }
}

// ✅ FIX — fail fast with UnsupportedOperationException
public void play(String f) {
    throw new UnsupportedOperationException(
        "play() not supported by this adapter");
}

How Other Patterns Relate

Interview Cheat Sheet

  1. What: Wraps an incompatible class (Adaptee) with the interface the client expects (Target). A translator between two interfaces — neither side is modified.
  2. Two variants: Object Adapter (composition — wraps via field, preferred) vs Class Adapter (inheritance — extends Adaptee and implements Target, less flexible).
  3. Key distinction from similar patterns: Adapter = changes interface. Decorator = same interface + adds behaviour. Facade = new simplified interface. Proxy = same interface + controls access.