RizTech Academy logo
RizTech Academy
Design Patterns in PracticeLesson 5 of 730 min

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

  1. Write DeliveryCharge and bill a list of deliveries through the Billable interface.
  2. Add the methods to Delivery directly instead. Say what you lost.
  3. Call add on the result of Arrays.asList and read the exception. Explain it in terms of adapters.
  4. Write LoggedBillable and stack it with a discount decorator. Confirm the order changes the result.
  5. Remove the description forwarding from LoggedBillable and find what breaks.
  6. Unwrap new BufferedReader(new InputStreamReader(new FileInputStream(p))) and name what each layer adds.
  7. Write a TiffinService facade and rewrite one command in Main to use it. Confirm DeliveryStore is still usable directly.
  8. Find a class in the JDK you believe is a facade. Defend it.
  9. Find a wrapper in your own code that changes nothing and delete it.
  10. 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