Worked examples — Design Patterns — Structural - Adapter, Bridge, Composite, Decorator, Facade, Flyweight, Proxy
The scenario matrix
Before any example, let us lay out every kind of situation these patterns can throw at you. Each row is a "case class"; the examples afterward are labelled with the cell they hit, so by the end every cell is covered.
| # | Case class | What makes it tricky | Pattern(s) hit |
|---|---|---|---|
| C1 | Interface mismatch (normal) | signatures differ, must translate | Adapter |
| C2 | Two-dimension explosion | vs classes | Bridge |
| C3 | Stacking wrappers | order of forwarding matters | Decorator |
| C4 | Degenerate tree — empty | a folder with no children | Composite |
| C5 | Deep / recursive tree | nested many levels | Composite (Recursion) |
| C6 | Zero / first-time creation | flyweight pool empty at start | Flyweight |
| C7 | Repeated identical requests | must return the same shared object | Flyweight |
| C8 | Access control / laziness | do work only when needed | Proxy |
| C9 | Orchestration ordering | many subsystem calls in fixed order | Facade |
| C10 | Word problem (real-world) | translate a scenario into a pattern | mixed |
| C11 | Exam twist — "which pattern?" | code shape identical, intent differs | Adapter vs Decorator vs Proxy |
Read the matrix once. Every example below names its cell in the callout title.
Example 1 — Adapter with a translated signature (C1)
Forecast: guess now — does the client ever see the name start_mp4?
- The client interface is fixed: it will only ever call
.play(file). Why this step? The whole point of Adapter is the client stays ignorant of the adaptee. We anchor to the interface the client already speaks. - The adapter holds the adaptee by composition (
self._legacy = legacy) and forwards:play(file)callsself._legacy.start_mp4(file). Why this step? Forwarding-with-a-renamed-call is the translation. No inheritance from the legacy class is needed. - Trace the call:
play("clip.mp4")→start_mp4("clip.mp4")→ returns"playing clip.mp4". Why this step? This confirms the return value passes straight through, unchanged in content, changed only in which method delivered it.
Verify: The client learned exactly 1 method (play). The output string is "playing clip.mp4". Units sanity: input file name in, same file name echoed out — no data invented.
Example 2 — Bridge kills the class explosion (C2)
Forecast: guess both numbers before reading.
- Inheritance path: every combination is its own class: That is classes. Why this step? Inheritance bakes both dimensions into the type, so every pairing needs a fresh class — the "explosion".
- Bridge path: shapes form one hierarchy ( classes), colours another ( classes), joined by a reference. That is classes.
Why this step? A
Shapehas-aColor; the two axes vary independently, so we add, not multiply. - Growth check: add a 4th shape. Inheritance jumps by (to 16); Bridge jumps by (to 8). Why this step? This shows why Bridge scales — new work is per axis, not .

Verify: vs ; Bridge saves classes here, and the gap widens as either axis grows. This is Open-Closed Principle made concrete: extend by adding, not editing.
Example 3 — Decorator stacking, order matters (C3)
Forecast: guess whether both orders give the same number.
- Innermost first:
Coffee().cost(). Why this step? A decorator always calls the wrapped object first, then adds its bit — so evaluation flows outward from the core. - Stack A
Sugar(Milk(Coffee())): Milk sees , returns ; Sugar sees , returns . Why this step? Each layer only knows "ask inner, add mine" — no layer knows the full stack. - Stack B
Milk(Sugar(Coffee())): Sugar sees , returns ; Milk returns . Why this step? Because addition is commutative here, order does not change the total — but it would if a decorator multiplied or logged, where order is visible.
Verify: Both give . Sanity: regardless of grouping — matches associativity of .
Example 4 — Composite on a degenerate empty folder (C4)
Forecast: guess the number before trusting the code.
Folder.size()=sum(c.size() for c in self.children), andchildrenis[]. Why this step? We must check the base of the recursion. An empty composite is the degenerate case that breaks naive code if forgotten.sum([])in Python is0. Why this step? The uniform interface still returns a valid number — no special-casing, no crash. That robustness is the payoff of the pattern.
Verify: Folder().size() . Sanity: an empty folder occupies KB of file content — matches reality.
Example 5 — Composite deep recursion (C5)
Forecast: guess the total KB.
- Draw the tree (see figure):
root → [File 10, docs],docs → [File 3, File 7]. Why this step? Composite is a tree; visualising it shows where recursion descends. - Leaf sizes return themselves:
File(3).size()=3,File(7).size()=7. Why this step? Leaves are the recursion's stopping points — they return their own value directly. docs.size()sums its children: . Why this step? A composite forwardssize()to each child without checking the type — leaf and composite answered the same call.root.size()sumsFile(10)anddocs: . Why this step? The call recursed one extra level automatically; no code knew how deep it went — that is Recursion doing the traversal.

