LLD Dojo

Patterns you will actually be asked for · chapter 26 of 33

Adapter and Facade: making other people's code fit

Chapter 3.8 · Part 3, Patterns you will actually be asked for · about 30 minutes

What you need before this chapter: Part 1 in full, especially interfaces (1.4). Part 2 in full, especially dependency inversion (2.8). Chapter 3.4, Observer, for Appender, which this chapter reuses directly.

When you finish this chapter you will be able to:


1. The situation

Your logging setup needs a new destination. An audit service the security team already runs, with a client already written and already used elsewhere in the codebase.

One honest note before you start. No problem in this course's corpus happens to need this pattern, so the example below was built for this chapter rather than drawn from graded material like the earlier ones. The shape is the one you will meet in practice, and it is worth saying so plainly if anybody asks you where it lives here.

interface Appender {
    void write(String formattedLine);
}

final class AuditClient {
    void record(String who, String what, long whenEpochMillis) { /* sends to the service */ }
}

The obvious move is to hand AuditClient straight to Logger.addAppender.

AuditClient auditClient = new AuditClient();
addAppender(auditClient);
javac Step1Bad.java
Step1Bad.java:26: error: incompatible types: AuditClient cannot be converted to Appender
        addAppender(auditClient);
                    ^

That is not a typo you can fix. AuditClient genuinely has no write(String) method. Somebody wrote it for a different caller, with a different shape of call, before your logger existed.

2. Two tempting fixes, and why both are worse

Option one: add a write(String) method to AuditClient, parsing a formatted line back apart into who, what, and when.

That means editing code the security team owns, which already has other callers, to serve a shape it was never designed around.

It is also strictly worse than what AuditClient already offers. Logger had the who, the what and the when as three separate values before it formatted them into one string. Parsing them back out throws information away and then tries to reconstruct it.

Option two: widen Appender itself, so it looks more like AuditClient.

That fixes this one destination and breaks the console and file destinations from chapter 3.4. Both were written against write(String), and neither knows anything about a who, a what, or a timestamp.

3. The move

Write a small class that implements the interface Logger already expects, holds the thing that does not, and translates one call into the other.

final class AuditAppender implements Appender {
    private final AuditClient client;

    AuditAppender(AuditClient client) {
        this.client = client;
    }

    @Override
    public void write(String formattedLine) {
        client.record("logger", formattedLine, 0L);
    }
}
javac Step2.java
java Step2
logger: payment processed @0

AuditAppender is the only class that knows both shapes exist. Nothing else has to.

Logger calls write(String) exactly the way it calls every other appender, never learning that the destination underneath expects three arguments. AuditClient is untouched, so whatever else calls it keeps working exactly as before.

That is the Adapter pattern. One small class, standing between two shapes that were never going to match, changing neither.

4. What modern Java changes here

When the mismatch is purely about the shape of the call, and the adaptee needs no extra argument, a method reference can be your whole adapter. No class, no lambda body.

Appender toConsole = System.out::println;
toConsole.write("server started");
javac Step3.java
java Step3
server started

println(String) already returns nothing and takes one String, which is exactly what Appender.write needs, so the method reference satisfies the interface directly.

Now ask why AuditAppender could not be written this way. record needs two arguments write does not have, and something has to decide what goes in their place.

That decision is the actual reason AuditAppender is a class and not one line.

5. When naming it is wrong

If the only destination your logger will ever have already speaks write(String), there is no second shape to reconcile and nothing to name Adapter.

Wrap a type that already implements the interface it needs and you add a class forwarding every call unchanged, for no reason a reader can find.

Your threshold has two halves here too. A real, already-written type exists with the behaviour you need and the wrong method shape. And changing that type is off the table, because something else already depends on its current shape.

A brand-new type, written for this one caller, should implement the interface directly. No wrapper.

6. A different situation

Switch problems. An API gateway has to decide whether a request may proceed under a rate limit.

One budget, checked and charged directly, is small enough to write right at the call site.

budget.lock();
try {
    if (budget.hasRoom()) {
        budget.charge();
        System.out.println("allowed");
    } else {
        System.out.println("denied");
    }
} finally {
    budget.unlock();
}
javac Step5.java
java Step5
allowed

7. A new requirement

A shared backstop arrives. Every client now also draws down a cap the whole service shares, on top of its own budget.

A request now has to satisfy both budgets. Getting that right means several things at once.

Both budgets have to be locked in a fixed order, so two requests locking them in different orders can never deadlock each other.

Both have to be checked before either is charged, or a request that fails the backstop can still spend the client's own budget on the way there.

Both have to be released in reverse order, even if something throws in between.

And a second caller needs most of the same protocol again, minus the charge. That caller is a background job, and it only wants to know how much room is left.

8. The move

Put the whole check-lock-charge protocol behind one class, with methods that say what a caller wants instead of how the budgets underneath are held.

final class RateLimiter {
    private final Map<ClientKey, KeyBudget> perClient = new HashMap<>();
    private final KeyBudget backstop;

    boolean tryAcquire(ClientKey key) {
        List<KeyBudget> scope = scopeFor(key);
        return locked(scope, () -> decide(scope));
    }

