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
2
3
4
5
6
Application
-> Service
-> ProviderClient
-> Cache
-> Mapper
-> MetricsReporter

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
2
3
FeedProvider
-> connect()
-> next_event()

But an external provider client may expose a different API:

1
2
3
SportradarClient
-> open_stream()
-> read_message()

Instead of changing the application to understand every provider API, we can write an adapter:

1
2
3
SportradarAdapter
-> implements FeedProvider
-> wraps SportradarClient

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
2
3
4
Notification
-> EmailSender
-> SmsSender
-> SlackSender

If we also have different notification types:

1
2
3
AlertNotification
ReportNotification
ReminderNotification

Using inheritance for every combination can become messy:

1
2
3
4
5
6
EmailAlertNotification
SmsAlertNotification
SlackAlertNotification
EmailReportNotification
SmsReportNotification
SlackReportNotification

Bridge avoids this by separating the high-level abstraction from the implementation:

1
2
Notification
-> sender: NotificationSender

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
2
3
4
5
MenuItem
MenuGroup
-> MenuItem
-> MenuItem
-> MenuGroup

The caller can call the same operation on both:

1
2
render(menu_item)
render(menu_group)

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
2
3
MetricsProviderDecorator
-> RetryProviderDecorator
-> SportradarAdapter

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
2
3
4
5
ProviderRegistry
FeedProvider adapters
RetryPolicy
StatusReporter
EventPublisher

But other modules should not need to know all of these details. They can use a facade:

1
2
3
4
PushFeedService
-> start()
-> stop()
-> status()

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 AppState or 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
2
3
4
SportMetadata
-> sport_name
-> league_name
-> provider_mapping

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
2
CachedProviderProxy
-> RealProviderClient

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.

Reference