The creational patterns are about making objects; structural patterns are about putting them together. The examples continue at Kestrel Energy, whose customer portal and meter-api need data from KBS, the billing system: a large, older application whose code nobody wants to change without a good reason. That makes it a good place for patterns that add or adapt behavior around existing code instead of inside it.

Wrappers: one object around another

A wrapper is an object (or function) that holds another object and stands between it and its callers: every call goes to the wrapper first, which passes it on, possibly changed, and possibly does something before or after. "Wrapper" is the everyday word; four of the GoF structural patterns are wrappers with a specific purpose, and the difference between them is what happens to the interface, the set of methods callers use.

Four wrappers side by side. Adapter: the caller uses a different interface than the wrapped KBS function get_cust. Decorator: the same interface as the wrapped ReadingStore, with behavior added. Proxy: the same interface as the wrapped StatementArchive, with access controlled. Facade: one simpler interface in front of three KBS modules. caller Adapter different interface get_cust() caller Decorator same interface + behavior ReadingStore caller Proxy same interface + access control StatementArchive caller Facade one simpler interface three KBS modules
The four wrappers look the same from a distance: a caller, an object in the middle, and the object being wrapped. What tells them apart is what the middle object does to the interface.

Wrapping works because of composition: the wrapper has the wrapped object and delegates to it, instead of inheriting from it. That is what makes it possible to wrap code you cannot change, to wrap at run time, and to put several wrappers around each other.

Adapter: making a legacy interface fit

The portal team wants to show customer details from KBS. KBS offers a function from 2009, get_cust(nr), that takes a number, returns a tuple, stores the balance in cents and uses 1 and 0 for active and closed. The portal's own code works with a Customer object and a directory with a find("C-10442") method. Changing KBS is not an option, and sprinkling tuple indexes and cent conversions through the portal would spread KBS's quirks into new code.

An adapter is a wrapper that converts the interface of an existing object or function into the interface its callers expect, so two pieces of code that do not fit can work together without changing either of them:

from dataclasses import dataclass
from decimal import Decimal


# --- KBS, written in 2009: cannot be changed ------------------------------
_KBS_ROWS = {
    10442: ("A. de Jong", "Lindelaan 4, Utrecht", 1, 11865),
    10587: ("M. Visser", "Kade 17, Zwolle", 0, 0),
}

def get_cust(nr):
    """Returns (name, address, active flag, balance in cents) or None."""
    return _KBS_ROWS.get(nr)


# --- What the portal's code expects ---------------------------------------
@dataclass(frozen=True)
class Customer:
    customer_id: str
    name: str
    active: bool
    balance: Decimal          # euros


class KbsCustomerDirectory:
    """Adapter: offers find() on the outside, calls get_cust() inside."""

    def find(self, customer_id):
        row = get_cust(int(customer_id.removeprefix("C-")))
        if row is None:
            return None
        name, _address, active_flag, cents = row
        return Customer(customer_id, name, active_flag == 1, Decimal(cents) / 100)


# Portal code: knows nothing about tuples, cents or flags.
directory = KbsCustomerDirectory()
for customer_id in ("C-10442", "C-10587", "C-99999"):
    customer = directory.find(customer_id)
    if customer is None:
        print(f"{customer_id}: not found")
    else:
        status = "active" if customer.active else "closed"
        print(f"{customer_id}: {customer.name}, {status}, balance EUR {customer.balance:.2f}")
C-10442: A. de Jong, active, balance EUR 118.65
C-10587: M. Visser, closed, balance EUR 0.00
C-99999: not found

All knowledge of KBS's format now sits in one class of about ten lines. When KBS is replaced one day, or when a test needs customers without KBS, a different class with the same find() method takes its place and the portal does not notice. This is what "wrapping a legacy system" means in practice.

Facade: one simple entry point

