--- title: "Structural Design Patterns: Adapter, Decorator, Facade, Proxy, Composite, Bridge, Flyweight" description: "Cross-language guide to the classic structural Gang of Four patterns for composing objects and classes into larger structures, with modern Python, TypeScript, Go, and Rust idioms and honest trade-off analysis for 2026 codebases." language: null framework: null category: design_patterns tags: - design-patterns - structural-patterns - adapter-pattern - decorator-pattern - facade-pattern - proxy-pattern - composite-pattern - best-practices keywords: - adapter pattern vs facade difference - decorator pattern python middleware - proxy pattern lazy loading caching - composite pattern tree structures - bridge pattern abstraction implementation - flyweight pattern memory optimization last_updated: 2026-07-07 difficulty: intermediate related: - ../principles/solid.md - ../principles/clean_code.md - ../principles/dry_kiss_yagni.md - ./creational.md - ./behavioral.md - ../architecture/clean_architecture.md - ../testing/unit_testing_principles.md - ../../languages/python/README.md - ../../languages/python/frameworks/fastapi/best_practices.md search_priority: high status: published --- # Structural Design Patterns ## Introduction / Overview Structural patterns describe how classes and objects are composed to form larger, more capable structures while keeping those structures flexible and efficient. Where [creational patterns](./creational.md) govern *how objects come into existence*, structural patterns govern *how existing objects relate to and wrap one another*. In modern codebases, several of these patterns show up so often that they are barely recognized as "patterns" anymore - middleware pipelines are Decorator, ORMs are Proxy, and most public SDK clients are Facade - which is exactly the point: a good pattern eventually becomes invisible infrastructure. This document covers Adapter, Decorator, Facade, Proxy, Composite, Bridge, and Flyweight, with idiomatic 2026 implementations and clear guidance on when the formal pattern name is worth invoking versus when it is over-engineering a simple wrapper function. **When to use this guidance**: When integrating a third-party or legacy interface, when adding cross-cutting behavior (caching, logging, access control) around existing objects, when simplifying a complex subsystem for callers, or when a class hierarchy is exploding due to conflating two independent dimensions of variation. ## Core Concepts - **Composition over inheritance**: Every pattern in this document favors composing objects at runtime over building rigid class hierarchies at compile time - the same instinct behind [preferring interfaces over concrete coupling](../principles/solid.md#5-dependency-inversion-principle-dip). - **Wrapping vs. translating vs. simplifying**: Adapter *translates* one interface into another it was not designed for; Decorator *adds* behavior while preserving the interface; Facade *simplifies* a complex subsystem behind a narrow interface; Proxy *controls access* to another object while preserving its interface exactly. - **Structure patterns often compose with each other**: A real HTTP client might be an Adapter (translating a third-party SDK) wrapped in a Decorator (adding retry logic) exposed through a Facade (a simple `ApiClient` class used by the rest of the app). - **Two independent axes of variation call for Bridge**: When a class would otherwise need `RedShape`, `BlueShape`, `RedCircle`, `BlueCircle`, etc. (a combinatorial explosion), Bridge splits abstraction from implementation so each axis varies independently. ## Best Practices ### 1. Adapter - Translate an Incompatible Interface Without Modifying It **Rationale**: Legacy systems, third-party SDKs, and generated clients rarely match your application's internal interfaces exactly. Adapter isolates that mismatch in one place instead of letting the foreign interface's shape leak throughout the codebase. ```python from typing import Protocol class PaymentGateway(Protocol): async def charge(self, amount_cents: int, token: str) -> str: ... class LegacyStripeSDK: def create_charge(self, amount: float, currency: str, source: str) -> dict: ... class StripeAdapter: def __init__(self, sdk: LegacyStripeSDK): self._sdk = sdk async def charge(self, amount_cents: int, token: str) -> str: result = self._sdk.create_charge(amount=amount_cents / 100, currency="usd", source=token) return result["id"] ``` **Why this works well in production**: Business logic depends on `PaymentGateway`, never on `LegacyStripeSDK` directly; replacing the SDK version (or the provider entirely) only touches the adapter, satisfying [Dependency Inversion](../principles/solid.md#5-dependency-inversion-principle-dip). ### 2. Decorator - Add Behavior Without Modifying the Original Class **Rationale**: Cross-cutting concerns (caching, retries, logging, authorization checks) should be composable and independent of core logic, per [Single Responsibility](../principles/solid.md#1-single-responsibility-principle-srp). Decorator lets you stack these concerns without subclass explosion. ```typescript interface Repository { findById(id: string): Promise; } class CachingRepositoryDecorator implements Repository { constructor( private readonly inner: Repository, private readonly cache: Map = new Map(), ) {} async findById(id: string): Promise { if (this.cache.has(id)) return this.cache.get(id)!; const result = await this.inner.findById(id); if (result) this.cache.set(id, result); return result; } } // Usage: stack decorators freely const repo: Repository = new CachingRepositoryDecorator(new PostgresUserRepository()); ``` **Why this works well in production**: Python's `@decorator` syntax, FastAPI/Express/ASGI middleware chains, and this object-based form are all the same pattern - adding behavior at a well-defined seam without touching the wrapped class, and each concern (caching, retry, logging) stays independently testable. ### 3. Facade - Provide a Simple Interface to a Complex Subsystem **Rationale**: Client code should not need to understand the internal wiring of five collaborating subsystems (parsing, validation, persistence, notification) just to perform one coherent operation. A Facade exposes the operation, not the machinery. ```python class OrderCheckoutFacade: def __init__(self, inventory: InventoryService, payments: PaymentGateway, shipping: ShippingCalculator, notifier: NotificationSender): self._inventory = inventory self._payments = payments self._shipping = shipping self._notifier = notifier async def checkout(self, cart: Cart, customer: Customer) -> OrderConfirmation: await self._inventory.reserve(cart.items) cost = self._shipping.calculate(cart, customer.address) await self._payments.charge(cart.total_cents + cost, customer.payment_token) await self._notifier.send(customer.email, "Order confirmed") return OrderConfirmation(order_id=cart.id) ``` **Why this works well in production**: A single call site (`await checkout_facade.checkout(cart, customer)`) hides four collaborators from callers such as a FastAPI route handler, matching the ["thin path operation, rich service"](../../languages/python/frameworks/fastapi/best_practices.md#core-concepts) philosophy. ### 4. Proxy - Control Access to an Object Transparently **Rationale**: When you need to add lazy initialization, access control, or remote-call semantics *without changing the interface the caller sees*, Proxy preserves the illusion of talking directly to the real object. ```python class LazyModelProxy: def __init__(self, loader: Callable[[], "MLModel"]): self._loader = loader self._model: "MLModel | None" = None def predict(self, features: list[float]) -> float: if self._model is None: self._model = self._loader() # expensive load deferred until first use return self._model.predict(features) ``` **Why this works well in production**: Callers use `proxy.predict(...)` exactly as they would the real `MLModel`; the expensive load cost is deferred and paid only once, transparently. The same shape underlies ORM lazy-loaded relationships and remote-procedure-call stubs (gRPC clients are Proxies to a remote service). ### 5. Composite - Treat Individual Objects and Groups Uniformly **Rationale**: Tree-shaped domains (file systems, UI component trees, organizational hierarchies, nested permission groups) are far easier to reason about when a leaf node and a group of nodes expose the same interface. ```go type FileSystemNode interface { Size() int64 } type File struct{ size int64 } func (f File) Size() int64 { return f.size } type Directory struct{ children []FileSystemNode } func (d Directory) Size() int64 { var total int64 for _, child := range d.children { total += child.Size() } return total } ``` **Why this works well in production**: Callers computing `.Size()` do not need to know or care whether they hold a single `File` or an arbitrarily deep `Directory` tree - recursion is encapsulated inside `Directory` itself. ### 6. Bridge - Decouple an Abstraction From Its Implementation **Rationale**: When a hierarchy varies along two independent dimensions (e.g., *what* operation to perform and *which backend* performs it), Bridge prevents an (M × N) explosion of subclasses. ```python class RenderBackend(Protocol): def draw_pixel(self, x: int, y: int) -> None: ... class SvgBackend: def draw_pixel(self, x: int, y: int) -> None: ... class CanvasBackend: def draw_pixel(self, x: int, y: int) -> None: ... class Shape: def __init__(self, backend: RenderBackend): self._backend = backend # the "bridge" to the implementation class Circle(Shape): def draw(self, radius: int) -> None: for x, y in circle_points(radius): self._backend.draw_pixel(x, y) ``` **Why this works well in production**: Adding a new shape (`Square`) or a new backend (`WebGpuBackend`) each requires exactly one new class, not a new class per combination. ### 7. Flyweight - Share Common State Across Many Fine-Grained Objects **Rationale**: When an application needs to create very large numbers of similar objects (glyphs in a text renderer, tiles in a game map, tokens in a large parse tree), separating shared *intrinsic* state from per-instance *extrinsic* state can cut memory usage by orders of magnitude. ```python class GlyphStyle: # intrinsic, shared, immutable __slots__ = ("font", "size", "color") def __init__(self, font: str, size: int, color: str): self.font, self.size, self.color = font, size, color class GlyphStyleFactory: _cache: dict[tuple, GlyphStyle] = {} @classmethod def get(cls, font: str, size: int, color: str) -> GlyphStyle: key = (font, size, color) if key not in cls._cache: cls._cache[key] = GlyphStyle(font, size, color) return cls._cache[key] # Each on-screen character stores only (char, x, y, style_reference) -- # thousands of characters share a handful of GlyphStyle instances. ``` **Why this works well in production**: Memory scales with the number of *distinct* styles, not the number of glyphs on screen - critical for rendering engines, large-scale simulations, and any system generating millions of small, mostly-identical objects. ## Common Pitfalls & Anti-Patterns ### Pitfall: Confusing Adapter With Facade **Problem**: Adapter and Facade both "wrap" something, leading developers to use them interchangeably, which confuses intent for future readers. **Recommended approach**: Adapter exists because the wrapped interface is *incompatible* (translation). Facade exists because the wrapped subsystem is *complex* (simplification), even if every underlying interface is already compatible. Name classes accordingly (`StripeAdapter` vs `CheckoutFacade`) so intent is unambiguous. ### Pitfall: Decorator Chains That Obscure Control Flow **Problem**: Stacking six decorators (`RetryDecorator(CachingDecorator(LoggingDecorator(TimeoutDecorator(...))))`) makes it hard to reason about ordering (does caching happen before or after retry?), especially when failures occur. **Recommended approach**: Keep decorator order explicit and documented at the composition root; prefer a small, named number of standard "stacks" over ad hoc per-call-site compositions. Middleware frameworks (ASGI, Express) solve this by making ordering an explicit, visible list. ### Pitfall: Proxy Used to Silently Hide Expensive or Failing Operations **Problem**: A caching or remote Proxy that swallows errors or silently serves stale data can violate the "errors must never pass silently" rule and surprise callers who believe they are talking to the real object. **Recommended approach**: Preserve the real object's error semantics faithfully; if the proxy changes behavior (e.g., returns stale cache on a timeout), that must be an explicit, documented policy - not an accidental side effect. ### Pitfall: Composite Applied to Non-Recursive Data **Problem**: Forcing a flat list of unrelated items into a Composite hierarchy "for consistency" adds indirection with no recursive structure to justify it. **Recommended approach**: Reserve Composite for genuinely tree-shaped domains; a flat collection should just be a flat collection. ## Testing Strategies - **Adapter**: Test against the adapter's own interface using a fake of the wrapped SDK; a separate contract test can verify the adapter still matches the real SDK's actual behavior (useful after third-party SDK upgrades). - **Decorator**: Test each decorator in isolation with a fake "inner" implementation, asserting only the added behavior (e.g., the caching decorator returns a cached value without calling the inner repository twice). - **Facade**: Integration-test the facade's public method with fakes/mocks for each collaborator; this is a natural boundary for [integration tests](../architecture/clean_architecture.md) as opposed to full end-to-end tests. - **Proxy**: Test lazy/deferred behavior explicitly (assert the real object is not constructed until first use) and test that error/timeout semantics match documented policy. - **Composite**: Property-based tests (e.g., Hypothesis) work well here - generate arbitrary trees and assert invariants (`total size == sum of leaf sizes`) hold regardless of shape. - See [Unit Testing Principles](../testing/unit_testing_principles.md) for isolation techniques applicable across all of the above. ## Performance, Security & Scalability Considerations - **Decorator/Proxy indirection cost**: Each layer adds a function call; in Python this is usually negligible next to I/O, but in tight numerical loops (Rust, Go), excessive layering can matter - profile before stacking many decorators in a hot path. - **Facade as a security boundary**: A well-designed Facade is a natural place to enforce authorization checks once, rather than scattering them across every subsystem call - reducing the chance of a missed check. - **Proxy caching correctness under concurrency**: Caching proxies must handle concurrent first-access races (multiple coroutines/goroutines triggering the expensive load simultaneously) with a lock or single-flight pattern to avoid duplicate work or thundering-herd effects. - **Flyweight and memory scalability**: Flyweight is one of the few patterns whose primary justification is a measured memory constraint; do not apply it speculatively - verify with a profiler that per-object overhead is actually a bottleneck before adding the shared-state indirection. - **Adapter and third-party API changes**: Isolating third-party SDKs behind an Adapter significantly reduces the blast radius of upstream breaking changes, a meaningful operational/scalability concern as external APIs evolve. ## Edge Cases & Advanced Usage - **Adapter chains for multi-version SDK support**: When supporting two versions of an upstream API simultaneously (migration period), a version-selecting Adapter factory can route to `V1Adapter` or `V2Adapter` behind one stable internal interface. - **Structural Proxy vs. dynamic language proxies**: Python's `__getattr__`, JavaScript's `Proxy` object, and Go's interface embedding all offer language-level proxy mechanisms; prefer the explicit class-based Proxy pattern shown above when behavior (caching, access control) needs to be reasoned about clearly, and reserve dynamic proxies for generic tooling (e.g., ORMs, mocking libraries). - **Composite with heterogeneous operations**: When different node types need genuinely different behavior beyond the shared interface, combine Composite with [Visitor](./behavioral.md#visitor) to add operations without polluting every node class. - **Bridge in cross-platform codebases**: Bridge is the standard shape behind cross-platform abstractions (e.g., a shared UI abstraction with platform-specific rendering backends, or a storage abstraction with local/cloud backend implementations). ## When *Not* to Use / Alternatives | Situation | Recommendation | |---|---| | Wrapped interface is already compatible, just verbose | A simple wrapper function may suffice; skip formal Adapter machinery | | Only one cross-cutting concern, applied in one place | A plain function wrapper or single `if` check may be simpler than a Decorator class | | Subsystem has only one or two collaborators | Facade may be unnecessary ceremony; call collaborators directly | | Object construction is cheap and side-effect-free | Skip Proxy's lazy-loading machinery | | Data is genuinely flat, not hierarchical | Skip Composite; use a plain list/array | | Only one implementation will ever exist for an abstraction | Skip Bridge; a single class is simpler | | Object count is small (thousands, not millions) | Skip Flyweight; the memory savings do not justify the complexity | ## Related Documentation - [SOLID Principles in Modern Software Design](../principles/solid.md) - composition-over-inheritance and interface segregation underpin every pattern here. - [Clean Code Principles](../principles/clean_code.md) - naming conventions for adapters, decorators, and facades that keep intent clear. - [DRY, KISS, and YAGNI](../principles/dry_kiss_yagni.md) - guidance on when the ceremony of a formal pattern is justified. - [Creational Design Patterns](./creational.md) - patterns often producing the objects these structural patterns then wrap. - [Behavioral Design Patterns](./behavioral.md) - frequently composed with structural patterns (e.g., Composite + Visitor). - [Clean Architecture](../architecture/clean_architecture.md) - Adapter is the standard shape for the "interface adapters" layer. - [Unit Testing Principles](../testing/unit_testing_principles.md) - isolating and testing wrapped/decorated components. - [FastAPI Best Practices](../../languages/python/frameworks/fastapi/best_practices.md) - middleware and dependency layering as real-world Decorator/Facade usage. ## Glossary - **Intrinsic State**: Shared, immutable state factored out in the Flyweight pattern (e.g., font and color of a glyph). - **Extrinsic State**: Per-instance state that cannot be shared (e.g., a glyph's screen position). - **Composition Root**: The place where wrapped/decorated object graphs are assembled, typically near application startup. - **Contract Test**: A test verifying that an Adapter's behavior still matches the real external system it wraps, distinct from a unit test of the adapter's internal logic. - **Single-Flight**: A concurrency pattern ensuring only one concurrent caller triggers an expensive operation while others wait for its result, often used inside caching Proxies.