Structural Patterns «Prev Next»

Lesson 2What is a Structural Pattern?
ObjectiveDefine Structural Patterns.

What is a Structural Pattern?

A structural design pattern explains how classes and objects can be connected to form a larger unit without forcing clients to understand every participant. The subject is the boundary between components: which interface a client sees, which object performs the work, and where variation may occur. A successful structure contains change. Replacing a vendor library, adding a responsibility, or substituting a remote service should affect a deliberate integration point instead of spreading conditionals and conversions throughout the application.

This definition is narrower than general software architecture. A package diagram or deployment topology can describe structure, but a Gang of Four pattern names a recurring object-level collaboration with known consequences. The pattern is also more than a data structure. A tree records how nodes are arranged; Composite adds a common contract that lets a client invoke an operation on either one leaf or an entire subtree. Structural patterns define both the relationships and the behavior that crosses them.

The Seven Gang of Four Structural Patterns

The catalog contains seven structural patterns. Each responds to a different design pressure, so their shared category does not make them interchangeable.

  1. Adapter translates an existing interface into the contract a client requires.
  2. Bridge separates an abstraction from an implementation when both dimensions must evolve independently.
  3. Composite gives individual objects and object groups a uniform part-whole interface.
  4. Decorator layers optional responsibilities around one object while preserving its contract.
  5. Facade offers a focused entry point to a broader or more complicated subsystem.
  6. Flyweight shares reusable intrinsic state when numerous logical objects would otherwise duplicate it.
  7. Proxy stands in for another object to control creation, location, access, or communication.

Several of these patterns use a wrapper, yet their intent distinguishes them. An Adapter changes the interface, a Decorator adds responsibility, and a Proxy usually preserves the interface while controlling access. Facade also presents an interface, but it represents a subsystem rather than one wrapped peer. Naming the intent prevents a class called Wrapper from hiding an important architectural decision.

Composition, Delegation, and Dependency Direction

Most modern structural implementations favor object composition. A client depends on an interface, while a containing object delegates part of its work to another implementation. This arrangement is a has-a relationship, not necessarily an is-a relationship. The delegate can be supplied by a constructor, selected from configuration, or replaced by a test double. Shallow inheritance can still express a stable type family, but inheritance alone fixes more of the structure at compile time and can expose protected implementation details to subclasses.

Dependency direction matters as much as the class diagram. Domain code should own the contract that represents its need. Infrastructure code can then adapt a database, framework, or external API to that contract. Reversing this direction causes business rules to depend on volatile technology types. Dependency injection supports the preferred direction by making construction explicit, but a container does not choose the boundary automatically. The design remains the programmer's responsibility.

Structural Correctness Is Behavioral

A wrapper is correct only if it preserves the promises clients rely on. Besides method names and types, those promises may include exception behavior, ordering, thread safety, identity, transaction boundaries, and resource ownership. For example, a caching Proxy that returns stale values violates a freshness requirement even though it implements the right Java interface. A Decorator that closes a shared stream too early changes lifecycle semantics. An Adapter that retries a non-idempotent operation may perform it twice.

Tests should therefore exercise the contract through each interchangeable implementation. A contract test can run against the original object, an adapter, and a proxy. Focused tests can then verify the wrapper's added policy, such as cache expiration, authorization failure, or metrics recording. Structural patterns improve testability when seams are intentional; excessive layers make tests harder when every call must traverse unrelated indirection.

Structural Intent: Stable Collaboration Across Change

A structural pattern is useful when the difficult part of a design is not an individual algorithm, but the way several classes, objects, or services must fit together. The pattern gives the collaboration a name and identifies which interfaces should remain stable while implementations change. This distinction matters in a long-lived system. A client should depend on a small contract, not on the internal class graph, network protocol, storage engine, or vendor library that happens to satisfy that contract today.

