bigonotes
CS ConceptsDesign Patterns

Pattern Reference Cards

One-page GoF-style cards for seven core patterns: intent, problem, structure, when to use, trade-offs and known uses.

8 min readLast updated Design Patterns

These cards follow the template from How to Read a Design Pattern: intent, motivation, structure, applicability, consequences, known uses and related patterns. Use them for quick revision before an interview; each card links to the full lesson with complete code.

Singleton#

Classification: Creational (GoF)

Intent: Ensure a class has only one instance and provide a global point of access to it.

Problem: Some things should exist once per application: a connection pool, configuration, a print spooler. A global variable gives access but doesn't stop Database() from being called twice, which wastes resources and splits state.

Structure: a class attribute holding the instance, and a __new__ that creates it once and returns it on every later call.

import threading


class Singleton:
    _instance: "Singleton | None" = None
    _lock = threading.Lock()

    def __new__(cls) -> "Singleton":
        if cls._instance is None:  # fast path, no lock
            with cls._lock:
                if cls._instance is None:  # re-check under the lock
                    cls._instance = super().__new__(cls)
        return cls._instance


assert Singleton() is Singleton()

Use when: exactly one instance must exist and be reachable from a well-known access point.

Pros Cons
Controlled access to the sole instance Hard to unit test; hidden dependencies
No polluting global variables Can be bypassed (e.g. object.__new__(cls)) unless defended
Can later be changed to allow a fixed number of instances Mixes lifecycle management with business logic
Needs care to be thread-safe

Known uses: None, True and False are singletons; logging.getLogger(name) returns one logger per name; imported modules are cached once in sys.modules.

Related: Abstract Factory and Builder instances are often singletons. Prototype is its opposite (many copies).

→ Singleton Pattern

Simple Factory#

Classification: Creational (an idiom, not a GoF pattern). Also called a static factory method.

Intent: Put object-creation logic in one place, hiding concrete classes from clients.

Problem: if kind == "pizza": return Pizza() followed by elif ... scattered through the codebase couples clients to every concrete class and duplicates creation logic.

Structure: one class with a (usually static) create(kind) method that switches on the type and returns the product interface.

class Product: ...
class ProductA(Product): ...
class ProductB(Product): ...


class SimpleFactory:
    @staticmethod
    def create(kind: str) -> Product:
        match kind:
            case "A":
                return ProductA()
            case "B":
                return ProductB()
            case _:
                raise ValueError(f"Unknown type: {kind}")

Use when: there are a few stable types and you want creation centralized.

Pros Cons
Centralized creation; clients see only the interface Adding a type means editing the factory (violates Open/Closed)
Very easy to understand The switch grows with every type

Known uses: open() returns a text or binary file object depending on the mode; pathlib.Path() returns a PosixPath or WindowsPath.

Related: Factory Method (more flexible, uses inheritance), Abstract Factory (families).

→ Factory Method Pattern

Factory Method#

Classification: Creational (GoF). Also known as virtual constructor.

Intent: Define an interface for creating an object, but let subclasses decide which class to instantiate.

Problem: A framework class needs to create objects but can't know their exact type in advance. Hard-coding ConcreteProduct() in the creator couples it to one product and leaves subclasses no way to customize.

Structure: an abstract creator whose business_logic() calls its own abstract create_product(); each concrete creator overrides that one method.

Factory Method structure: CreatorA and CreatorB extend an abstract Creator and override create_product to return ProductA or ProductB, which implement the Product interface

The client picks a creator and calls the shared logic: CreatorA().business_logic().

Relationships: concrete creators extend the creator, concrete products implement the product interface, each concrete creator creates its product, and business_logic() delegates creation to the subclass.

Use when: a class can't anticipate the class of objects it must create, or wants subclasses to specify them (logistics by road/sea/air, documents in different formats, platform-specific UI).

Pros Cons
Removes binding to concrete classes Can require many subclasses
Follows Open/Closed and Single Responsibility More complex than a simple factory
Parallel creator and product hierarchies

Known uses: __iter__(): each collection class returns its own iterator type.

Related: Abstract Factory is often built from factory methods; Template Method often calls factory methods; Prototype is an alternative.

→ Factory Method Pattern

Builder#

Classification: Creational (GoF)

Intent: Separate the construction of a complex object from its representation, so the same construction process can create different representations.

Problem: House(foundation, walls, roof, windows, doors, garage, pool, garden): huge constructors, many optional parameters, unreadable True, False, True arguments, and no easy way to build immutable objects.

Structure: a director (optional) drives a builder interface (build_part_a(), build_part_b(), get_result()); concrete builders assemble the product. The common modern form is a fluent builder whose setters return self:

from dataclasses import dataclass


@dataclass(frozen=True)  # the finished product is immutable
class Product:
    part_a: str
    part_b: str


class ProductBuilder:
    def __init__(self) -> None:
        self._part_a = ""
        self._part_b = ""

    def part_a(self, v: str) -> "ProductBuilder":
        self._part_a = v
        return self

    def part_b(self, v: str) -> "ProductBuilder":
        self._part_b = v
        return self

    def build(self) -> Product:
        return Product(self._part_a, self._part_b)


