Design Pattern Guide

23 patterns · one lesson per page

Design
Patterns

A focused learning guide to the 23 Gang of Four patterns — one clear lesson at a time.

Pradeep Kumar

Harsh Sharma · Sys Titan, Live LLD Weekday 2.0 Batch

Pradeep Kumar · Senior Backend Engineer, ex-NetApp & Qualcomm

Section 1 — Foundations

What Is a Design Pattern?

No prior experience needed. We start from scratch — with a real-world story before a single line of code.

Think of an architect designing buildings. Over centuries, architects noticed that certain structural problems kept appearing again and again — how to build a load-bearing archway, how to ventilate a room without losing heat. Rather than solving these problems from scratch each time, they documented solutions that worked. Not exact blueprints, but patterns — general approaches you adapt to your specific situation.

A software design pattern is exactly this: a documented, proven solution to a recurring problem in software design. It is a template — not actual code — for how to tackle a situation that skilled developers have faced thousands of times before.

Pattern ≠ Algorithm, Pattern ≠ Library

An algorithm is a specific step-by-step recipe with a definite input and output. "Sort these numbers using quicksort" — there is one correct implementation. A design pattern, by contrast, is a general strategy. Two developers using the same pattern might write completely different code.

A library or framework is actual code you import and run. A pattern is a concept — a shared vocabulary. When a senior dev says "we need a Facade here," that is a communication shortcut, not a library call.

Cooking analogy: A recipe (algorithm) says "add exactly 200ml of milk, stir 3 times clockwise." A pattern is more like the concept of "sautéing" — you understand the goal and adapt it to your ingredients.
Why Do Patterns Exist? The Cost of Reinventing the Wheel

Without patterns, every developer who faces "how do I notify many parts of my app when data changes?" must discover the solution independently. With patterns, they can say "that is the Observer pattern" — apply a proven solution in minutes instead of days, and avoid all the pitfalls others discovered the hard way.

  • Shared vocabulary between developers
  • Proven solutions to known problems
  • A way to think at a higher level of abstraction
  • Faster onboarding: a new developer who knows patterns can understand a codebase faster
Section 1.2

What Does a Pattern Consist Of?

Every well-documented pattern has four essential parts.

① Intent

A brief, precise statement of what the pattern does and what problem it solves. Usually one or two sentences. Example: "Define an interface for creating objects, letting subclasses decide which class to instantiate."

② Motivation

A scenario that illustrates the design problem and why an ordinary approach fails. This is the story that makes the pattern click intuitively.

③ Structure

A diagram showing the classes/objects involved and how they relate. Usually a UML class diagram. Gives you the "shape" of the solution.

④ Code Example

A concrete implementation showing the pattern in action. Makes the abstract tangible and actionable.

Section 1.3

History of Design Patterns

Patterns did not originate in software. They started in architecture — then crossed disciplines in a way that changed how software is written forever.

1977
Christopher Alexander — "A Pattern Language"
Architect Christopher Alexander published a landmark book documenting 253 patterns for designing buildings, towns, and communities. His key insight: good design is not arbitrary — it follows recognisable, teachable patterns.
1987
Ward Cunningham & Kent Beck Apply Patterns to Software
Two pioneers of Agile methodology began exploring whether Alexander's pattern idea could apply to software design. They published "Using Pattern Languages for Object-Oriented Programs" — the first bridge between architectural patterns and code.
1994
The Gang of Four — "Design Patterns"
Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides published the book that defined the field. They documented 23 patterns for object-oriented software. The "GoF book" became one of the most influential programming books ever written — its 23 patterns remain the canonical vocabulary of software design.
1995–Present
Patterns Enter the Mainstream & Scale to Architecture
GoF patterns become standard curriculum in CS degrees worldwide. Major frameworks like Spring, .NET, and Ruby on Rails are built around patterns. The concept scales upward into architectural patterns: MVC, microservices, event sourcing, CQRS.
Section 1.4

Why Should You Learn Patterns?

Beyond the theoretical — here is what patterns actually do for your career and your code.

🗣️ Shared Language

"We need a Factory here" communicates more in 5 words than 5 paragraphs of explanation. Patterns are vocabulary — they let teams think and talk faster.

🛡️ Avoid Known Traps

Every pattern comes with hard-won knowledge about what goes wrong. Knowing patterns means you inherit decades of collective experience — including failure modes.

🏗️ Think at Scale

Patterns let you think about structure instead of syntax. Senior engineers think in patterns before they think in code.

💼 Career Leverage

System design interviews, architecture roles, and code reviews all expect fluency in patterns. It is a professional signal of seniority.

Before & After: Without vs With Pattern

