OOD Fundamentals
OOP Foundations
SOLID Principles
Creational Patterns
Behavioral Patterns
Classic OOD Problems: Part 1
Classic OOD Problems: Part 2
Composite and Proxy
Most real-world data is hierarchical. A directory contains files and other directories. A GUI panel contains buttons, labels, and other panels. An organization has departments that contain teams that contain people. The moment you have this kind of part-whole hierarchy, you face a design question: should client code treat individual objects and groups of objects differently, or the same?
The Composite pattern answers: the same. It defines a common interface that both individual objects (leaves) and containers (composites) implement. Client code works with the interface and never asks "are you a leaf or a composite?" This eliminates type-checking conditionals and makes the tree infinitely extensible.

The Three Participants
Every Composite pattern has three roles:
Component: the shared interface. It declares operations that both leaves and composites support.
Leaf: an individual object with no children. It implements the component interface directly.
Composite: a container that holds child components. It implements the component interface by delegating to its children.
File System Example
A file system is the classic Composite example. Both files and directories respond to getSize(), but they compute it differently:
The key insight is in Directory.get_size(). It sums its children's sizes: and those children might be files (base case) or other directories (recursive case). The recursion terminates naturally at leaves. Client code calls root.get_size() and gets the total size of the entire tree without knowing or caring about its structure.
Uniform treatment is not just convenient: it is what makes the tree extensible. When you add a new leaf type (SymbolicLink, CompressedFile), existing composites and all client code continue working unchanged because they depend on the Component interface, not concrete types. This is the Open-Closed Principle applied to tree structures.
Why Not Just Use Type Checks?
Without Composite, you end up with code like this:
This works for two types. But add SymbolicLink, CompressedFile, and VirtualMount and every function that walks the tree needs updating. The Composite pattern eliminates this: each type knows how to compute its own size, and the tree walks itself through polymorphism.
The Composite pattern is not just a way to model trees: it is a way to make trees disappear from client code. When the pattern is applied well, the code that uses the tree never thinks about depth, branching, or node types. It calls a method on the root and the tree handles itself. This section explores why that property is so powerful and where you see it in real systems.
Recursive Operations Flow Naturally
Consider three operations on a file system tree: calculating total size, counting all files, and searching for a file by name. Without Composite, you write three separate recursive traversal functions. With Composite, each operation is a method on the Component interface, and the recursion is built into each class:
The pattern is identical every time: leaves handle the base case, composites delegate to children and aggregate results. Once you internalize this pattern, you can add any operation to the tree by adding one method to each class.
Real-World Composite Structures
GUI Widget Trees: Every GUI framework uses Composite. A Window contains Panels, Panels contain Buttons and TextFields. Calling setEnabled(false) on a Panel disables all its children recursively. The Window does not need to know every widget type: it just calls the interface method.
HTML/XML DOM: The Document Object Model is a Composite. Elements contain other elements and text nodes. Methods like getElementsByTagName() recurse through the tree. JavaScript can call element.remove() on any node regardless of whether it is a leaf text node or a subtree with hundreds of descendants.
Organization Hierarchies: A company has divisions containing departments containing teams containing employees. Computing total headcount, total salary budget, or finding all managers follows the same recursive Composite pattern.
Menu Systems: A menu bar contains menus, which contain menu items and sub-menus. Rendering the menu bar calls render() on the root, which cascades to every level. Adding a new sub-menu to any depth requires no changes to the rendering code.
The Open-Closed Principle in Practice
The most valuable property of Composite is extensibility. Suppose you add a CompressedFile type to the file system. It implements FileSystemComponent with its own get_size() that returns the compressed size. You add it to a directory with add(). Every existing operation, get_size(), count_files(), find(), works immediately on trees containing CompressedFile nodes because the operations depend on the interface, not concrete types.
Compare this with a non-Composite design where a utility function switches on type:
Every function that processes the tree must be updated. With Composite, zero functions change. The new class is self-contained.
When Composite Adds Unnecessary Complexity
Not every hierarchy needs Composite. If your tree has exactly two levels (a list of groups, each containing a flat list of items) and you are certain it will never go deeper, a simple list-of-lists is clearer. Composite pays off when the depth is variable, when new node types are likely, or when many operations must traverse the tree. For a flat grouping, a simpler data structure avoids over-engineering.
Sometimes you need to control how and when an object is accessed without changing the object itself. You might want to delay creating an expensive object until it is actually needed, restrict who can call its methods, or log every call for debugging. The Proxy pattern solves all of these by placing a stand-in object between the client and the real object.
The proxy implements the same interface as the real object. The client does not know it is talking to a proxy: it calls the same methods with the same signatures. The proxy decides whether to forward the call, when to forward it, and what to do before or after forwarding.
The Structure
Three participants define the pattern:
Subject: the shared interface that both the proxy and real object implement.
RealSubject: the actual object that does the work.
Proxy: holds a reference to the RealSubject and controls access to it.
In interviews, when asked about Proxy, immediately clarify which variant you mean. Virtual proxy (lazy loading), protection proxy (access control), and caching proxy (performance) solve different problems. Stating the variant shows you understand that Proxy is a family of solutions, not a single trick.
Virtual Proxy Example
Image loading is the classic virtual proxy scenario. Loading a high-resolution image from disk takes 500ms. If a document has 50 images but the user only sees 5 on screen, loading all 50 upfront wastes 22 seconds. A virtual proxy delays loading until the image is actually displayed:

