CHAPTER 4 · DESIGN PATTERNS

Structural Design Patterns

Creational patterns (Chapter 3) were about making objects. Structural patterns are about composing them — how classes and objects fit together into larger structures that stay flexible, so you can add capabilities or swap parts without rewriting everything. Seven patterns, one big idea: prefer wiring small objects together over building one giant rigid class.

The big idea

A structural pattern answers the question: "I have several classes/objects — how do I arrange them so the whole is flexible?" The Gang of Four split these into two flavours:

🧠 Why this matters

Almost every structural pattern here is a specific, disciplined way of applying two principles you met in Chapter 2: composition over inheritance (hold an object instead of subclassing) and the Open/Closed Principle (extend by adding new small classes, not by editing existing ones). If those two ideas feel solid, these patterns are just named recipes for them.

Jump to a pattern:

ℹ️ A heads-up on the word "wrapper"

Five of these patterns — Adapter, Bridge, Decorator, Proxy, Facade — all work by holding another object and forwarding calls to it. So they can look identical in code. What separates them is intent, not shape. We call that out for each pattern, and there's a full disambiguation at the end of this page and in Chapter 12.

The seven structural patterns

Adapter structural a.k.a. Wrapper

Intent — one line: convert the interface a class has into the interface a client expects, so two things that were never designed to work together suddenly can.

🌍 Real-world analogy

You fly from Sri Lanka to the UK with a phone charger that has the wrong plug. The wall socket and your plug both do their job perfectly — they just don't fit. A travel adapter sits between them: it exposes the shape the UK socket expects on one side, and accepts your plug on the other. Neither the socket nor your charger changes. That's exactly the pattern.

The problem

You're building a stock-market monitoring app. It downloads quotes in XML and draws charts. Later you buy a slick third-party analytics library to spot trends — but it only accepts JSON. You can't edit the library: you don't have its source, and changing it would break everyone else who uses it. Your XML data and the library simply speak different languages.

❌ Naive: try to force them together
// The analytics library, which you cannot change:
public class SmartAnalytics
{
    public string Analyze(IJsonSource source) // wants JSON
        => $"Trend for {source.GetJson()}";
}

// Your existing data source speaks XML:
public class XmlStockFeed
{
    public string GetXml() => "<stock price='142.5'/>";
}

var feed = new XmlStockFeed();
var analytics = new SmartAnalytics();
analytics.Analyze(feed); // ✖ won't compile:
                         //   XmlStockFeed is not an IJsonSource
Your only "fixes" are both bad: rewrite the closed library, or scatter XML→JSON conversion code across every call site.
✅ With Adapter
// One small class that IS a JSON source
// but WRAPS the XML feed and translates.
public class XmlToJsonAdapter : IJsonSource
{
    private readonly XmlStockFeed _feed;
    public XmlToJsonAdapter(XmlStockFeed feed) => _feed = feed;

    public string GetJson()
    {
        string xml = _feed.GetXml();      // ask adaptee
        // ...real code parses XML here...
        return "{ \"price\": 142.5 }";    // hand back JSON
    }
}

var adapter = new XmlToJsonAdapter(new XmlStockFeed());
analytics.Analyze(adapter); // ✔ library is happy
The library, the feed, and every call site stay untouched. One new class absorbs the mismatch. That's the Open/Closed Principle in action.
💡 The one-sentence insight

An adapter implements the interface the client wants and holds a reference to the object that has the wrong interface, translating every call between them.

Solution — the full cast

Four roles: the Target (interface the client wants — IJsonSource), the Adaptee (the existing, incompatible class — XmlStockFeed), the Adapter (translator), and the Client (SmartAnalytics). Now — there are two ways to build the adapter, and the slides call this out explicitly.

The adapter holds an adaptee and delegates to it. This is the version above, and the one you should reach for by default — it's the "object" flavour of the pattern.

public interface IJsonSource            // Target
{
    string GetJson();
}

public class XmlStockFeed               // Adaptee (unchangeable)
{
    public string GetXml() => "<stock price='142.5'/>";
}

public class XmlToJsonAdapter : IJsonSource   // Object adapter
{
    private readonly XmlStockFeed _feed;      // ← composition
    public XmlToJsonAdapter(XmlStockFeed feed) => _feed = feed;
    public string GetJson() => Translate(_feed.GetXml());

    private static string Translate(string xml) => "{ \"price\": 142.5 }";
}
💡 Why prefer this

Because it holds the adaptee by reference, one object adapter can adapt an adaptee and all its subclasses, and you can even swap the adaptee at run-time.

The GoF "class adapter" inherits from both the target and the adaptee at once — so it needs multiple inheritance of classes, which only languages like C++ allow.

⚠️ C# can't do the textbook version

