Skip to main content

Structural Design Patterns

The seven GoF structural patterns describe how to compose classes and objects into larger structures. Each controls what the caller sees (the interface) and what actually executes behind it. They enforce Software Design Principles — SRP, OCP — through composition rather than inheritance. See also: Creational Design Patterns, Behavioral Design Patterns, Deep Modules

Agent Trigger

Apply when: Composing or adapting objects/interfaces — Adapter, Decorator, Facade, Proxy, Composite, Bridge, or Flyweight. Rule of thumb: Pick by intent — Adapter converts, Facade simplifies, Proxy controls access, Decorator adds behavior.

Adapter

Intent: Convert an incompatible interface into one the client expects — without modifying either side. When to use:
  • Integrating a third-party or legacy class whose interface you cannot change.
  • Reusing existing subclasses that share behavior but differ in interface.
  • Translating between data formats (e.g., XML → JSON) at a boundary.
When NOT to use:
  • You control both sides — just align the interfaces directly.
  • The mismatch is so deep that the adapter becomes a full reimplementation (consider a new service instead).
Structure:
Anti-patterns:
  • Wrapping every class “just in case” — Adapter is a boundary tool, not a default abstraction.
  • Class adapter via multiple inheritance: TypeScript lacks it; prefer object composition.
  • Bidirectional adapters: almost always a sign the design needs rethinking.

Bridge

Intent: Decouple an abstraction from its implementation so both can vary independently. When to use:
  • A class hierarchy is growing across two orthogonal dimensions (e.g., shapes × renderers, remotes × devices).
  • You want to switch implementations at runtime.
  • Designed up-front when you know two axes of variation will coexist long-term.
When NOT to use:
  • Only one dimension varies — Bridge adds unnecessary indirection.
  • The class is already cohesive; splitting it creates more complexity than it removes.
Structure:
Anti-patterns:
  • Confusing Bridge with Strategy — Bridge separates structural hierarchies at design time; Strategy swaps algorithms at runtime within one object.
  • Applying Bridge reactively to an existing monolith without first identifying the two independent dimensions.

Composite

Intent: Compose objects into tree structures and treat leaves and containers uniformly through a common interface. When to use:
  • The domain naturally forms a recursive hierarchy (filesystem, UI component tree, org chart, AST).
  • Client code should not distinguish between single elements and collections of elements.
  • Operations need to propagate recursively (render, calculate, validate).
When NOT to use:
  • The data is not actually recursive — force-fitting a flat list into Composite adds noise.
  • Leaf and container operations diverge so much that a shared interface becomes meaningless.
Structure:
Anti-patterns:
  • Putting add/remove on the Component interface — leaves must throw or no-op, violating ISP. Keep child management on Container.
  • Composite with a single level of nesting — just use an array.

Decorator

Intent: Attach additional responsibilities to an object at runtime by wrapping it in another object that shares the same interface. When to use:
  • You need combinatorial behavior without a subclass explosion (logging + caching + auth on a handler).
  • Behavior should be stackable and independently removable at composition time.
  • The class is final or you cannot modify it.
When NOT to use:
  • Order of decorators matters in non-obvious ways — prefer an explicit pipeline.
  • You only need one variation — a simple subclass is clearer.
Structure:
Anti-patterns:
  • Stateful decorators that depend on their position in the stack — behavior becomes order-sensitive and fragile.
  • Deep decorator chains for a single cross-cutting concern — collapse into one class.
  • Confusing Decorator with Proxy: Decorator adds behavior the client explicitly composed; Proxy controls access that the client doesn’t manage.

Facade

Intent: Provide a simplified, opinionated interface to a complex subsystem. When to use:
  • Integrating a framework with many moving parts where clients need only a small slice.
  • Isolating the rest of the codebase from a third-party dependency so a swap changes only the facade.
  • Layering a subsystem — each layer gets a facade; layers communicate through facades only.
When NOT to use:
  • Clients need the full subsystem power — a facade that re-exposes everything is just indirection.
  • The subsystem has only 2-3 classes — the facade adds a maintenance layer for nothing.
Structure:
Anti-patterns:
  • God-object facade that owns all subsystem logic — it becomes the monolith you were hiding.
  • Facade that leaks subsystem types through its return values — callers become coupled to internals anyway.
  • Multiple facades for the same subsystem with overlapping responsibilities.

Flyweight

Intent: Share the immutable (intrinsic) state among many fine-grained objects, passing mutable (extrinsic) state in per-operation calls, to reduce memory consumption. When to use:
  • The app creates enormous numbers of similar objects (particles, glyphs, map tiles) that exhaust available RAM.
  • Objects can be cleanly split into intrinsic (shared, immutable) and extrinsic (per-instance, mutable) state.
  • Profiling confirms the memory problem exists — apply this pattern deliberately, not speculatively.
When NOT to use:
  • Object count is small — the factory and split state add complexity for no measurable gain.
  • State cannot be cleanly separated — you end up passing most state as arguments anyway.
Structure:
Anti-patterns:
  • Making flyweights mutable — shared state becomes a data-race source; flyweights must be immutable after construction.
  • Using Flyweight without a factory — callers manually manage the pool and create duplicate instances.
  • Treating it as a general caching mechanism — Flyweight is specifically about object identity sharing for memory reduction.

Proxy

Intent: Provide a drop-in substitute that controls access to the real object — adding lazy init, caching, access control, logging, or lifecycle management transparently. When to use:
  • Lazy initialization of a heavyweight resource you don’t always need (virtual proxy).
  • Access control — only authorized callers reach the real service (protection proxy).
  • Caching repeated identical calls (caching proxy).
  • Logging or auditing every call without touching the service class (logging proxy).
  • Hiding network call complexity behind a local interface (remote proxy).
When NOT to use:
  • You only need to intercept one method — a simple wrapper function is less indirection.
  • The pre/post logic changes what the interface means for the caller — use Decorator instead (client-controlled composition, not infrastructure interception).
Structure:
Anti-patterns:
  • Proxy that changes the observable behavior of the interface — that’s Decorator.
  • Combining caching + logging + auth into one proxy class — compose separate proxies or extract middleware.
  • Proxy that swallows errors silently — control access, don’t hide failures from callers.

Confusion Table: Adapter vs Facade vs Proxy

These three all wrap something. The differences are precise: Quick rule: need a different interface → Adapter. Need a simpler interface to a subsystem → Facade. Need the same interface with interception → Proxy.

Pattern Selection Signals