Structural Patterns «Prev Next»

Lesson 4Common structural patterns
ObjectiveList and distinguish the common Structural Patterns.

Common Structural Patterns used with Design Patterns

The Gang of Four structural catalog contains Adapter, Bridge, Composite, Decorator, Facade, Flyweight, and Proxy. All seven organize relationships, but each protects a different kind of change. Selecting one by its diagram alone is unreliable because several use an interface and a forwarding object. Start with the design pressure: incompatible interfaces, independent dimensions, part-whole hierarchy, optional responsibility, subsystem complexity, repeated state, or controlled access.

Adapter: Translate an Existing Contract

Adapter translates an existing interface into the interface required by a client.
An Adapter contains interface conversion at one boundary.

Adapter lets a client use an existing class whose interface does not match the client's contract. A Java adapter may convert domain values into a vendor SDK request and translate vendor exceptions back into application errors. This is especially valuable at infrastructure boundaries because it keeps external types out of the domain model. An adapter should preserve meaning, not just rename methods. Unit conversion, null rules, error semantics, and retry safety belong in its tests.

Bridge: Separate Two Dimensions of Change

Bridge separates an abstraction hierarchy from an implementation hierarchy.
A Bridge allows abstraction and implementation to vary independently.

Bridge is appropriate when both the client abstraction and its implementation have meaningful variations. A notification abstraction might offer alerts and digests while implementation objects deliver through email, SMS, or a test channel. Composition avoids a subclass for every combination. Dependency injection can assemble the pair, while contract tests ensure every implementation honors the abstraction's expectations.

Composite: Treat a Part and a Whole Uniformly

Composite gives leaf objects and object groups a common interface.
Composite represents recursive part-whole hierarchies.

Composite defines a common operation for leaves and containers. File systems, graphical scenes, organization trees, and rule groups are familiar examples. Clients can traverse or evaluate the root without branching on every node type. The difficult decisions concern mutation, child ownership, cycle prevention, error aggregation, and whether every operation makes sense for both leaves and containers.

Decorator: Add Responsibilities by Composition

Decorator wraps an object with the same contract and adds one responsibility.
Decorators compose optional behavior without subclass combinations.

A Decorator implements the component contract, stores another component, and adds behavior before or after delegation. Logging, metrics, compression, and validation can be independent layers. Order is part of the design: retry outside a metrics decorator records a different result from metrics outside retry. Decorators must preserve lifecycle and exception contracts, and they should not quietly alter business meaning.

Facade: Present a Focused Subsystem Entry Point

Facade presents a focused interface over several subsystem collaborators.
A Facade reduces the subsystem knowledge required by common clients.

Facade coordinates a useful operation across several subsystem classes. A checkout facade might validate a cart, reserve inventory, authorize payment, and schedule fulfillment. Clients depend on the use case rather than its internal sequence. The facade should not become an unrestricted service locator or a single class containing every business rule. Keep domain rules in the objects that own them and expose cohesive operations.

Flyweight: Share Repeated Intrinsic State

Flyweight replaces duplicated immutable state with shared canonical objects. Each logical item supplies its changing extrinsic state when an operation runs. Text glyphs can share font metrics while retaining position outside the glyph; the course project can share vehicle descriptions while queues retain arrival and lane state. A factory maps canonical keys to instances. The pattern is justified by measurement, because lookup, key construction, and lifecycle management add cost.

Proxy: Control Access Without Changing the Contract

Proxy stands in for another object while preserving its client-facing interface.
A Proxy can manage location, access, lazy creation, or caching.

Proxy represents another object and normally preserves its interface. A virtual proxy delays expensive creation, a protection proxy enforces access, and a remote proxy represents a service in another process. Remote calls add timeouts, partial failure, and serialization, so an in-memory interface alone cannot express the entire operational contract. Security must be enforced at a trusted boundary; a client-side proxy is convenience, not the only control.

Compare Intent Before Choosing

Adapter changes the interface. Proxy controls access while retaining it. Decorator retains the interface and adds a responsibility. Facade defines a simpler interface for a subsystem. Bridge separates two planned dimensions, while Composite represents recursive structure and Flyweight addresses repeated state. These intent statements are better selection criteria than class counts.

Patterns can collaborate. A facade may depend on adapters for external services, receive decorated implementations, and call a proxy. Keep each boundary independently motivated and observable. If a direct function or ordinary composition communicates the design, use it. Pattern vocabulary is valuable only when it makes the reason for a structure clearer.

