Skip to main content

Structural Design Patterns

What Are Structural Patterns?​

Structural patterns deal with how classes and objects are composed to form larger structures. They focus on the relationships between entities β€” making it easy to change or extend functionality without rewriting everything. Think of them as the glue that holds your architecture together.

Where creational patterns answer "how do we build objects?", structural patterns answer "how do we wire them together?"

Why They Matter​

ProblemPattern That Solves It
Incompatible interfaces need to work togetherAdapter
Want to decouple abstraction from implementation so both can vary independentlyBridge
Need to treat individual objects and compositions uniformlyComposite
Want to add responsibilities to objects dynamically, without subclassingDecorator
Need a simplified interface to a complex subsystemFacade
Need to share common state across many similar objects to save memoryFlyweight
Need a placeholder to control access to another objectProxy

The Coffee Shop Domain​

All examples in this section continue with the Coffee Shop domain used in creational patterns. We build on the same core model:

// Base product β€” carried over from creational patterns
public abstract class Coffee
{
public string Name { get; init; } = "";
public double Price { get; init; }
public List<string> Ingredients { get; init; } = new();
public CupSize Size { get; init; } = CupSize.Medium;

public override string ToString() =>
$"{Name} ({Size}) β€” ${Price:F2} [{string.Join(", ", Ingredients)}]";
}

public enum CupSize { Small, Medium, Large }

Pattern Comparison​

PatternIntentWhen to Use
AdapterConvert one interface into anotherThird-party integration, legacy code wrapping
BridgeDecouple abstraction from implementationMulti-dimensional feature variations (platform + format)
CompositeTree structure of objectsMenus, file systems, organization charts
DecoratorAdd behavior dynamicallyToppings, middleware, streaming wrappers
FacadeSimplified interface to a subsystemComplex APIs, service orchestration
FlyweightShare fine-grained objects efficientlyLarge numbers of similar objects (game objects, text characters)
ProxyControl access to another objectLazy loading, access control, caching, logging

Common Pitfalls​

  • Adapter overuse: Don't create adapters for every class. Use one when you genuinely need to bridge incompatible interfaces.
  • Decorator explosion: Too many decorators can make debugging hard β€” the call stack becomes deeply nested. Keep the chain short and meaningful.
  • Facade as god object: A facade simplifies access, not ownership. It should delegate, not contain business logic.
  • Flyweight premature optimization: Only use Flyweight when you've measured memory issues. Don't optimize speculatively.