Adapter, decorator and facade
Three patterns that all sit between a caller and something it wants to use, and are constantly confused because the code looks nearly identical. What separates them is intent, and the JDK has a clean example of each.
| Pattern | Intent | Interface |
|---|---|---|
| Adapter | make an incompatible thing fit | changes |
| Decorator | add behaviour | stays the same |
| Facade | hide complexity | simplifies |
Adapter: make it fit
You have something useful with the wrong shape.
The capstone bills anything that implements Billable:
public interface Billable {
long amountPaise();
String description();
}
A delivery is not Billable — it is a record of a date, a customer and a
count, and it knows nothing about money. You could add the methods to
Delivery, but then your data type depends on your billing type, and it stops
being a plain record of what happened.
An adapter keeps them apart:
public record DeliveryCharge(Delivery delivery, long ratePaise) implements Billable {
@Override
public long amountPaise() {
return ratePaise * delivery.tiffins();
}
@Override
public String description() {
return "%s — %d tiffin%s on %s".formatted(
delivery.customer(),
delivery.tiffins(),
delivery.tiffins() == 1 ? "" : "s",
delivery.date());
}
}
Delivery stays a record of a fact. Billable stays the billing contract.
DeliveryCharge is the only thing that knows both, and it is the only file that
changes if either moves.
The JDK's example: Arrays.asList(array) — an adapter making an array look
like a List. It is a genuine adapter, which is why it has the surprising
behaviour that add throws: the underlying array is fixed size, and an adapter
cannot invent capability the adapted thing does not have.
Decorator: add something, keep the shape
Same interface in, same interface out, with something extra in between.
public final class LoggedBillable implements Billable {
private final Billable inner;
private final Consumer<String> log;
public LoggedBillable(Billable inner, Consumer<String> log) {
this.inner = inner;
this.log = log;
}
@Override
public long amountPaise() {
long amount = inner.amountPaise();
log.accept("billed %s: %d paise".formatted(inner.description(), amount));
return amount;
}
@Override
public String description() {
return inner.description();
}
}
Because the type is unchanged, decorators stack:
Billable b = new LoggedBillable(
new DiscountedBillable(
new DeliveryCharge(delivery, 8_000L), 10),
System.out::println);
Anything taking a Billable accepts that, and none of them know how many
wrappers are involved.
The JDK's example is the one you have typed a hundred times:
new BufferedReader(new InputStreamReader(new FileInputStream(path)))
Each wraps a Reader and returns a Reader. BufferedReader adds buffering
and readLine; InputStreamReader adds character decoding. That is why the
constructors nest — it is decorators all the way down, and once you see it the
whole java.io package stops looking arbitrary.
Collections.unmodifiableList(list) is a decorator too: same List interface,
with the mutators changed to throw.
The thing to be careful about
A decorator must forward everything it does not change. LoggedBillable
overrides amountPaise and still has to pass description through. Miss one on
a wide interface and you get behaviour that silently disappears — which is why
decorating a five-method interface is fine and decorating a thirty-method one is
a job for a generated delegate.
Facade: one door into something complicated
Not a wrapper around one object — a simple entry point in front of several.
The capstone's Main does this without making a fuss about it:
DeliveryStore store = new DeliveryStore(path);
Parsed<Delivery> parsed = store.load();
BillingService billing = new BillingService(subscribers);
System.out.print(billing.renderReport(parsed.rows(), month));
A facade collapses that into one call:
public final class TiffinService {
private final DeliveryStore store;
private final BillingService billing;
public TiffinService(DeliveryStore store, BillingService billing) {
this.store = store;
this.billing = billing;
}
/** Everything the "report" command needs, behind one method. */
public String monthlyReport(YearMonth month) {
Parsed<Delivery> parsed = store.load();
return billing.renderReport(parsed.rows(), month);
}
}
The caller no longer knows there is a store, a parse result and a billing service. It asks for a report.
A facade does not forbid the pieces. DeliveryStore is still public and
still usable directly. A facade offers the common path; it does not lock the
door behind it.
The distinction from adapter matters: an adapter exists because a shape is wrong, a facade because a sequence is tedious. And the warning is that a facade with forty methods has stopped hiding complexity and started being a second copy of it.
Telling them apart
When you meet a class wrapping another, ask what changed:
- The interface changed — adapter.
- The interface is the same, behaviour was added — decorator.
- Several objects behind one simpler interface — facade.
And if nothing changed at all, it is not a pattern. It is a layer somebody added because layers felt professional, and it should be deleted.
Check your work
What separates the three: intent. Adapter changes the interface, decorator keeps it, facade simplifies several.
Why the adapter is its own class: so the data type does not depend on the billing type, and one file changes when either moves.
Why Arrays.asList(...).add(...) throws: an adapter cannot invent capacity
the adapted array does not have.
Why decorators stack: the type is unchanged, so a decorated object is accepted everywhere the original was.
Where you already use decorators: every nested java.io constructor, and
Collections.unmodifiableList.
The decorator trap: forwarding everything you do not change, on every method.
How a facade differs from an adapter: the shape was not wrong, the sequence was tedious.
What a facade must not do: forbid direct use of the pieces.
What a wrapper that changes nothing is: a layer to delete.
Practice
- Write
DeliveryChargeand bill a list of deliveries through theBillableinterface. - Add the methods to
Deliverydirectly instead. Say what you lost. - Call
addon the result ofArrays.asListand read the exception. Explain it in terms of adapters. - Write
LoggedBillableand stack it with a discount decorator. Confirm the order changes the result. - Remove the
descriptionforwarding fromLoggedBillableand find what breaks. - Unwrap
new BufferedReader(new InputStreamReader(new FileInputStream(p)))and name what each layer adds. - Write a
TiffinServicefacade and rewrite one command inMainto use it. ConfirmDeliveryStoreis still usable directly. - Find a class in the JDK you believe is a facade. Defend it.
- Find a wrapper in your own code that changes nothing and delete it.
- Write one sentence each distinguishing adapter, decorator and facade, without using the words "wrap" or "layer".
Next: telling other parts of the system that something happened.
Stuck on this lesson?
Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.
About the internship