❌ Without Pattern — Notification System
// Every component directly pokes every other class OrderService { placeOrder(order) { // Manually notify EVERYTHING emailService.sendEmail(order) smsService.sendSms(order) inventoryService.reduce(order) analyticsService.track(order) // Add a new subscriber? Edit THIS file. // Tight coupling. Fragile. Untestable. } }
✅ With Observer Pattern
class OrderService { constructor() { this.subscribers = [] } subscribe(listener) { this.subscribers.push(listener) } placeOrder(order) { // Notify without knowing who listens this.subscribers.forEach( s => s.onOrderPlaced(order) ) // Add new service? Register externally. // OrderService is clean and testable. } }
Section 1.5

Criticism of Patterns — The Honest Truth

This guide will not cheerlead for patterns. They are tools — and like all tools, they can be misused.

"If all you have is a hammer, everything looks like a nail." — Developers who have just learned patterns have a dangerous tendency to use them everywhere, whether needed or not.
⚠️ Patterns as Workarounds for Missing Language Features

Some critics — especially Peter Norvig — have pointed out that several GoF patterns are only needed because most languages lack certain features. The Visitor pattern exists primarily because languages like Java do not support multiple dispatch. In a language like Common Lisp that does have multiple dispatch, Visitor is simply not needed. The Iterator pattern is built into Python via __iter__ and for loops. As languages evolve, some patterns become obsolete — which is healthy.

⚠️ Overengineering — The Pattern Abuse Problem

A junior developer who just learned Abstract Factory might restructure a simple object creation into five new classes, two interfaces, and three packages — for a problem that a plain constructor would have solved in three lines. Patterns should reduce complexity, not add it. If applying a pattern makes your code harder to read than the original mess, you have made it worse.

Rule of Three: Only extract a pattern when you see the same problem recurring in at least three places. One instance does not justify the abstraction cost.
⚠️ Patterns Can Hide Simple Solutions

Sometimes a 5-line function is the right answer, and a pattern is the wrong one. The right question is never "which pattern should I use?" but "does this problem actually need a pattern, or is there a simpler solution?"

Section 1.6

Classification of the 23 GoF Patterns

The Gang of Four organised their 23 patterns into three families based on the fundamental type of problem each solves.

🧱 Creational

How objects are born. These patterns control the creation process, giving you flexibility over what gets created, who creates it, and how it comes to exist.

🔗 Structural

How objects are assembled. These patterns describe how classes and objects are composed into larger structures.

💬 Behavioral

How objects communicate. These patterns define how responsibility is distributed and how objects interact.

All 23 Patterns at a Glance

Factory Method
Let subclasses decide which object to create
Beginner Creational
Abstract Factory
Create families of related objects
Intermediate Creational
Builder
Construct complex objects step by step
Beginner Creational
Prototype
Clone existing objects instead of creating new ones
Beginner Creational
Singleton
Ensure only one instance ever exists
Beginner Creational
Adapter
Make incompatible interfaces work together
Beginner Structural
Bridge
Separate abstraction from implementation
Advanced Structural
Composite
Treat trees of objects uniformly
Intermediate Structural
Decorator
Add behavior by wrapping objects
Intermediate Structural
Facade
Provide a simple interface to a complex system
Beginner Structural
Flyweight
Share data to support massive numbers of objects
Advanced Structural
Proxy
Control access to another object
Intermediate Structural
Chain of Responsibility
Pass requests along a chain of handlers
Intermediate Behavioral
Command
Turn actions into objects
Intermediate Behavioral
Iterator
Traverse a collection without knowing its structure
Beginner Behavioral
Interpreter
Evaluate sentences in a simple grammar
Advanced Behavioral
Mediator
Centralise complex communications
Intermediate Behavioral
Memento
Save and restore object state
Intermediate Behavioral
Observer
Notify many objects when one changes
Beginner Behavioral
State
Change object behavior when state changes
Intermediate Behavioral
Strategy
Swap algorithms at runtime
Beginner Behavioral
Template Method
Define a skeleton, let subclasses fill in steps
Beginner Behavioral
Visitor
Add operations to objects without modifying them
Advanced Behavioral
Section 2 — Creational Patterns

Object Creation

These patterns are all about HOW objects are born into existence — and giving you control over that birth. Instead of simply calling new SomeClass() everywhere, creational patterns let you decide what gets created, when, and by whom.

FM
Creational Pattern 1 of 5
Factory Method
Define an interface for creating objects, but let subclasses decide which class to instantiate.
Beginner Creational
① What Is It?
A logistics company expanded from trucks to ships and planes. The company has a standard "deliver package" process, but the type of transport is determined by each division. The main process does not change; only the vehicle creation varies.

Factory Method defines a method for creating objects in a superclass, allowing subclasses to alter the type of objects created. Instead of calling a constructor directly (new Dog()), you call a factory method (createAnimal()) which subclasses override to return different types.

② The Problem It Solves

You are building a UI framework. You have a Dialog class with a render() method that creates buttons. On Windows it should create WindowsButton; on Mac it should create MacButton. If you hardcode new WindowsButton() inside Dialog.render(), you can never use the Dialog on Mac without modifying its source code. You have tightly coupled the creator to a specific product type.

③ The Solution

Move the object creation into a dedicated factory method. The base Dialog class calls createButton(). WindowsDialog overrides it to return a WindowsButton. MacDialog overrides it to return a MacButton. The render() logic is written once in the base class — the what to create changes per subclass.

④ Structure
+---------------------------+ | «abstract» Creator | +---------------------------+ | + createProduct(): Product| ← Factory Method | + someOperation() | +-------------+-------------+ | extends +--------+--------+ | | +----+----------+ +---+-----------+ | ConcreteCreatorA| | ConcreteCreatorB| | createProduct() | | createProduct() | | returns ProductA| | returns ProductB| +-----------------+ +-----------------+
▶ See It In Action
Press Play — each step maps the story to the pattern.
Ready
See it in action
Press Play. Watch the highlight, read the role labels, then the “In code” line.
⑤ Pseudocode
Language:
interface Transport { deliver() } class Truck implements Transport { deliver() { print("Deliver by road") } } class Ship implements Transport { deliver() { print("Deliver by sea") } } abstract class Logistics { // ← The Factory Method abstract createTransport() : Transport planDelivery() { t = this.createTransport() t.deliver() } } class RoadLogistics extends Logistics { createTransport() { return new Truck() } } class SeaLogistics extends Logistics { createTransport() { return new Ship() } } logistics = new RoadLogistics() logistics.planDelivery() // "Deliver by road"
from abc import ABC, abstractmethod class Transport(ABC): @abstractmethod def deliver(self): ... class Truck(Transport): def deliver(self): print("Deliver by road") class Ship(Transport): def deliver(self): print("Deliver by sea") class Logistics(ABC): @abstractmethod def create_transport(self) -> Transport: ... # the Factory Method def plan_delivery(self): transport = self.create_transport() transport.deliver() class RoadLogistics(Logistics): def create_transport(self): return Truck() class SeaLogistics(Logistics): def create_transport(self): return Ship() logistics = RoadLogistics() logistics.plan_delivery() # "Deliver by road"
#include <memory> #include <iostream> class Transport { public: virtual void deliver() = 0; virtual ~Transport() = default; }; class Truck : public Transport { public: void deliver() override { std::cout << "Deliver by road\n"; } }; class Ship : public Transport { public: void deliver() override { std::cout << "Deliver by sea\n"; } }; class Logistics { public: virtual std::unique_ptr<Transport> createTransport() = 0; // Factory Method void planDelivery() { auto transport = createTransport(); transport->deliver(); } virtual ~Logistics() = default; }; class RoadLogistics : public Logistics { public: std::unique_ptr<Transport> createTransport() override { return std::make_unique<Truck>(); } }; class SeaLogistics : public Logistics { public: std::unique_ptr<Transport> createTransport() override { return std::make_unique<Ship>(); } }; int main() { RoadLogistics logistics; logistics.planDelivery(); // "Deliver by road" }
interface Transport { void deliver(); } class Truck implements Transport { public void deliver() { System.out.println("Deliver by road"); } } class Ship implements Transport { public void deliver() { System.out.println("Deliver by sea"); } } abstract class Logistics { abstract Transport createTransport(); // the Factory Method void planDelivery() { Transport transport = createTransport(); transport.deliver(); } } class RoadLogistics extends Logistics { Transport createTransport() { return new Truck(); } } class SeaLogistics extends Logistics { Transport createTransport() { return new Ship(); } } public class Main { public static void main(String[] args) { Logistics logistics = new RoadLogistics(); logistics.planDelivery(); // "Deliver by road" } }
class Transport { deliver() { throw new Error("not implemented"); } } class Truck extends Transport { deliver() { console.log("Deliver by road"); } } class Ship extends Transport { deliver() { console.log("Deliver by sea"); } } class Logistics { createTransport() { throw new Error("override me"); } // Factory Method planDelivery() { const transport = this.createTransport(); transport.deliver(); } } class RoadLogistics extends Logistics { createTransport() { return new Truck(); } } class SeaLogistics extends Logistics { createTransport() { return new Ship(); } } const logistics = new RoadLogistics(); logistics.planDelivery(); // "Deliver by road"
⑥ Real Industry Example
☕ Java's Calendar.getInstance() & Android View Inflation

Java's Calendar.getInstance() is a classic Factory Method — it returns a Calendar subclass appropriate for your locale (GregorianCalendar for most, BuddhistCalendar in Thailand). Android's LayoutInflater.inflate() creates View objects (Button, TextView, ImageView) from XML — a factory method that reads the XML tag and instantiates the correct View subclass automatically.

⑦ Trade-offs
✓ Benefit⚠ Cost🚫 When It Hurts
Open/Closed: add new products without changing existing codeRequires a new subclass for each product type — class explosionWhen you only ever need one product type
Decouples creator from concrete products — easier to testCode becomes more complex and harder to traceWhen product types rarely change
⑨ When to Use It
  • You do not know which class to instantiate until runtime
  • You want subclasses to control which objects they create
  • You are building a framework that should be extensible
  • You have related classes that all implement the same interface
⑩ Quick Quiz
Q1: What distinguishes Factory Method from a simple static factory function?
AF
Creational Pattern 2 of 5
Abstract Factory
Produce families of related objects without specifying their concrete classes.
Intermediate Creational
① What Is It?
Think of furniture stores. IKEA's "KALLAX" series has a shelf, a desk, a TV unit, and a chair — all matching in material, color, and style. An Abstract Factory is like a furniture factory brand: it guarantees that everything you order from it will match as a family.

Abstract Factory provides an interface for creating families of related or dependent objects without specifying their concrete classes. Where Factory Method creates one kind of product, Abstract Factory creates multiple kinds designed to work together.

② The Problem It Solves

You are building a cross-platform UI library with Button and Checkbox widgets. On Windows, both must be Windows-styled. On Mac, both must be Mac-styled. Using separate Factory Methods for each widget type, nothing prevents a developer from accidentally mixing Windows buttons with Mac checkboxes — creating visual inconsistency.

③ The Solution

Create a GUIFactory interface with createButton() and createCheckbox(). WindowsFactory always returns Windows-styled widgets; MacFactory returns Mac-styled widgets. The client talks only to GUIFactory — and gets a guaranteed consistent family.

④ Structure
+------------------+ +------------------+ | AbstractFactory | | ProductA | | createA()/createB| +------------------+ +--------+---------+ +------------------+ ^ | ProductB | +-----+------+ +------------------+ | | +--+---+ +----+----+ |WinFac| | MacFac | +------+ +---------+
▶ See It In Action
Press Play — each step maps the story to the pattern.
Ready
See it in action
Press Play. Watch the highlight, read the role labels, then the “In code” line.
⑤ Pseudocode
Language:
interface GUIFactory { createButton() : Button createCheckbox() : Checkbox } class WindowsFactory implements GUIFactory { createButton() { return new WindowsButton() } createCheckbox() { return new WindowsCheckbox() } } class MacFactory implements GUIFactory { createButton() { return new MacButton() } createCheckbox() { return new MacCheckbox() } } // Client only talks to the abstract factory class Application { constructor(factory: GUIFactory) { this.button = factory.createButton() this.checkbox = factory.createCheckbox() // Guaranteed to be a consistent family } } if (OS == "Windows") { app = new Application(new WindowsFactory()) } else { app = new Application(new MacFactory()) }
from abc import ABC, abstractmethod class Button(ABC): @abstractmethod def render(self): ... class Checkbox(ABC): @abstractmethod def render(self): ... class WindowsButton(Button): def render(self): print("Windows button") class WindowsCheckbox(Checkbox): def render(self): print("Windows checkbox") class MacButton(Button): def render(self): print("Mac button") class MacCheckbox(Checkbox): def render(self): print("Mac checkbox") class GUIFactory(ABC): @abstractmethod def create_button(self) -> Button: ... @abstractmethod def create_checkbox(self) -> Checkbox: ... class WindowsFactory(GUIFactory): def create_button(self): return WindowsButton() def create_checkbox(self): return WindowsCheckbox() class MacFactory(GUIFactory): def create_button(self): return MacButton() def create_checkbox(self): return MacCheckbox() class Application: def __init__(self, factory: GUIFactory): self.button = factory.create_button() self.checkbox = factory.create_checkbox() # Guaranteed to be a consistent family app = Application(WindowsFactory() if os_name == "Windows" else MacFactory())
#include <memory> #include <iostream> #include <string> struct Button { virtual void render() = 0; virtual ~Button() = default; }; struct Checkbox { virtual void render() = 0; virtual ~Checkbox() = default; }; struct WindowsButton : Button { void render() override { std::cout << "Windows button\n"; } }; struct WindowsCheckbox : Checkbox { void render() override { std::cout << "Windows checkbox\n"; } }; struct MacButton : Button { void render() override { std::cout << "Mac button\n"; } }; struct MacCheckbox : Checkbox { void render() override { std::cout << "Mac checkbox\n"; } }; struct GUIFactory { virtual std::unique_ptr<Button> createButton() = 0; virtual std::unique_ptr<Checkbox> createCheckbox() = 0; virtual ~GUIFactory() = default; }; struct WindowsFactory : GUIFactory { std::unique_ptr<Button> createButton() override { return std::make_unique<WindowsButton>(); } std::unique_ptr<Checkbox> createCheckbox() override { return std::make_unique<WindowsCheckbox>(); } }; struct MacFactory : GUIFactory { std::unique_ptr<Button> createButton() override { return std::make_unique<MacButton>(); } std::unique_ptr<Checkbox> createCheckbox() override { return std::make_unique<MacCheckbox>(); } }; class Application { public: explicit Application(std::unique_ptr<GUIFactory> factory) { button = factory->createButton(); checkbox = factory->createCheckbox(); // guaranteed consistent family } private: std::unique_ptr<Button> button; std::unique_ptr<Checkbox> checkbox; };
interface Button { void render(); } interface Checkbox { void render(); } class WindowsButton implements Button { public void render() { System.out.println("Windows button"); } } class WindowsCheckbox implements Checkbox { public void render() { System.out.println("Windows checkbox"); } } class MacButton implements Button { public void render() { System.out.println("Mac button"); } } class MacCheckbox implements Checkbox { public void render() { System.out.println("Mac checkbox"); } } interface GUIFactory { Button createButton(); Checkbox createCheckbox(); } class WindowsFactory implements GUIFactory { public Button createButton() { return new WindowsButton(); } public Checkbox createCheckbox() { return new WindowsCheckbox(); } } class MacFactory implements GUIFactory { public Button createButton() { return new MacButton(); } public Checkbox createCheckbox() { return new MacCheckbox(); } } class Application { Application(GUIFactory factory) { Button button = factory.createButton(); Checkbox checkbox = factory.createCheckbox(); // Guaranteed to be a consistent family } }
class WindowsButton { render() { console.log("Windows button"); } } class WindowsCheckbox { render() { console.log("Windows checkbox"); } } class MacButton { render() { console.log("Mac button"); } } class MacCheckbox { render() { console.log("Mac checkbox"); } } class WindowsFactory { createButton() { return new WindowsButton(); } createCheckbox() { return new WindowsCheckbox(); } } class MacFactory { createButton() { return new MacButton(); } createCheckbox() { return new MacCheckbox(); } } class Application { constructor(factory) { this.button = factory.createButton(); this.checkbox = factory.createCheckbox(); // Guaranteed to be a consistent family } } const app = new Application(os === "Windows" ? new WindowsFactory() : new MacFactory());
⑥ Real Industry Example
📱 Flutter's ThemeData & Qt Widget Toolkit

Flutter's theming system is an Abstract Factory: a ThemeData object defines a complete family of coordinated UI elements — colors, text styles, button styles — guaranteed to be visually consistent. Qt's style system has QStyle subclasses (WindowsStyle, MacStyle, FusionStyle) that act as Abstract Factories producing matching widget appearances. Hibernate's SessionFactory abstracts over different databases, returning properly configured sessions for each.

⑦ Trade-offs
✓ Benefit⚠ Cost🚫 When It Hurts
Guarantees consistency among products in a familyAdding new product types requires changing all factory interfaces and implementationsWhen product families are unstable — constantly adding new product types
Isolates concrete product classes from client codeMore complex than Factory Method — significant boilerplateWhen you only have one product variant — massive overkill
⑨ When to Use It
  • You need to ensure that products from a family are used together
  • You want to support multiple platform families (Windows/Mac, MySQL/PostgreSQL)
  • You anticipate switching between entire product families at runtime
⑩ Quick Quiz
What is the main advantage of Abstract Factory over multiple independent Factory Methods?
Bu
Creational Pattern 3 of 5
Builder
Construct complex objects step by step, separating construction from representation.
Beginner Creational
① What Is It?
Ordering a custom burger at Five Guys. You build it step by step: "Add the patty. Add cheese. Add pickles. No onions. Add jalapeños." The cashier (Director) guides the process; the kitchen (Builder) assembles it. The final result is a complex object built through a controlled sequence of steps.

Builder separates the construction of a complex object from its representation. It solves the "telescoping constructor" problem — where constructors accumulate so many parameters that you end up passing null for most of them.

② The Problem It Solves

You need to construct an object with many optional fields. A single constructor either forces dozens of parameters (most null) or explodes into overloaded variants that are hard to read and easy to misuse.

③ The Solution

Put construction steps on a Builder that returns itself after each call. A Director can orchestrate a fixed recipe, while clients can also call steps fluently and finish with build().

④ Structure
+------------------+ +------------------+ | Director |--------->| «interface» | | construct() | | Builder | +------------------+ +------------------+ ^ | +-------+--------+ | ConcreteBuilder| | buildProduct() | +----------------+
▶ See It In Action
Press Play — each step maps the story to the pattern.
Ready
See it in action
Press Play. Watch the highlight, read the role labels, then the “In code” line.
⑤ Pseudocode
Language:
class QueryBuilder { constructor() { this.table = "" this.conditions = [] this.columns = ["*"] this.limitVal = null } from(table) { this.table = table; return this } select(...cols) { this.columns = cols; return this } where(cond) { this.conditions.push(cond); return this } limit(n) { this.limitVal = n; return this } build() { return `SELECT ${this.columns} FROM ${this.table} WHERE ${this.conditions.join(' AND ')} LIMIT ${this.limitVal}` } } // Fluent, readable — no null parameters needed query = new QueryBuilder() .from("users") .select("id", "name", "email") .where("age > 18") .limit(100) .build()
class QueryBuilder: def __init__(self): self.table = "" self.conditions = [] self.columns = ["*"] self.limit_val = None def from_table(self, table): self.table = table return self def select(self, *cols): self.columns = cols return self def where(self, cond): self.conditions.append(cond) return self def limit(self, n): self.limit_val = n return self def build(self): return (f"SELECT {', '.join(self.columns)} FROM {self.table} " f"WHERE {' AND '.join(self.conditions)} LIMIT {self.limit_val}") # Fluent, readable -- no null parameters needed query = (QueryBuilder() .from_table("users") .select("id", "name", "email") .where("age > 18") .limit(100) .build())
#include <string> #include <vector> #include <sstream> class QueryBuilder { public: QueryBuilder& from(const std::string& t) { table = t; return *this; } QueryBuilder& select(std::vector<std::string> cols) { columns = std::move(cols); return *this; } QueryBuilder& where(const std::string& cond) { conditions.push_back(cond); return *this; } QueryBuilder& limit(int n) { limitVal = n; return *this; } std::string build() { std::ostringstream out; out << "SELECT ... FROM " << table << " LIMIT " << limitVal; return out.str(); } private: std::string table; std::vector<std::string> conditions; std::vector<std::string> columns{"*"}; int limitVal = 0; }; int main() { // Fluent, readable -- no null parameters needed std::string query = QueryBuilder() .from("users") .select({"id", "name", "email"}) .where("age > 18") .limit(100) .build(); }
import java.util.*; class QueryBuilder { private String table = ""; private List<String> conditions = new ArrayList<>(); private List<String> columns = List.of("*"); private Integer limitVal; QueryBuilder from(String table) { this.table = table; return this; } QueryBuilder select(String... cols) { this.columns = List.of(cols); return this; } QueryBuilder where(String cond) { this.conditions.add(cond); return this; } QueryBuilder limit(int n) { this.limitVal = n; return this; } String build() { return "SELECT " + columns + " FROM " + table + " LIMIT " + limitVal; } } public class Main { public static void main(String[] args) { // Fluent, readable -- no null parameters needed String query = new QueryBuilder() .from("users") .select("id", "name", "email") .where("age > 18") .limit(100) .build(); } }
class QueryBuilder { constructor() { this.table = ""; this.conditions = []; this.columns = ["*"]; this.limitVal = null; } from(table) { this.table = table; return this; } select(...cols) { this.columns = cols; return this; } where(cond) { this.conditions.push(cond); return this; } limit(n) { this.limitVal = n; return this; } build() { return `SELECT ${this.columns} FROM ${this.table} WHERE ${this.conditions.join(" AND ")} LIMIT ${this.limitVal}`; } } // Fluent, readable -- no null parameters needed const query = new QueryBuilder() .from("users") .select("id", "name", "email") .where("age > 18") .limit(100) .build();
⑥ Real Industry Example
📄 SQLAlchemy / Hibernate Query Builders & OkHttp Request Builder

SQLAlchemy's Query API is a textbook Builder: session.query(User).filter(User.age > 18).order_by(User.name).limit(10).all() — each method adds a step, .all() triggers final execution. OkHttp's Request.Builder similarly constructs HTTP requests step by step. Lombok's @Builder annotation in Java auto-generates Builder code for any class.

⑦ Trade-offs
✓ Benefit⚠ Cost🚫 When It Hurts
Eliminates telescoping constructor problemRequires a separate Builder class (doubles the code)When objects are simple — a 3-parameter constructor is fine
Highly readable fluent API — reads like a sentenceBuilder must be synchronised with product — maintenance burdenWhen the product has very few optional fields
⑨ When to Use It
  • Your constructor needs more than 4-5 parameters, many optional
  • You want a readable, fluent API for object creation
  • You need to produce different representations of the same object
⑩ Quick Quiz
What problem does Builder primarily solve?
Pr
Creational Pattern 4 of 5
Prototype
Clone existing objects without coupling your code to their classes.
Beginner Creational
① What Is It?
Biology class: cell division. A cell does not build a new cell from raw atoms — it copies itself. The new cell is a near-identical clone of the original. Instead of constructing a new object from scratch, you copy an existing one.

Prototype lets you copy existing objects without making your code dependent on their classes. You define a clone() method — the object knows how to copy itself — so any object can be a template for creating new ones.

② The Problem It Solves

Creating objects from scratch is expensive (DB loads, complex config). You need many similar instances that differ only slightly, but copying by hand duplicates setup code and risks incomplete copies.

③ The Solution

Give objects a clone() (or copy) operation so they copy themselves — including private state — and return a new instance the client can tweak.

④ Structure
+------------------+ | «interface» | | Prototype | | + clone() | +--------+---------+ ^ +-----+------+ | | +--+---+ +----+----+ |Concrete| |Concrete B| | clone()| | clone() | +--------+ +----------+
▶ See It In Action
Press Play — each step maps the story to the pattern.
Ready
See it in action
Press Play. Watch the highlight, read the role labels, then the “In code” line.
⑤ Pseudocode
Language:
interface Cloneable { clone() : Cloneable } class GameCharacter implements Cloneable { constructor(name, health, weapons) { this.name = name this.health = health this.weapons = weapons } clone() { // Deep copy — not just a reference return new GameCharacter( this.name, this.health, [...this.weapons] // copy the array ) } } // Create a "boss template" once bossTemplate = new GameCharacter("Boss", 1000, ["gun", "sword"]) // Spawn many boss instances cheaply by cloning boss1 = bossTemplate.clone() boss1.name = "Boss-Room-1" boss2 = bossTemplate.clone() boss2.health = 1500 // slightly harder
import copy class GameCharacter: def __init__(self, name, health, weapons): self.name = name self.health = health self.weapons = weapons def clone(self): # deepcopy -- not just a reference return copy.deepcopy(self) # Create a "boss template" once boss_template = GameCharacter("Boss", 1000, ["gun", "sword"]) # Spawn many boss instances cheaply by cloning boss1 = boss_template.clone() boss1.name = "Boss-Room-1" boss2 = boss_template.clone() boss2.health = 1500 # slightly harder
#include <string> #include <vector> #include <memory> class GameCharacter { public: GameCharacter(std::string name, int health, std::vector<std::string> weapons) : name(std::move(name)), health(health), weapons(std::move(weapons)) {} std::unique_ptr<GameCharacter> clone() const { return std::make_unique<GameCharacter>(*this); // copy constructor -- deep copy } std::string name; int health; std::vector<std::string> weapons; }; int main() { GameCharacter bossTemplate("Boss", 1000, {"gun", "sword"}); auto boss1 = bossTemplate.clone(); boss1->name = "Boss-Room-1"; auto boss2 = bossTemplate.clone(); boss2->health = 1500; // slightly harder }
import java.util.*; class GameCharacter implements Cloneable { String name; int health; List<String> weapons; GameCharacter(String name, int health, List<String> weapons) { this.name = name; this.health = health; this.weapons = weapons; } public GameCharacter clone() { return new GameCharacter(name, health, new ArrayList<>(weapons)); // deep copy the list } } public class Main { public static void main(String[] args) { GameCharacter bossTemplate = new GameCharacter("Boss", 1000, List.of("gun", "sword")); GameCharacter boss1 = bossTemplate.clone(); boss1.name = "Boss-Room-1"; GameCharacter boss2 = bossTemplate.clone(); boss2.health = 1500; // slightly harder } }
class GameCharacter { constructor(name, health, weapons) { this.name = name; this.health = health; this.weapons = weapons; } clone() { // deep copy -- not just a reference return new GameCharacter(this.name, this.health, [...this.weapons]); } } // Create a "boss template" once const bossTemplate = new GameCharacter("Boss", 1000, ["gun", "sword"]); // Spawn many boss instances cheaply by cloning const boss1 = bossTemplate.clone(); boss1.name = "Boss-Room-1"; const boss2 = bossTemplate.clone(); boss2.health = 1500; // slightly harder
⑥ Real Industry Example
🎮 Unity/Unreal Engine Object Spawning & JavaScript's Prototype Chain

Unity's Instantiate() function is Prototype in action — you create a "prefab" (template object), then clone it to spawn many instances. JavaScript's entire inheritance model is prototype-based: every object has a prototype it inherits from, and Object.create(proto) creates a new object that clones the prototype's structure. Python's copy.deepcopy() implements Prototype for any object.

⑦ Trade-offs
✓ Benefit⚠ Cost🚫 When It Hurts
Clone complex objects without knowing their classDeep cloning objects with circular references is trickyWhen objects contain non-copyable resources like file handles
Avoid expensive initialisation — clone from a known-good stateCloning complex hierarchies can be error-proneWhen creation is cheap anyway — cloning adds no value
⑨ When to Use It
  • Object creation is expensive (many API calls or database reads)
  • You need many similar objects with minor variations
  • You do not know the concrete class of objects you need to copy
⑩ Quick Quiz
When is Prototype the best fit?
Si
Creational Pattern 5 of 5
Singleton
Ensure a class has only one instance, and provide a global access point to it.
Beginner Creational
① What Is It?
The President of a country. There can only be one president at a time. Whenever anyone needs to know who the president is, they get the same person. You cannot create a second president — the existing office is the only one. Singleton enforces this: one instance, globally accessible, created once.

Singleton solves two problems: (1) guaranteeing a class has only one instance, and (2) providing a global access point. Note: solving two problems in one pattern is itself a design smell — which is why Singleton is the most controversial GoF pattern.

② The Problem It Solves

Some resources must exist exactly once (config, logger, connection pool). Scattered `new` calls create multiple instances, conflicting state, and hard-to-test globals.

③ The Solution

Hide the constructor and expose a single static accessor (getInstance) that creates the object lazily (or eagerly) and always returns the same instance.

④ Structure
+---------------------+ | Singleton | +---------------------+ | - instance: Self | | - Singleton() | | + getInstance():Self| +---------------------+
▶ See It In Action
Press Play — each step maps the story to the pattern.
Ready
See it in action
Press Play. Watch the highlight, read the role labels, then the “In code” line.
⑤ Pseudocode
Language:
class DatabasePool { // Static field holds the single instance static instance = null // Private constructor private constructor() { this.connections = createPool(10) } static getInstance() { if (DatabasePool.instance == null) { // In multithreaded code, lock here DatabasePool.instance = new DatabasePool() } return DatabasePool.instance } query(sql) { /* ... */ } } pool1 = DatabasePool.getInstance() pool2 = DatabasePool.getInstance() print(pool1 === pool2) // true — same object
class DatabasePool: _instance = None def __init__(self): if DatabasePool._instance is not None: raise RuntimeError("Use get_instance() instead") self.connections = create_pool(10) @staticmethod def get_instance(): if DatabasePool._instance is None: DatabasePool._instance = DatabasePool() return DatabasePool._instance def query(self, sql): ... pool1 = DatabasePool.get_instance() pool2 = DatabasePool.get_instance() print(pool1 is pool2) # True -- same object
class DatabasePool { public: static DatabasePool& getInstance() { static DatabasePool instance; // thread-safe in C++11+, created once return instance; } void query(const std::string& sql) { /* ... */ } DatabasePool(const DatabasePool&) = delete; void operator=(const DatabasePool&) = delete; private: DatabasePool() { connections = createPool(10); } int connections; }; int main() { DatabasePool& pool1 = DatabasePool::getInstance(); DatabasePool& pool2 = DatabasePool::getInstance(); // &pool1 == &pool2 -- same object }
class DatabasePool { private static DatabasePool instance; private final ConnectionPool connections; private DatabasePool() { connections = createPool(10); } public static synchronized DatabasePool getInstance() { if (instance == null) { instance = new DatabasePool(); } return instance; } void query(String sql) { /* ... */ } } public class Main { public static void main(String[] args) { DatabasePool pool1 = DatabasePool.getInstance(); DatabasePool pool2 = DatabasePool.getInstance(); System.out.println(pool1 == pool2); // true -- same object } }
class DatabasePool { static #instance = null; constructor() { this.connections = createPool(10); } static getInstance() { if (DatabasePool.#instance === null) { DatabasePool.#instance = new DatabasePool(); } return DatabasePool.#instance; } query(sql) { /* ... */ } } const pool1 = DatabasePool.getInstance(); const pool2 = DatabasePool.getInstance(); console.log(pool1 === pool2); // true -- same object
⑥ Real Industry Example
🔵 Spring Framework Beans & Java Runtime

Spring's beans are Singleton by default — the container creates one instance of each bean and reuses it throughout the application. java.lang.Runtime.getRuntime() is a classic Singleton — one JVM runtime per process. Node.js's require() system implements Singleton via module caching: the first require('module') creates and caches it; all subsequent calls return the same object.

⚠ The Most Overused Pattern: Singleton is often called an anti-pattern because developers use it as a convenient global variable. Before using Singleton, ask: "Do I genuinely need one instance, or am I just being lazy about dependency management?" Dependency injection is almost always a better alternative.
⑦ Trade-offs
✓ Benefit⚠ Cost🚫 When It Hurts
Guaranteed single instance — prevents resource duplicationViolates Single Responsibility Principle — handles both instance control and core logicUnit tests — Singletons are notoriously hard to mock
Global access point — convenientIntroduces global state — the enemy of testabilityMulti-threaded environments — requires careful synchronisation
⑨ When to Use It
  • There genuinely must be exactly one instance (OS-level resources, hardware device access)
  • You need global access and have considered the testability implications
  • You need lazy initialisation of an expensive shared resource
⑩ Quick Quiz
What is the core guarantee of Singleton?
Section 3 — Structural Patterns

Object Composition

These patterns are about HOW objects and classes are assembled into larger structures — like Lego bricks snapping together. Each pattern defines a way of connecting pieces so the larger system is flexible, maintainable, and clean.

Ad
Structural Pattern 1 of 7
Adapter
Allow objects with incompatible interfaces to collaborate.
Beginner Structural
① What Is It?
A power plug adapter. Your laptop charger has a US plug (two flat pins). You are in the UK where all sockets have three rectangular holes. The adapter converts one interface to the other — neither the wall socket nor the charger was modified. The adapter is the bridge between incompatible interfaces.

The Adapter pattern allows classes with incompatible interfaces to work together by wrapping an object in an adapter that translates calls from one interface to another. It is the go-to pattern when integrating third-party code or legacy systems.

② The Problem It Solves

You need a class that speaks interface A, but the useful library only exposes incompatible interface B. Rewriting either side is costly or impossible.

③ The Solution

Write an Adapter that implements the target interface and translates calls to the adaptee, so clients stay coupled only to the interface they expect.

④ Structure
+--------+ +-----------+ +---------+ | Client |---->| Target | | Adaptee | +--------+ | request() | | specific| +-----+-----+ +----+----+ ^ | +-----+-----+ | | Adapter |----------+ | request() | +-----------+
▶ See It In Action
Press Play — each step maps the story to the pattern.
Ready
See it in action
Press Play. Watch the highlight, read the role labels, then the “In code” line.
⑤ Pseudocode
Language:
// Your application's expected interface interface PaymentProcessor { charge(amount, currency): Receipt } // Stripe's actual API — you cannot change this class StripeSDK { createCharge(options) { /* Stripe API call */ } } // Adapter wraps Stripe, speaks your interface class StripeAdapter implements PaymentProcessor { constructor(stripe: StripeSDK) { this.stripe = stripe } charge(amount, currency): Receipt { result = this.stripe.createCharge({ amount: amount * 100, // Stripe uses cents currency: currency.toLowerCase() }) return new Receipt(result.id) } } // Your code never knows it is talking to Stripe processor: PaymentProcessor = new StripeAdapter(new StripeSDK()) processor.charge(49.99, "USD")
from abc import ABC, abstractmethod class PaymentProcessor(ABC): @abstractmethod def charge(self, amount, currency): ... class StripeSDK: def create_charge(self, options): ... # actual Stripe API call class StripeAdapter(PaymentProcessor): def __init__(self, stripe: StripeSDK): self.stripe = stripe def charge(self, amount, currency): result = self.stripe.create_charge({ "amount": amount * 100, # Stripe uses cents "currency": currency.lower(), }) return Receipt(result["id"]) # Your code never knows it is talking to Stripe processor: PaymentProcessor = StripeAdapter(StripeSDK()) processor.charge(49.99, "USD")
#include <string> #include <memory> struct PaymentProcessor { virtual void charge(double amount, const std::string& currency) = 0; virtual ~PaymentProcessor() = default; }; class StripeSDK { public: void createCharge(double amountCents, const std::string& currency) { /* Stripe API call */ } }; class StripeAdapter : public PaymentProcessor { public: explicit StripeAdapter(std::unique_ptr<StripeSDK> sdk) : stripe(std::move(sdk)) {} void charge(double amount, const std::string& currency) override { stripe->createCharge(amount * 100, currency); // Stripe uses cents } private: std::unique_ptr<StripeSDK> stripe; }; int main() { std::unique_ptr<PaymentProcessor> processor = std::make_unique<StripeAdapter>(std::make_unique<StripeSDK>()); processor->charge(49.99, "USD"); }
interface PaymentProcessor { void charge(double amount, String currency); } class StripeSDK { void createCharge(double amountCents, String currency) { /* Stripe API call */ } } class StripeAdapter implements PaymentProcessor { private final StripeSDK stripe; StripeAdapter(StripeSDK stripe) { this.stripe = stripe; } public void charge(double amount, String currency) { stripe.createCharge(amount * 100, currency.toLowerCase()); // Stripe uses cents } } public class Main { public static void main(String[] args) { PaymentProcessor processor = new StripeAdapter(new StripeSDK()); processor.charge(49.99, "USD"); // your code never knows it's Stripe } }
class StripeSDK { createCharge(options) { /* actual Stripe API call */ } } class StripeAdapter { constructor(stripe) { this.stripe = stripe; } charge(amount, currency) { const result = this.stripe.createCharge({ amount: amount * 100, // Stripe uses cents currency: currency.toLowerCase(), }); return new Receipt(result.id); } } // Your code never knows it is talking to Stripe const processor = new StripeAdapter(new StripeSDK()); processor.charge(49.99, "USD");
⑥ Real Industry Example
💳 Payment Gateway Integrations & Java's InputStreamReader

Java's InputStreamReader is an Adapter: it wraps a byte-stream (InputStream) and adapts it to a character-stream interface (Reader). E-commerce platforms (Shopify, WooCommerce) use adapters for every payment gateway — Stripe, PayPal, Razorpay all have different APIs, all adapted to a common PaymentGateway interface. Spring Data's repository abstraction adapts between your domain model and various database engines.

⑦ Trade-offs
✓ Benefit⚠ Cost🚫 When It Hurts
Integrate third-party code without modifying itAdds an extra layer of indirection — harder to trace executionWhen you control both interfaces and can simply unify them
Single Responsibility: adaptation logic is isolatedImpedance mismatch can lead to data loss or translation errorsWhen interface differences are too large — translation becomes a liability
⑨ When to Use It
  • You want to use an existing class but its interface does not match what you need
  • You are integrating legacy or third-party code into a new system
  • You need to reuse existing subclasses that lack a common interface
⑩ Quick Quiz
What does Adapter primarily do?
Br
Structural Pattern 2 of 7
Bridge
Split a large class into two separate hierarchies — abstraction and implementation — that can be developed independently.
Advanced Structural
① What Is It?
A universal remote control and a TV. The remote (abstraction) has buttons and logic for volume, channels, power. The TV (implementation) has physical circuits that execute these commands. You can pair any remote with any TV — they are decoupled. Bridge separates what something does from how it does it.

Without Bridge, adding new shapes to a drawing program that supports multiple renderers (Raster, Vector) would require a class for every combination: RasterCircle, VectorCircle, RasterSquare, VectorSquare. With Bridge, Shape and Renderer are separate hierarchies — add a new shape or renderer without touching the other.

② The Problem It Solves

You need to vary an abstraction (e.g. shape) and its implementation (e.g. renderer) independently. Inheritance couples them into an explosion of subclasses (CircleOpenGL, CircleVulkan…).

③ The Solution

Split into two hierarchies: Abstraction holds a reference to Implementor. Clients compose any abstraction with any implementor at runtime.

④ Structure
+--------------+ +---------------+ | Abstraction |--------->| Implementor | | operation() | | opImpl() | +------+-------+ +-------+-------+ ^ ^ +------+-------+ +-------+-------+ | RefinedAbs | | ConcreteImpl | +--------------+ +---------------+
▶ See It In Action
Press Play — each step maps the story to the pattern.
Ready
See it in action
Press Play. Watch the highlight, read the role labels, then the “In code” line.
⑤ Pseudocode
Language:
interface Renderer { renderCircle(radius) } class VectorRenderer implements Renderer { renderCircle(r) { print("Drawing vector circle r=" + r) } } class RasterRenderer implements Renderer { renderCircle(r) { print("Drawing raster circle r=" + r) } } abstract class Shape { constructor(renderer: Renderer) { this.renderer = renderer // ← the bridge } abstract draw() } class Circle extends Shape { constructor(renderer, radius) { super(renderer) this.radius = radius } draw() { this.renderer.renderCircle(this.radius) } } // Mix and match — any shape with any renderer vc = new Circle(new VectorRenderer(), 5) rc = new Circle(new RasterRenderer(), 5)
from abc import ABC, abstractmethod class Renderer(ABC): @abstractmethod def render_circle(self, radius): ... class VectorRenderer(Renderer): def render_circle(self, r): print(f"Drawing vector circle r={r}") class RasterRenderer(Renderer): def render_circle(self, r): print(f"Drawing raster circle r={r}") class Shape(ABC): def __init__(self, renderer: Renderer): self.renderer = renderer # the bridge @abstractmethod def draw(self): ... class Circle(Shape): def __init__(self, renderer, radius): super().__init__(renderer) self.radius = radius def draw(self): self.renderer.render_circle(self.radius) # Mix and match -- any shape with any renderer vc = Circle(VectorRenderer(), 5) rc = Circle(RasterRenderer(), 5)
#include <memory> #include <iostream> struct Renderer { virtual void renderCircle(double radius) = 0; virtual ~Renderer() = default; }; struct VectorRenderer : Renderer { void renderCircle(double r) override { std::cout << "Drawing vector circle r=" << r << "\n"; } }; struct RasterRenderer : Renderer { void renderCircle(double r) override { std::cout << "Drawing raster circle r=" << r << "\n"; } }; class Shape { public: explicit Shape(std::shared_ptr<Renderer> r) : renderer(std::move(r)) {} // the bridge virtual void draw() = 0; virtual ~Shape() = default; protected: std::shared_ptr<Renderer> renderer; }; class Circle : public Shape { public: Circle(std::shared_ptr<Renderer> r, double radius) : Shape(std::move(r)), radius(radius) {} void draw() override { renderer->renderCircle(radius); } private: double radius; }; int main() { // Mix and match -- any shape with any renderer Circle vc(std::make_shared<VectorRenderer>(), 5); Circle rc(std::make_shared<RasterRenderer>(), 5); }
interface Renderer { void renderCircle(double radius); } class VectorRenderer implements Renderer { public void renderCircle(double r) { System.out.println("Drawing vector circle r=" + r); } } class RasterRenderer implements Renderer { public void renderCircle(double r) { System.out.println("Drawing raster circle r=" + r); } } abstract class Shape { protected Renderer renderer; // the bridge Shape(Renderer renderer) { this.renderer = renderer; } abstract void draw(); } class Circle extends Shape { private double radius; Circle(Renderer renderer, double radius) { super(renderer); this.radius = radius; } void draw() { renderer.renderCircle(radius); } } public class Main { public static void main(String[] args) { // Mix and match -- any shape with any renderer Circle vc = new Circle(new VectorRenderer(), 5); Circle rc = new Circle(new RasterRenderer(), 5); } }
class VectorRenderer { renderCircle(r) { console.log(`Drawing vector circle r=${r}`); } } class RasterRenderer { renderCircle(r) { console.log(`Drawing raster circle r=${r}`); } } class Shape { constructor(renderer) { this.renderer = renderer; // the bridge } } class Circle extends Shape { constructor(renderer, radius) { super(renderer); this.radius = radius; } draw() { this.renderer.renderCircle(this.radius); } } // Mix and match -- any shape with any renderer const vc = new Circle(new VectorRenderer(), 5); const rc = new Circle(new RasterRenderer(), 5);
⑥ Real Industry Example
📄 JDBC Drivers — Classic Bridge in Production

Java's JDBC is one of the most famous examples of Bridge in production. Your code uses the java.sql.Connection abstraction — always the same API. The driver (MySQL, PostgreSQL, Oracle) is the implementation. Switching databases means swapping the driver, not rewriting your SQL code. This decoupling has enabled Java to work with every database engine ever created without changing application code.

⑦ Trade-offs
✓ Benefit⚠ Cost🚫 When It Hurts
Platform-independent code — swap implementations without changing abstractionsIncreases overall code complexity — two hierarchies instead of oneWhen you only have one implementation — unnecessary overhead
Open/Closed in both dimensions — add shapes OR renderers independentlyHarder to understand — the indirect call path is non-obviousWhen abstraction and implementation are unlikely to vary independently
⑨ When to Use It
  • You want to divide a monolithic class with several variants into separate hierarchies
  • You need to extend a class in two independent dimensions
  • You want to switch implementations at runtime
⑩ Quick Quiz
Bridge is best when…
Co
Structural Pattern 3 of 7
Composite
Compose objects into tree structures, and work with them as if they were individual objects.
Intermediate Structural
① What Is It?
A file system. A folder can contain files. A folder can also contain other folders. Whether you are "deleting" a single file or a folder with 10,000 nested files, you call the same operation: delete(). The caller does not know or care if they are operating on a leaf or a container.

Composite organises objects into tree structures. Both simple (leaf) objects and complex (container) objects implement the same interface. This lets you write code that works on a single item and a collection of items without knowing which it is.

② The Problem It Solves

Clients must treat individual objects and groups of objects differently, flooding call sites with type checks whenever a tree of parts grows.

③ The Solution

Define a shared Component interface. Leaves and Composites both implement it; composites hold children and forward operations — clients talk to one interface.

④ Structure
+----------------+ | Component | | operation() | +--------+-------+ ^ +-----+------+ | | +--+---+ +----+----------+ | Leaf | | Composite | +------+ | +children[] | | operation() | +---------------+
▶ See It In Action
Press Play — each step maps the story to the pattern.
Ready
See it in action
Press Play. Watch the highlight, read the role labels, then the “In code” line.
⑤ Pseudocode
Language:
interface FileSystemItem { getName() : string getSize() : int } class File implements FileSystemItem { constructor(name, size) { this.name = name; this.size = size } getName() { return this.name } getSize() { return this.size } } class Folder implements FileSystemItem { constructor(name) { this.name = name this.children = [] } add(item) { this.children.push(item) } getName() { return this.name } getSize() { return this.children.reduce((sum, c) => sum + c.getSize(), 0) } } root = new Folder("/") docs = new Folder("docs") docs.add(new File("resume.pdf", 120)) docs.add(new File("cover.docx", 80)) root.add(docs) root.add(new File("notes.txt", 15)) print(root.getSize()) // 215
from abc import ABC, abstractmethod class FileSystemItem(ABC): @abstractmethod def get_size(self) -> int: ... class File(FileSystemItem): def __init__(self, name, size): self.name = name self.size = size def get_size(self): return self.size class Folder(FileSystemItem): def __init__(self, name): self.name = name self.children = [] def add(self, item: FileSystemItem): self.children.append(item) def get_size(self): return sum(c.get_size() for c in self.children) root = Folder("/") docs = Folder("docs") docs.add(File("resume.pdf", 120)) docs.add(File("cover.docx", 80)) root.add(docs) root.add(File("notes.txt", 15)) print(root.get_size()) # 215
#include <memory> #include <vector> #include <string> #include <numeric> struct FileSystemItem { virtual int getSize() const = 0; virtual ~FileSystemItem() = default; }; class File : public FileSystemItem { public: File(std::string name, int size) : name(std::move(name)), size(size) {} int getSize() const override { return size; } private: std::string name; int size; }; class Folder : public FileSystemItem { public: explicit Folder(std::string name) : name(std::move(name)) {} void add(std::unique_ptr<FileSystemItem> item) { children.push_back(std::move(item)); } int getSize() const override { int total = 0; for (auto& c : children) total += c->getSize(); return total; } private: std::string name; std::vector<std::unique_ptr<FileSystemItem>> children; }; int main() { auto root = std::make_unique<Folder>("/"); auto docs = std::make_unique<Folder>("docs"); docs->add(std::make_unique<File>("resume.pdf", 120)); docs->add(std::make_unique<File>("cover.docx", 80)); root->add(std::move(docs)); root->add(std::make_unique<File>("notes.txt", 15)); // root->getSize() == 215 }
import java.util.*; interface FileSystemItem { int getSize(); } class File implements FileSystemItem { String name; int size; File(String name, int size) { this.name = name; this.size = size; } public int getSize() { return size; } } class Folder implements FileSystemItem { String name; List<FileSystemItem> children = new ArrayList<>(); Folder(String name) { this.name = name; } void add(FileSystemItem item) { children.add(item); } public int getSize() { return children.stream().mapToInt(FileSystemItem::getSize).sum(); } } public class Main { public static void main(String[] args) { Folder root = new Folder("/"); Folder docs = new Folder("docs"); docs.add(new File("resume.pdf", 120)); docs.add(new File("cover.docx", 80)); root.add(docs); root.add(new File("notes.txt", 15)); System.out.println(root.getSize()); // 215 } }
class File { constructor(name, size) { this.name = name; this.size = size; } getSize() { return this.size; } } class Folder { constructor(name) { this.name = name; this.children = []; } add(item) { this.children.push(item); } getSize() { return this.children.reduce((sum, c) => sum + c.getSize(), 0); } } const root = new Folder("/"); const docs = new Folder("docs"); docs.add(new File("resume.pdf", 120)); docs.add(new File("cover.docx", 80)); root.add(docs); root.add(new File("notes.txt", 15)); console.log(root.getSize()); // 215
⑥ Real Industry Example
🌐 The Browser DOM & React Component Tree

The browser's Document Object Model is the most-used Composite in the world. A div can contain other divs and text nodes. element.innerHTML and element.querySelectorAll() work on any node — leaf or container — uniformly. React's component tree is identical: a Page contains Sections which contain Cards which contain Text. React's virtual DOM reconciliation traverses this tree treating all components uniformly.

⑦ Trade-offs
✓ Benefit⚠ Cost🚫 When It Hurts
Work with complex tree structures polymorphicallyHard to restrict which types can be added to a compositeWhen data is not genuinely hierarchical
Open/Closed — add new component types without changing existing codeOverly general design — making every component both leaf and container can be confusingWhen you need type-specific operations — the uniform interface hides distinctions
⑨ When to Use It
  • You need to represent part-whole hierarchies (file systems, org charts, menus)
  • You want client code to treat individual and composite objects uniformly
  • You are building a tree-structured data model
⑩ Quick Quiz
Composite lets clients…
De
Structural Pattern 4 of 7
Decorator
Attach new behaviors to objects by placing them inside special wrapper objects.
Intermediate Structural
① What Is It?
Ordering a coffee at Starbucks. You start with a plain espresso. Add milk — now it is a latte. Add caramel syrup. Add whipped cream. Each addition wraps the previous drink, adds a new feature, and can report its cost and description. You are decorating the base object step by step without modifying the original recipe.

Decorator lets you add behaviors to an object at runtime by wrapping it in a decorator object that has the same interface. Decorators can be stacked. Unlike inheritance (which adds behaviour to an entire class at compile time), Decorator adds behaviour dynamically to individual objects.

② The Problem It Solves

You need optional behaviors (logging, compression, borders) in many combinations. Subclassing every mix creates a combinatorial explosion.

③ The Solution

Wrap the object with decorators that implement the same interface, forward calls, and add behavior before/after — stackable at runtime.

④ Structure
+-------------+ | Component | +------+------+ ^ +-----+------+ | | +----+----+ +---------------+ |Concrete | | Decorator | |Component| | - wrap: Comp | +---------+ | operation() | +-------+-------+ ^ +-------+-------+ | ConcreteDecor | +---------------+
▶ See It In Action
Press Play — each step maps the story to the pattern.
Ready
See it in action
Press Play. Watch the highlight, read the role labels, then the “In code” line.
⑤ Pseudocode
Language:
interface DataSource { writeData(data) readData() : string } class FileDataSource implements DataSource { writeData(data) { /* write to file */ } readData() { /* read from file */ } } // Base decorator — wraps a DataSource class DataSourceDecorator implements DataSource { constructor(wrappee) { this.wrappee = wrappee } writeData(data) { this.wrappee.writeData(data) } readData() { return this.wrappee.readData() } } // Encryption decorator class EncryptionDecorator extends DataSourceDecorator { writeData(data) { super.writeData(encrypt(data)) } readData() { return decrypt(super.readData()) } } // Stack decorators — compress then encrypt source = new EncryptionDecorator( new CompressionDecorator( new FileDataSource("data.txt"))) source.writeData("Hello") // compress "Hello" → encrypt result → write to file
from abc import ABC, abstractmethod class DataSource(ABC): @abstractmethod def write_data(self, data): ... @abstractmethod def read_data(self) -> str: ... class FileDataSource(DataSource): def write_data(self, data): ... # write to file def read_data(self): ... # read from file class DataSourceDecorator(DataSource): def __init__(self, wrappee: DataSource): self.wrappee = wrappee def write_data(self, data): self.wrappee.write_data(data) def read_data(self): return self.wrappee.read_data() class EncryptionDecorator(DataSourceDecorator): def write_data(self, data): super().write_data(encrypt(data)) def read_data(self): return decrypt(super().read_data()) class CompressionDecorator(DataSourceDecorator): def write_data(self, data): super().write_data(compress(data)) def read_data(self): return decompress(super().read_data()) # Stack decorators -- compress then encrypt source = EncryptionDecorator(CompressionDecorator(FileDataSource())) source.write_data("Hello") # compress "Hello" -> encrypt result -> write to file
#include <memory> #include <string> struct DataSource { virtual void writeData(const std::string& data) = 0; virtual std::string readData() = 0; virtual ~DataSource() = default; }; class FileDataSource : public DataSource { public: void writeData(const std::string& data) override { /* write to file */ } std::string readData() override { return ""; /* read from file */ } }; class DataSourceDecorator : public DataSource { public: explicit DataSourceDecorator(std::unique_ptr<DataSource> w) : wrappee(std::move(w)) {} void writeData(const std::string& data) override { wrappee->writeData(data); } std::string readData() override { return wrappee->readData(); } protected: std::unique_ptr<DataSource> wrappee; }; class EncryptionDecorator : public DataSourceDecorator { public: using DataSourceDecorator::DataSourceDecorator; void writeData(const std::string& data) override { DataSourceDecorator::writeData(encrypt(data)); } std::string readData() override { return decrypt(DataSourceDecorator::readData()); } }; int main() { // Stack decorators -- compress then encrypt auto source = std::make_unique<EncryptionDecorator>( std::make_unique<FileDataSource>()); source->writeData("Hello"); }
interface DataSource { void writeData(String data); String readData(); } class FileDataSource implements DataSource { public void writeData(String data) { /* write to file */ } public String readData() { return ""; /* read from file */ } } abstract class DataSourceDecorator implements DataSource { protected DataSource wrappee; DataSourceDecorator(DataSource wrappee) { this.wrappee = wrappee; } public void writeData(String data) { wrappee.writeData(data); } public String readData() { return wrappee.readData(); } } class EncryptionDecorator extends DataSourceDecorator { EncryptionDecorator(DataSource wrappee) { super(wrappee); } public void writeData(String data) { super.writeData(encrypt(data)); } public String readData() { return decrypt(super.readData()); } } public class Main { public static void main(String[] args) { // Stack decorators -- compress then encrypt DataSource source = new EncryptionDecorator( new CompressionDecorator(new FileDataSource())); source.writeData("Hello"); } }
class FileDataSource { writeData(data) { /* write to file */ } readData() { /* read from file */ } } // Base decorator -- wraps a DataSource class DataSourceDecorator { constructor(wrappee) { this.wrappee = wrappee; } writeData(data) { this.wrappee.writeData(data); } readData() { return this.wrappee.readData(); } } // Encryption decorator class EncryptionDecorator extends DataSourceDecorator { writeData(data) { super.writeData(encrypt(data)); } readData() { return decrypt(super.readData()); } } // Stack decorators -- compress then encrypt const source = new EncryptionDecorator( new CompressionDecorator( new FileDataSource())); source.writeData("Hello"); // compress "Hello" -> encrypt result -> write to file
⑥ Real Industry Example
☕ Java I/O Streams & Express.js Middleware

Java's I/O library is Decorator throughout: new BufferedReader(new InputStreamReader(new FileInputStream("file.txt"))). Each wrapper adds buffering, character encoding, or file reading to the base stream. Express.js middleware is Decorator applied to HTTP handling — app.use(cors()), app.use(bodyParser()), app.use(auth()) each wraps the request/response pipeline. Python's @functools.wraps decorator is the language-level version of this pattern.

⑦ Trade-offs
✓ Benefit⚠ Cost🚫 When It Hurts
Add behaviors without subclassing — no inheritance neededMany small wrapper objects — hard to debugWhen decoration order matters but is not enforced
Mix and match behaviors at runtimeRemoving a decorator from the middle of a stack is hardWhen code needs to interact with specific decorators — wrapping hides identity
⑨ When to Use It
  • You want to add behavior to individual objects without affecting others of the same class
  • Extension by subclassing is impractical due to combinatorial explosion
  • You want to add responsibilities that can be withdrawn later
⑩ Quick Quiz
Decorator differs from inheritance mainly because…
Fa
Structural Pattern 5 of 7
Facade
Provide a simplified interface to a complex library, framework, or set of classes.
Beginner Structural
① What Is It?
Calling a hotel concierge. "I need a cab to the airport at 6am, book me a wake-up call at 4:30, and cancel my dinner reservation." You make one call to the concierge (facade); they coordinate with the taxi service, the hotel phone system, and the restaurant — all subsystems you never directly interact with.

Facade provides a high-level, simplified interface to a complex subsystem. It does not hide the subsystem — you can still use it directly if needed — but it provides a convenient shortcut for common operations and reduces coupling between client code and the subsystem.

② The Problem It Solves

Clients depend on many subsystem classes and their wiring. Every feature change ripples through call sites that should not know those details.

③ The Solution

Introduce a Facade with a small, high-level API that orchestrates the subsystem. Clients depend on the facade, not the internals.

④ Structure
+--------+ +----------+ +-----------+ | Client |---->| Facade |---->| Subsystem | +--------+ | doWork() | | A / B / C| +----------+ +-----------+
▶ See It In Action
Press Play — each step maps the story to the pattern.
Ready
See it in action
Press Play. Watch the highlight, read the role labels, then the “In code” line.
⑤ Pseudocode
Language:
// Complex subsystem classes class VideoDecoder { decode(file) { /* ... */ } } class AudioMixer { mix(audio) { /* ... */ } } class BitrateEncoder { encode(v, b) { /* ... */ } } class FileWriter { write(file, data) { /* ... */ } } // Facade — one simple method hides the complexity class VideoConversionFacade { convert(filename, format) { raw = (new VideoDecoder()).decode(filename) audio = (new AudioMixer()).mix(raw.audio) encoded = (new BitrateEncoder()).encode(raw.video, 720) return (new FileWriter()).write("output." + format, { encoded, audio }) } } // Client — dead simple, no subsystem knowledge needed new VideoConversionFacade().convert("movie.avi", "mp4")
class VideoDecoder: def decode(self, file): ... class AudioMixer: def mix(self, audio): ... class BitrateEncoder: def encode(self, video, bitrate): ... class FileWriter: def write(self, filename, data): ... class VideoConversionFacade: """One simple method hides the complexity.""" def convert(self, filename, fmt): raw = VideoDecoder().decode(filename) audio = AudioMixer().mix(raw.audio) encoded = BitrateEncoder().encode(raw.video, 720) return FileWriter().write(f"output.{fmt}", {"encoded": encoded, "audio": audio}) # Client -- dead simple, no subsystem knowledge needed VideoConversionFacade().convert("movie.avi", "mp4")
#include <string> class VideoDecoder { public: void decode(const std::string& f) {} }; class AudioMixer { public: void mix(void* audio) {} }; class BitrateEncoder { public: void encode(void* v, int bitrate) {} }; class FileWriter { public: void write(const std::string& f, void* data) {} }; class VideoConversionFacade { public: void convert(const std::string& filename, const std::string& format) { VideoDecoder decoder; decoder.decode(filename); AudioMixer mixer; mixer.mix(nullptr); BitrateEncoder encoder; encoder.encode(nullptr, 720); FileWriter writer; writer.write("output." + format, nullptr); } }; int main() { // Client -- dead simple, no subsystem knowledge needed VideoConversionFacade().convert("movie.avi", "mp4"); }
class VideoDecoder { void decode(String file) {} } class AudioMixer { void mix(Object audio) {} } class BitrateEncoder { void encode(Object video, int bitrate) {} } class FileWriter { void write(String file, Object data) {} } class VideoConversionFacade { void convert(String filename, String format) { Object raw = new VideoDecoder().decode(filename); Object audio = new AudioMixer().mix(raw); Object encoded = new BitrateEncoder().encode(raw, 720); new FileWriter().write("output." + format, encoded); } } public class Main { public static void main(String[] args) { // Client -- dead simple, no subsystem knowledge needed new VideoConversionFacade().convert("movie.avi", "mp4"); } }
class VideoDecoder { decode(file) { /* ... */ } } class AudioMixer { mix(audio) { /* ... */ } } class BitrateEncoder { encode(v, b) { /* ... */ } } class FileWriter { write(file, data) { /* ... */ } } class VideoConversionFacade { convert(filename, format) { const raw = new VideoDecoder().decode(filename); const audio = new AudioMixer().mix(raw.audio); const encoded = new BitrateEncoder().encode(raw.video, 720); return new FileWriter().write(`output.${format}`, { encoded, audio }); } } // Client -- dead simple, no subsystem knowledge needed new VideoConversionFacade().convert("movie.avi", "mp4");
⑥ Real Industry Example
☁ AWS SDK & Spring Boot Auto-configuration

The AWS SDK is a massive Facade: rather than making raw HTTP requests to AWS endpoints, signing them with AWS Signature V4, and parsing XML responses — you call s3.putObject({Bucket, Key, Body}). Spring Boot's auto-configuration is a Facade over hundreds of configuration decisions. Redux's createStore() is a Facade that sets up the store, middleware, and devtools in a single call.

⑦ Trade-offs
✓ Benefit⚠ Cost🚫 When It Hurts
Dramatically reduces cognitive complexity for clientsCan become a God Object if given too much responsibilityWhen you need full subsystem access regularly
Decouples client from subsystem — easier to evolve subsystemDoes not prevent direct subsystem access — clients can bypass facadeWhen the facade interface hardens and cannot evolve
⑨ When to Use It
  • You have a complex subsystem and want a simple interface for common use cases
  • You want to layer your system — facade defines boundaries between layers
  • You are writing a library and want a concise API for clients
⑩ Quick Quiz
Facade’s primary job is to…
Fl
Structural Pattern 6 of 7
Flyweight
Share the intrinsic state of many similar objects to reduce memory usage.
Advanced Structural
① What Is It?
Text rendering in a document editor. Your document has the letter "A" appearing 50,000 times. A naive implementation creates 50,000 separate "A" objects, each storing font, size, style, and character data. Flyweight says: create one "A" object that stores the shared (intrinsic) data. The 50,000 references store only the unique position (extrinsic data). One object shared by 50,000 references.

Flyweight splits object state into intrinsic (shared, immutable) and extrinsic (unique per instance). The Flyweight stores only intrinsic state and is shared. Extrinsic state is passed in by the caller.

② The Problem It Solves

You create huge numbers of similar objects; most of their data is identical. Memory balloons because every instance stores the shared bits again.

③ The Solution

Split intrinsic (shareable) state into Flyweight objects cached by a factory; keep extrinsic state outside and pass it into operations.

④ Structure
+---------------+ +------------------+ | FlyweightFact |---->| Flyweight | | get(key) | | intrinsic state | +---------------+ | op(extrinsic) | +------------------+
▶ See It In Action
Press Play — each step maps the story to the pattern.
Ready
See it in action
Press Play. Watch the highlight, read the role labels, then the “In code” line.
⑤ Pseudocode
Language:
// Flyweight — stores intrinsic state (shared) class ParticleFlyweight { constructor(color, sprite) { this.color = color // same for all "fire" particles this.sprite = sprite // expensive texture — shared } render(x, y) { // x, y are extrinsic — passed in, not stored drawAt(this.sprite, x, y) } } // Factory caches flyweights class ParticleFactory { constructor() { this.cache = {} } get(color, sprite) { key = color + sprite if (!this.cache[key]) { this.cache[key] = new ParticleFlyweight(color, sprite) } return this.cache[key] } } // Game spawns 10,000 fire particles — only 1 flyweight object factory = new ParticleFactory() for i in 0..10000: fw = factory.get("orange", "fire.png") fw.render(randomX(), randomY())
class ParticleFlyweight: """Stores intrinsic state (shared).""" def __init__(self, color, sprite): self.color = color # same for all "fire" particles self.sprite = sprite # expensive texture -- shared def render(self, x, y): # x, y are extrinsic -- passed in, not stored draw_at(self.sprite, x, y) class ParticleFactory: def __init__(self): self.cache = {} def get(self, color, sprite): key = (color, sprite) if key not in self.cache: self.cache[key] = ParticleFlyweight(color, sprite) return self.cache[key] # Game spawns 10,000 fire particles -- only 1 flyweight object factory = ParticleFactory() for _ in range(10000): fw = factory.get("orange", "fire.png") fw.render(random_x(), random_y())
#include <unordered_map> #include <string> #include <memory> class ParticleFlyweight { public: ParticleFlyweight(std::string color, std::string sprite) : color(std::move(color)), sprite(std::move(sprite)) {} void render(double x, double y) { drawAt(sprite, x, y); } // x,y are extrinsic private: std::string color, sprite; }; class ParticleFactory { public: ParticleFlyweight* get(const std::string& color, const std::string& sprite) { auto key = color + sprite; if (cache.find(key) == cache.end()) { cache[key] = std::make_unique<ParticleFlyweight>(color, sprite); } return cache[key].get(); } private: std::unordered_map<std::string, std::unique_ptr<ParticleFlyweight>> cache; }; int main() { ParticleFactory factory; for (int i = 0; i < 10000; i++) { auto* fw = factory.get("orange", "fire.png"); // only 1 flyweight object fw->render(randomX(), randomY()); } }
import java.util.*; class ParticleFlyweight { private final String color; // same for all "fire" particles private final String sprite; // expensive texture -- shared ParticleFlyweight(String color, String sprite) { this.color = color; this.sprite = sprite; } void render(double x, double y) { // x, y are extrinsic drawAt(sprite, x, y); } } class ParticleFactory { private final Map<String, ParticleFlyweight> cache = new HashMap<>(); ParticleFlyweight get(String color, String sprite) { String key = color + sprite; return cache.computeIfAbsent(key, k -> new ParticleFlyweight(color, sprite)); } } public class Main { public static void main(String[] args) { ParticleFactory factory = new ParticleFactory(); for (int i = 0; i < 10000; i++) { ParticleFlyweight fw = factory.get("orange", "fire.png"); // only 1 flyweight object fw.render(randomX(), randomY()); } } }
class ParticleFlyweight { constructor(color, sprite) { this.color = color; // same for all "fire" particles this.sprite = sprite; // expensive texture -- shared } render(x, y) { // x, y are extrinsic -- passed in, not stored drawAt(this.sprite, x, y); } } class ParticleFactory { constructor() { this.cache = {}; } get(color, sprite) { const key = color + sprite; if (!this.cache[key]) { this.cache[key] = new ParticleFlyweight(color, sprite); } return this.cache[key]; } } // Game spawns 10,000 fire particles -- only 1 flyweight object const factory = new ParticleFactory(); for (let i = 0; i < 10000; i++) { const fw = factory.get("orange", "fire.png"); fw.render(randomX(), randomY()); }
⑥ Real Industry Example
🔰 String Interning in Java/Python & Game Particle Systems

Java's string pool is Flyweight built into the language: "hello" == "hello" is true because Java reuses the same string object from a shared pool. Python interns short strings similarly. Game engines (Unity, Unreal) use Flyweight for particle systems — thousands of particles sharing one sprite/texture object. Modern text rendering engines (HarfBuzz, FreeType) cache glyph outlines using Flyweight.

⑦ Trade-offs
✓ Benefit⚠ Cost🚫 When It Hurts
Massive memory savings when many similar objects existCPU overhead — you spend computation to look up shared flyweightsWhen the number of shared objects is small
Enables scenarios impossible with full per-object stateCode becomes harder to understand — intrinsic/extrinsic split is non-obviousWhen objects have mostly unique state — no shared state to exploit
⑨ When to Use It
  • Your application needs to support huge numbers of objects (thousands to millions)
  • Most object state can be made extrinsic (passed in, not stored)
  • Memory is the bottleneck, not CPU
⑩ Quick Quiz
Flyweight saves memory by…
Px
Structural Pattern 7 of 7
Proxy
Provide a substitute for another object, controlling access to the original.
Intermediate Structural
① What Is It?
A credit card is a proxy for your bank account. When you pay at a restaurant, you hand over the card — not your actual bank account. The card has the same interface (you can "pay" with it), but it controls access to the real resource, checks your balance, logs the transaction, and adds fraud protection. You interact with the proxy; the proxy controls access to the real thing.

Proxy creates a class that has the same interface as the real service object. A Proxy object controls access to the real object — adding logging, caching, access control, lazy initialization, or remote communication — without the client knowing it is talking to a proxy.

② The Problem It Solves

Clients should not access a real object directly — creation is costly, access needs checks, or you need caching/logging around every call.

③ The Solution

Provide a Proxy with the same interface as the real subject. The proxy controls access, then forwards to the real object when appropriate.

④ Structure
+--------+ +-----------+ +-------------+ | Client |---->| Subject | | RealSubject | +--------+ | request() | | request() | +-----+-----+ +------+------+ ^ | +-----+-----+ | | Proxy |------------+ +-----------+
▶ See It In Action
Press Play — each step maps the story to the pattern.
Ready
See it in action
Press Play. Watch the highlight, read the role labels, then the “In code” line.
⑤ Pseudocode
Language:
interface Image { display() } class RealImage implements Image { constructor(url) { this.url = url this.data = fetchFromServer(url) // expensive! } display() { render(this.data) } } // Proxy — lazy loading class LazyImageProxy implements Image { constructor(url) { this.url = url this.realImage = null // not loaded yet } display() { if (this.realImage == null) { this.realImage = new RealImage(this.url) } this.realImage.display() } } image = new LazyImageProxy("https://big-image.jpg") // Image NOT fetched yet image.display() // NOW fetched and rendered
from abc import ABC, abstractmethod class Image(ABC): @abstractmethod def display(self): ... class RealImage(Image): def __init__(self, url): self.url = url self.data = fetch_from_server(url) # expensive! def display(self): render(self.data) class LazyImageProxy(Image): """Proxy -- lazy loading.""" def __init__(self, url): self.url = url self.real_image = None # not loaded yet def display(self): if self.real_image is None: self.real_image = RealImage(self.url) self.real_image.display() image = LazyImageProxy("https://big-image.jpg") # image NOT fetched yet image.display() # NOW fetched and rendered
#include <string> #include <memory> struct Image { virtual void display() = 0; virtual ~Image() = default; }; class RealImage : public Image { public: explicit RealImage(std::string url) : url(std::move(url)) { data = fetchFromServer(url); // expensive! } void display() override { render(data); } private: std::string url; std::string data; }; class LazyImageProxy : public Image { public: explicit LazyImageProxy(std::string url) : url(std::move(url)) {} void display() override { if (!realImage) realImage = std::make_unique<RealImage>(url); // load on first use realImage->display(); } private: std::string url; std::unique_ptr<RealImage> realImage; // not loaded yet }; int main() { LazyImageProxy image("https://big-image.jpg"); // Image NOT fetched yet image.display(); // NOW fetched and rendered }
interface Image { void display(); } class RealImage implements Image { private final String url; private final byte[] data; RealImage(String url) { this.url = url; this.data = fetchFromServer(url); // expensive! } public void display() { render(data); } } class LazyImageProxy implements Image { private final String url; private RealImage realImage; // not loaded yet LazyImageProxy(String url) { this.url = url; } public void display() { if (realImage == null) { realImage = new RealImage(url); } realImage.display(); } } public class Main { public static void main(String[] args) { Image image = new LazyImageProxy("https://big-image.jpg"); // Image NOT fetched yet image.display(); // NOW fetched and rendered } }
class RealImage { constructor(url) { this.url = url; this.data = fetchFromServer(url); // expensive! } display() { render(this.data); } } // Proxy -- lazy loading class LazyImageProxy { constructor(url) { this.url = url; this.realImage = null; // not loaded yet } display() { if (this.realImage === null) { this.realImage = new RealImage(this.url); } this.realImage.display(); } } const image = new LazyImageProxy("https://big-image.jpg"); // Image NOT fetched yet image.display(); // NOW fetched and rendered
⑥ Real Industry Example
🍃 Spring AOP & Nginx Reverse Proxy

Spring's AOP creates proxy objects around your beans — when you annotate a method with @Transactional or @Cacheable, Spring wraps the bean in a proxy that intercepts calls and adds transaction or caching behavior before and after. Nginx is a server-level proxy: clients think they are talking to your application; Nginx intercepts, adds TLS, load-balancing, rate limiting, and caching. Hibernate's lazy-loaded entities are proxy objects — accessing a field triggers the actual database query.

⑦ Trade-offs
✓ Benefit⚠ Cost🚫 When It Hurts
Control service access without client changesResponse delayed — a proxy adds latencyWhen added indirection creates more confusion than value
Lazy init — do not pay the cost until neededMore complex code — extra class, extra layerHigh-performance systems where every millisecond matters
⑨ When to Use It
  • Lazy initialization — do not create expensive objects until needed
  • Access control — restrict who can use the real service
  • Logging — log calls without modifying the service
  • Caching — cache results and return them without hitting the service again
⑩ Quick Quiz
A Proxy is most useful when…
Section 4 — Behavioral Patterns

Object Communication

These patterns define HOW objects talk to each other — who is responsible for what, how work flows between objects, and how to keep that communication clean and decoupled.

Ch
Behavioral Pattern 1 of 11
Chain of Responsibility
Pass requests along a chain of handlers — each decides to process or pass it on.
Intermediate Behavioral
① What Is It?
A customer complaint escalation chain. You call customer support. The first agent tries to help. If they cannot, they escalate to a supervisor. If the supervisor cannot resolve it, they escalate to the director. Each handler tries, and if they cannot resolve it, they pass the request up the chain.

Chain of Responsibility lets you pass requests along a chain of handlers. Each handler either processes the request or passes it to the next handler. The sender does not know which handler will ultimately process it — or even if any will.

② The Problem It Solves

A request might be handled by several possible processors. Hard-coding who handles what couples senders to receivers and resists runtime reordering.

③ The Solution

Link handlers into a chain. Each handler processes the request or passes it to the next — the sender only knows the first link.

④ Structure
+------------+ next +------------+ | Handler |------------->| Handler | | handle() | | handle() | +------+-----+ +------------+ ^ +------+-------+ |ConcreteHandle| +--------------+
▶ See It In Action
Press Play — each step maps the story to the pattern.
Ready
See it in action
Press Play. Watch the highlight, read the role labels, then the “In code” line.
⑤ Pseudocode
Language:
interface Handler { setNext(handler: Handler) handle(request) } class AuthHandler implements Handler { handle(req) { if (!req.isAuthenticated) { return reject("401 Unauthorized") } this.next?.handle(req) } } class RateLimitHandler implements Handler { handle(req) { if (tooManyRequests(req.ip)) { return reject("429 Too Many Requests") } this.next?.handle(req) } } class LoggingHandler implements Handler { handle(req) { log(req) this.next?.handle(req) } } // Build the chain auth = new AuthHandler() rate = new RateLimitHandler() log = new LoggingHandler() auth.setNext(rate).setNext(log) auth.handle(incomingRequest)
from abc import ABC, abstractmethod class Handler(ABC): def __init__(self): self.next = None def set_next(self, handler): self.next = handler return handler @abstractmethod def handle(self, request): ... class AuthHandler(Handler): def handle(self, req): if not req.is_authenticated: return reject("401 Unauthorized") if self.next: self.next.handle(req) class RateLimitHandler(Handler): def handle(self, req): if too_many_requests(req.ip): return reject("429 Too Many Requests") if self.next: self.next.handle(req) class LoggingHandler(Handler): def handle(self, req): log(req) if self.next: self.next.handle(req) # Build the chain auth = AuthHandler() rate = RateLimitHandler() logger = LoggingHandler() auth.set_next(rate).set_next(logger) auth.handle(incoming_request)
#include <memory> class Handler { public: virtual void handle(Request& req) = 0; Handler* setNext(Handler* handler) { next = handler; return handler; } virtual ~Handler() = default; protected: Handler* next = nullptr; }; class AuthHandler : public Handler { public: void handle(Request& req) override { if (!req.isAuthenticated) { reject("401 Unauthorized"); return; } if (next) next->handle(req); } }; class RateLimitHandler : public Handler { public: void handle(Request& req) override { if (tooManyRequests(req.ip)) { reject("429 Too Many Requests"); return; } if (next) next->handle(req); } }; class LoggingHandler : public Handler { public: void handle(Request& req) override { log(req); if (next) next->handle(req); } }; int main() { AuthHandler auth; RateLimitHandler rate; LoggingHandler logger; auth.setNext(&rate)->setNext(&logger); // build the chain auth.handle(incomingRequest); }
abstract class Handler { protected Handler next; Handler setNext(Handler handler) { this.next = handler; return handler; } abstract void handle(Request req); } class AuthHandler extends Handler { void handle(Request req) { if (!req.isAuthenticated()) { reject("401 Unauthorized"); return; } if (next != null) next.handle(req); } } class RateLimitHandler extends Handler { void handle(Request req) { if (tooManyRequests(req.getIp())) { reject("429 Too Many Requests"); return; } if (next != null) next.handle(req); } } class LoggingHandler extends Handler { void handle(Request req) { log(req); if (next != null) next.handle(req); } } public class Main { public static void main(String[] args) { Handler auth = new AuthHandler(); Handler rate = new RateLimitHandler(); Handler logger = new LoggingHandler(); auth.setNext(rate).setNext(logger); // build the chain auth.handle(incomingRequest); } }
class AuthHandler { setNext(handler) { this.next = handler; return handler; } handle(req) { if (!req.isAuthenticated) return reject("401 Unauthorized"); this.next?.handle(req); } } class RateLimitHandler { setNext(handler) { this.next = handler; return handler; } handle(req) { if (tooManyRequests(req.ip)) return reject("429 Too Many Requests"); this.next?.handle(req); } } class LoggingHandler { setNext(handler) { this.next = handler; return handler; } handle(req) { log(req); this.next?.handle(req); } } // Build the chain const auth = new AuthHandler(); const rate = new RateLimitHandler(); const logger = new LoggingHandler(); auth.setNext(rate).setNext(logger); auth.handle(incomingRequest);
⑥ Real Industry Example
☁ Express.js Middleware Pipeline & Java Servlet Filters

Express.js middleware is Chain of Responsibility: app.use(cors()), app.use(helmet()), app.use(bodyParser()) each is a handler that either modifies the request/response and calls next(), or short-circuits with an error. Java's javax.servlet.Filter chain works identically. Django's middleware stack, ASP.NET Core's pipeline, and AWS API Gateway's request processing are all Chain of Responsibility implementations.

⑦ Trade-offs
✓ Benefit⚠ Cost🚫 When It Hurts
Decouple sender from receiver — sender does not know who handlesNo guarantee a request gets handled — it might fall throughWhen you need guaranteed handling — use a catch-all at the end
Add/remove handlers dynamically — flexible pipelineDebugging is harder — hard to trace where a request was droppedSimple conditional logic — a switch statement may be cleaner
⑨ When to Use It
  • Multiple objects may handle a request but you do not know which until runtime
  • You want to issue a request to a pipeline of handlers without coupling to a specific one
  • The set of handlers or their order should be configurable at runtime
⑩ Quick Quiz
In Chain of Responsibility, the sender…
Cm
Behavioral Pattern 2 of 11
Command
Turn a request into a stand-alone object containing all information about the request.
Intermediate Behavioral
① What Is It?
A restaurant order slip. When you order at a restaurant, the waiter writes your order on a slip of paper. That slip is a Command object: it has all the information needed to execute the request (table number, items, modifications). The waiter queues it; the kitchen processes it in order. The order slip can be held, replayed if lost, or cancelled — independent of who placed it.

Command encapsulates a request as an object, letting you parameterise methods with different requests, delay or queue their execution, and support undoable operations. The Command object stores everything needed to execute the action later.

② The Problem It Solves

UI buttons, macros, and queues need to invoke actions later, undo them, or log them — but binding the invoker directly to concrete methods makes that impossible.

③ The Solution

Wrap each request as a Command object with execute() (and often undo()). Invokers store and run commands without knowing the receiver’s details.

④ Structure
+---------+ +-----------+ +----------+ | Invoker |---->| Command |---->| Receiver | | execute | | execute() | | action() | +---------+ +-----------+ +----------+
▶ See It In Action
Press Play — each step maps the story to the pattern.
Ready
See it in action
Press Play. Watch the highlight, read the role labels, then the “In code” line.
⑤ Pseudocode
Language:
interface Command { execute() undo() } class MoveCommand implements Command { constructor(shape, dx, dy) { this.shape = shape this.dx = dx this.dy = dy } execute() { this.shape.move(this.dx, this.dy) } undo() { this.shape.move(-this.dx, -this.dy) } } class Editor { constructor() { this.history = [] } execute(cmd) { cmd.execute() this.history.push(cmd) } undo() { cmd = this.history.pop() cmd?.undo() } } editor = new Editor() editor.execute(new MoveCommand(circle, 10, 5)) editor.execute(new MoveCommand(circle, 3, -2)) editor.undo() // reverses last move
from abc import ABC, abstractmethod class Command(ABC): @abstractmethod def execute(self): ... @abstractmethod def undo(self): ... class MoveCommand(Command): def __init__(self, shape, dx, dy): self.shape = shape self.dx = dx self.dy = dy def execute(self): self.shape.move(self.dx, self.dy) def undo(self): self.shape.move(-self.dx, -self.dy) class Editor: def __init__(self): self.history = [] def execute(self, cmd: Command): cmd.execute() self.history.append(cmd) def undo(self): if self.history: self.history.pop().undo() editor = Editor() editor.execute(MoveCommand(circle, 10, 5)) editor.execute(MoveCommand(circle, 3, -2)) editor.undo() # reverses last move
#include <memory> #include <vector> struct Command { virtual void execute() = 0; virtual void undo() = 0; virtual ~Command() = default; }; class MoveCommand : public Command { public: MoveCommand(Shape& shape, int dx, int dy) : shape(shape), dx(dx), dy(dy) {} void execute() override { shape.move(dx, dy); } void undo() override { shape.move(-dx, -dy); } private: Shape& shape; int dx, dy; }; class Editor { public: void execute(std::unique_ptr<Command> cmd) { cmd->execute(); history.push_back(std::move(cmd)); } void undo() { if (!history.empty()) { history.back()->undo(); history.pop_back(); } } private: std::vector<std::unique_ptr<Command>> history; }; int main() { Editor editor; editor.execute(std::make_unique<MoveCommand>(circle, 10, 5)); editor.execute(std::make_unique<MoveCommand>(circle, 3, -2)); editor.undo(); // reverses last move }
import java.util.*; interface Command { void execute(); void undo(); } class MoveCommand implements Command { private final Shape shape; private final int dx, dy; MoveCommand(Shape shape, int dx, int dy) { this.shape = shape; this.dx = dx; this.dy = dy; } public void execute() { shape.move(dx, dy); } public void undo() { shape.move(-dx, -dy); } } class Editor { private final Deque<Command> history = new ArrayDeque<>(); void execute(Command cmd) { cmd.execute(); history.push(cmd); } void undo() { if (!history.isEmpty()) history.pop().undo(); } } public class Main { public static void main(String[] args) { Editor editor = new Editor(); editor.execute(new MoveCommand(circle, 10, 5)); editor.execute(new MoveCommand(circle, 3, -2)); editor.undo(); // reverses last move } }
class MoveCommand { constructor(shape, dx, dy) { this.shape = shape; this.dx = dx; this.dy = dy; } execute() { this.shape.move(this.dx, this.dy); } undo() { this.shape.move(-this.dx, -this.dy); } } class Editor { constructor() { this.history = []; } execute(cmd) { cmd.execute(); this.history.push(cmd); } undo() { const cmd = this.history.pop(); cmd?.undo(); } } const editor = new Editor(); editor.execute(new MoveCommand(circle, 10, 5)); editor.execute(new MoveCommand(circle, 3, -2)); editor.undo(); // reverses last move
⑥ Real Industry Example
📋 Git Commits & Redux Actions

Git commits are Commands: each stores exactly what changed, who changed it, and when — enabling perfect undo (git revert) and replay (git cherry-pick). Redux actions are Commands: plain objects with {type, payload} that encapsulate every state change. The Redux store's history enables time-travel debugging — replaying all Commands from any checkpoint. Database transactions are Commands encapsulated in ACID-safe wrappers with rollback (undo) support.

⑦ Trade-offs
✓ Benefit⚠ Cost🚫 When It Hurts
Full undo/redo — store and reverse any actionLots of small command classes — one per operationSimple operations where undo is not needed
Queue, schedule, or defer actionsUndo logic can be complex — especially for operations with side effectsWhen function references achieve the same result more simply
⑨ When to Use It
  • You need undo/redo functionality
  • You want to queue operations, schedule them, or execute them remotely
  • You want to implement transaction-style operations with rollback
⑩ Quick Quiz
Command turns a request into…
It
Behavioral Pattern 3 of 11
Iterator
Traverse elements of a collection without exposing its underlying representation.
Beginner Behavioral
① What Is It?
A Netflix "Continue Watching" list. Netflix shows you the next item to watch without telling you how the list is stored internally (is it a linked list? database query? sorted array?). You just press "next." Iterator abstracts away the internal storage mechanism — you get a consistent "give me the next item" interface regardless of how the collection is organised.

Iterator defines an interface for accessing and traversing elements of a collection sequentially without exposing its underlying structure. The pattern separates the traversal algorithm from the collection, enabling multiple simultaneous traversal strategies.

② The Problem It Solves

Clients that walk a collection must know whether it is a list, tree, or graph. Exposing internals couples them to the data structure.

③ The Solution

Provide an Iterator with hasNext/next (or equivalent). The collection creates the iterator; clients traverse without seeing storage details.

④ Structure
+-----------+ +----------+ | Aggregate |--------->| Iterator | | createIt()| | next() | +-----------+ +----------+
▶ See It In Action
Press Play — each step maps the story to the pattern.
Ready
See it in action
Press Play. Watch the highlight, read the role labels, then the “In code” line.
⑤ Pseudocode
Language:
interface Iterator { hasNext() : bool next() : any } class TreeIterator implements Iterator { constructor(root) { this.stack = [] this.pushLeft(root) } pushLeft(node) { while (node) { this.stack.push(node) node = node.left } } hasNext() { return this.stack.length > 0 } next() { node = this.stack.pop() this.pushLeft(node.right) return node.value } } // Client never sees tree internals it = new TreeIterator(binarySearchTree) while (it.hasNext()) { print(it.next()) // values in sorted order }
class TreeIterator: def __init__(self, root): self.stack = [] self._push_left(root) def _push_left(self, node): while node: self.stack.append(node) node = node.left def has_next(self) -> bool: return len(self.stack) > 0 def __next__(self): node = self.stack.pop() self._push_left(node.right) return node.value # Client never sees tree internals it = TreeIterator(binary_search_tree) while it.has_next(): print(next(it)) # values in sorted order
#include <vector> class TreeIterator { public: explicit TreeIterator(Node* root) { pushLeft(root); } bool hasNext() const { return !stack.empty(); } int next() { Node* node = stack.back(); stack.pop_back(); pushLeft(node->right); return node->value; } private: void pushLeft(Node* node) { while (node) { stack.push_back(node); node = node->left; } } std::vector<Node*> stack; }; int main() { // Client never sees tree internals TreeIterator it(binarySearchTree); while (it.hasNext()) { int value = it.next(); // values in sorted order } }
import java.util.*; class TreeIterator implements Iterator<Integer> { private final Deque<Node> stack = new ArrayDeque<>(); TreeIterator(Node root) { pushLeft(root); } private void pushLeft(Node node) { while (node != null) { stack.push(node); node = node.left; } } public boolean hasNext() { return !stack.isEmpty(); } public Integer next() { Node node = stack.pop(); pushLeft(node.right); return node.value; } } public class Main { public static void main(String[] args) { // Client never sees tree internals Iterator<Integer> it = new TreeIterator(binarySearchTree); while (it.hasNext()) { System.out.println(it.next()); // values in sorted order } } }
class TreeIterator { constructor(root) { this.stack = []; this.pushLeft(root); } pushLeft(node) { while (node) { this.stack.push(node); node = node.left; } } hasNext() { return this.stack.length > 0; } next() { const node = this.stack.pop(); this.pushLeft(node.right); return node.value; } } // Client never sees tree internals const it = new TreeIterator(binarySearchTree); while (it.hasNext()) { console.log(it.next()); // values in sorted order }
⑥ Real Industry Example
🐍 Python Generators & Java's for-each Loop

Python's generators (yield keyword) are Iterator pattern as a first-class language feature. Java's Iterable interface and for-each loop implement Iterator — any class implementing Iterable can be traversed with for (Item item : collection). JavaScript's Symbol.iterator protocol does the same. Database cursors are iterators over query result sets.

⑦ Trade-offs
✓ Benefit⚠ Cost🚫 When It Hurts
Single Responsibility — move traversal out of collectionOverkill for simple collections — adds classes for no benefitWhen your language already handles iteration natively
Multiple iterators can traverse the same collection simultaneouslyLess efficient than direct access if you need random access by indexWhen random access by index is the primary use case
⑨ When to Use It
  • You need to traverse a complex data structure (tree, graph) without exposing its internals
  • You want multiple traversal strategies for the same collection
  • Your language does not have built-in iterator support
⑩ Quick Quiz
Iterator’s main benefit is…
In
Behavioral Pattern 4 of 11
Interpreter
Define a grammar for a simple language and an interpreter that evaluates sentences.
Advanced Behavioral
① What Is It?
A pocket calculator for a tiny language. The expression true AND (false OR true) is a sentence. Terminals are the words true/false; non-terminals are operators like AND/OR that combine smaller expressions. The interpreter walks that tree and returns a value.

Interpreter represents grammar rules as classes. Each rule knows how to interpret itself in a context — ideal for small DSLs, filters, and configuration languages where a full parser generator would be overkill.

② The Problem It Solves

You need to evaluate many sentences in a simple domain language (boolean filters, math snippets, route rules). Hard-coding every sentence as custom code does not scale, and shipping a full compiler is too heavy for a tiny grammar.

③ The Solution

Map each grammar rule to a class implementing interpret(context). Terminals return literal values; non-terminals combine child expressions. Clients build (or parse into) an abstract syntax tree and call interpret on the root.

④ Structure
+--------------------+ | «interface» | | Expression | | + interpret(ctx) | +----------+---------+ ^ +-----+------+ | | +----+-----+ +---+-----------+ | Terminal | | NonTerminal | | literal | | left / right | +----------+ +---------------+
▶ See It In Action
Press Play — each step maps the story to the pattern.
Ready
See it in action
Press Play. Watch the highlight, read the role labels, then the “In code” line.
⑤ Pseudocode
Language:
interface Expression { interpret(ctx: Context) : Boolean } class Literal implements Expression { constructor(value) { this.value = value } interpret(ctx) { return this.value } } class AndExpr implements Expression { constructor(left, right) { this.left = left; this.right = right } interpret(ctx) { return this.left.interpret(ctx) AND this.right.interpret(ctx) } } // true AND (false OR true) ast = new AndExpr( new Literal(true), new OrExpr(new Literal(false), new Literal(true)) ) ast.interpret(ctx) // true
from abc import ABC, abstractmethod class Expression(ABC): @abstractmethod def interpret(self, ctx): ... class Literal(Expression): def __init__(self, value: bool): self.value = value def interpret(self, ctx): return self.value class AndExpr(Expression): def __init__(self, left, right): self.left, self.right = left, right def interpret(self, ctx): return self.left.interpret(ctx) and self.right.interpret(ctx) ast = AndExpr(Literal(True), Literal(True)) print(ast.interpret({})) # True
struct Expression { virtual bool interpret() = 0; virtual ~Expression() = default; }; struct Literal : Expression { bool v; Literal(bool x): v(x) {} bool interpret() override { return v; } }; struct AndExpr : Expression { Expression* l; Expression* r; bool interpret() override { return l->interpret() && r->interpret(); } };
interface Expression { boolean interpret(); } class Literal implements Expression { private final boolean value; Literal(boolean value) { this.value = value; } public boolean interpret() { return value; } } class AndExpr implements Expression { private final Expression left, right; AndExpr(Expression l, Expression r) { left=l; right=r; } public boolean interpret() { return left.interpret() && right.interpret(); } }
class Literal { constructor(value) { this.value = value; } interpret() { return this.value; } } class AndExpr { constructor(left, right) { this.left = left; this.right = right; } interpret() { return this.left.interpret() && this.right.interpret(); } } const ast = new AndExpr(new Literal(true), new Literal(true)); console.log(ast.interpret());
⑥ Real Industry Example
SQL WHERE clauses & rule engines

Many ORMs and policy engines compile filter strings into expression trees — each predicate and boolean operator is a node that can evaluate against a row or request context. Regex engines and simple config DSLs use the same idea at smaller scale: grammar as objects, evaluate by walking the tree.

⑦ Trade-offs
✓ Benefit⚠ Cost🚫 When It Hurts
Easy to extend the grammar with new expression typesClass-per-rule can explode for rich languagesWhen you need a full programming language — use a real parser
Clear mapping from grammar to codeInterpreting deep trees can be slowHot paths that need compiled bytecode
⑨ When to Use It
  • You have a small, stable grammar that users or configs express repeatedly
  • Sentences should be stored, combined, or evaluated in different contexts
  • A full parser generator would be disproportionate to the language size
⑩ Quick Quiz
Interpreter maps each grammar rule to…
Me
Behavioral Pattern 5 of 11
Mediator
Reduce chaotic dependencies between objects by making them communicate only through a mediator.
Intermediate Behavioral
① What Is It?
Air traffic control. Without it, each pilot would need to communicate directly with every other pilot to coordinate — an impossible web of communication. Instead, all pilots talk only to the control tower (mediator), which coordinates all movement. No pilot knows what every other pilot is doing; the control tower does.

Mediator centralises complex communications between many objects. Instead of objects knowing about each other directly (creating a web of dependencies), they all know only about the mediator. The mediator handles all cross-component communication.

② The Problem It Solves

Many components talk to each other directly. The web of references becomes brittle — every new colleague needs updates in several places.

③ The Solution

Route interactions through a Mediator. Components notify the mediator; the mediator coordinates colleagues so they stay decoupled.

④ Structure
+-----------+ +------------+ | Colleague |------>| Mediator | | notify() | | coordinate | +-----------+ +------+-----+ | +------+------+ |Concrete Med | +-------------+
▶ See It In Action
Press Play — each step maps the story to the pattern.
Ready
See it in action
Press Play. Watch the highlight, read the role labels, then the “In code” line.
⑤ Pseudocode
Language:
interface Mediator { notify(sender, event) } class DialogMediator implements Mediator { constructor(checkbox, textfield, button) { this.checkbox = checkbox this.textfield = textfield this.button = button } notify(sender, event) { if (sender == this.checkbox && event == "check") { // When checkbox toggled, enable/disable text this.textfield.setEnabled(this.checkbox.isChecked) this.button.setEnabled(this.checkbox.isChecked) } } } class Checkbox { constructor(mediator) { this.mediator = mediator } toggle() { this.isChecked = !this.isChecked // Never talks directly to textfield this.mediator.notify(this, "check") } }
from abc import ABC, abstractmethod class Mediator(ABC): @abstractmethod def notify(self, sender, event): ... class DialogMediator(Mediator): def __init__(self, checkbox, textfield, button): self.checkbox = checkbox self.textfield = textfield self.button = button def notify(self, sender, event): if sender is self.checkbox and event == "check": # When checkbox toggled, enable/disable text field + button self.textfield.set_enabled(self.checkbox.is_checked) self.button.set_enabled(self.checkbox.is_checked) class Checkbox: def __init__(self, mediator: Mediator): self.mediator = mediator self.is_checked = False def toggle(self): self.is_checked = not self.is_checked # Never talks directly to textfield self.mediator.notify(self, "check")
class Mediator { public: virtual void notify(void* sender, const std::string& event) = 0; virtual ~Mediator() = default; }; class DialogMediator : public Mediator { public: DialogMediator(Checkbox* cb, TextField* tf, Button* btn) : checkbox(cb), textfield(tf), button(btn) {} void notify(void* sender, const std::string& event) override { if (sender == checkbox && event == "check") { // When checkbox toggled, enable/disable text field + button textfield->setEnabled(checkbox->isChecked()); button->setEnabled(checkbox->isChecked()); } } private: Checkbox* checkbox; TextField* textfield; Button* button; }; class Checkbox { public: explicit Checkbox(Mediator* mediator) : mediator(mediator) {} void toggle() { checked = !checked; mediator->notify(this, "check"); // never talks directly to textfield } bool isChecked() const { return checked; } private: Mediator* mediator; bool checked = false; };
interface Mediator { void notify(Object sender, String event); } class DialogMediator implements Mediator { private final Checkbox checkbox; private final TextField textfield; private final Button button; DialogMediator(Checkbox checkbox, TextField textfield, Button button) { this.checkbox = checkbox; this.textfield = textfield; this.button = button; } public void notify(Object sender, String event) { if (sender == checkbox && event.equals("check")) { // When checkbox toggled, enable/disable text field + button textfield.setEnabled(checkbox.isChecked()); button.setEnabled(checkbox.isChecked()); } } } class Checkbox { private final Mediator mediator; private boolean isChecked; Checkbox(Mediator mediator) { this.mediator = mediator; } void toggle() { isChecked = !isChecked; mediator.notify(this, "check"); // never talks directly to textfield } boolean isChecked() { return isChecked; } }
class DialogMediator { constructor(checkbox, textfield, button) { this.checkbox = checkbox; this.textfield = textfield; this.button = button; } notify(sender, event) { if (sender === this.checkbox && event === "check") { // When checkbox toggled, enable/disable text field + button this.textfield.setEnabled(this.checkbox.isChecked); this.button.setEnabled(this.checkbox.isChecked); } } } class Checkbox { constructor(mediator) { this.mediator = mediator; } toggle() { this.isChecked = !this.isChecked; // Never talks directly to textfield this.mediator.notify(this, "check"); } }
⑥ Real Industry Example
💬 Slack / Chat Applications & MVC's Controller

Slack is literally a Mediator: instead of every team member messaging every other directly, they communicate through Slack channels (the mediator). MVC's Controller is a Mediator: the View does not talk to the Model directly — all communication flows through the Controller. React Redux's store acts as a Mediator: components dispatch actions to the store, and the store notifies relevant components of state changes.

⑦ Trade-offs
✓ Benefit⚠ Cost🚫 When It Hurts
Dramatically reduces coupling between componentsMediator can become a God Object — knowing too much about too many thingsWhen it grows to 1000+ lines — split into multiple mediators
Reuse components without dependent couplingCentralises control — introduces single point of failure/congestionWhen components have simple, direct relationships
⑨ When to Use It
  • Many objects communicate in complex, tightly coupled ways
  • You cannot reuse a component in a different context because it depends on others
  • You are building complex UI forms where UI elements affect each other
⑩ Quick Quiz
Mediator reduces chaos by…
Mm
Behavioral Pattern 6 of 11
Memento
Save and restore the previous state of an object without revealing its implementation details.
Intermediate Behavioral
① What Is It?
The save game feature in video games. Before a difficult boss fight, you press "Save." If you die, you press "Load" and return to that exact state — health, inventory, position — all perfectly restored. The save file is the Memento: a snapshot of the game state, stored without the save system needing to understand the game's internal logic.

Memento allows you to save and restore snapshots of an object's state without violating encapsulation. The originator creates and restores mementos; the caretaker stores them. The caretaker never inspects the contents of a memento — the originator's private state stays private.

② The Problem It Solves

You need undo/restore, but exposing internal fields to outsiders breaks encapsulation — or you end up with messy getters just for history.

③ The Solution

The Originator creates opaque Memento objects capturing state. A Caretaker stores them and never peeks inside; only the Originator can restore.

④ Structure
+------------+ +----------+ +-----------+ | Originator |---->| Memento |<----| Caretaker | | save/rest | | (opaque) | | history[] | +------------+ +----------+ +-----------+
▶ See It In Action
Press Play — each step maps the story to the pattern.
Ready
See it in action
Press Play. Watch the highlight, read the role labels, then the “In code” line.
⑤ Pseudocode
Language:
// Originator — creates and restores mementos class TextEditor { constructor() { this.content = "" } type(text) { this.content += text } save() { return { content: this.content } // snapshot } restore(memento) { this.content = memento.content } } // Caretaker — manages history of mementos class History { constructor() { this.stack = [] } push(m) { this.stack.push(m) } pop() { return this.stack.pop() } } editor = new TextEditor() history = new History() editor.type("Hello") history.push(editor.save()) // checkpoint editor.type(" World") print(editor.content) // "Hello World" editor.restore(history.pop()) // undo print(editor.content) // "Hello"
class TextEditor: """Originator -- creates and restores mementos.""" def __init__(self): self.content = "" def type(self, text): self.content += text def save(self): return {"content": self.content} # snapshot def restore(self, memento): self.content = memento["content"] class History: """Caretaker -- manages history of mementos.""" def __init__(self): self.stack = [] def push(self, m): self.stack.append(m) def pop(self): return self.stack.pop() editor = TextEditor() history = History() editor.type("Hello") history.push(editor.save()) # checkpoint editor.type(" World") print(editor.content) # "Hello World" editor.restore(history.pop()) # undo print(editor.content) # "Hello"
#include <string> #include <vector> struct Memento { std::string content; }; class TextEditor { // Originator public: void type(const std::string& text) { content += text; } Memento save() const { return {content}; } // snapshot void restore(const Memento& m) { content = m.content; } std::string content; }; class History { // Caretaker public: void push(Memento m) { stack.push_back(std::move(m)); } Memento pop() { auto m = stack.back(); stack.pop_back(); return m; } private: std::vector<Memento> stack; }; int main() { TextEditor editor; History history; editor.type("Hello"); history.push(editor.save()); // checkpoint editor.type(" World"); // editor.content == "Hello World" editor.restore(history.pop()); // undo // editor.content == "Hello" }
import java.util.*; class Memento { final String content; Memento(String content) { this.content = content; } } class TextEditor { // Originator String content = ""; void type(String text) { content += text; } Memento save() { return new Memento(content); } // snapshot void restore(Memento m) { content = m.content; } } class History { // Caretaker private final Deque<Memento> stack = new ArrayDeque<>(); void push(Memento m) { stack.push(m); } Memento pop() { return stack.pop(); } } public class Main { public static void main(String[] args) { TextEditor editor = new TextEditor(); History history = new History(); editor.type("Hello"); history.push(editor.save()); // checkpoint editor.type(" World"); System.out.println(editor.content); // "Hello World" editor.restore(history.pop()); // undo System.out.println(editor.content); // "Hello" } }
// Originator -- creates and restores mementos class TextEditor { constructor() { this.content = ""; } type(text) { this.content += text; } save() { return { content: this.content }; // snapshot } restore(memento) { this.content = memento.content; } } // Caretaker -- manages history of mementos class History { constructor() { this.stack = []; } push(m) { this.stack.push(m); } pop() { return this.stack.pop(); } } const editor = new TextEditor(); const history = new History(); editor.type("Hello"); history.push(editor.save()); // checkpoint editor.type(" World"); console.log(editor.content); // "Hello World" editor.restore(history.pop()); // undo console.log(editor.content); // "Hello"
⑥ Real Industry Example
📄 Microsoft Word Undo & Database Transaction Checkpoints

Microsoft Word's undo history is Memento: each keystroke saves a snapshot; Ctrl+Z restores the previous one. Database transactions use Memento in savepoints — you can ROLLBACK TO SAVEPOINT to restore state to any checkpoint without undoing the entire transaction. Git's stash feature is Memento applied to your working directory state.

⑦ Trade-offs
✓ Benefit⚠ Cost🚫 When It Hurts
Save and restore state without exposing internalsMemory cost — each snapshot stores full stateWhen objects have large state — memory pressure is significant
Enables complete undo/redo and checkpointingCaretakers must manage the memento lifecycleWhen state is immutable anyway — no need for snapshots
⑨ When to Use It
  • You need to produce snapshots of an object's state for undo/redo
  • Direct access to the object's private fields violates encapsulation
  • You need checkpoint and rollback functionality
⑩ Quick Quiz
Who is allowed to read a Memento’s internals?
Ob
Behavioral Pattern 7 of 11
Observer
Define a subscription mechanism to notify multiple objects about events in the object they are observing.
Beginner Behavioral
① What Is It?
A newspaper subscription. Readers subscribe to The Times. When a new edition is published, the newspaper is delivered to all subscribers automatically. Readers do not check the newspaper office every hour — they are notified when something happens. They can also unsubscribe at any time.

Observer defines a one-to-many dependency: when one object (Subject/Publisher) changes state, all its dependents (Observers/Subscribers) are notified and updated automatically. This is the most widely used pattern in all of modern software.

② The Problem It Solves

When one object changes, many others must react. Hard-wiring dependents creates tight coupling and missed updates.

③ The Solution

Subjects keep a list of Observers and notify them on change. Observers subscribe/unsubscribe without the subject knowing concrete types.

④ Structure
+----------+ notify +----------+ | Subject |--------->| Observer | | attach() | | update() | +----------+ +----------+
▶ See It In Action
Press Play — each step maps the story to the pattern.
Ready
See it in action
Press Play. Watch the highlight, read the role labels, then the “In code” line.
⑤ Pseudocode
Language:
interface Observer { update(event) } class EventEmitter { constructor() { this.listeners = {} } on(event, fn) { this.listeners[event] ??= [] this.listeners[event].push(fn) } off(event, fn) { this.listeners[event] = this.listeners[event]?.filter(f => f !== fn) } emit(event, data) { this.listeners[event]?.forEach(fn => fn(data)) } } class Store extends EventEmitter { setUser(user) { this.user = user this.emit("userChanged", user) } } store = new Store() store.on("userChanged", u => updateHeader(u)) store.on("userChanged", u => logAnalytics(u)) store.setUser({ name: "Alice" }) // Both listeners fire automatically
from collections import defaultdict class EventEmitter: def __init__(self): self.listeners = defaultdict(list) def on(self, event, fn): self.listeners[event].append(fn) def off(self, event, fn): self.listeners[event] = [f for f in self.listeners[event] if f is not fn] def emit(self, event, data): for fn in self.listeners[event]: fn(data) class Store(EventEmitter): def set_user(self, user): self.user = user self.emit("userChanged", user) store = Store() store.on("userChanged", lambda u: update_header(u)) store.on("userChanged", lambda u: log_analytics(u)) store.set_user({"name": "Alice"}) # Both listeners fire automatically
#include <unordered_map> #include <vector> #include <functional> #include <string> class EventEmitter { public: void on(const std::string& event, std::function<void(void*)> fn) { listeners[event].push_back(fn); } void emit(const std::string& event, void* data) { for (auto& fn : listeners[event]) fn(data); } private: std::unordered_map<std::string, std::vector<std::function<void(void*)>>> listeners; }; class Store : public EventEmitter { public: void setUser(void* user) { this->user = user; emit("userChanged", user); } private: void* user; }; int main() { Store store; store.on("userChanged", [](void* u) { updateHeader(u); }); store.on("userChanged", [](void* u) { logAnalytics(u); }); store.setUser(&aliceUser); // Both listeners fire automatically }
import java.util.*; import java.util.function.Consumer; class EventEmitter { private final Map<String, List<Consumer<Object>>> listeners = new HashMap<>(); void on(String event, Consumer<Object> fn) { listeners.computeIfAbsent(event, k -> new ArrayList<>()).add(fn); } void emit(String event, Object data) { listeners.getOrDefault(event, List.of()).forEach(fn -> fn.accept(data)); } } class Store extends EventEmitter { void setUser(Object user) { emit("userChanged", user); } } public class Main { public static void main(String[] args) { Store store = new Store(); store.on("userChanged", u -> updateHeader(u)); store.on("userChanged", u -> logAnalytics(u)); store.setUser(new User("Alice")); // Both listeners fire automatically } }
class EventEmitter { constructor() { this.listeners = {}; } on(event, fn) { (this.listeners[event] ??= []).push(fn); } off(event, fn) { this.listeners[event] = this.listeners[event]?.filter(f => f !== fn); } emit(event, data) { this.listeners[event]?.forEach(fn => fn(data)); } } class Store extends EventEmitter { setUser(user) { this.user = user; this.emit("userChanged", user); } } const store = new Store(); store.on("userChanged", u => updateHeader(u)); store.on("userChanged", u => logAnalytics(u)); store.setUser({ name: "Alice" }); // Both listeners fire automatically
⑥ Real Industry Example
✨ React, Vue, Kafka — Observer is Everywhere

React's useState hook is Observer: components "observe" state and re-render when it changes. Vue's reactivity system uses Proxy-based observation — access a property and Vue registers a dependency; mutate it and Vue re-renders all dependents. Apache Kafka is Observer at infrastructure scale: producers publish events to topics, and multiple consumer groups independently subscribe and process them. DOM's addEventListener is Observer built into the browser.

⑦ Trade-offs
✓ Benefit⚠ Cost🚫 When It Hurts
Open/Closed — add new observers without modifying the subjectObservers notified in random order — do not rely on sequenceWhen notification chains create cascading updates (event storms)
Loose coupling — publisher knows nothing about subscribersMemory leaks if observers are not properly removedWhen observers need to know about each other's updates
⑨ When to Use It
  • Changes to one object require notifying an unknown number of other objects
  • You want loose coupling between related objects
  • You are building event systems, reactive UIs, or pub/sub architectures
⑩ Quick Quiz
Observer establishes…
St
Behavioral Pattern 8 of 11
State
Allow an object to alter its behavior when its internal state changes — it appears to change class.
Intermediate Behavioral
① What Is It?
A traffic light. When a traffic light is RED, pressing "go" does nothing. When it is GREEN, pressing "go" starts the crossing timer. The same button, the same action — but the result depends entirely on which state the light is in. The light appears to change its behaviour based on its current state.

State allows an object to change its behaviour when its internal state changes. Instead of one large class with conditional logic (if state == "locked" do X, else if state == "unlocked" do Y), each state is a separate class. Transitioning from one state to another means swapping the state object.

② The Problem It Solves

Behavior depends on internal state, so methods fill with giant switch/if chains that grow with every new state.

③ The Solution

Extract each state into a State object. Context delegates to the current state; states may trigger transitions to other states.

④ Structure
+---------+ +--------+ | Context |---->| State | | request | | handle | +---------+ +---+----+ ^ +-------+--------+ | ConcreteStates | +----------------+
▶ See It In Action
Press Play — each step maps the story to the pattern.
Ready
See it in action
Press Play. Watch the highlight, read the role labels, then the “In code” line.
⑤ Pseudocode
Language:
interface OrderState { confirm(order) ship(order) deliver(order) } class PendingState implements OrderState { confirm(o) { o.setState(new ConfirmedState()) } ship(o) { throw "Cannot ship unconfirmed order" } deliver(o) { throw "Cannot deliver unconfirmed order" } } class ConfirmedState implements OrderState { confirm(o) { throw "Already confirmed" } ship(o) { o.setState(new ShippedState()) } deliver(o) { throw "Ship first" } } class Order { constructor() { this.state = new PendingState() } setState(s) { this.state = s } confirm() { this.state.confirm(this) } ship() { this.state.ship(this) } }
from abc import ABC, abstractmethod class OrderState(ABC): @abstractmethod def confirm(self, order): ... @abstractmethod def ship(self, order): ... class PendingState(OrderState): def confirm(self, order): order.set_state(ConfirmedState()) def ship(self, order): raise Exception("Cannot ship unconfirmed order") class ConfirmedState(OrderState): def confirm(self, order): raise Exception("Already confirmed") def ship(self, order): order.set_state(ShippedState()) class ShippedState(OrderState): def confirm(self, order): raise Exception("Already shipped") def ship(self, order): raise Exception("Already shipped") class Order: def __init__(self): self.state = PendingState() def set_state(self, state: OrderState): self.state = state def confirm(self): self.state.confirm(self) def ship(self): self.state.ship(self)
#include <memory> #include <stdexcept> class Order; struct OrderState { virtual void confirm(Order& order) = 0; virtual void ship(Order& order) = 0; virtual ~OrderState() = default; }; class Order { public: Order(); void setState(std::unique_ptr<OrderState> s) { state = std::move(s); } void confirm() { state->confirm(*this); } void ship() { state->ship(*this); } private: std::unique_ptr<OrderState> state; }; struct ShippedState : OrderState { void confirm(Order&) override { throw std::runtime_error("Already shipped"); } void ship(Order&) override { throw std::runtime_error("Already shipped"); } }; struct ConfirmedState : OrderState { void confirm(Order&) override { throw std::runtime_error("Already confirmed"); } void ship(Order& o) override { o.setState(std::make_unique<ShippedState>()); } }; struct PendingState : OrderState { void confirm(Order& o) override { o.setState(std::make_unique<ConfirmedState>()); } void ship(Order&) override { throw std::runtime_error("Cannot ship unconfirmed order"); } }; Order::Order() { state = std::make_unique<PendingState>(); }
interface OrderState { void confirm(Order order); void ship(Order order); } class PendingState implements OrderState { public void confirm(Order o) { o.setState(new ConfirmedState()); } public void ship(Order o) { throw new IllegalStateException("Cannot ship unconfirmed order"); } } class ConfirmedState implements OrderState { public void confirm(Order o) { throw new IllegalStateException("Already confirmed"); } public void ship(Order o) { o.setState(new ShippedState()); } } class ShippedState implements OrderState { public void confirm(Order o) { throw new IllegalStateException("Already shipped"); } public void ship(Order o) { throw new IllegalStateException("Already shipped"); } } class Order { private OrderState state = new PendingState(); void setState(OrderState s) { this.state = s; } void confirm() { state.confirm(this); } void ship() { state.ship(this); } }
class PendingState { confirm(o) { o.setState(new ConfirmedState()); } ship(o) { throw "Cannot ship unconfirmed order"; } deliver(o) { throw "Cannot deliver unconfirmed order"; } } class ConfirmedState { confirm(o) { throw "Already confirmed"; } ship(o) { o.setState(new ShippedState()); } deliver(o) { throw "Ship first"; } } class ShippedState { confirm(o) { throw "Already shipped"; } ship(o) { throw "Already shipped"; } deliver(o) { o.setState(new DeliveredState()); } } class Order { constructor() { this.state = new PendingState(); } setState(s) { this.state = s; } confirm() { this.state.confirm(this); } ship() { this.state.ship(this); } }
⑥ Real Industry Example
🚛 Uber Driver State Machine & TCP Connection States

Uber's driver app is a State machine: drivers transition through Offline → Available → Matched → OnTrip → Returning. Each state has strict allowed transitions — you cannot go from OnTrip directly back to Offline. TCP connection protocol is one of the most famous State machines in computing: CLOSED → LISTEN → SYN_SENT → ESTABLISHED → TIME_WAIT — each state dictates exactly which packets can be sent and received.

⑦ Trade-offs
✓ Benefit⚠ Cost🚫 When It Hurts
Eliminates massive if-else or switch chains for state-dependent behaviourOverkill for few states — a simple enum + switch might sufficeWhen states have very little unique behaviour
Single Responsibility — each state class has one jobStates may need to know about each other to manage transitionsWhen states share extensive behaviour — lots of duplication
⑨ When to Use It
  • An object's behaviour depends heavily on its state and must change at runtime
  • You have massive conditionals that choose behaviour based on state
  • State transitions need to be explicit and enforced (invalid transitions should be impossible)
⑩ Quick Quiz
State pattern replaces…
Sy
Behavioral Pattern 9 of 11
Strategy
Define a family of algorithms, encapsulate each one, and make them interchangeable at runtime.
Beginner Behavioral
① What Is It?
Navigation apps offering multiple route types. Google Maps shows your destination. You choose: fastest route, avoid tolls, avoid highways, or walking route. The destination stays the same; the algorithm for getting there changes. Each routing strategy is encapsulated and swappable while the navigation context remains unchanged.

Strategy defines a family of algorithms, encapsulates each one, and makes them interchangeable. The Strategy pattern lets you change the algorithm used by an object at runtime without changing the object itself.

② The Problem It Solves

You need interchangeable algorithms (sorts, pricing, routing). Embedding them as conditionals or subclasses locks clients to one approach.

③ The Solution

Define a Strategy interface; concrete strategies implement variants. Context holds a strategy and delegates — swap anytime.

④ Structure
+---------+ +----------+ | Context |---->| Strategy | | execute | | algorithm| +---------+ +----+-----+ ^ +-------+--------+ |ConcreteStrategy| +----------------+
▶ See It In Action
Press Play — each step maps the story to the pattern.
Ready
See it in action
Press Play. Watch the highlight, read the role labels, then the “In code” line.
⑤ Pseudocode
Language:
interface SortStrategy { sort(data: array) : array } class QuickSort implements SortStrategy { sort(data) { /* ... */ return data } } class MergeSort implements SortStrategy { sort(data) { /* ... */ return data } } class InsertionSort implements SortStrategy { sort(data) { /* ... */ return data } } class DataSorter { constructor(strategy: SortStrategy) { this.strategy = strategy } setStrategy(s) { this.strategy = s } sort(data) { return this.strategy.sort(data) } } sorter = new DataSorter(new QuickSort()) result1 = sorter.sort(largeDataSet) // Runtime switch — no if-else in DataSorter sorter.setStrategy(new InsertionSort()) result2 = sorter.sort(nearlyOrderedSmallArray)
from abc import ABC, abstractmethod class SortStrategy(ABC): @abstractmethod def sort(self, data: list) -> list: ... class QuickSort(SortStrategy): def sort(self, data): return sorted(data) # ... class MergeSort(SortStrategy): def sort(self, data): return sorted(data) # ... class InsertionSort(SortStrategy): def sort(self, data): return sorted(data) # ... class DataSorter: def __init__(self, strategy: SortStrategy): self.strategy = strategy def set_strategy(self, strategy: SortStrategy): self.strategy = strategy def sort(self, data): return self.strategy.sort(data) sorter = DataSorter(QuickSort()) result1 = sorter.sort(large_data_set) # Runtime switch -- no if/else in DataSorter sorter.set_strategy(InsertionSort()) result2 = sorter.sort(nearly_ordered_small_array)
#include <vector> #include <memory> #include <algorithm> struct SortStrategy { virtual std::vector<int> sort(std::vector<int> data) = 0; virtual ~SortStrategy() = default; }; struct QuickSort : SortStrategy { std::vector<int> sort(std::vector<int> data) override { std::sort(data.begin(), data.end()); return data; } }; struct InsertionSort : SortStrategy { std::vector<int> sort(std::vector<int> data) override { /* ... */ return data; } }; class DataSorter { public: explicit DataSorter(std::unique_ptr<SortStrategy> s) : strategy(std::move(s)) {} void setStrategy(std::unique_ptr<SortStrategy> s) { strategy = std::move(s); } std::vector<int> sort(std::vector<int> data) { return strategy->sort(std::move(data)); } private: std::unique_ptr<SortStrategy> strategy; }; int main() { DataSorter sorter(std::make_unique<QuickSort>()); auto result1 = sorter.sort(largeDataSet); // Runtime switch -- no if/else in DataSorter sorter.setStrategy(std::make_unique<InsertionSort>()); auto result2 = sorter.sort(nearlyOrderedSmallArray); }
import java.util.*; interface SortStrategy { List<Integer> sort(List<Integer> data); } class QuickSort implements SortStrategy { public List<Integer> sort(List<Integer> data) { /* ... */ return data; } } class InsertionSort implements SortStrategy { public List<Integer> sort(List<Integer> data) { /* ... */ return data; } } class DataSorter { private SortStrategy strategy; DataSorter(SortStrategy strategy) { this.strategy = strategy; } void setStrategy(SortStrategy s) { this.strategy = s; } List<Integer> sort(List<Integer> data) { return strategy.sort(data); } } public class Main { public static void main(String[] args) { DataSorter sorter = new DataSorter(new QuickSort()); List<Integer> result1 = sorter.sort(largeDataSet); // Runtime switch -- no if/else in DataSorter sorter.setStrategy(new InsertionSort()); List<Integer> result2 = sorter.sort(nearlyOrderedSmallArray); } }
class QuickSort { sort(data) { return [...data].sort((a, b) => a - b); } } class MergeSort { sort(data) { /* ... */ return data; } } class InsertionSort { sort(data) { /* ... */ return data; } } class DataSorter { constructor(strategy) { this.strategy = strategy; } setStrategy(s) { this.strategy = s; } sort(data) { return this.strategy.sort(data); } } const sorter = new DataSorter(new QuickSort()); const result1 = sorter.sort(largeDataSet); // Runtime switch -- no if-else in DataSorter sorter.setStrategy(new InsertionSort()); const result2 = sorter.sort(nearlyOrderedSmallArray);
⑥ Real Industry Example
💳 Payment Methods at Checkout & Java's Comparator

E-commerce checkout is textbook Strategy. The PaymentService context accepts any payment strategy: CreditCardStrategy, UPIStrategy, WalletStrategy, NetBankingStrategy. All implement the same pay(amount) interface; the checkout process never changes. Java's Arrays.sort() accepts a Comparator — this is Strategy applied to comparison logic. Spring Security's AuthenticationProvider is a Strategy: swap out LDAP, database, or OAuth authentication without changing the security framework.

⑦ Trade-offs
✓ Benefit⚠ Cost🚫 When It Hurts
Swap algorithms at runtime — eliminates conditional logicClients must know which strategy to chooseWhen you only have one algorithm — over-engineering
Each strategy is independently testableMore classes — one per strategyIn functional languages — first-class functions make this unnecessary
⑨ When to Use It
  • You want to switch between different variants of an algorithm at runtime
  • You have multiple classes that only differ in how they execute a certain behaviour
  • A class has massive conditionals that switch between algorithm variants
⑩ Quick Quiz
Strategy is ideal when…
Strategy vs State: They look structurally identical but serve different purposes. Strategy swaps algorithms chosen by the client. State changes behaviour based on internal conditions — states know about each other and manage transitions. Strategy is chosen externally; State transitions autonomously.
TM
Behavioral Pattern 10 of 11
Template Method
Define the skeleton of an algorithm in the superclass, deferring some steps to subclasses.
Beginner Behavioral
① What Is It?
A cooking show format. Every episode follows the same structure: introduce the dish, gather ingredients, prepare, cook, plate, taste. The template is always the same. But the specific steps vary by dish. Template Method says: define the structure once in a base class, and let subclasses fill in the specific steps.

Template Method defines the skeleton of an algorithm in the base class, deferring some steps to subclasses. Subclasses can override certain steps without changing the algorithm's overall structure. It is inheritance applied to algorithms.

② The Problem It Solves

Several classes share the same algorithm outline but differ in a few steps. Copy-pasting the outline causes drift; forcing one concrete class loses flexibility.

③ The Solution

Put the skeleton in a base-class template method that calls overridable steps. Subclasses implement only the varying pieces.

④ Structure
+------------------+ | AbstractClass | | templateMethod() | | stepA() | | stepB() | +--------+---------+ ^ +--------+---------+ | ConcreteClass | | stepA()/stepB() | +------------------+
▶ See It In Action
Press Play — each step maps the story to the pattern.
Ready
See it in action
Press Play. Watch the highlight, read the role labels, then the “In code” line.
⑤ Pseudocode
Language:
abstract class DataMigrator { // Template method — the skeleton (final) migrate() { data = this.extractData() clean = this.transformData(data) this.validateData(clean) this.loadData(clean) this.sendReport() } // Abstract — subclasses MUST implement abstract extractData() abstract transformData(data) abstract loadData(data) // Hooks — subclasses CAN override validateData(data) { /* default: no-op */ } sendReport() { print("Migration done") } } class CSVToPostgresMigrator extends DataMigrator { extractData() { return readCSV("data.csv") } transformData(d) { return mapToSchema(d) } loadData(d) { postgresInsert(d) } validateData(d) { checkRequired(d) } } new CSVToPostgresMigrator().migrate()
from abc import ABC, abstractmethod class DataMigrator(ABC): def migrate(self): """Template method -- the skeleton (never overridden).""" data = self.extract_data() clean = self.transform_data(data) self.validate_data(clean) self.load_data(clean) self.send_report() @abstractmethod def extract_data(self): ... @abstractmethod def transform_data(self, data): ... @abstractmethod def load_data(self, data): ... # Hooks -- subclasses CAN override def validate_data(self, data): pass # default: no-op def send_report(self): print("Migration done") class CSVToPostgresMigrator(DataMigrator): def extract_data(self): return read_csv("data.csv") def transform_data(self, d): return map_to_schema(d) def load_data(self, d): postgres_insert(d) def validate_data(self, d): check_required(d) CSVToPostgresMigrator().migrate()
#include <iostream> class DataMigrator { public: void migrate() { // template method -- the skeleton (not virtual) auto data = extractData(); auto clean = transformData(data); validateData(clean); loadData(clean); sendReport(); } virtual ~DataMigrator() = default; protected: virtual void* extractData() = 0; virtual void* transformData(void* data) = 0; virtual void loadData(void* data) = 0; // Hooks -- subclasses CAN override virtual void validateData(void* data) {} // default: no-op virtual void sendReport() { std::cout << "Migration done\n"; } }; class CSVToPostgresMigrator : public DataMigrator { protected: void* extractData() override { return readCSV("data.csv"); } void* transformData(void* d) override { return mapToSchema(d); } void loadData(void* d) override { postgresInsert(d); } void validateData(void* d) override { checkRequired(d); } }; int main() { CSVToPostgresMigrator().migrate(); }
abstract class DataMigrator { // Template method -- the skeleton (final -- cannot be overridden) final void migrate() { Object data = extractData(); Object clean = transformData(data); validateData(clean); loadData(clean); sendReport(); } abstract Object extractData(); abstract Object transformData(Object data); abstract void loadData(Object data); // Hooks -- subclasses CAN override void validateData(Object data) { /* default: no-op */ } void sendReport() { System.out.println("Migration done"); } } class CSVToPostgresMigrator extends DataMigrator { Object extractData() { return readCSV("data.csv"); } Object transformData(Object d) { return mapToSchema(d); } void loadData(Object d) { postgresInsert(d); } void validateData(Object d) { checkRequired(d); } } public class Main { public static void main(String[] args) { new CSVToPostgresMigrator().migrate(); } }
class DataMigrator { // Template method -- the skeleton (not meant to be overridden) migrate() { const data = this.extractData(); const clean = this.transformData(data); this.validateData(clean); this.loadData(clean); this.sendReport(); } // Subclasses MUST implement extractData() { throw new Error("override me"); } transformData(data) { throw new Error("override me"); } loadData(data) { throw new Error("override me"); } // Hooks -- subclasses CAN override validateData(data) { /* default: no-op */ } sendReport() { console.log("Migration done"); } } class CSVToPostgresMigrator extends DataMigrator { extractData() { return readCSV("data.csv"); } transformData(d) { return mapToSchema(d); } loadData(d) { postgresInsert(d); } validateData(d) { checkRequired(d); } } new CSVToPostgresMigrator().migrate();
⑥ Real Industry Example
🧪 JUnit Test Lifecycle & Spring's JdbcTemplate

JUnit's test lifecycle is Template Method: the framework calls @BeforeEach, then @Test, then @AfterEach — the skeleton is fixed; you fill in the test methods. Spring's JdbcTemplate abstracts the template: get connection → prepare statement → execute → handle results → release. You provide only the SQL and result mapping. Django's class-based views (ListView, DetailView) are Template Method — override get_queryset() to customise; the HTTP handling skeleton is inherited.

⑦ Trade-offs
✓ Benefit⚠ Cost🚫 When It Hurts
Eliminates code duplication in algorithms with the same skeletonClients bound to the template — cannot change the skeletonWhen subclasses need to radically alter the algorithm — too rigid
Easy to extend — just override the variable stepsInheritance-based — tightly coupled hierarchyWhen "composition over inheritance" is your design principle
⑨ When to Use It
  • Multiple classes share the same algorithm structure but differ in specific steps
  • You want to eliminate code duplication in algorithm skeletons
  • You want to control extension points — only certain steps can be overridden
⑩ Quick Quiz
Template Method puts the algorithm skeleton…
Vi
Behavioral Pattern 11 of 11
Visitor
Separate algorithms from the objects on which they operate.
Advanced Behavioral
① What Is It?
A health inspector visiting a restaurant. The inspector does not work at the restaurant — they visit and apply their own rules. The restaurant (the object) does not change; the inspector (visitor) brings their own logic. Next week a fire inspector visits — same restaurant, completely different operations. Visitor lets you add new operations to objects without modifying them.

Visitor separates an algorithm from the object structure it operates on. New operations can be added without modifying the classes. Core trade-off: adding new operations is easy (just write a new Visitor), but adding new element types requires updating all existing Visitors.

② The Problem It Solves

You need many operations over a stable object structure. Adding each operation into every element class pollutes the model and forces constant edits.

③ The Solution

Elements accept a Visitor; the visitor implements visitX for each element type (double dispatch). New operations become new visitors.

④ Structure
+---------+ accept +---------+ | Element |---------->| Visitor | | accept | | visitA()| +---------+ | visitB()| +---------+
▶ See It In Action
Press Play — each step maps the story to the pattern.
Ready
See it in action
Press Play. Watch the highlight, read the role labels, then the “In code” line.
⑤ Pseudocode
Language:
interface Expression { accept(visitor: Visitor) } class NumberNode implements Expression { constructor(v) { this.value = v } accept(v) { return v.visitNumber(this) } } class AddNode implements Expression { constructor(l, r) { this.left=l; this.right=r } accept(v) { return v.visitAdd(this) } } class EvalVisitor { visitNumber(n) { return n.value } visitAdd(n) { return n.left.accept(this) + n.right.accept(this) } } // New operation: print — no AST changes! class PrintVisitor { visitNumber(n) { return String(n.value) } visitAdd(n) { return "(" + n.left.accept(this) + " + " + n.right.accept(this) + ")" } } tree = new AddNode(new NumberNode(3), new NumberNode(4)) print(tree.accept(new EvalVisitor())) // 7 print(tree.accept(new PrintVisitor())) // "(3 + 4)"
from abc import ABC, abstractmethod class Expression(ABC): @abstractmethod def accept(self, visitor): ... class NumberNode(Expression): def __init__(self, v): self.value = v def accept(self, visitor): return visitor.visit_number(self) class AddNode(Expression): def __init__(self, left, right): self.left = left self.right = right def accept(self, visitor): return visitor.visit_add(self) class EvalVisitor: def visit_number(self, n): return n.value def visit_add(self, n): return n.left.accept(self) + n.right.accept(self) # New operation: print -- no AST changes! class PrintVisitor: def visit_number(self, n): return str(n.value) def visit_add(self, n): return f"({n.left.accept(self)} + {n.right.accept(self)})" tree = AddNode(NumberNode(3), NumberNode(4)) print(tree.accept(EvalVisitor())) # 7 print(tree.accept(PrintVisitor())) # "(3 + 4)"
#include <memory> #include <string> #include <iostream> class Visitor; struct Expression { virtual double accept(Visitor& v) = 0; virtual ~Expression() = default; }; class NumberNode : public Expression { public: explicit NumberNode(double v) : value(v) {} double accept(Visitor& v) override; double value; }; class AddNode : public Expression { public: AddNode(std::unique_ptr<Expression> l, std::unique_ptr<Expression> r) : left(std::move(l)), right(std::move(r)) {} double accept(Visitor& v) override; std::unique_ptr<Expression> left, right; }; struct Visitor { virtual double visitNumber(NumberNode& n) = 0; virtual double visitAdd(AddNode& n) = 0; }; double NumberNode::accept(Visitor& v) { return v.visitNumber(*this); } double AddNode::accept(Visitor& v) { return v.visitAdd(*this); } class EvalVisitor : public Visitor { public: double visitNumber(NumberNode& n) override { return n.value; } double visitAdd(AddNode& n) override { return n.left->accept(*this) + n.right->accept(*this); } }; int main() { auto tree = AddNode(std::make_unique<NumberNode>(3), std::make_unique<NumberNode>(4)); EvalVisitor eval; std::cout << tree.accept(eval) << "\n"; // 7 }
interface Visitor { double visitNumber(NumberNode n); double visitAdd(AddNode n); } interface Expression { double accept(Visitor visitor); } class NumberNode implements Expression { double value; NumberNode(double v) { this.value = v; } public double accept(Visitor v) { return v.visitNumber(this); } } class AddNode implements Expression { Expression left, right; AddNode(Expression l, Expression r) { left = l; right = r; } public double accept(Visitor v) { return v.visitAdd(this); } } class EvalVisitor implements Visitor { public double visitNumber(NumberNode n) { return n.value; } public double visitAdd(AddNode n) { return n.left.accept(this) + n.right.accept(this); } } // New operation: print -- no Expression classes need to change! public class Main { public static void main(String[] args) { Expression tree = new AddNode(new NumberNode(3), new NumberNode(4)); System.out.println(tree.accept(new EvalVisitor())); // 7.0 } }
class NumberNode { constructor(v) { this.value = v; } accept(v) { return v.visitNumber(this); } } class AddNode { constructor(l, r) { this.left = l; this.right = r; } accept(v) { return v.visitAdd(this); } } class EvalVisitor { visitNumber(n) { return n.value; } visitAdd(n) { return n.left.accept(this) + n.right.accept(this); } } // New operation: print -- no AST changes! class PrintVisitor { visitNumber(n) { return String(n.value); } visitAdd(n) { return "(" + n.left.accept(this) + " + " + n.right.accept(this) + ")"; } } const tree = new AddNode(new NumberNode(3), new NumberNode(4)); console.log(tree.accept(new EvalVisitor())); // 7 console.log(tree.accept(new PrintVisitor())); // "(3 + 4)"
⑥ Real Industry Example
⚙ Babel & TypeScript Compiler — AST Traversal

Babel's plugin system is Visitor at scale. The compiler parses JavaScript into an AST. Each Babel plugin is a Visitor that traverses the tree — converting JSX to function calls, transpiling arrow functions — without modifying the core AST node classes. TypeScript's type checker similarly visits AST nodes. OpenDocument's export visitors generate PDF, HTML, and DOCX from the same document AST without modifying the document model.

⑦ Trade-offs
✓ Benefit⚠ Cost🚫 When It Hurts
Add new operations without modifying element classesAdding a new element type requires updating ALL visitorsWhen element types change frequently
All related operations in one Visitor — easy to findComplex double-dispatch mechanism — hard to understand at firstWhen the hierarchy is simple — overkill
⑨ When to Use It
  • You need to perform many distinct and unrelated operations on an object structure
  • You do not want to pollute classes with unrelated operations
  • New operations are added frequently but the element class hierarchy is stable
⑩ Quick Quiz
Visitor makes it easy to…
Section 5 — Find Your Pattern

Find Your Pattern

One guided path — answer what you are trying to solve, and land on the right pattern. Knowing when to use a pattern is the skill that separates junior from architect.

5.1 Decision Path

What is your core problem?
🧱 Creating objects in a flexible way
🔗 Assembling objects into structures
💬 Managing communication / responsibility

5.2 Pattern Combinations

MVC Architecture
Model ←— Observer —→ View ↑ ↑ Controller —— Strategy ——┘ └———— Composite (View tree)

Model notifies View via Observer. Controller applies Strategy for different actions. View is a Composite tree.

Event-Driven Architecture
Producer —Observer—→ EventBus ↓ Chain of Responsibility ↓ Command (stored/replayed) ↓ Consumer

Events published via Observer. Processed through Chain of Responsibility filters. Actions as Commands for replay/audit.

Microservices Gateway
Client Request ↓ Proxy (API Gateway) ↓ Chain of Responsibility (auth → rate-limit → log) ↓ Facade (service aggregator) ↓ Strategy (load balancer)

Gateway is a Proxy. Middleware is Chain of Responsibility. Aggregation is Facade. Load balancing is Strategy.

Plugin System
PluginRegistry (Factory) ↓ creates Plugin (Strategy interface) ↓ wrapped by Decorator (logging, auth) ↓ composed in Composite (plugin chains)

Factory creates plugins. Strategy makes them swappable. Decorators add cross-cutting concerns. Composite chains them.

5.3 Anti-Patterns

☠ God Object

What it is: One class that knows everything and does everything. How it happens: "Let me just add this method here." Repeated 500 times. Fix: Extract distinct responsibilities into focused classes. Apply Single Responsibility Principle.

☠ Golden Hammer

What it is: Applying the same familiar pattern to every problem. ("We always use Singleton.") How it happens: Expertise in one pattern, ignorance of alternatives. Fix: Learn the full pattern vocabulary. Match patterns to problems, not comfort zones.

☠ Lava Flow

What it is: Dead code nobody dares remove because nobody understands what it does. How it happens: Fear of breaking things, no tests, no documentation. Fix: Write tests first, then refactor. Delete code aggressively. If it is not tested, assume it is dead.

☠ Boat Anchor

What it is: Keeping unused components "just in case." How it happens: Over-engineering, speculative generality. Fix: YAGNI — You Aren't Gonna Need It. Version control is your safety net.

Practice Tool

Compare Patterns

Pick two patterns and see intent, structure, when to use, and when to avoid — side by side.

VS

Select two different patterns to compare them side by side.

Section 6 — Professional Level

Industry Standards & Professional Context

How patterns manifest in real, named production systems used by billions of people.

6.1 Patterns in Famous Systems

🔴 Netflix
  • Observer: Microservices subscribe to topic events via Apache Kafka
  • Strategy: Adaptive bitrate streaming selects encoding strategy (4K, 1080p, 480p) at runtime
  • Proxy: API Gateway proxies all internal services — adds auth, rate limiting transparently
  • Facade: The API Gateway hides hundreds of microservices behind a single consumer API
🔵 Google
  • Flyweight: Search index stores billions of posting list entries with shared data structures
  • Composite: Protocol Buffers messages compose other messages — recursive composite structures
  • Template Method: MapReduce — the framework defines the skeleton; devs fill in map/reduce
  • Proxy: API Gateway wraps all internal services with auth, logging, and rate limiting
⬛ Uber
  • Command: Trip lifecycle — each action (request, accept, start, complete, cancel) is a Command
  • State: Driver state machine — Offline → Available → OnTrip → Returning with strict transitions
  • Strategy: Surge pricing algorithm selected by demand/supply ratio at runtime
  • Chain of Responsibility: Payment pipeline tries methods in order until one succeeds
⚛ React / Spring
  • Observer: React's core reactivity — state changes trigger re-renders in observing components
  • Composite: Component tree from root to leaf — uniform interface for all components
  • Proxy: Spring AOP wraps beans — @Transactional, @Cacheable work via transparent proxying
  • Template Method: Spring's JdbcTemplate, RestTemplate — fixed skeleton with customisable steps

6.2 Patterns in System Design Interviews

When to Name Patterns in Interviews
Interview ScenarioExpected PatternsWhat to Say
"Design a notification system"Observer + Chain of Responsibility"Observer so subscribers register for events. Chain for notification rules (email → SMS fallback)."
"Design undo/redo"Command + Memento"Commands encapsulate each action with undo(). Memento stores snapshots for complex rollbacks."
"Design a payment gateway"Strategy + Adapter + Facade"Each gateway (Stripe, PayPal) is an Adapter. Strategy selects which to use. Facade for clients."
"Design a logging system"Singleton + Chain + Decorator"Logger is Singleton. Log levels form a Chain. Formatters are Decorators on the pipeline."
Red Flag: Applying a pattern where it is not needed. "I'd use Abstract Factory for this two-class system" signals over-engineering — interviewers penalise this.

6.3 SOLID Principles & Their Relationship to Patterns

Click any SOLID principle to see which patterns enforce it.

S
Single Responsibility
A class should have only one reason to change.

Patterns: Facade (separates subsystem from client), Decorator (separates concerns from core class), Command (separates invocation from execution), Iterator (separates traversal from collection).

O
Open/Closed
Open for extension, closed for modification.

Patterns: Strategy (add strategies without changing context), Observer (add observers without modifying subject), Decorator (add wrappers without changing core), Factory Method (add creators without changing base), Visitor (add operations without modifying elements).

L
Liskov Substitution
Subclasses must be substitutable for their base classes.

Patterns: Factory Method (client uses Creator interface — any subclass is substitutable), Composite (leaf and composite share an interface), Template Method (subclasses must honour the algorithm contract).

I
Interface Segregation
Don't force clients to depend on interfaces they don't use.

Patterns: Facade (provides a narrow interface tailored to client needs), Adapter (exposes only the interface the client needs), Abstract Factory (each factory method is a narrow, specific creation interface).

D
Dependency Inversion
Depend on abstractions, not on concretions.

Patterns: Factory Method (client depends on Creator interface, not concrete classes), Abstract Factory (client never references concrete factory), Bridge (abstraction depends on implementation interface), Strategy (context depends on strategy interface).

6.4 From Patterns to Architecture

The GoF-to-Architecture Evolution

GoF patterns operate at class level. Architectural patterns operate at system level — but they are built from GoF patterns:

CQRS

Separates read (Query) and write (Command) models. Directly applies the Command pattern at architectural level — every write is an explicit Command through a pipeline of handlers.

Event Sourcing

Store the full history of Commands (events) instead of current state. Current state computed by replaying events — Memento at architectural scale. The event log is an immutable Command history.

Saga Pattern

Manages distributed transactions. Each step is a Command; on failure, compensating Commands execute in reverse — Chain of Responsibility meets Command meets Memento at microservices scale.

MVC → MVP → MVVM

MVC evolved because Observer coupling between Model and View was too tight for testing. MVP introduced a Presenter (Mediator). MVVM added data binding (Observer at property level) for declarative UI.

Interactive Tool

Master Quiz

15 randomised questions across all patterns. Get a knowledge-level badge at the end.

Question 1 of 15Score: 0
Credits

Acknowledgements

Where this guide's content, patterns, and tooling come from.

Course & Content

Built by Harsh Sharma (Sys Titan, Live LLD Weekday 2.0 Batch — LinkedIn →) and Pradeep Kumar (Senior Backend Engineer, ex-NetApp & Qualcomm — full bio →) for the Live LLD & HLD Cohorts.

Thanks a lot to Harsh Sharma for your contribution in getting this page ready and helping learners quickly revise and understand the design patterns.

Original Patterns

The 23 patterns covered here were first catalogued by the "Gang of Four" — Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides — in Design Patterns: Elements of Reusable Object-Oriented Software (1994). This guide re-explains their work with modern, real-world examples.

Tools & Technology

Typography — Syne, DM Sans & IBM Plex Mono (Google Fonts). 3D background — Three.js.

🎓Live Cohort →