Decorator & Composite
Decorator layers behavior (logging, caching, extra fees) without subclass explosion; composite treats a tree of account groups and single accounts uniformly.
On this page
Two more structural patterns, both built on a clever idea: a wrapper that is the same type as the thing it wraps. Decorator layers behavior onto an object without subclassing, by wrapping it in something that implements the same interface. Composite lets you treat a tree of objects and individual objects uniformly, through one shared interface. Both replace rigid hierarchies with flexible composition.
Decorator: layering behavior without subclass explosion
You need to add responsibilities to objects - logging, caching, an extra surcharge - in combinations. Doing
it with inheritance explodes: LoggingCachingSurchargedFeePolicy, LoggingSurchargedFeePolicy, ... a class per
combination. Decorator wraps an object in another object of the same interface, adding behavior around the
inner one:
interface FeePolicy { BigDecimal fee(Transaction t); }
class LoggingFeePolicy implements FeePolicy { // a decorator
private final FeePolicy inner; // wraps another FeePolicy
public BigDecimal fee(Transaction t) {
log.info("fee for {}", t.id());
BigDecimal result = inner.fee(t); // delegate to the wrapped policy
log.info("= {}", result);
return result;
}
}
class SurchargeFeePolicy implements FeePolicy { // another decorator
private final FeePolicy inner;
public BigDecimal fee(Transaction t) {
return inner.fee(t).add(new BigDecimal("2.00")); // add behavior, then delegate
}
}Now you stack them at runtime, in any order, without new classes:
FeePolicy policy = new LoggingFeePolicy(new SurchargeFeePolicy(new DomesticFee()));
// logs around → adds surcharge → base domestic feeEach decorator is one small class; combinations are assembled by nesting. This is exactly how java.io works -
new BufferedReader(new InputStreamReader(new FileInputStream(f))) is three decorators stacking behavior.
Composite: trees treated as leaves
ledger-legacy has individual accounts and groups of accounts (a department holds many accounts; a division
holds many departments). You want to compute a total balance without caring whether you're holding one account
or a whole tree. Composite gives leaves and containers the same interface:
interface AccountComponent { BigDecimal balance(); } // shared by leaf AND group
class Account implements AccountComponent { // a LEAF
public BigDecimal balance() { return this.balance; }
}
class AccountGroup implements AccountComponent { // a COMPOSITE
private final List<AccountComponent> children; // holds leaves OR groups
public BigDecimal balance() {
return children.stream()
.map(AccountComponent::balance) // recurse - each child sums itself
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
}Client code calls component.balance() and never branches on "is this a single account or a group?" - the tree
sums itself recursively. Adding a new level of nesting requires zero client changes. Composite shines for any
part-whole hierarchy: file systems, UI component trees, org charts.
Decorator vs. Adapter vs. Proxy - same shape, different intent
All three wrap an object of the same/related interface, so they look alike. Decorator ADDS behavior (and you can stack many). Adapter CHANGES the interface to a different one. Proxy CONTROLS access (lazy-loading, permissions, remote calls) while keeping the interface identical. When you see a wrapper, ask its intent - add, translate, or control - to name it correctly.
Decorator is layering clothing: your body (the base object) works on its own, then you add a sweater, then a raincoat - each layer wraps the last, adds its function (warmth, waterproofing), and you mix and match without needing a distinct 'sweater-and-raincoat person' class. Composite is a folder tree: a folder can hold files or other folders, and asking 'total size?' works the same whether you ask a single file or the whole nested tree - each folder just asks its children and adds up. One stacks wrappers to add behavior; the other nests containers that behave like their leaves.
Two needs in ledger-legacy: (1) a report's total must roll up across an arbitrary hierarchy - portfolios contain
sub-portfolios contain accounts - and code shouldn't special-case each level; (2) a TransactionValidator needs
optional, stackable extras - rate-limiting, audit-logging, and caching - applied in different combinations per
environment, without a class for every combination. Which pattern for each?
What problem does the Decorator pattern solve?
Key takeaways
- Decorator and Composite both rely on a wrapper that shares the same interface as what it wraps.
- Decorator adds behavior by wrapping an object in another of the same interface; stack them in any combination at runtime (like java.io streams) to avoid subclass explosion.
- Composite gives leaves and containers one shared interface, so a part-whole tree (accounts within groups) is treated uniformly and sums itself recursively.
- Composite client code never branches on 'single or group?' - adding nesting levels requires no client changes.
- Decorator, Adapter, and Proxy share the wrapper shape but differ in intent: add behavior, change interface, or control access.