C# forbids inheriting from two classes. But — because the Target is an interface, C# gives you a legitimate hybrid: inherit the adaptee class and implement the target interface. You get the adaptee's methods "for free" (no wrapping field), which is the spirit of a class adapter.

// Hybrid "class adapter": inherit the adaptee, implement the target.
public class XmlToJsonClassAdapter : XmlStockFeed, IJsonSource
{
    // GetXml() is inherited directly — no field, no delegation object.
    public string GetJson() => Translate(GetXml());

    private static string Translate(string xml) => "{ \"price\": 142.5 }";
}

Downside: you're now permanently bound to XmlStockFeed as a base class, and you can't adapt its subclasses selectively. That rigidity is exactly why the object adapter is usually the better call.

Client SmartAnalytics Target IJsonSource.GetJson() Adapter XmlToJsonAdapter Adaptee XmlStockFeed.GetXml() uses wraps & translates →
Figure: the Adapter implements the Target the client expects, and wraps the Adaptee that has the wrong interface. Dashed triangle = "implements"; the arrow to the Adaptee = delegation.
Try it: run analytics on an XML feed, with and without the adapter
What to notice: without the adapter the JSON-only library simply can't accept the XML feed. The adapter changes nothing about the feed or the library — it just sits between them and translates on each call.

When to use it — and the costs

Reach for Adapter when…
  • You want to use an existing class but its interface doesn't match what you need.
  • You're integrating a third-party / legacy library you can't modify.
  • You need several existing subclasses to cooperate and subclassing each one is impractical (use an object adapter — it adapts the parent and its children).
Costs / drawbacks
  • More classes and one extra layer of indirection.
  • Translation can be lossy or slow (parsing XML→JSON on every call).
  • If you control both sides, you probably don't need an adapter — just fix the interface.
ℹ️ Where .NET uses Adapter

StreamReader / StreamWriter adapt a raw byte Stream into a character-based TextReader / TextWriter. The Encoding classes wrap and adapt between byte[] and string. Older still, System.Data's DataAdapter literally has "Adapter" in its name — it bridges a database and an in-memory DataSet.

🔀 Easily confused

Adapter vs Bridge: the deciding difference is intent — an adapter converts one existing interface into another so mismatched classes cooperate, while Bridge splits an abstraction from its implementation so the two dimensions vary independently. A handy clue (not a definition): adapters usually appear after the fact around code you can't change, and a bridge is typically designed in up front — but a bridge introduced later, or an adapter planned early, doesn't stop being itself.
Adapter vs Decorator: a decorator keeps the same interface and adds behaviour; an adapter gives a new interface and (usually) adds nothing.
Adapter vs Proxy/Facade: a proxy keeps the same interface but controls access; a facade invents a new, simpler interface over many objects. Full side-by-sides in Chapter 12.

Bridge structural

Intent — one line: when a class varies in two independent directions at once, split it into two separate hierarchies — an abstraction and an implementation — and connect them with a reference (the "bridge"), so each direction grows on its own.

⚠️ This is the trickiest structural pattern — read slowly

Bridge trips people up because the code looks just like an object adapter, and the words "abstraction" and "implementation" sound vague. The whole pattern is really about one combinatorial problem. Nail that problem and the rest is easy. Let's build up to it.

🌍 Real-world analogy

A remote control and a device. A remote (basic, advanced, voice) is the abstraction — the high-level controls. A device (TV, radio, speaker) is the implementation — the thing that actually changes volume or channel. Any remote can drive any device, because they meet at a thin common interface. You can invent a new remote without touching any device, and add a new device without touching any remote. Same idea as a GUI (abstraction) sitting on top of different operating-system APIs (implementations), exactly as the slides describe.

The problem — combinatorial explosion

Say you have a Shape class. You add two shapes: Circle and Square. Fine. Now the shapes need colours. The tempting move is to subclass:

❌ Naive: one subclass per (shape × colour)
public abstract class Shape { }

public class RedCircle  : Shape { }
public class BlueCircle : Shape { }
public class RedSquare  : Shape { }
public class BlueSquare : Shape { }
// 2 shapes × 2 colours = 4 classes
Change request that kills it: "add a Triangle." Now you need RedTriangle and BlueTriangle — and every future colour multiplies again. Shapes and colours are two independent dimensions, but inheritance forces them into one exploding tree: M shapes × N colours = M×N classes.
✅ With Bridge: two hierarchies, one reference
public interface IColor { string Fill(); }          // implementation side
public class Red  : IColor { public string Fill() => "red"; }
public class Blue : IColor { public string Fill() => "blue"; }

public abstract class Shape                          // abstraction side
{
    protected readonly IColor color;                 // ← the bridge
    protected Shape(IColor color) => this.color = color;
    public abstract string Draw();
}
public class Circle : Shape
{
    public Circle(IColor c) : base(c) { }
    public override string Draw() => $"Circle filled {color.Fill()}";
}
Now "add a Triangle" = one class. "Add Green" = one class. M + N, and they never multiply. Any shape composes with any colour at run-time: new Circle(new Blue()).
💡 The one-sentence insight