Verify: KB. Total of all leaves — matches, confirming no node was double-counted or dropped.
Example 6 — Flyweight: empty pool, first creation (C6) and reuse (C7)
Forecast: guess the count of created objects (there are 6 letters).
- Start with an empty pool
{}. Why this step? C6 — the zero case. The first request for any char must create, because nothing is cached yet. - Walk the letters:
b(new),a(new),n(new),a(cached),n(cached),a(cached). Why this step? Distinct characters are{b, a, n}= . Only first-sightings create; the rest hit the pool — C7, reuse. - Identity, not equality: the two
arequests return the same object (istrue), because the pool returns the stored instance. Why this step? This is the memory win — 6 letters, only 3 glyph objects, shared by identity.
Verify: Created objects (unique letters in "banana"). Total letters . Memory saved objects. Positions/colours (extrinsic) are passed at draw time and are not stored in the pool.
Example 7 — Proxy: lazy loading does work only once (C8)
Forecast: guess: 0, 1, or 2?
- Construct the proxy — it stores the filename but creates no
RealImageyet. Why this step? Proxy controls access: the expensive load is deferred (laziness). Nothing loads at construction. - First
draw(): proxy seesself._real is None, so it createsRealImage→ prints"loading"once, then draws. Why this step? Access finally happened, so the guarded work runs — exactly once. - Second
draw():self._realis now set, so the proxy skips creation and just draws. Why this step? Caching the subject means the costly step never repeats — the "control access" intent.
Verify: "loading" prints exactly 1 time across two draws. Contrast with Decorator (Example 3), which adds behaviour every call; Proxy guards and may do the work zero or one time.
Example 8 — Facade orchestrates in a fixed order (C9)
Forecast: guess the client's call count.
- Client makes one call:
start(). Why this step? Facade's promise is one simple door — the client's cognitive load drops to a single method. - Facade emits the ordered sequence:
freeze → load → read → jump. Why this step? Boot order is not arbitrary: you freeze the CPU before loading memory, and jump only after disk is read. The facade encodes this ordering so the client can't get it wrong.
Verify: Client calls ; subsystem calls ; recorded order is exactly ["freeze","load","read","jump"]. Facade did not hide the subsystems (advanced clients can still reach computer.cpu), it only orchestrated them — this respects Single Responsibility Principle.
Example 9 — Word problem: pick the pattern (C10)
Forecast: name the pattern before reading.
- Nothing new is added, no access is controlled, no tree, no sharing — only the interface/units mismatch. Why this step? We eliminate by intent: not Decorator (no new behaviour), not Proxy (no guarding), not Composite/Flyweight/Bridge. What's left is translation.
- Answer: Adapter. Body:
def charge(self, amount): self._gw.processPayment(int(amount * 100)). Why this step? It translates both the name (charge→processPayment) and the units (dollars→cents) — a signature+unit adapter. - Unit trace:
charge(2.30)→processPayment(230). Why this step? — confirms the conversion factor.
Verify: cents. Pattern = Adapter (C1 in disguise, dressed as a word problem).
Example 10 — Exam twist: identical code shape, different intent (C11)
Forecast: guess all three before the reveal.
- A adds behaviour around the forward (logging) with the same interface → Decorator. Why this step? "Same interface + adds responsibility" is the Decorator signature.
- B controls whether the forward happens (permission) → Proxy. Why this step? Guarding access, possibly skipping the call, is Proxy's job.
- C exists to bridge a name mismatch to existing code → Adapter. Why this step? The reason-for-being is a mismatch fix, applied after the fact.
Verify: A=Decorator, B=Proxy, C=Adapter. All share the code shape "hold one object, forward request()" — the exam tests whether you read intent, not syntax. This is the parent note's 80/20 point made into a question.
Recall
Recall Empty folder size (C4)
What does Folder().size() return and why? ::: 0, because sum([]) is 0 — the uniform interface handles the degenerate base case with no special code.
Recall Flyweight object count for "banana" (C6+C7)
How many Glyph objects are created? ::: 3 — one per unique character {b,a,n}; repeated letters return the same shared instance by identity.
Recall Lazy proxy print count (C8)
Calling draw() twice on a lazy ProxyImage, how many times does "loading" print? ::: 1 — created on first access, cached after.
Recall Bridge vs inheritance class count (C2)
shapes colours: classes with inheritance vs Bridge? ::: () vs ().
Recall Decorator order (C3)
Does Sugar(Milk(c)) differ from Milk(Sugar(c)) for the coffee costs? ::: No, both are — but only because both add; non-commutative effects (tax/discount) would differ.