To show a customer "your usage this month and how it compares with your advance payment", the portal needs four KBS modules: contracts (which meter does this customer have?), usage, tariffs and invoices. If the portal calls all four directly, it has to know how they fit together, and every change inside KBS can break the portal.

A facade is an object that offers one simpler, higher-level interface to a group of objects or modules, so callers can do the common tasks without knowing the parts behind it:

from decimal import Decimal


# Four KBS modules, each with its own interface.
class Contracts:
    def contract_for(self, customer_id):
        return {"contract": "CT-20417", "ean": "E-0412", "tariff": "fixed"}


class Usage:
    def kwh(self, ean, month):
        return Decimal("303.2")


class Tariffs:
    def price(self, tariff, month):
        return Decimal("0.2700")


class Invoices:
    def advance_payment(self, contract):
        return Decimal("95.00")


class MonthlyStatement:
    """Facade: one simple call for the portal, four modules behind it."""

    def __init__(self, contracts, usage, tariffs, invoices):
        self.contracts = contracts
        self.usage = usage
        self.tariffs = tariffs
        self.invoices = invoices

    def summary(self, customer_id, month):
        contract = self.contracts.contract_for(customer_id)
        kwh = self.usage.kwh(contract["ean"], month)
        cost = kwh * self.tariffs.price(contract["tariff"], month)
        paid = self.invoices.advance_payment(contract["contract"])
        return {"month": month, "kwh": kwh, "cost": round(cost, 2),
                "advance paid": paid, "difference": round(paid - cost, 2)}


statement = MonthlyStatement(Contracts(), Usage(), Tariffs(), Invoices())
for key, value in statement.summary("C-10442", "2026-09").items():
    print(f"{key:13} {value}")
month         2026-09
kwh           303.2
cost          81.86
advance paid  95.00
difference    13.14

The portal now depends on one method, summary(); how the four modules work together is the facade's knowledge. A facade does not hide the modules completely: code that needs something unusual can still use them directly. An API gateway is the same idea at network level: one entry point in front of several back-end services.

Decorator: adding behavior without changing code

meter-api reads the latest meter reading many times per second, often for the same meter. Ravi Kumar wants a cache and a log line for every call, but the reading store that talks to db01 is tested and trusted, and adding caching and logging inside it would mix three concerns in one class.

A decorator is a wrapper that has exactly the same interface as the object it wraps and adds behavior before or after passing each call on. Because it looks like the original, the caller cannot tell the difference, and decorators can be stacked:

class DatabaseReadingStore:
    """Stands in for the real store on db01."""

    def __init__(self):
        self.rows = {"E-0412": 18745.2, "E-0413": 6120.9}

    def latest(self, ean):
        print(f"    db01: SELECT latest reading for {ean}")
        return self.rows[ean]


class CachingStore:
    """Decorator: same latest() method, adds a cache."""

    def __init__(self, inner):
        self.inner = inner
        self.cache = {}

    def latest(self, ean):
        if ean not in self.cache:
            self.cache[ean] = self.inner.latest(ean)
        else:
            print(f"    cache hit for {ean}")
        return self.cache[ean]


class LoggingStore:
    """Decorator: same latest() method, adds a log line."""

    def __init__(self, inner):
        self.inner = inner

    def latest(self, ean):
        print(f"log: latest({ean})")
        return self.inner.latest(ean)


# Stack them like layers; the caller sees one object with latest().
store = LoggingStore(CachingStore(DatabaseReadingStore()))
for ean in ("E-0412", "E-0412", "E-0413"):
    print(f"  -> {store.latest(ean)} kWh")
log: latest(E-0412)
    db01: SELECT latest reading for E-0412
  -> 18745.2 kWh
log: latest(E-0412)
    cache hit for E-0412
  -> 18745.2 kWh
log: latest(E-0413)
    db01: SELECT latest reading for E-0413
  -> 6120.9 kWh

