Composite for part-whole hierarchies — purpose, tree modelling, how it simplifies hierarchical data, benefits and limitations (transparency vs safety), Java implementation — and Decorator — dynamic behaviour vs inheritance, implementation, advantages, real examples (I/O streams, HTTP clients), and why it's flexible.
Published September 25, 2026
Composite and Decorator both rely on recursive composition through a shared interface. Composite builds trees (one-to-many). Decorator builds chains (one-to-one wrappers). Tie both to real code you've touched, such as java.io streams, UI trees, pricing rules or Spring's ClientHttpRequestInterceptor.
Short answer: Composite lets you build tree structures of objects, and treat individual objects (leaves) and groups (composites) through the same interface. Clients call operation() on any node, and composites forward the call to their children recursively.
Learn it in depth → Composite & Proxy
Short answer: Decorator adds responsibilities to individual objects at runtime, by wrapping them in objects with the same interface. Each wrapper does its extra work before or after delegating. Decorators can be stacked, and apply only to the objects you wrap, not to every instance of the class.
Learn it in depth → Decorator Pattern
Short answer: It's most useful for part-whole hierarchies, where clients shouldn't care whether they hold one item or a group:
Short answer: A product-bundle pricing tree: a bundle's price is the sum of its children, which are either products or nested bundles.
public sealed interface CatalogItem permits Product, Bundle {
BigDecimal price();
String name();
}
public record Product(String name, BigDecimal price) implements CatalogItem { }
public record Bundle(String name, List<CatalogItem> items, BigDecimal discountPct) implements CatalogItem {
public Bundle { items = List.copyOf(items); }
public BigDecimal price() {
BigDecimal total = items.stream().map(CatalogItem::price).reduce(BigDecimal.ZERO, BigDecimal::add); // recursion
return total.multiply(BigDecimal.ONE.subtract(discountPct.movePointLeft(2)));
}
}
CatalogItem office = new Bundle("Office Starter",
List.of(new Product("Laptop", new BigDecimal("55000")),
new Bundle("Accessories", List.of(new Product("Mouse", new BigDecimal("900")),
new Product("Bag", new BigDecimal("1500"))), BigDecimal.ZERO)),
new BigDecimal("10"));
office.price(); // the client never checks leaf vs composite
Short answer: Client code applies one operation to the root, and recursion handles the depth: no instanceof checks, and no manual traversal logic scattered around. Totals, rendering, searching, validation and permissions all become methods on the component, and adding new node types doesn't change the clients.
Key points to cover:
Short answer:
add/remove on the common interface lets clients call add on a leaf, where it throws. Putting them only on the composite forces type checks. Choose deliberately.Short answer:
List<Component>, delegates operations to its children, and provides add/remove.The "safe" variant keeps the child management on the composite only.
public interface FileNode { long size(); String name(); }
public record FileLeaf(String name, long size) implements FileNode { }
public final class Folder implements FileNode {
private final String name; private final List<FileNode> children = new ArrayList<>();
public Folder(String name) { this.name = name; }
public Folder add(FileNode n) { children.add(n); return this; } // only on the composite (safe)
public long size() { return children.stream().mapToLong(FileNode::size).sum(); }
public String name() { return name; }
}
Short answer: Inheritance adds behaviour statically, to every instance of the subclass. To combine features, you need a subclass for each combination: that's a class explosion. Decorator adds behaviour dynamically, per object, by composition. You combine features by stacking wrappers in any order, at runtime, without touching the original class, and each decorator stays small and single-purpose.
Short answer: Implement the same interface as the component, hold a reference to the wrapped component, and delegate, adding behaviour around the call. An abstract base decorator can forward everything by default.
public interface QuoteService { BigDecimal quote(String sku); }
public final class CachingQuoteService implements QuoteService { // decorator 1
private final QuoteService delegate; private final Map<String, BigDecimal> cache = new ConcurrentHashMap<>();
public CachingQuoteService(QuoteService d) { this.delegate = d; }
public BigDecimal quote(String sku) { return cache.computeIfAbsent(sku, delegate::quote); }
}
public final class TimedQuoteService implements QuoteService { // decorator 2
private final QuoteService delegate; private final MeterRegistry meters;
public TimedQuoteService(QuoteService d, MeterRegistry m) { this.delegate = d; this.meters = m; }
public BigDecimal quote(String sku) { return meters.timer("quote.latency").record(() -> delegate.quote(sku)); }
}
QuoteService service = new TimedQuoteService(new CachingQuoteService(new PricingApiQuoteService()), meters);
Key points to cover:
Short answer:
Drawbacks:
==) and instanceof see the wrapper, not the core.Short answer:
java.io: new BufferedReader(new InputStreamReader(new GZIPInputStream(new FileInputStream(f)))). Each layer adds buffering, decoding or decompression.Collections.unmodifiableList, synchronizedMap: add restrictions or synchronisation to an existing collection.RestClient/RestTemplate interceptors (auth headers, logging, retries), and servlet filters that wrap the request and response.TransactionAwareDataSourceProxy, LazyConnectionDataSourceProxy, and bean post-processors wrapping beans. These are technically proxies that act as decorators.Short answer: Behaviours become independent, composable units, assembled per object, per environment, at runtime:
Configuration code, or a DI container, builds the right stack, which keeps business classes free of cross-cutting clutter.
Q: Decorator vs Proxy? A: Both wrap an object with the same interface. A decorator adds behaviour, is usually stacked, and is chosen by the client. A proxy controls access (lazy loading, remote, security, caching), and often manages the real object's lifecycle. The client may not know it's there.
Q: Decorator vs AOP? A: AOP applies decorator-like behaviour across many beans declaratively (through pointcuts and proxies). Hand-written decorators are explicit and typed, and suit a few specific interfaces.
Q: How do you avoid writing delegation boilerplate for large interfaces?
A: Extend an abstract forwarding decorator, use Java dynamic proxies (Proxy.newProxyInstance), or apply AOP. For JDK types, use the existing wrapper classes (FilterInputStream, HttpServletRequestWrapper).
Q: Composite vs Decorator structurally? A: A composite holds many children, and aggregates their results. A decorator holds exactly one wrapped component, and enhances it.