Bridge = "convert inheritance into composition." Instead of one class hierarchy that tries to capture every combination of two traits, keep two small hierarchies and let one hold a reference to the other. M×N collapses to M+N.

Without Bridge: 3 × 3 = 9 classes RedCirc BluCirc GrnCirc RedSqr BluSqr GrnSqr RedTri BluTri GrnTri +1 colour → +3 classes. +1 shape → +3 classes. With Bridge: 3 + 3 = 6 classes Shape (abstract) Circle Square Triangle IColor Red Blue Green the bridge →
Figure: the left tree grows multiplicatively; the right splits into two trees joined by a single reference (the bridge). Adding a shape or a colour now costs one class, not a whole row.
Try it: watch M×N explode vs M+N stay calm
Shapes (M): 3 Colours (N): 3
Without Bridge (subclass every combo)
9 classes
= M × N
With Bridge (two hierarchies)
6 classes
= M + N
What to notice: push the shape count to 8 and colours to 8 — without Bridge you'd write 64 classes; with Bridge, 16. Each "+" on one dimension adds a whole row/column without the pattern, but just one class with it.
Deep dive: the slides' device × remote example in C#

Here the Remote is the abstraction (high-level control logic) and the Device is the implementation (low-level work). A BasicRemote works with any device through the thin IDevice interface — and you could add an AdvancedRemote (refined abstraction) without touching a single device.

public interface IDevice                 // Implementation
{
    int Volume { get; set; }
    string Name { get; }
}
public class Radio : IDevice { public int Volume { get; set; } public string Name => "Radio"; }
public class Tv    : IDevice { public int Volume { get; set; } public string Name => "TV"; }

public class BasicRemote                  // Abstraction (holds an IDevice = the bridge)
{
    protected readonly IDevice device;
    public BasicRemote(IDevice device) => this.device = device;
    public void VolumeUp()   { device.Volume++; Console.WriteLine($"{device.Name}: {device.Volume}"); }
    public void VolumeDown() { device.Volume--; Console.WriteLine($"{device.Name}: {device.Volume}"); }
}

public class AdvancedRemote : BasicRemote  // Refined abstraction — new control logic, same devices
{
    public AdvancedRemote(IDevice device) : base(device) { }
    public void Mute() { device.Volume = 0; Console.WriteLine($"{device.Name}: muted"); }
}

// Client links an abstraction to an implementation:
var remote = new AdvancedRemote(new Tv());
remote.VolumeUp();
remote.Mute();

Notice the client's job (as the slides stress): it links the abstraction object with one of the implementation objects.

Reach for Bridge when…
  • A class has (or will have) two or more orthogonal dimensions of variation.
  • You want to avoid a permanent compile-time binding between an abstraction and its implementation.
  • Changes to an implementation must not ripple out to clients.
  • Several objects should share one implementation, hidden from the client.
Costs / drawbacks
  • Up-front complexity: two hierarchies and an indirection where beginners see none needed.
  • Overkill if there's genuinely only one dimension of change — don't pre-split "just in case."
ℹ️ Where .NET uses Bridge

System.Data.Common: the ADO.NET provider model is a bridge — your data-access abstraction (DbConnection, DbCommand) is bridged to concrete providers (SQL Server, SQLite, PostgreSQL). Logging abstractions (ILogger) bridged to sinks (console, file, Seq) are the same idea.

🔀 Easily confused

Bridge vs Adapter is the classic mix-up. Same code shape (one object holds another), different intent: Adapter converts a mismatched interface so two existing classes cooperate; Bridge separates an abstraction from its implementation so two dimensions can evolve independently. Timing is only a clue — adapters commonly show up after the fact and bridges are usually planned up front, but that's a tendency, not the definition. If you're solving a "these two don't fit" problem → Adapter. If you're solving a "this class explodes in two directions" problem → Bridge. See Chapter 12.

Composite structural a.k.a. Object Tree

Intent — one line: arrange objects into a tree and let the client treat a single object (a leaf) and a whole group of objects (a composite) through the same interface.

🌍 Real-world analogy

An army. A squad contains soldiers; a platoon contains squads; a brigade contains platoons. An order given at the top ("advance") passes down the hierarchy until every soldier acts — and whoever gives the order doesn't care whether they're commanding one soldier or a whole division. Same interface at every level. (Files and folders on your computer are the same tree.)

The problem

You're building an ordering system. An order holds products, but also boxes — and a box can hold products and other boxes, nested arbitrarily. How do you compute the total price?

