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:
- Recognise when an existing type you cannot change has the behaviour you need but the wrong method shape
- Write an adapter that translates one call shape into another, touching neither side
- Write a facade that hides a multi-step protocol behind a small, honest set of methods
- Say when either pattern is ceremony, and explain the one kind of mismatch an adapter cannot fix
1. The situation
A 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. No problem in this course's own corpus happens to need this pattern, so the example below is built for this chapter rather than drawn from graded material the way the earlier chapters' examples were. The shape is the one you will meet in practice, and it is worth saying so plainly if you are ever asked 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 and be done with it.
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 to fix. AuditClient genuinely has no write(String) method, because it was written for a different caller with a different shape of call, before this logger ever existed.
2. Two tempting fixes, and why both are worse
One option is to add a write(String) method to AuditClient itself, parsing a formatted line back apart into who, what, and when. That means editing code the security team owns and already has other callers for, 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 ever formatted them into one string, and parsing them back out of that string throws information away and then tries to reconstruct it.
The other option is to 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 written against write(String). Neither knows anything about a who, a what, or a timestamp of its own.
3. The move
Write a small class that implements the interface Logger already expects, and holds the thing that does not, translating 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. Logger calls write(String) exactly the way it calls every other appender, never learning that the destination underneath expects three separate arguments. AuditClient is untouched, so whatever else calls it keeps working exactly as it did before this logger knew it existed. This is the Adapter pattern: one small class, standing between two shapes that were never going to match on their own, changing neither one.
4. What modern Java changes here
Where the mismatch is purely about the shape of the call, and the adaptee needs no extra argument at all, a method reference can be the whole adapter, with no class and 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, exactly what Appender.write needs, so the method reference satisfies the interface directly. AuditAppender could not be written this way, because record needs two arguments write does not have, and something has to decide what to put 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 a logger will ever have already speaks write(String), there is no second shape to reconcile and nothing here to name Adapter. Wrapping a type that already implements the interface it needs adds a class that forwards every call unchanged, for no reason a reader can find.
The threshold: reach for an adapter once a real, already-written type exists with the behaviour you need but 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 instead of being wrapped in an adapter it never needed.
6. A different situation
An API gateway needs 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 also draws down a cap the whole service shares, on top of its own budget. A request now has to satisfy both budgets, not one, and 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. A second caller — a background job that only wants to know how much room is left, without charging anything — needs most of the same protocol again, minus the charge.
8. The move
Put the whole check-lock-charge protocol behind one class, with methods that say what a caller wants rather than 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
The client's own budget holds 2, so the first two calls succeed and charge both the client's budget and the backstop. The third fails, because the client's own budget is now full, even though the backstop still has one unit of room left; the caller never has to know that the decision came from two separate objects. This is the Facade pattern: tryAcquire and remaining are the whole public surface, and scopeFor, decide, and locked carry the actual complexity behind them, private and unreachable from outside.
9. What modern Java changes here
Nothing here needs an interface separate from RateLimiter, since it has exactly one implementation and no second one is in view. A facade earns its place by what it hides, not by how many types back it — a single final class with a narrow set of public methods is the ordinary shape, 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 directly from one place, are already the whole answer. Wrapping them in a class before a second concern or a second caller exists adds a layer nobody reads through.
The threshold: 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 have to 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
An adapter can only paper over a difference in shape. It cannot recover information that was already thrown away before it got involved. AuditAppender.write receives one String, and by the time a formatted line reaches it, whatever service actually logged it is gone. The "logger" value it hands to record 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, because both went through the same AuditAppender, and nothing about that class can tell them apart. AuditClient.record was built to answer "who did this," and this adapter cannot make that question answerable, because the information it would need was never passed into write in the first place. Widening Appender to carry a caller identity would fix it, but 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 that it lets you avoid that edit. Recognising when the mismatch is this deep, rather than 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 they are told two shapes do not match. Fewer stop to ask whether the mismatch is only syntactic, a different method name or argument order, or whether real information would be lost translating one into the other. Naming that distinction out loud, the way section 4 versus the Going deeper example does, is the difference between a design that looks finished and one that actually 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 →