Introduce Design Pattern II
In the previous article, we introduced Creational patterns. Creational patterns focus on object creation: where objects are created, which concrete implementation should be used, and how to avoid coupling callers to construction details.
This article focuses on Structural patterns.
According to Refactoring.Guru, structural patterns explain how to assemble objects and classes into larger structures while keeping those structures flexible and efficient.
In other words, structural patterns help us answer this question:
How should we compose objects and modules without making the system too rigid?
Why structural patterns matter
In a real system, a single object rarely works alone. Objects usually depend on other objects, wrap other objects, expose simplified APIs, or adapt incompatible interfaces.
For example:·
1 | Application |
This structure can become hard to change if every layer knows too much about the others.
Structural patterns help us organize these relationships. Their goal is not just to connect objects together. Their goal is to keep the connection flexible, understandable, and replaceable.
Structural patterns catalog
Refactoring.Guru lists seven common structural patterns:
| Pattern | Main idea | Common use case |
|---|---|---|
| Adapter | Convert one interface into another interface expected by the caller. | Integrating external APIs or legacy code. |
| Bridge | Separate abstraction from implementation so both can change independently. | Avoiding large inheritance trees. |
| Composite | Treat individual objects and groups of objects through the same interface. | Tree structures, nested components, grouped operations. |
| Decorator | Add behavior by wrapping an object. | Adding logging, caching, metrics, validation, or retry logic. |
| Facade | Provide a simple interface over a complex subsystem. | Hiding module internals behind a clean API. |
| Flyweight | Share common state between many similar objects. | Reducing memory usage when creating many small objects. |
| Proxy | Control access to another object through a placeholder. | Lazy loading, access control, caching, remote calls. |
Adapter
Adapter is useful when two interfaces do not match.
For example, our application may expect this interface:
1 | FeedProvider |
But an external provider client may expose a different API:
1 | SportradarClient |
Instead of changing the application to understand every provider API, we can write an adapter:
1 | SportradarAdapter |
The application depends on FeedProvider, and provider-specific details stay inside the adapter.
Adapter pros
- Isolates external API differences.
- Keeps application code independent from provider-specific interfaces.
- Makes integration testing easier.
- Makes replacing a provider less risky.
Adapter cons
- Adds one more layer.
- If the common interface is badly designed, every adapter becomes awkward.
- Debugging sometimes requires jumping between adapter and wrapped object.
Bridge
Bridge separates an abstraction from its implementation.
This is useful when two dimensions can change independently.
For example:
1 | Notification |
If we also have different notification types:
1 | AlertNotification |
Using inheritance for every combination can become messy:
1 | EmailAlertNotification |
Bridge avoids this by separating the high-level abstraction from the implementation:
1 | Notification |
Now notification type and sender implementation can change separately.
Bridge pros
- Avoids class explosion.
- Separates high-level logic from implementation details.
- Makes both sides easier to extend independently.
Bridge cons
- Adds abstraction even when the system may not need it yet.
- Can be harder to understand than direct composition.
- Not useful if there is only one dimension of change.
Composite
Composite is useful for tree-like structures.
It lets the caller treat a single object and a group of objects through the same interface.
For example:
1 | MenuItem |
The caller can call the same operation on both:
1 | render(menu_item) |
This is useful when the structure can be nested.
Composite pros
- Works well for tree structures.
- Lets callers handle single objects and groups uniformly.
- Makes recursive operations easier.
Composite cons
- Can make the model too general.
- Some operations may not make sense for both leaf and group objects.
- Incorrect abstraction can hide important differences.
Decorator
Decorator adds behavior by wrapping an object instead of modifying the original object.
For example, we may have a provider:
1 | FeedProvider |
Then we can wrap it with extra behavior:
1 | MetricsProviderDecorator |
Each wrapper adds one responsibility.
Decorator pros
- Adds behavior without modifying the original object.
- Keeps each behavior small and focused.
- Can combine behaviors flexibly.
- Useful for logging, metrics, retry, validation, and caching.
Decorator cons
- Too many wrappers can make the call stack harder to trace.
- Object construction becomes more complex.
- Order matters, and the wrong order can cause bugs.
Facade
Facade provides a simple interface over a complex subsystem.
For example, push feed may internally include:
1 | ProviderRegistry |
But other modules should not need to know all of these details. They can use a facade:
1 | PushFeedService |
Facade is useful when we want to protect the rest of the system from module complexity.
Facade pros
- Gives a module a clear entry point.
- Reduces coupling between modules.
- Keeps
AppStateor bootstrap code cleaner. - Makes the subsystem easier to understand from outside.
Facade cons
- Can become too large if it starts owning too much logic.
- May hide important behavior if the API is too simple.
- Can become a new god object if responsibilities are not controlled.
Flyweight
Flyweight reduces memory usage by sharing common state between many similar objects.
For example, if many objects repeat the same static data:
1 | SportMetadata |
We may share that common state instead of duplicating it in every object.
Flyweight is usually more relevant when there are many small objects and memory usage matters.
Flyweight pros
- Reduces memory usage.
- Avoids duplicated shared data.
- Useful for large numbers of similar objects.
Flyweight cons
- Adds complexity.
- Shared state must be immutable or carefully controlled.
- Usually unnecessary unless memory pressure is real.
Proxy
Proxy controls access to another object.
The proxy has the same interface as the real object, but it can add behavior before or after forwarding the call.
For example:
1 | CachedProviderProxy |
The proxy can decide:
- Should the real object be created now or later?
- Should this request be cached?
- Is the caller allowed to access the real object?
- Should this call be sent to a remote service?
Proxy pros
- Controls access to expensive or sensitive objects.
- Can support lazy initialization.
- Can add caching or permission checks.
- Keeps the caller using the same interface.
Proxy cons
- Can hide network, cache, or permission behavior.
- Adds another layer to debug.
- If overused, it becomes hard to know when the real object is called.
How to choose
Structural patterns should be chosen based on the relationship problem we actually have.
| Situation | Better pattern |
|---|---|
| External API does not match our interface | Adapter |
| Abstraction and implementation need to vary independently | Bridge |
| Need to treat single objects and groups uniformly | Composite |
| Need to add behavior without modifying the original object | Decorator |
| Need a simple API over a complex subsystem | Facade |
| Need to share repeated state across many objects | Flyweight |
| Need to control access to another object | Proxy |
The main idea is that structure matters. If objects know too much about each other, the system becomes hard to change. Structural patterns give us different ways to compose objects while keeping boundaries clear.
The next article will introduce Behavioral patterns, which focus on how objects communicate and share responsibilities.