❌ Naive: check the type at every node
decimal TotalPrice(object item)
{
    if (item is Product p) return p.Price;
    if (item is Box b)
    {
        decimal sum = 0;
        foreach (var child in b.Items)
            sum += TotalPrice(child); // recurse, re-checking types
        return sum;
    }
    throw new InvalidOperationException("unknown item");
}
Change request that kills it: "add gift-wrap items and bundles." Now this method — and every method that walks the tree — needs another if (item is ...). The client must know every concrete type and how they nest. Brittle.
✅ With Composite: one interface, recursion built in
public interface IOrderComponent { decimal GetPrice(); }

public class Product : IOrderComponent          // Leaf
{
    public decimal Price { get; init; }
    public decimal GetPrice() => Price;
}

public class Box : IOrderComponent              // Composite
{
    private readonly List<IOrderComponent> _items = new();
    public void Add(IOrderComponent c) => _items.Add(c);
    public decimal GetPrice() => _items.Sum(c => c.GetPrice()); // delegates down
}
The client just calls order.GetPrice(). A box asks each child for its price — and a child might be a product or another box; it doesn't matter. New leaf types plug in with zero changes to traversal code.
💡 The one-sentence insight

Give leaves and containers a common interface. A container implements each operation by delegating to its children and combining their results — so recursion lives in the structure, not in the client.

The three roles: Component (the shared interface — IOrderComponent), Leaf (no children, does the real work — Product), and Composite / Container (has children, delegates — Box). Here it is as a file system, which we'll drive in the demo:

public interface IFileNode
{
    string Name { get; }
    long GetSize();               // both leaves and folders answer this
}

public class FileLeaf : IFileNode                 // Leaf
{
    public string Name { get; }
    private readonly long _size;
    public FileLeaf(string name, long size) { Name = name; _size = size; }
    public long GetSize() => _size;
}

public class Folder : IFileNode                   // Composite
{
    public string Name { get; }
    private readonly List<IFileNode> _children = new();
    public Folder(string name) { Name = name; }
    public void Add(IFileNode node) => _children.Add(node);
    public long GetSize() => _children.Sum(c => c.GetSize()); // recurse
}
project/ (Folder) readme.md (File) src/ (Folder) logo.png (File) app.cs (File) util.cs (File) GetSize() sums downward
Figure: folders (composites, blue) and files (leaves) share one interface. project.GetSize() asks each child, and folders recurse — the client never type-checks.
Try it: build a file tree, then run GetSize() and watch it recurse
Add into:
Tree
Traversal log
What to notice: "Compute total size" makes one call on the root. Folders don't know their size — they ask their children and add up the answers. The same GetSize() call works on a single file and on a folder nested three levels deep.
Reach for Composite when…
  • Your core model is naturally a tree (files, org charts, UI widgets, menus, ASTs).
  • You want clients to ignore the difference between individual objects and groups of them.
Costs / drawbacks
  • The shared interface can get "too general" — leaves may be forced to expose Add/Remove that make no sense for them (a design tension GoF openly acknowledges).
  • Type-safety loosens: everything is an IComponent.
ℹ️ Where .NET uses Composite

WPF / WinForms controls: a Panel contains controls, which may be panels containing more controls — and layout/rendering walk the tree uniformly. The Roslyn syntax tree and any XML/JSON DOM (XElement with child XElements) are composites too.

🔀 Easily confused

Composite vs Decorator: both build a tree of same-interface objects, but a decorator wraps exactly one child to add behaviour; a composite holds many children to aggregate them. GoF: "a decorator is a degenerate composite with only one component." Composite pairs naturally with Iterator (to traverse) and Visitor (to add operations). More in Chapter 12.

Decorator structural a.k.a. Wrapper

Intent — one line: add responsibilities to an individual object at run-time by wrapping it in another object that shares its interface — as many layers deep as you like.

🌍 Real-world analogy

Getting dressed. Cold? Add a sweater. Still cold? A jacket over it. Raining? A raincoat on top. Each garment "extends" you without becoming part of you, and you can peel any layer off whenever you like. Each layer wraps the ones beneath and adds something — exactly what a decorator does.

The problem

A coffee shop sells an espresso, and customers add milk, sugar, whipped cream, oat milk… in any combination. You need a price and a description for each order. The obvious approach — a subclass per combination — detonates.

❌ Naive: a subclass per combination
public class Espresso { }
public class EspressoWithMilk { }
public class EspressoWithMilkAndSugar { }
public class EspressoWithMilkSugarWhip { }
public class EspressoWithOatMilk { }
// ...4 add-ons → up to 2⁴ = 16 classes,
//    and every new add-on DOUBLES the count.
Change request that kills it: "add caramel." You now need a new class for caramel times every existing combination. This is the "explosion of subclasses" the slides warn about — and it's fixed at compile time, so a customer can't invent their own combo.
✅ With Decorator: stack wrappers at run-time
IBeverage order = new Espresso();      // 2.50
order = new Milk(order);               // +0.50
order = new Sugar(order);              // +0.25
order = new Whip(order);               // +0.75
Console.WriteLine($"{order.Description()} = {order.Cost():C}");
// Espresso, milk, sugar, whip = $4.00
One class per add-on (4 classes cover all combinations), and orders are built at run-time by stacking. Adding caramel = one new decorator, nothing else changes.
💡 The one-sentence insight