The second request for E-0412 never reaches db01, and the store itself did not change. The order of the layers matters: with LoggingStore on the outside, every request is logged, including cache hits. Swap them, CachingStore(LoggingStore(...)), and only the requests that reach the database are logged. Both can be right; decide which one you mean.

In Python, "same interface" is a promise nobody checks: if CachingStore forgot to implement a method that the store has, you find out at run time. A statically typed language makes the promise explicit. In C#, every layer implements the same interface and holds another instance of it, and the compiler refuses a decorator that does not implement every method:

IReadingStore store = new LoggingStore(new CachingStore(new DatabaseReadingStore()));

foreach (var ean in new[] { "E-0412", "E-0412" })
{
    Console.WriteLine($"  -> {store.Latest(ean)} kWh");
}

// The contract every layer must keep.
interface IReadingStore
{
    decimal Latest(string ean);
}

sealed class DatabaseReadingStore : IReadingStore
{
    private readonly Dictionary<string, decimal> rows = new() { ["E-0412"] = 18745.2m };

    public decimal Latest(string ean)
    {
        Console.WriteLine($"    db01: SELECT latest reading for {ean}");
        return rows[ean];
    }
}

// Each decorator implements the interface AND holds another IReadingStore.
sealed class CachingStore(IReadingStore inner) : IReadingStore
{
    private readonly Dictionary<string, decimal> cache = new();

    public decimal Latest(string ean)
    {
        if (cache.TryGetValue(ean, out var value))
        {
            Console.WriteLine($"    cache hit for {ean}");
            return value;
        }
        return cache[ean] = inner.Latest(ean);
    }
}

sealed class LoggingStore(IReadingStore inner) : IReadingStore
{
    public decimal Latest(string ean)
    {
        Console.WriteLine($"log: Latest({ean})");
        return inner.Latest(ean);
    }
}
log: Latest(E-0412)
    db01: SELECT latest reading for E-0412
  -> 18745.2 kWh
log: Latest(E-0412)
    cache hit for E-0412
  -> 18745.2 kWh

The class declarations with parameters in the header, such as CachingStore(IReadingStore inner), are C# primary constructors. The same structure appears in Java's I/O library, where a BufferedReader wraps another Reader and adds buffering.

Not the same as Python's @decorator

Python's @ syntax is also called a decorator, and the idea is related but not the same. A Python decorator is a function that takes a function and returns a replacement, usually a wrapper function, at the moment the function is defined. The pattern on this page wraps objects, while the program runs:

import functools


def logged(func):
    @functools.wraps(func)
    def wrapper(*args):
        print(f"calling {func.__name__}{args}")
        return func(*args)
    return wrapper


@logged                      # the same as: advance = logged(advance)
def advance(annual_kwh, price):
    return round(annual_kwh * price / 12, 2)


print(advance(3600, 0.27))
print(advance.__name__)
calling advance(3600, 0.27)
81.0
advance

functools.wraps copies the original's name and docstring onto the wrapper, so tools and error messages still show advance. TypeScript has a decorator syntax of its own for classes and their members, again a language feature rather than the pattern.

Proxy: controlling access

Customers download their annual statements as PDF files from a document archive that is slow to connect to. The portal must make sure a customer only ever gets their own statements, and it should not connect to the archive for visitors who never open one.

A proxy is a stand-in object with the same interface as the real one that controls access to it: it can check permissions, create the real object only when it is first needed (lazy loading), count or limit calls, or forward them to another process or machine:

class StatementArchive:
    """The real object: slow to set up, serves annual statement PDFs."""

    def __init__(self):
        print("  (connecting to the document archive)")

    def pdf(self, customer_id, year):
        return f"annual-statement-{customer_id}-{year}.pdf"


class StatementArchiveProxy:
    """Proxy: same pdf() method; checks access and connects only when needed."""

    def __init__(self, logged_in_customer):
        self.customer = logged_in_customer
        self._archive = None

    def pdf(self, customer_id, year):
        if customer_id != self.customer:
            raise PermissionError(f"{self.customer} may not read {customer_id}'s statements")
        if self._archive is None:
            self._archive = StatementArchive()
        return self._archive.pdf(customer_id, year)