    int remaining(ClientKey key) {
        List<KeyBudget> scope = scopeFor(key);
        return locked(scope, () -> fewestRemaining(scope));
    }

    private List<KeyBudget> scopeFor(ClientKey key) {
        return List.of(perClient.get(key), backstop);
    }
    // decide, fewestRemaining, and locked are private below
}
ClientKey key = new ClientKey("service-a");
RateLimiter limiter = new RateLimiter(new KeyBudget(3));
limiter.configure(key, new KeyBudget(2));

System.out.println("attempt 1: " + limiter.tryAcquire(key));
System.out.println("attempt 2: " + limiter.tryAcquire(key));
System.out.println("attempt 3: " + limiter.tryAcquire(key));
System.out.println("remaining: " + limiter.remaining(key));
javac Step6.java
java Step6
attempt 1: true
attempt 2: true
attempt 3: false
remaining: 0

Follow the numbers. The client's own budget holds 2, so the first two calls succeed and charge both that budget and the backstop. The third fails, because the client's own budget is full.

Now notice what the caller never had to know. The backstop still had one unit of room, and the decision came from two separate objects, and none of that reached the call site.

That is the Facade pattern. tryAcquire and remaining are the whole public surface, while scopeFor, decide and locked carry the real complexity behind them, private and unreachable.

9. What modern Java changes here

Nothing here needs an interface separate from RateLimiter. It has one implementation and no second in view.

A facade earns its place by what it hides, and not by how many types back it. A single final class with a narrow set of public methods is the ordinary shape here, and not a compromise.

Java's own module system makes the same choice one level up. A module exports specific packages and keeps the rest unreachable from outside, however many classes that rest contains.

10. When naming it is wrong

A single budget, checked once, with no backstop and no second caller needing the same protocol, does not need a facade in front of it.

Section 6's seven lines, called from one place, are already the whole answer. Wrap them in a class before a second concern or a second caller exists and you add a layer nobody reads through.

Your threshold has two halves. Reach for a facade once two or more collaborators have to be driven together, in an order or under a discipline a caller should not reproduce by hand. Or once a second caller already needs the same sequence.

One collaborator, one caller, wrapped in a class around a single method call, is the same over-engineered (premature interface) failure named in every earlier chapter.

Your turn

Part A — Adapter. A metrics team has a MetricsRecorder with one method, count(String label). Adapt it to Appender, counting every logged line under the label "log-line".

final class MetricsAppender implements Appender {
    private final MetricsRecorder recorder;

    MetricsAppender(MetricsRecorder recorder) { this.recorder = recorder; }

    @Override
    public void write(String formattedLine) {
        recorder.count("log-line");
    }
}
javac Step4.java
java Step4
log-line count: 2

Part B — Facade. Add reset(ClientKey) to RateLimiter, reusing locked rather than writing a new pair of lock calls.

void reset(ClientKey key) {
    List<KeyBudget> scope = List.of(perClient.get(key));
    locked(scope, () -> {
        perClient.get(key).reset();
        return null;
    });
}
javac Step8.java
java Step8
attempt 1: true
attempt 2: false
attempt 3 (after reset): true

Going deeper

Here is the limit of this pattern, and it is the part worth carrying away.

An adapter can only paper over a difference in shape. It cannot recover information that was thrown away before it got involved.

AuditAppender.write receives one String. By the time a formatted line reaches it, whatever service actually logged that line is gone.

So look again at the "logger" value it hands to record. That is not a placeholder waiting to be filled in properly later. It is the only value that method can ever supply, because the real caller's identity never survived the trip through Appender.

logger.log("payment-service: charged $50");
logger.log("shipping-service: label printed");
javac Step7.java
java Step7
logger: payment-service: charged $50 @0
logger: shipping-service: label printed @0

Both entries say logger as the who. Both went through the same AuditAppender. Nothing about that class can tell them apart.

AuditClient.record was built to answer "who did this". Your adapter cannot make that question answerable, because the information it needs was never passed into write.

Could you widen Appender to carry a caller identity? Yes, and that edits the interface every other destination depends on — which section 2 already ruled out for exactly this reason.

Some mismatches are only ever solvable by changing the interface itself, and an adapter's whole value is letting you avoid that edit. Recognising when a mismatch runs this deep, instead of reaching for an adapter and declaring the problem solved, is the harder skill.

Why this matters in an interview

Most candidates can write an adapter once told two shapes do not match. That is the easy half.

Fewer stop to ask what kind of mismatch it is. A different method name, or parameters in a different order, is one thing. A value that never reaches the other side at all is another.

Name that distinction out loud, the way section 4 and the Going deeper example do between them. That is the difference between a design that looks finished and one that is.


Next: chapter 3.9, Singleton, and why interviewers hope you avoid it. Facade hid a protocol behind a class with one instance in every example above. The next chapter asks the harder question: when should a class enforce that there is only ever one of it, rather than a caller simply choosing to build one.

← 3.7 Template method: a fixed skeleton with holes · All chapters · 3.9 Singleton, and why interviewers hope you avoid it →