Structural Design Patterns

Structural design patterns (GoF) compose classes and objects into larger structures without leaking every part’s internals. Use them when the shape of the object graph is the problem — wrapping, tree-shaped parts, or “make this look like that.” They are not reliability tactics and not architectural patterns.

The set that actually shows up

  • Adapter — wrap an existing type so it matches the interface you own. Prefer this over rewriting a third-party SDK. Classic: your IClock over DateTime.UtcNow in tests.
  • Decorator — add behavior by wrapping the same interface (logging, retry, auth). Stack is explicit. Do not subclass just to add a cross-cutting concern.
  • Facade — a coarse API over a messy subsystem. Good at a bounded-context edge. Bad if it becomes a god object that knows every table.
  • Composite — treat a tree of parts as one part (UI widgets, expression trees, file/folder). Requires a shared operation that is valid for leaf and node.
  • Proxy — stand-in with the same interface (lazy load, remote stub, access check). Virtual proxy vs remote proxy vs protection proxy are different forces; name which one.
  • Bridge — split abstraction from implementation so both can vary (shape × renderer). Rare; if you only have one implementation, you wanted a strategy or a plain interface.
  • Flyweight — share immutable intrinsic state (glyphs, tiles). Profile first; interned strings already do this for you in many runtimes.

Example: decorator, not subclass

interface IOrders { Order Get(Id id); }

sealed class TracingOrders(IOrders inner, ILogger log) : IOrders
{
    public Order Get(Id id)
    {
        log.LogInformation("get {Id}", id);
        return inner.Get(id);
    }
}

TracingOrders is still an IOrders. Tests can skip it. A subclass of SqlOrders would couple tracing to SQL.

What breaks it

A subclass that logs from inside SqlOrders couples tracing to SQL. The decorator above implements the same interface, so a test can skip it.

class SqlOrders : IOrders
{
    public virtual Order Get(Id id) => new(id);
}

class TracingSqlOrders : SqlOrders
{
    public override Order Get(Id id)
    {
        // The log call is stuck to the SQL class. A test of Get now needs the logger.
        return base.Get(id);
    }
}

Do not confuse

  • Adapter vs facade — adapter matches an interface you already have; facade invents a simpler one.
  • Decorator vs proxy — decorator adds responsibility; proxy controls access or lifetime. Same wrapping shape, different intent.
  • Composite vs inheritance trees — composite is a runtime tree of parts, not a class hierarchy of “is-a”.

If the problem is “this call might fail” or “this message is huge,” you want reliability or messaging patterns, not GoF structural ones.