archive = StatementArchiveProxy(logged_in_customer="C-10442")
print("proxy created")
for customer_id, year in [("C-10587", 2025), ("C-10442", 2025), ("C-10442", 2024)]:
    try:
        print(archive.pdf(customer_id, year))
    except PermissionError as error:
        print("refused:", error)
proxy created
refused: C-10442 may not read C-10587's statements
  (connecting to the document archive)
annual-statement-C-10442-2025.pdf
annual-statement-C-10442-2024.pdf

The refused request never touched the archive, and the connection was made once, at the first legitimate request. Proxy and decorator have the same shape; the difference is intent. A decorator adds to what the object does, a proxy decides whether and when the real object is used. Object-relational mappers use lazy-loading proxies for related records, and a remote-procedure-call client object, such as a generated gRPC stub, is a proxy for an object on another machine.

Composite: trees that act like one object

A Kestrel bill is a tree: the month is a group of energy costs, network costs and discounts, and energy costs are a group of several charges. The code that prints and totals the bill should not need separate loops for "a single charge" and "a group of charges, which may contain more groups".

The composite pattern builds part-whole hierarchies as a tree in which single items (leaves) and groups (composites) have the same interface, so callers treat a whole tree exactly like one item. Money is kept in Decimal for the reasons on Floating-Point Numbers:

from decimal import Decimal


class Charge:
    """Leaf: one amount on the bill."""

    def __init__(self, name, amount):
        self.name = name
        self.amount = Decimal(amount)

    def total(self):
        return self.amount

    def lines(self, depth=0):
        yield f"{'  ' * depth}{self.name:<{28 - 2 * depth}}{self.total():>9}"


class ChargeGroup:
    """Composite: holds charges AND other groups, and has the same methods."""

    def __init__(self, name, parts):
        self.name = name
        self.parts = parts

    def total(self):
        return sum((part.total() for part in self.parts), Decimal("0.00"))

    def lines(self, depth=0):
        yield f"{'  ' * depth}{self.name:<{28 - 2 * depth}}{self.total():>9}"
        for part in self.parts:
            yield from part.lines(depth + 1)


bill = ChargeGroup("October 2026", [
    ChargeGroup("Energy", [
        Charge("Electricity, 303.2 kWh", "81.86"),
        Charge("Fixed supply costs", "6.95"),
    ]),
    Charge("Network costs", "31.25"),
    ChargeGroup("Discounts", [Charge("Loyalty discount", "-5.00")]),
])

for line in bill.lines():
    print(line)
October 2026                   115.06
  Energy                        88.81
    Electricity, 303.2 kWh      81.86
    Fixed supply costs           6.95
  Network costs                 31.25
  Discounts                     -5.00
    Loyalty discount            -5.00

bill.total() and bill.lines() work on the whole tree because each group asks its parts, whatever they are. Moving the network costs into a new "Grid" group, or adding a group inside a group, needs no change to the printing or totaling code. File systems (folders and files), HTML documents (elements inside elements) and organization charts are composites too.

Telling the patterns apart

Pattern Interface toward the caller Typical job At system scale
Adapter different from the wrapped object make an existing or legacy interface fit anti-corruption layer, integration adapters
Facade new and simpler, over many objects one entry point for common tasks API gateway, backend for a front end
Decorator the same add behavior: caching, logging, retries, metrics sidecars and middleware
Proxy the same control access: permissions, lazy loading, remote calls reverse and forward proxies
Composite the same for one item and for a tree treat part-whole hierarchies uniformly (mostly inside one program)

The GoF book lists two more structural patterns that are less common in everyday business code. Bridge separates an abstraction from its implementation so both can vary independently (for example, report types and output formats), and Flyweight shares many small, identical objects to save memory, the way CPython reuses one object for each small integer.