Consider an application that sends notifications. The application may begin with an in-process email component, then add a cloud messaging service and a test double. An Adapter can translate a vendor SDK into the application's notification interface. A Decorator can add retry, metrics, or auditing without changing the adapted component. A Proxy can delay a network call, apply access control, or cache a response. These patterns solve different problems, but each preserves a useful client-facing boundary.

Class Structure and Object Structure

The Gang of Four distinguish class patterns from object patterns. Class patterns rely heavily on inheritance and therefore establish relationships at compile time. Object patterns rely on composition and delegation, so the participants can often be selected or replaced at runtime. Modern Java and C++ designs usually prefer composition when the relationship represents a capability that may vary. Inheritance remains appropriate when there is a genuine substitutable type relationship and the base contract is stable.

Generics make structural contracts more precise. Dependency injection makes collaborators explicit and replaceable. Lambdas can remove a class that exists only to represent one small operation. None of these language features makes structural reasoning obsolete. They change the implementation cost. The architect must still decide where adaptation belongs, which state is shared, who owns object lifetime, and whether a wrapper preserves every semantic promise of the wrapped component.

Structural Patterns in Cloud-Native Systems

In a distributed system, an object boundary may become a process or network boundary. A Facade may be implemented as an application service or API gateway that offers a smaller use-case-oriented interface over several subsystems. A Proxy may become a client stub, service mesh sidecar, or policy-enforcement component. An Adapter may isolate a domain model from a message broker, database driver, identity provider, or external REST API.

The resemblance should not be treated as proof that two architectures are identical. A remote call can fail, time out, retry, or complete more than once. Those behaviors do not exist in an ordinary in-memory delegation call. Therefore, a network-facing structural pattern must document latency, idempotency, authentication, observability, and partial failure. The classic pattern supplies the collaboration vocabulary, while distributed-systems engineering supplies the operational contract.

A Practical Selection Test

Before applying a structural pattern, state the pressure in one sentence. Examples include: the client interface cannot change, object creation is too expensive, a subsystem exposes too much detail, or responsibilities must be combined independently. Then identify the smallest pattern that directly addresses that pressure. If a simple function or direct composition already communicates the design, use it.

  1. Write the client contract and the behavior it must preserve.
  2. Identify the implementation detail that is likely to vary.
  3. Choose who owns creation, state, cleanup, and error translation.
  4. Test substitution with a real implementation and a test double.
  5. Measure any performance claim instead of assuming the pattern is faster.

This process keeps the pattern subordinate to the design problem. The result is not merely a recognizable class diagram. It is a structure that explains why change is contained and how clients remain correct.

Document the decision in terms that can be verified later. Record the rejected alternatives, the interface clients depend on, and the behavior wrappers must preserve. Include ownership and lifecycle rules for in-process objects, plus timeouts and failure translation for remote collaborators. This short design record prevents a recognizable pattern name from becoming a substitute for reasoning. It also gives maintainers a testable basis for removing the pattern if the original pressure disappears.

The best structural design makes its dependency direction obvious in both source code and deployment documentation.

Code review should ask whether the collaboration can be explained without reciting its implementation. The answer should identify the client, the stable contract, the object being adapted or composed, and the reason for the extra boundary. It should also state what must not change. If reviewers cannot find that information, the pattern may be accidental complexity rather than an architectural aid.

Structural choices also influence operations. A local wrapper adds call-stack depth, while a remote proxy adds latency and new failure modes. Shared flyweights need capacity and eviction policies. Facades can become bottlenecks if every use case is forced through one oversized interface. Useful telemetry therefore follows the boundary: record duration, failures, cache behavior, and resource use where they reveal whether the original design pressure is still present. Measurements turn a pattern decision into a maintainable engineering choice rather than a permanent assumption.

The objective is a collaboration whose purpose remains visible when the original author is absent. Clear names, narrow interfaces, contract tests, and measured consequences make that purpose durable across releases and during real production change cycles.

That clarity reduces accidental coupling during maintenance.