The client holds an Image reference. It might be a HighResImage or an ImageProxy: the client cannot tell and does not care. When display() is called for the first time, the proxy creates the real image. Subsequent calls forward directly to the cached real image. If display() is never called, the expensive loading never happens.
Why Proxy Instead of Modifying the Real Object
You could add lazy-loading logic directly to HighResImage. But that violates the Single Responsibility Principle: HighResImage should know how to load and display images, not when to load them. The proxy separates the "when" from the "how." This also lets you swap proxy strategies without touching the real object. A virtual proxy today, a caching proxy tomorrow, a logging proxy in staging: all wrapping the same HighResImage.
The Proxy pattern is not a single solution: it is a family of solutions that share the same structure but differ in intent. Each variant wraps a real object behind the same interface, but the reason for wrapping determines the behavior. Understanding the variants matters because interviewers expect you to name the specific type when proposing a proxy in a design.
Virtual Proxy (Lazy Loading)
A virtual proxy delays creating an expensive object until the client actually needs it. You saw this with ImageProxy in the previous section. Other common uses include:
- Database connection proxies that open the connection on first query, not on construction
- Heavy report objects that only generate content when the user clicks "View"
- 3D model proxies in games that load geometry only when the player approaches
The pattern is always the same: store enough metadata to create the real object later, check if it exists on each method call, create it on first access, and delegate all subsequent calls.
Protection Proxy (Access Control)
A protection proxy checks permissions before forwarding a call. The real object has no security logic; it trusts that whoever calls it is authorized. The proxy enforces that trust boundary:

The RealDocument stays clean: no permission checks, no role logic. The protection proxy is the security boundary. Swap it out for a different proxy to change the access policy without touching the document class. This is how many ORMs implement field-level permissions: the model object is pure data, and a proxy layer enforces who can read or write each field.
Logging and Caching Proxies
A logging proxy records every method call for debugging or auditing. It forwards the call to the real object unchanged but logs the method name, arguments, timestamp, and result before returning. This is how many production systems implement request tracing without modifying business logic.
A caching proxy stores the result of expensive calls and returns the cached value on subsequent calls with the same arguments. If the real object computes a report that takes 5 seconds, the caching proxy serves the same result instantly for repeated requests:
Remote Proxy
A remote proxy represents an object in a different address space: a different machine, a different process, or a different container. The client calls methods on a local proxy object. The proxy serializes the arguments, sends them over the network, deserializes the response, and returns it. Java's RMI stubs, gRPC client stubs, and REST client wrappers are all remote proxies. The client writes code as if the object is local; the proxy hides the network boundary.
Choosing the Right Variant
| Problem | Variant | Key Behavior |
| Object is expensive to create | Virtual | Defer creation until first use |
| Need to restrict access | Protection | Check permissions before delegating |
| Need to track method calls | Logging | Record calls, then delegate |
| Need to avoid repeated expensive work | Caching | Store and return previous results |
| Object lives on another machine | Remote | Serialize calls across the network |
Composite, Decorator, and Proxy look similar in UML diagrams. All three involve an object that wraps another object behind a shared interface. All three use composition and delegation. If you only look at the structure, they are nearly identical. But they solve fundamentally different problems, and confusing them is one of the most common mistakes in design pattern interviews.
The structural similarity between Composite, Decorator, and Proxy trips up many candidates. When an interviewer asks you to compare them, never start with how they look in a class diagram: start with why you would use each one. The intent is the differentiator, not the mechanism.
The Key Differentiator Is Intent
Composite manages a group of objects as a single object. Its purpose is uniform treatment of parts and wholes. A directory does not enhance a file: it aggregates files. The relationship is one-to-many.
Decorator adds behavior to a single object. Its purpose is extending functionality without subclassing. A compression decorator does not control access to a stream: it transforms what the stream produces. The relationship is one-to-one wrapping with added behavior.
Proxy controls access to a single object. Its purpose is managing when, how, or whether the real object is used. A virtual proxy does not add new behavior: it delays the same behavior. The relationship is one-to-one wrapping with controlled access.

Side-by-Side Comparison
| Aspect | Composite | Decorator | Proxy |
| Wraps | Multiple children | One object | One object |
| Intent | Uniform tree traversal | Extend behavior | Control access |
| Children | Zero to many | Exactly one | Exactly one |
| Adds behavior | No (aggregates existing) | Yes (new functionality) | No (same functionality, different timing or conditions) |
| Example | Directory containing files | BufferedStream wrapping FileStream | ImageProxy delaying HighResImage load |
How to Tell Them Apart in Code
Ask three questions:
- Does the wrapper hold multiple children? If yes, it is Composite.
- Does the wrapper add new behavior the original cannot do? If yes, it is Decorator.
- Does the wrapper control when or whether the original is accessed? If yes, it is Proxy.
In practice, you might combine them. A caching proxy around a composite tree is perfectly valid. A decorator that wraps a proxy wrapping a real object is fine. The patterns compose because they share the same structural foundation: composition with interface delegation.
Interview Strategy
When an interviewer asks you to compare these three patterns, structure your answer around intent, not structure:
- Name what each pattern solves (aggregation, extension, access control)
- Give a one-sentence concrete example for each
- Point out that the structural similarity is a feature, not a coincidence: all three leverage composition and polymorphism, but for different purposes
This demonstrates that you understand patterns at the design-reasoning level, not just the class-diagram level.
Loading problem...
Loading problem...
Loading problem...