Structural Patterns
Patterns for combining objects: adapter, facade, decorator, proxy and composite, with examples of wrapping legacy systems and adding behavior without changing code.
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.
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.