A decorator implements the same interface as the thing it wraps and holds a reference to it — so it can be used wherever the original is used, and it typically forwards each call to the inner object while adding a little something before or after. Forwarding is the common case, not a law: a decorator may short-circuit — a caching decorator, for instance, can return a stored result without calling the wrapped object at all.

public interface IBeverage                 // Component
{
    string Description();
    decimal Cost();
}

public class Espresso : IBeverage          // Concrete component
{
    public string Description() => "Espresso";
    public decimal Cost() => 2.50m;
}

// Base decorator: itself an IBeverage, WRAPS an IBeverage.
public abstract class BeverageDecorator : IBeverage
{
    protected readonly IBeverage inner;
    protected BeverageDecorator(IBeverage inner) => this.inner = inner;
    public virtual string Description() => inner.Description();
    public virtual decimal Cost()       => inner.Cost();
}
public class Milk : BeverageDecorator
{
    public Milk(IBeverage b) : base(b) { }
    public override string Description() => inner.Description() + ", milk";
    public override decimal Cost()       => inner.Cost() + 0.50m;
}

public class Sugar : BeverageDecorator
{
    public Sugar(IBeverage b) : base(b) { }
    public override string Description() => inner.Description() + ", sugar";
    public override decimal Cost()       => inner.Cost() + 0.25m;
}
// Whip, OatMilk, Caramel… all follow the identical shape.
🧠 Decorator vs plain inheritance

Inheritance is decided at compile time; decoration is decided at run-time. With subclasses you must know every combination before you build. With decorators you compose combinations while the program runs — from user input, config, data — and you can even wrap the same type twice (double sugar). This is composition over inheritance made concrete.

Whip+0.75 Sugar+0.25 Milk+0.50 Espresso2.50 Cost() flows inward, each layer adds its bit → 4.00 Every box is an IBeverage — interchangeable
Figure: Cost() on the outer Whip calls Sugar → Milk → Espresso, and each adds its charge on the way back. The whole stack is still just an IBeverage.
Try it: stack and un-stack decorators, watch cost & description build up
Order: Espresso
$2.50
What to notice: the base espresso never changes — each add-on is a new layer wrapped around the current order. Remove a layer and the wrapper simply drops off. This is run-time composition; no subclass exists for "espresso + oat + whip."
Deep dive: the canonical .NET example — the Stream family

The .NET I/O stack is textbook Decorator. Every wrapper is a Stream and holds a Stream, adding one capability. You compose behaviour by nesting:

// Read → Decrypt → Decompress, all through one Stream interface:
using FileStream file = File.OpenRead("data.bin");     // Concrete component
using var decrypt     = new CryptoStream(file, decryptor, CryptoStreamMode.Read);
using var decompress  = new GZipStream(decrypt, CompressionMode.Decompress);
using var buffered    = new BufferedStream(decompress); // adds buffering

// The consumer just sees a Stream — it has no idea it's 4 layers deep.
using var reader = new StreamReader(buffered);
string text = reader.ReadToEnd();

This mirrors the slides' example exactly (encrypt + compress decorators around a data source). Each layer is independent and reorderable — the essence of Decorator.

Reach for Decorator when…
  • You need to add responsibilities to individual objects dynamically and transparently.
  • Extension by subclassing would cause a combinatorial explosion of classes.
  • You can't or shouldn't subclass (e.g. a sealed class, or behaviour chosen at run-time).
Costs / drawbacks
  • Lots of tiny objects; a deep stack is harder to debug ("why is this so slow?" — 6 wrappers).
  • Order can matter (encrypt-then-compress ≠ compress-then-encrypt).
  • A decorator isn't identical to its component — reference equality and type checks can surprise you.
🔀 Easily confused

Decorator vs Adapter: a decorator keeps the same interface and adds behaviour; an adapter changes the interface. Decorator vs Proxy: nearly identical code — but a decorator enhances the object, while a proxy controls access to it (lazy-loading, permissions). Decorator vs Strategy: a decorator changes an object's "skin" (wrapping from outside); a strategy changes its "guts" (a swapped-in inner algorithm). See Chapter 12.

Facade structural

Intent — one line: put a single, simplified interface in front of a complicated subsystem, so clients push one button instead of wiring a dozen parts together in the right order.

🌍 Real-world analogy