product = ProductBuilder().part_a("x").part_b("y").build()

Use when: an object has many optional parameters, you want immutable results, or the same process should produce different representations.

Pros Cons
Readable construction; no telescoping constructors More code: a whole builder class
Validation before the object exists Builder duplicates the product's fields
Immutable products Overkill for simple objects

Known uses: SQLAlchemy's select(...).where(...), Protocol Buffers builders, SQL query builders.

Related: Abstract Factory (also builds complex objects, but in one call); Composite (what builders often build).

→ Builder Pattern

Prototype#

Classification: Creational (GoF). Also known as clone.

Intent: Specify the kinds of objects to create using a prototypical instance, and create new objects by copying this prototype.

Problem: Creating an object is expensive (loading textures and animations for every enemy), or you need many near-identical objects. Repeating the setup each time wastes time and duplicates initialization code.

Structure: a Prototype interface with clone(); concrete prototypes implement it; the client asks a prototype to clone itself instead of calling the constructor.

import copy


class Enemy:
    def __init__(self) -> None:
        self.abilities: list[str] = []

    def clone(self) -> "Enemy":
        new = copy.copy(self)  # shallow copy
        # Deep-copy mutable fields
        new.abilities = list(self.abilities)
        return new

Use when: creation is expensive, you need many similar objects with small variations, or the classes to create are chosen at runtime.

Pros Cons
Avoids repeated expensive setup Deep copying is easy to get wrong
Hides concrete classes; prototypes can be registered at runtime Circular references complicate cloning
Less subclassing Every class needs a correct clone()

Known uses: Python's copy.copy / copy.deepcopy (customizable with __copy__ / __deepcopy__), JavaScript's prototype-based objects.

Related: Factory Method (create from scratch instead); Abstract Factory can store prototypes; Composite trees are often cloned.

→ Prototype Pattern

Strategy#

Classification: Behavioral (GoF). Also known as policy.

Intent: Define a family of algorithms, encapsulate each one, and make them interchangeable.

Problem: if kind == "A": ... / elif kind == "B": ... for sorting, payments or routing. Big conditionals, hard to add algorithms, violates Open/Closed.

Structure: the context holds a reference to the strategy interface and delegates to it; concrete strategies implement that interface.

Strategy structure: Context holds a reference to the Strategy interface, which StrategyA and StrategyB implement
from abc import ABC, abstractmethod


class Strategy(ABC):
    @abstractmethod
    def execute(self) -> None: ...


class Context:
    def __init__(self, strategy: Strategy) -> None:
        self._strategy = strategy

    def set_strategy(self, s: Strategy) -> None:
        self._strategy = s

    def execute_strategy(self) -> None:
        self._strategy.execute()

Use when: related classes differ only in behavior, you need algorithm variants, or a class is full of conditionals selecting behavior (payments, routes, compression, sorting).

Pros Cons
Interchangeable algorithm families Clients must know the strategies to choose one
Eliminates conditionals; Open/Closed Context and strategy must agree on the data passed
More objects

Known uses: the key= function passed to sorted() and list.sort(), the default= hook of json.dumps().

Related: State (same structure, different intent); Template Method (varies steps via inheritance instead); stateless strategies can be shared as flyweights.

→ Strategy Pattern

Observer#

Classification: Behavioral (GoF). Also known as publish-subscribe, dependents.

Intent: Define a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically.

Problem: A weather station calls updateCurrentDisplay(), updateStatsDisplay(), updateForecast() directly. It knows every display, adding one means editing the station, and displays can't come and go at runtime.

Structure: the subject keeps a list of observers with attach() and detach() and notifies them on every change; observers implement update().

from typing import Protocol


class Observer(Protocol):
    def update(self, temp: float, humidity: float) -> None: ...


class WeatherStation:
    def __init__(self) -> None:
        self._observers: list[Observer] = []
        self.temperature = 0.0
        self.humidity = 0.0

    def attach(self, o: Observer) -> None:
        self._observers.append(o)

    def detach(self, o: Observer) -> None:
        self._observers.remove(o)

    def set_measurements(self, t: float, h: float) -> None:
        self.temperature, self.humidity = t, h
        for o in self._observers:
            o.update(self.temperature, self.humidity)

Use when: a change to one object requires changing others, you don't know how many, and the subject shouldn't know who they are (newsletters, stock alerts, GUI events).

Pros Cons
Loose coupling between subject and observers Notification order isn't guaranteed
Observers added and removed at runtime Forgotten observers leak memory
Broadcast communication Update cascades are hard to debug; many observers slow notification

Known uses: Tkinter event callbacks, logging handlers, JavaScript EventEmitter and DOM events, .NET events, model-to-view updates in MVC.

Related: Mediator (centralizes communication); an event bus is a centralized variant.

→ Observer Pattern

Key takeaways#

  • Every card answers the same questions: intent, problem, structure, when to use, trade-offs.
  • Read the problem line first; it tells you when the pattern applies.
  • Check the cons before choosing a pattern; each one has real costs.
  • Use the links for full, runnable examples.

Found a mistake? Tell me on LinkedIn ↗

Further reading#