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
IClockoverDateTime.UtcNowin 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.