Composite and Proxy

Topics Covered

The Composite Pattern

The Three Participants

File System Example

Why Not Just Use Type Checks?

Tree Structures and Uniform Treatment

Recursive Operations Flow Naturally

Real-World Composite Structures

The Open-Closed Principle in Practice

When Composite Adds Unnecessary Complexity

The Proxy Pattern

The Structure

Virtual Proxy Example

Why Proxy Instead of Modifying the Real Object

Proxy Variants

Virtual Proxy (Lazy Loading)

Protection Proxy (Access Control)

Logging and Caching Proxies

Remote Proxy

Choosing the Right Variant

Composite vs Decorator vs Proxy

The Key Differentiator Is Intent

Side-by-Side Comparison

How to Tell Them Apart in Code

Interview Strategy

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.

Composite tree recursive size calculation

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:

python
1from abc import ABC, abstractmethod
2
3class FileSystemComponent(ABC):
4    @abstractmethod
5    def get_size(self) -> int:
6        pass
7
8    @abstractmethod
9    def display(self, indent: int = 0) -> str:
10        pass
11
12class File(FileSystemComponent):
13    def __init__(self, name: str, size: int):
14        self.name = name
15        self.size = size
16
17    def get_size(self) -> int:
18        return self.size
19
20    def display(self, indent: int = 0) -> str:
21        return " " * indent + f"File: {self.name} ({self.size} bytes)"
22
23class Directory(FileSystemComponent):
24    def __init__(self, name: str):
25        self.name = name
26        self.children: list[FileSystemComponent] = []
27
28    def add(self, component: FileSystemComponent):
29        self.children.append(component)
30
31    def get_size(self) -> int:
32        return sum(child.get_size() for child in self.children)
33
34    def display(self, indent: int = 0) -> str:
35        lines = [" " * indent + f"Dir: {self.name}"]
36        for child in self.children:
37            lines.append(child.display(indent + 2))
38        return "\n".join(lines)

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.

Key Insight

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:

python
1def calculate_size(item):
2    if isinstance(item, File):
3        return item.size
4    elif isinstance(item, Directory):
5        total = 0
6        for child in item.children:
7            total += calculate_size(child)
8        return total

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:

python
1class FileSystemComponent(ABC):
2    @abstractmethod
3    def get_size(self) -> int:
4        pass
5
6    @abstractmethod
7    def count_files(self) -> int:
8        pass
9
10    @abstractmethod
11    def find(self, name: str) -> list:
12        pass
13
14class File(FileSystemComponent):
15    def get_size(self) -> int:
16        return self.size
17
18    def count_files(self) -> int:
19        return 1
20
21    def find(self, name: str) -> list:
22        return [self] if self.name == name else []
23
24class Directory(FileSystemComponent):
25    def get_size(self) -> int:
26        return sum(c.get_size() for c in self.children)
27
28    def count_files(self) -> int:
29        return sum(c.count_files() for c in self.children)
30
31    def find(self, name: str) -> list:
32        results = []
33        for child in self.children:
34            results.extend(child.find(name))
35        return results

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:

python
1# Without Composite — every new type requires updating every function
2def get_size(node):
3    if isinstance(node, File):
4        return node.size
5    elif isinstance(node, Directory):
6        return sum(get_size(c) for c in node.children)
7    elif isinstance(node, CompressedFile):  # must add this
8        return node.compressed_size
9    # ... every new type needs a branch here

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.

Interview Tip

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:

python
1from abc import ABC, abstractmethod
2
3class Image(ABC):
4    @abstractmethod
5    def display(self) -> str:
6        pass
7
8    @abstractmethod
9    def get_dimensions(self) -> tuple:
10        pass
11
12class HighResImage(Image):
13    def __init__(self, filename: str):
14        self.filename = filename
15        # Expensive operation: loading from disk
16        self.data = self._load_from_disk()
17
18    def _load_from_disk(self) -> bytes:
19        print(f"Loading {self.filename} from disk...")
20        # Simulate expensive I/O
21        return b"image data"
22
23    def display(self) -> str:
24        return f"Displaying {self.filename}"
25
26    def get_dimensions(self) -> tuple:
27        return (1920, 1080)
28
29class ImageProxy(Image):
30    def __init__(self, filename: str):
31        self.filename = filename
32        self._real_image = None  # Not loaded yet
33
34    def display(self) -> str:
35        if self._real_image is None:
36            self._real_image = HighResImage(self.filename)
37        return self._real_image.display()
38
39    def get_dimensions(self) -> tuple:
40        # Can return cached/default dimensions without loading
41        return (1920, 1080)
Virtual proxy lazy loading

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:

python
1class Document(ABC):
2    @abstractmethod
3    def read(self) -> str:
4        pass
5
6    @abstractmethod
7    def write(self, content: str) -> None:
8        pass
9
10class RealDocument(Document):
11    def __init__(self, content: str):
12        self._content = content
13
14    def read(self) -> str:
15        return self._content
16
17    def write(self, content: str) -> None:
18        self._content = content
19
20class ProtectedDocument(Document):
21    def __init__(self, document: RealDocument, user_role: str):
22        self._document = document
23        self._user_role = user_role
24
25    def read(self) -> str:
26        return self._document.read()
27
28    def write(self, content: str) -> None:
29        if self._user_role != "editor":
30            raise PermissionError("Read-only access")
31        self._document.write(content)
Protection proxy access control

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:

python
1class CachingReportProxy(ReportGenerator):
2    def __init__(self, real_generator: ReportGenerator):
3        self._real = real_generator
4        self._cache = {}
5
6    def generate(self, report_id: str) -> Report:
7        if report_id not in self._cache:
8            self._cache[report_id] = self._real.generate(report_id)
9        return self._cache[report_id]

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

ProblemVariantKey Behavior
Object is expensive to createVirtualDefer creation until first use
Need to restrict accessProtectionCheck permissions before delegating
Need to track method callsLoggingRecord calls, then delegate
Need to avoid repeated expensive workCachingStore and return previous results
Object lives on another machineRemoteSerialize 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.

Common Pitfall

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.

Composite vs decorator vs proxy

Side-by-Side Comparison

AspectCompositeDecoratorProxy
WrapsMultiple childrenOne objectOne object
IntentUniform tree traversalExtend behaviorControl access
ChildrenZero to manyExactly oneExactly one
Adds behaviorNo (aggregates existing)Yes (new functionality)No (same functionality, different timing or conditions)
ExampleDirectory containing filesBufferedStream wrapping FileStreamImageProxy delaying HighResImage load

How to Tell Them Apart in Code

Ask three questions:

  1. Does the wrapper hold multiple children? If yes, it is Composite.
  2. Does the wrapper add new behavior the original cannot do? If yes, it is Decorator.
  3. 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:

  1. Name what each pattern solves (aggregation, extension, access control)
  2. Give a one-sentence concrete example for each
  3. 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...