You phone a shop to place an order. The operator is your facade: behind that one friendly voice sit the inventory system, payment gateway, and delivery service. You say "one large pepperoni to this address," and they orchestrate all of it. You never talk to the warehouse directly.

The problem

To make a pizza, six subsystems must run in the right order: prepare dough, add sauce, add toppings, bake in the oven, take payment, dispatch delivery. If every client does this by hand, your business logic gets welded to the internals of six classes.

❌ Naive: the client orchestrates six subsystems
var dough   = new DoughStation();
var sauce   = new SauceStation();
var oven    = new Oven();
var payment = new PaymentGateway();
var courier = new DeliveryService();

var pizza = dough.Prepare("thin");
sauce.Apply(pizza, "tomato");
oven.Preheat(220);                 // forget this → raw pizza
oven.Bake(pizza, minutes: 12);
payment.Charge(customer, 14.99m);  // wrong order → deliver unpaid
courier.Dispatch(pizza, address);
Every caller repeats this dance and must know the correct order and every dependency. Change the flow (add a "quality check" step) and you edit every call site.
✅ With Facade: one call
public class PizzaShopFacade
{
    private readonly DoughStation _dough = new();
    private readonly SauceStation _sauce = new();
    private readonly Oven _oven = new();
    private readonly PaymentGateway _payment = new();
    private readonly DeliveryService _courier = new();

    public void OrderPizza(string kind, Customer c, string addr)
    {
        var pizza = _dough.Prepare("thin");
        _sauce.Apply(pizza, "tomato");
        _oven.Preheat(220);
        _oven.Bake(pizza, 12);
        _payment.Charge(c, 14.99m);
        _courier.Dispatch(pizza, addr);
    }
}

new PizzaShopFacade().OrderPizza("pepperoni", customer, address);
Clients call one method. The correct order and all the dependencies live in one place. Change the flow once, inside the facade.
💡 The one-sentence insight

A facade doesn't add features and doesn't hide the subsystem (you can still reach in when you need to) — it just offers a convenient default path for the 90% case, decoupling clients from the subsystem's moving parts.

Client FacadeOrderPizza() DoughStation SauceStation Oven PaymentGateway DeliveryService
Figure: the client talks only to the Facade; the Facade knows the five subsystem classes and the order to call them in.
Try it: order a pizza — via the facade vs by hand
What to notice: both runs execute the same six subsystem steps — but with the facade the client writes one line and can't get the order wrong. The complexity didn't vanish; it moved behind the facade.
Reach for Facade when…
  • You want a simple entry point to a complex or sprawling subsystem/library.
  • There are many dependencies between clients and a subsystem's internal classes.
  • You want to layer your system — a facade per layer, talking only to the layer below.
Costs / drawbacks
  • A facade can become a "god object" that knows too much if you keep piling features on.
  • It may hide power users need — so keep the subsystem accessible, don't seal it off.
ℹ️ Where .NET uses Facade

HttpClient is a facade over sockets, DNS, connection pools, TLS, and message handlers. File.ReadAllText(path) is a facade over opening a stream, wrapping a reader, reading, and disposing. ASP.NET Core's WebApplication.CreateBuilder(args) hides a mountain of hosting/DI/config setup behind a couple of calls.

🔀 Easily confused