Selection Questions for Real Code

Begin with the client that experiences the problem. If its required operations already exist under different names or data types, investigate Adapter. If clients are overwhelmed by the coordination of many subsystem objects, consider a Facade. If the same contract needs independent policy layers, Decorator may be appropriate. When the contract must represent something expensive, remote, protected, or cached, examine Proxy and document the new failure semantics.

For model structure, ask different questions. A recursive part-whole model suggests Composite only when clients genuinely perform common operations on leaves and containers. Two independent axes of variation suggest Bridge only when both axes are real, not hypothetical. A high object count suggests Flyweight only after profiling demonstrates repeated state and a meaningful memory cost. This sequence prevents pattern names from becoming solutions in search of problems.

Patterns in Frameworks and Distributed Systems

Frameworks frequently provide these structures indirectly. Java streams use wrapping and delegation, dependency-injection containers assemble bridges and decorators, and persistence frameworks adapt database protocols. Recognizing the intent helps programmers navigate unfamiliar code, but framework annotations do not remove the need to understand ownership, ordering, and errors. Generated proxies are still proxies, and their transaction or interception behavior belongs in tests.

In distributed systems the resemblance to an object pattern has limits. An API gateway can act like a Facade, and a client stub can act like a Proxy, but network boundaries introduce latency, authentication, retries, rate limits, and partial failure. An Adapter around a message broker must define delivery and idempotency semantics. A shared cache resembles Flyweight state but also requires consistency and eviction rules. The classic intent remains useful; the production design must add the operational contract.

Record the chosen pattern in a short decision note. State the pressure, the client contract, ownership, rejected alternatives, and measurable success condition. This evidence lets a future maintainer preserve the useful boundary or remove it when the original pressure disappears. It also gives reviewers a concrete basis for checking that a wrapper preserves behavior instead of merely sharing the expected method signatures under expected production load and failure conditions.

Revisit the decision when the external interface stabilizes, a subsystem shrinks, object counts fall, or framework capabilities replace custom infrastructure. Removing an unnecessary wrapper is also successful design maintenance. The goal is not to maximize the number of patterns in use; it is to keep each important dependency and responsibility easy to locate, test, and change throughout the system's supported lifetime and across team ownership changes.

Choosing Among Similar Wrappers

Adapter, Decorator, and Proxy can all appear as a wrapper that delegates to another object, but their intent is different. Adapter changes the interface that a client sees. Decorator keeps the interface and adds an independently composable responsibility. Proxy keeps the interface while controlling access to the real subject. The correct choice follows from the reason for the extra object, not from the shape of the code.

Facade and Adapter also differ. A Facade offers a convenient, higher-level entry point to a subsystem and may leave the original interfaces available. An Adapter is normally created because an existing interface does not match what a particular client expects. Bridge is different again: it is designed up front to let an abstraction and its implementation vary independently.

Modern Examples and Failure Modes

  • Adapter: translate a cloud provider's storage SDK into a domain-owned object-store interface. Avoid leaking vendor exceptions into the domain.
  • Decorator: add tracing, validation, or retry to a repository. Avoid wrappers whose order silently changes business meaning.
  • Facade: expose one application operation over inventory, pricing, and shipping components. Avoid a god object that accumulates every use case.
  • Proxy: enforce authorization or lazy remote access. Avoid pretending that a network call has the same failure behavior as a local method.
  • Composite: treat individual rules and rule groups uniformly. Avoid cycles and undefined ownership in the object graph.
  • Flyweight: share immutable descriptors across many logical objects. Avoid using it without measurements that show memory pressure.

Generics, dependency injection, and functional programming can reduce boilerplate, but they do not select the pattern. Start with the client contract and the force that makes change difficult. Then use the smallest structure that makes the extension point visible, testable, and safe.

When several patterns appear together, assign each one a narrow responsibility. A Facade may expose an application operation, an Adapter may translate a vendor contract behind it, and a Decorator may add telemetry. Keeping those roles separate makes ordering, ownership, and error translation testable. Combining them in one vaguely named wrapper may save a class initially, but it usually obscures the reason each boundary exists.

Common Structural Patterns - Quiz

Here is a short quiz on the material we have just covered.
Common Structural Patterns - Quiz

SEMrush Software 4 SEMrush Banner 4