Adapter, decorator and facade - what concrete problem does each one solve, and where would reaching for it be over-engineering?
Adapter converts a type you do not own into the interface your code already speaks. Decorator adds behaviour while keeping the same interface, so wrappers stack. Facade gives callers one narrow entry point into a subsystem. Each becomes over-engineering the moment nothing on the other side varies.
What the interviewer is scoring
- Does the candidate anchor each pattern to the pressure that produces it rather than reciting its class diagram
- Whether the adapter example makes clear which side of the boundary the candidate owns
- That they know a decorator implements the same interface it wraps, and can say why that is what makes stacking possible
- Whether the candidate volunteers a case where the wrapper is dead weight instead of defending all three patterns
- Can they separate decorator from proxy on intent, given the two have an identical shape
Answer
Adapter: the other side of the boundary is not yours to change
The pressure is a mismatch you are not allowed to fix. Your code has a notion of sending a message, expressed as an interface you defined for your own convenience. The vendor SDK you must call has different method names, a different notion of a phone number, its own result object, and its own exception type. You cannot change the SDK, and you should not let its vocabulary spread through your code, because that is what makes replacing it a six-week project instead of a one-file change.
// --- not yours: the vendor ships this ---
final class AcmeGatewayClient {
static final class AcmeResult {
final int status;
AcmeResult(int status) { this.status = status; }
}
AcmeResult dispatch(String countryCode, String national, String text) {
return new AcmeResult(200);
}
}
// --- yours: the vocabulary your domain uses ---
interface SmsSender {
boolean send(String e164Number, String body);
}
// --- the adapter: one class, and the only file that knows Acme exists ---
final class AcmeSmsSender implements SmsSender {
private final AcmeGatewayClient client;
AcmeSmsSender(AcmeGatewayClient client) { this.client = client; }
@Override public boolean send(String e164Number, String body) {
// Splitting +447700900123 into "44" and the rest is exactly the kind of
// vendor-shaped detail that should exist in one place.
String digits = e164Number.startsWith("+") ? e164Number.substring(1) : e164Number;
String countryCode = digits.substring(0, 2);
String national = digits.substring(2);
return client.dispatch(countryCode, national, body).status == 200;
}
}
Everything above the interface is testable with a two-line fake, and the day the vendor changes you write a second adapter and change one line of wiring. The JDK's canonical example is InputStreamReader, which adapts a byte stream to a character stream: same idea, different types on each side, one class that knows the encoding.
Decorator: same interface, extra behaviour, and the ability to stack
The pressure here is different. The interface is right, and you want to add something orthogonal to it — caching, timing, retries, rate limiting, an audit log. Subclassing each implementation to add caching would mean writing the caching once per implementation; putting the cache inside the implementation would mean every future implementation reimplements it. A decorator implements the interface and holds an instance of it, which is what lets the additions compose.
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
interface PriceSource {
long priceMinor(String sku);
}
final class RemotePriceSource implements PriceSource {
@Override public long priceMinor(String sku) {
return 1999L; // stands in for a network call
}
}
final class CachingPriceSource implements PriceSource {
private final PriceSource delegate;
private final Map<String, Long> cache = new HashMap<>();
CachingPriceSource(PriceSource delegate) { this.delegate = delegate; }
@Override public long priceMinor(String sku) {
return cache.computeIfAbsent(sku, delegate::priceMinor);
}
}
final class TimedPriceSource implements PriceSource {
private final PriceSource delegate;
private final Map<String, Duration> lastCall = new HashMap<>();
TimedPriceSource(PriceSource delegate) { this.delegate = delegate; }
@Override public long priceMinor(String sku) {
long start = System.nanoTime();
try {
return delegate.priceMinor(sku);
} finally {
lastCall.put(sku, Duration.ofNanos(System.nanoTime() - start));
}
}
Duration lastCallFor(String sku) { return lastCall.get(sku); }
}
final class Wiring {
// Order is a design decision, not a formality: this measures cache misses only.
static PriceSource cheapReads() {
return new CachingPriceSource(new TimedPriceSource(new RemotePriceSource()));
}
// Swapping the wrappers measures every call, including the ones the cache served.
static PriceSource measuredReads() {
return new TimedPriceSource(new CachingPriceSource(new RemotePriceSource()));
}
}
That ordering point is the part interviewers listen for, because it is the difference between having used decorators and having read about them. java.io is built this way, which is why new BufferedInputStream(new FileInputStream(f)) and its inverse are both legal expressions with different meanings.
Decorators have two costs worth stating. Stack traces and debugger step-throughs grow a frame per layer, so a five-deep stack is genuinely harder to read. And a decorator is a different object from the thing it wraps, so anything relying on object identity, equals, or downcasting to the concrete type quietly stops working.
Facade: one door into a subsystem you already own
Adapter and decorator both deal with a single collaborator. A facade deals with several. Checkout involves a basket, a tax calculator, a payment gateway, an inventory reservation and an email. The web controller does not need to know that, and the moment it does, the ordering of those five calls is duplicated in every entry point that ever needs to check out — the API, the admin tool, the retry job.
record Basket(String customerId, long subtotalMinor) {}
record Receipt(String orderId, long chargedMinor) {}
interface TaxCalculator { long taxMinor(Basket basket); }
interface PaymentGateway { String charge(String customerId, long amountMinor); }
interface Inventory { boolean reserve(Basket basket); }
/** One method the callers want, over four collaborators they should not see. */
final class CheckoutFacade {
private final TaxCalculator tax;
private final PaymentGateway payments;
private final Inventory inventory;
CheckoutFacade(TaxCalculator tax, PaymentGateway payments, Inventory inventory) {
this.tax = tax;
this.payments = payments;
this.inventory = inventory;
}
Receipt checkout(Basket basket) {
if (!inventory.reserve(basket)) throw new IllegalStateException("out of stock");
long total = basket.subtotalMinor() + tax.taxMinor(basket);
String orderId = payments.charge(basket.customerId(), total);
return new Receipt(orderId, total);
}
}
The value is not that it is tidier. It is that the sequence — reserve before charging, tax before totalling — now exists in exactly one place, and a caller cannot get it wrong because a caller cannot see it. Notice also that the facade does not forbid direct access to the collaborators; a reporting job that only needs the tax calculator uses it directly. A facade that its own subsystem is forced to route through has become a bottleneck rather than a convenience.
Decorator and proxy have the same shape and different intent
Interviewers ask this because the class diagrams are identical, so the answer has to come from intent. A decorator adds behaviour that the assembler chose, and the caller wiring it up knows the layers exist and deliberately ordered them. A proxy controls access to a subject on the subject's behalf — lazy loading, access checks, or a remote call — and its whole point is that the caller believes it is talking to the real thing. That is why proxies are usually created by infrastructure rather than by application code, and why java.lang.reflect.Proxy exists at all: a framework generates them so your code never has to know.
Practical consequence: if a reader of the wiring code has to see the layer for the design to make sense, it is a decorator; if the design depends on the caller not knowing, it is a proxy.
Where the wrapper is dead weight
The over-engineering case is worth having ready, because volunteering it is a stronger signal than any of the three explanations. Three specific shapes, all common in real codebases.
The pass-through facade. A class named something like UserManager whose fifteen methods each call exactly one method on UserRepository and return the result. It adds a file, a layer of indirection, a set of tests that assert delegation, and nothing else — no sequencing, no narrowing, no vocabulary translation. Every future field added to the repository has to be added twice. The honest version of this design is for callers to use the repository.
The adapter over code you own. If both interfaces are yours, an adapter is a decision to keep two vocabularies for the same concept. Deleting one of them is nearly always the better change, and the only good reason to keep both is that one of them is a published API you cannot break yet — in which case say that, because it turns the adapter from a habit into a documented migration step.
The decorator for behaviour that never varies. If timing is always on, in every deployment, for every implementation, then a wrapper is a way of making the code longer. Put it in the one implementation and revisit when there is a second. The decorator earns its place when the composition varies — on in production and off in tests, caching in one wiring and not another.
There is a broader version of this. All three patterns exist to absorb variation on one side of a boundary, so the question that decides whether to use one is what will vary. If the answer is "nothing, and nothing is going to", the wrapper is an unpaid tax on every reader of the code.
Likely follow-ups
- How do you keep a stack of decorators debuggable when a stack trace is ten frames of wrappers?
- What breaks when a decorator wraps an object whose identity or equals contract callers rely on?
- Where would you put a retry - a decorator, the adapter, or the caller?
- If a facade grows to forty methods, what has gone wrong and what do you do?
Related questions
- When would you use the observer pattern, and what goes wrong with it at scale?mediumAlso on coupling4 min
- Give me a real single-responsibility violation you have seen, and how would you refactor it?mediumAlso on coupling5 min
- Service A needs something from service B. When should that be a synchronous call and when should it be an event?mediumAlso on coupling3 min
- How does your order placement change on a venue that allocates pro rata within a price level instead of first in, first out?hardSame kind of round: concept6 min
- How would you design a thread-safe component, and why is adding synchronized to every method not a design?hardSame kind of round: concept7 min
- Where does encapsulation or polymorphism change a design, and how do you decide between composition and inheritance?mediumSame kind of round: concept6 min
- When is a factory the right answer, when is a builder, and when should you just call the constructor?mediumSame kind of round: concept7 min
- How do you test a Node service, and what do you refuse to mock?mediumSame kind of round: concept5 min