Facade vs Adapter: an adapter makes one existing interface fit another; a facade invents a brand-new, simpler interface over many objects. Facade vs Mediator: a facade is one-directional (client → subsystem, and subsystem classes don't know it exists); a Mediator coordinates two-way chatter between peers that all know the mediator. Facades are often Singletons.

Flyweight structural

Intent — one line: when you need a huge number of similar objects, share the parts they have in common instead of duplicating them, so memory stays small. Caching is one implementation mechanism a flyweight factory happens to use — it isn't what the pattern is named for or defined by.

🧠 Two terms you must nail first: intrinsic vs extrinsic state

Intrinsic state is the data that is the same across many objects and never changes per object — the tree's species name, its mesh, its texture bitmap. It lives inside the shared flyweight and is read-only.
Extrinsic state is the data that is unique per object and often changes from outside — this tree's (x, y) position, its height, its current sway. The flyweight does not store it; the caller supplies it (passes it into methods, or stores it in a lightweight context object).
The whole pattern is: pull the extrinsic state out, keep only the intrinsic state, and share.

The problem

You're rendering a forest with a million trees. Each Tree stores its position and its species name, colour, and a big texture/mesh. But there are only three species — so you're storing the same heavy texture a million times.

❌ Naive: every tree carries its own heavy data
public class Tree                     // ~1 KB each (texture + mesh inline)
{
    public int X, Y;                  // unique  (extrinsic)
    public string Species;            // repeated (intrinsic)
    public string Color;              // repeated
    public byte[] Texture;            // repeated — the expensive part!
}

var forest = new List<Tree>();
for (int i = 0; i < 1_000_000; i++)
    forest.Add(new Tree { X = rx(), Y = ry(),
        Species = "Oak", Color = "green", Texture = LoadOakTexture() });
// 1,000,000 copies of the SAME oak texture in RAM.
What breaks: memory. A million trees × 1 KB ≈ 1 GB, almost all of it identical texture bytes duplicated a million times.
✅ With Flyweight: share the heavy part
// Intrinsic state — created once per species, shared by all.
public class TreeType                          // Flyweight
{
    private readonly byte[] _texture;          // private defensive copy — never handed out raw

    public string Species { get; }
    public string Color { get; }
    // Any holder of a raw byte[] could mutate it and corrupt every tree that
    // shares this flyweight, so we only ever expose a read-only view.
    public ReadOnlyMemory<byte> Texture => _texture;

    public TreeType(string s, string c, byte[] t) => (Species, Color, _texture) = (s, c, (byte[])t.Clone());
    public void Draw(int x, int y) => Console.WriteLine($"Drawing {Species} at ({x},{y}) with shared texture");
}

public class Tree                              // ~32 bytes: 16-byte header + 2 ints + a reference
{
    public int X, Y;                           // extrinsic
    private readonly TreeType _type;           // shared reference
    public Tree(int x, int y, TreeType type) => (X, Y, _type) = (x, y, type);
    public void Draw() => _type.Draw(X, Y);
}
Now there are 3 heavy TreeType objects total; each of the million trees is a tiny record holding coordinates and a reference. Memory drops from ~1 GB to roughly tens of MB — still a dramatic win, just not literally "a few MB."

The Flyweight Factory guarantees sharing: it hands back an existing flyweight if one already exists for that intrinsic state, and only creates a new one otherwise. Texture is intrinsic too, so it belongs inside the cache key's inputs — the factory loads it itself from the species, rather than accepting it as a separate parameter that could vary independently and silently break sharing.

public class TreeFactory
{
    private readonly Dictionary<string, TreeType> _cache = new();

    public TreeType GetTreeType(string species, string color)
    {
        string key = $"{species}:{color}";
        if (!_cache.TryGetValue(key, out var type))
            _cache[key] = type = new TreeType(species, color, LoadTexture(species)); // create once
        return type;                                                                   // otherwise share
    }

    private static byte[] LoadTexture(string species) => /* load from disk/atlas */ new byte[1024];
}
Try it: plant a forest, with and without the flyweight
Trees to plant:
What to notice: both forests draw the same number of trees on screen, but the flyweight version creates only a handful of heavy objects (one per species) and holds the rest as tiny position records. Bump the count to a million and watch the memory gap explode.
Reach for Flyweight when…
  • An app holds a very large number of objects and memory is the bottleneck.
  • Most of each object's state can be made extrinsic (pulled out and passed in).
  • Many groups of objects collapse to a few shared ones once extrinsic state is removed.
  • The app doesn't rely on object identity (shared flyweights are ==).
Costs / drawbacks
  • Code gets harder to read: extrinsic state now sloshes around in method parameters.
  • You trade memory for CPU (computing/looking up extrinsic state each call).
  • Flyweights must be immutable and can't know their context — no back-pointers to owners.
ℹ️ Where .NET uses Flyweight

String interning: the runtime keeps one shared copy of each identical string literal (string.Intern); every reference points at it. Shared Encoding.UTF8 and Brushes.Red-style singletons are flyweights too. (Ordinary C# boxing does not do this — two separately boxed bool/int values are distinct objects, so ReferenceEquals is false. Java's Integer.valueOf caches small ints this way; .NET's boxing doesn't.)

🔀 Easily confused

Flyweight vs Singleton: a singleton is exactly one instance of a class; a flyweight is one shared instance per distinct intrinsic state (three tree species → three flyweights). Flyweight pairs well with Composite (shared leaf nodes) and is a good way to implement State / Strategy objects. More in Chapter 12.

Proxy structural

Intent — one line: provide a stand-in with the same interface as a real object, so the stand-in can control access to it — delaying its creation, checking permissions, caching, or logging.

🌍 Real-world analogy

A credit card is a proxy for your bank account, which is a proxy for a pile of cash. All three "make a payment," but the card controls access to the money: it checks your limit, records the transaction, and saves you carrying the cash around. Same interface, extra control.

The problem

A gallery shows high-resolution images. Each RealImage loads its (large) file from disk the moment it's created — so opening the gallery loads every image up front, even the ones the user never scrolls to. Slow start, wasted memory.

❌ Naive: load everything eagerly
public class RealImage : IImage
{
    private readonly string _file;
    public RealImage(string file)
    {
        _file = file;
        LoadFromDisk(file);           // heavy — happens immediately
    }
    public void Display() => Console.WriteLine($"Showing {_file}");
    private void LoadFromDisk(string f) => Console.WriteLine($"Loading {f} (4 MB)...");
}

// Opening the gallery loads ALL 200 images now,
// even though only ~5 are on screen.
var gallery = files.Select(f => new RealImage(f)).ToList();
You could sprinkle lazy-loading if (notLoaded) Load(); checks everywhere, but that duplicates the same guard across every caller.
✅ With a virtual (lazy-loading) Proxy
public class ProxyImage : IImage        // same interface as RealImage
{
    private readonly string _file;
    private RealImage? _real;           // created on first use
    public ProxyImage(string file) => _file = file;

    public void Display()
    {
        _real ??= new RealImage(_file); // load the heavy object ONCE, on demand
        _real.Display();                // then cache it for next time
    }
}

// Cheap to create 200 proxies; each loads only when first shown.
var gallery = files.Select(f => new ProxyImage(f)).ToList();
The gallery opens instantly. Clients can't tell a proxy from a real image — same IImage interface — so nothing else changes.
💡 The one-sentence insight

A proxy has the same interface as the real object and holds a reference to it, but it runs some control logic (create-on-demand, permission check, cache, log) before or after forwarding the call.

Flavour of ProxyWhat it controlsExample
VirtualCreation cost — lazy-init an expensive objectThe image gallery above; Lazy<T>
ProtectionAccess rights — allow/deny by permissionOnly admins may call Delete()
RemoteLocation — a local stand-in for an object elsewheregRPC / WCF client proxy over the network
CachingRepetition — reuse results of expensive callsMemoise a slow query response
LoggingObservability — record every callAudit trail around a service
IImage (Subject)Display() ProxyImagelazy-load + cache RealImageheavy: loads from disk Client controls →
Figure: Proxy and RealImage implement the same IImage. The client holds a proxy and can't tell the difference; the proxy decides when to create and call the real object.
Try it: a lazy-loading image gallery — first click loads, later clicks hit the cache
What to notice: the first time you view an image the proxy loads the heavy RealImage from disk; every later view of that image is an instant cache hit. Creating the proxies cost nothing — the expensive work is deferred until it's actually needed.
Reach for Proxy when…
  • You need a smarter reference than a plain pointer: lazy creation, access control, caching, logging, or a local handle to a remote object.
  • You want that control logic to be transparent — callers keep using the same interface.
Costs / drawbacks
  • Another layer of indirection; the deferred work can cause a surprising pause "later."
  • Care needed with threading (two callers triggering lazy-init at once).
ℹ️ Where .NET uses Proxy

Entity Framework generates lazy-loading proxy classes so a navigation property loads from the database only when you first touch it. Lazy<T> is a virtual proxy in the box. gRPC / WCF client stubs are remote proxies, and System.Reflection.DispatchProxy lets you build your own (great for logging/caching interceptors).

🔀 Easily confused

Proxy vs Decorator: code looks the same, but a decorator adds responsibilities you choose to stack; a proxy controls access to one fixed real object (and often manages its lifecycle). Proxy vs Adapter: a proxy keeps the same interface; an adapter gives a different one. Proxy vs Facade: a facade wraps many classes behind a new, simpler interface; a proxy wraps one object behind the same interface. See Chapter 12.

Structural patterns at a glance

PatternIntent in one lineYou reach for it when…
AdapterMake an incompatible interface fit.You must use a class whose interface is wrong for you.
BridgeSplit two independent dimensions into two hierarchies.A class varies in two directions and subclasses explode (M×N).
CompositeTreat a tree of parts and wholes uniformly.Your model is a tree and clients shouldn't care about node type.
DecoratorAdd behaviour by wrapping, at run-time.Add-on combinations would explode into subclasses.
FacadeOne simple door to a complex subsystem.Clients are drowning in a subsystem's moving parts.
FlyweightShare common data across many objects.Millions of similar objects blow the memory budget.
ProxyA stand-in that controls access.You need lazy-load, access control, caching, or a remote handle.
🔀 The five "wrappers" — how to tell them apart

Adapter, Bridge, Decorator, Proxy, and Facade all hold another object and forward calls, so they're endlessly confused. Sort them by intent, not code shape: Adapter changes an interface to make things fit; Decorator keeps the interface and adds behaviour (stackable); Proxy keeps the interface and controls access (one fixed target); Facade invents a new, simpler interface over many objects; and Bridge is a designed-in split of an abstraction from its implementation so both vary freely. When two still feel the same, the deciding question is "is the interface the same, different, or brand-new — and am I adding, controlling, or simplifying?" The full side-by-side grids live in Chapter 12 · Confusable Patterns.

Check yourself

💡 Where to go next

You've now seen how objects are composed into flexible structures. Next, Chapter 5 · Behavioral Patterns tackles how those objects talk to each other. And whenever two patterns blur together, the Confusable Patterns chapter has the side-by-side grids.