Filter concepts by levelShowing all levels.

Python · Design patterns

Patterns

Factory, Abstract Factory, Strategy, Adapter, Repository, Service Layer, Dependency Injection, Observer, Command, Builder, and Decorator — worked, executed Python for each

  • CreationalFactory, Abstract Factory, Builder — control how objects get built
  • StructuralAdapter, Decorator — reshape or extend an interface without editing it
  • BehavioralStrategy, Observer, Command — swap or react to behavior at runtime
  • ArchitecturalRepository, Service Layer, Dependency Injection — the seams between layers

1.Factory and Abstract Factory

A Factory is a function that picks which class to instantiate based on input, so callers never write ClassName(...) directly. An Abstract Factory is a family of factories that each produce a matching set of related objects.

Key features

  • Factory: one function or method that returns a concrete instance based on input, hiding the class name from the caller
  • Abstract Factory: an interface with one creation method per product; each concrete factory implementation produces a matching family
  • Both patterns exist so calling code depends on an interface (Notifier, Button) rather than a concrete class name
  • Factory picks ONE product from a type; Abstract Factory picks a whole SET of related products that must stay consistent with each other
  • Adding a new variant means adding one new factory/class, not editing every place the old ones were instantiated

Factory vs. Abstract Factory

Factory vs. Abstract Factory
PatternProducesAdding a new variant
FactoryOne object of a chosen typeAdd one branch or one new factory function
Abstract FactoryA matching family of related objectsAdd one new concrete factory implementing every creation method
A factory hides the concrete class behind one call

notifier_factory("email")

caller states intent, not a class name

picks EmailNotifier

the factory owns the if/elif branching

notifier.send(msg)

caller only ever sees the shared interface

  1. notifier_factory("email") — caller states intent, not a class name
  2. picks EmailNotifier — the factory owns the if/elif branching
  3. notifier.send(msg) — caller only ever sees the shared interface

Command

python
def notifier_factory(channel: str):
    if channel == "email":
        return EmailNotifier()
    if channel == "sms":
        return SmsNotifier()
    raise ValueError(f"unknown channel: {channel}")

notifier = notifier_factory("email")
notifier.send("your order shipped")

Factory centralizes "which class do I instantiate?" behind one function; Abstract Factory centralizes it for a whole family of related objects that must stay consistent with each other.

2.Strategy

Strategy pulls an algorithm out into its own object and hands it to the class that needs it. Cart takes a shipping_strategy object instead of an if/elif on a shipping-method string.

Key features

  • Every strategy implements the same interface (here, one calculate method), so the context class can call any of them identically
  • The context (Cart) holds a strategy object and delegates to it — it never branches on a type flag itself
  • Swapping cart.shipping_strategy = ExpressShipping() changes behavior at runtime with zero changes to Cart
  • Adding a new algorithm means adding one new class, not editing an existing if/elif chain
  • A plain function passed as a callable can substitute for a one-method Strategy class in Python — the pattern does not require a class at all

Strategy vs. branching inline

Strategy vs. branching inline
ApproachAdding a new algorithmTesting one algorithm alone
if/elif inside the classEdit the class, risk breaking existing branchesMust exercise the whole class to reach it
Strategy objectAdd one new class implementing the interfaceInstantiate and test the strategy directly, in isolation
Cart delegates to whichever strategy it holds

Cart(strategy)

holds one ShippingStrategy, does not branch itself

strategy.calculate(weight)

delegates — same call for any strategy

swap at runtime

cart.shipping_strategy = ExpressShipping()

  1. Cart(strategy) — holds one ShippingStrategy, does not branch itself
  2. strategy.calculate(weight) — delegates — same call for any strategy
  3. swap at runtime — cart.shipping_strategy = ExpressShipping()

Command

python
class ShippingStrategy(ABC):
    @abstractmethod
    def calculate(self, weight_kg): ...

class Cart:
    def __init__(self, shipping_strategy: ShippingStrategy):
        self.shipping_strategy = shipping_strategy

    def shipping_cost(self, weight_kg):
        return self.shipping_strategy.calculate(weight_kg)

Strategy moves an algorithm into its own object with a shared interface, so the class using it delegates instead of branching — swap behavior by swapping the object, not by editing an if/elif.

3.Adapter

An Adapter wraps an object whose interface does not match what your code expects, translating calls between the two. LegacyXmlAdapter makes an old XML-returning client usable anywhere a get_items() list is expected.

Key features

  • Adapter wraps an existing object and exposes the interface callers expect, translating each call internally
  • Used when the incompatible class is third-party or legacy — code you cannot or should not modify directly
  • The adapter implements the TARGET interface (what callers want) and holds a reference to the ADAPTEE (what actually does the work)
  • Callers depend only on the target interface — swapping the legacy client for a modern one later means writing a new adapter, not touching every caller
  • Different from Decorator: Adapter changes an interface to match what is expected; Decorator keeps the same interface and adds behavior

Adapter's three parts

Adapter's three parts
RoleIn the exampleResponsibility
Target interfaceJsonDataSource.get_items()What calling code actually expects to call
AdapteeLegacyXmlParser.fetch_data()The existing, incompatible interface being wrapped
AdapterLegacyXmlAdapterImplements the target, translates to/from the adaptee internally
Adapter translates between two incompatible interfaces

caller wants get_items()

the interface the rest of the code expects

LegacyXmlAdapter

implements get_items(), calls fetch_data() internally

LegacyXmlParser

unchanged third-party class, returns XML

  1. caller wants get_items() — the interface the rest of the code expects
  2. LegacyXmlAdapter — implements get_items(), calls fetch_data() internally
  3. LegacyXmlParser — unchanged third-party class, returns XML

Command

python
class JsonDataSource(ABC):
    @abstractmethod
    def get_items(self): ...

class LegacyXmlAdapter(JsonDataSource):
    def __init__(self, legacy_parser):
        self.legacy_parser = legacy_parser

    def get_items(self):
        raw = self.legacy_parser.fetch_data()
        return [parse(raw)]

Adapter wraps an incompatible interface behind the one your code expects, translating each call internally — the wrapped class never changes, and callers never know it is there.

4.Repository

A Repository hides how objects are stored behind methods that look like a collection — get_by_id, add — so calling code never writes a query directly. Swapping SQLite for Postgres means changing the repository, not every caller.

Key features

  • A repository exposes collection-like methods — get_by_id, add, list, remove — never raw queries
  • Business logic depends on the repository interface, never on a specific database driver or ORM query
  • An in-memory repository implementing the same interface makes unit tests run with no real database
  • A repository usually manages one aggregate/entity type — UserRepository, OrderRepository — not the whole schema
  • Different from a generic Adapter: a repository's shape is always collection-like (get/add/remove), an Adapter's shape can be anything the target interface requires

Repository interface vs. two implementations

Repository interface vs. two implementations
MethodInMemoryUserRepositoryA real SqlUserRepository (not shown)
get_by_id(id)self._users.get(id)SELECT * FROM users WHERE id = %s
add(user)self._users[user["id"]] = userINSERT INTO users (...) VALUES (...)
Business logic depends on the repository interface, not on SQL

OrderService

calls repository.get(order_id) — never SQL directly

UserRepository

the interface: get_by_id, add

storage

SQL table, dict, or API — swappable behind the interface

  1. OrderService — calls repository.get(order_id) — never SQL directly
  2. UserRepository — the interface: get_by_id, add
  3. storage — SQL table, dict, or API — swappable behind the interface

Command

python
class UserRepository(ABC):
    @abstractmethod
    def get_by_id(self, user_id): ...
    @abstractmethod
    def add(self, user): ...

class InMemoryUserRepository(UserRepository):
    def __init__(self):
        self._users = {}

    def get_by_id(self, user_id):
        return self._users.get(user_id)

    def add(self, user):
        self._users[user["id"]] = user

A repository exposes collection-like methods (get_by_id, add) instead of queries — business logic depends on that interface, so swapping the storage backend, or testing with an in-memory one, never touches the caller.

5.Service Layer

A service layer is one class per use case — OrderPlacementService.place_order() — that coordinates repositories and domain rules for that one operation. Views and CLI commands call the service; the service never talks to a database driver directly.

Key features

  • A service method is named after a use case — place_order, cancel_subscription — not a CRUD verb like update
  • The service coordinates repositories and domain objects; it does not contain SQL, and does not duplicate rules the domain object already enforces
  • Multiple entry points (a web view, a CLI command, a background job) call the same service method instead of re-implementing the use case
  • A service layer sits between the presentation layer and the domain/repository layer in a layered architecture
  • Not every CRUD operation needs a service — a thin layer that adds nothing beyond repository.add() is unnecessary indirection

Command

python
class OrderPlacementService:
    def __init__(self, product_repository):
        self.product_repository = product_repository

    def place_order(self, sku, quantity):
        available = self.product_repository.get_stock(sku)
        if quantity > available:
            raise InsufficientStockError(f"only {available} left of {sku}")
        self.product_repository.decrement_stock(sku, quantity)
        return {"sku": sku, "quantity": quantity, "status": "placed"}

A service layer is one class per use case, coordinating repositories and domain objects — every entry point (web, CLI, background job) calls the same service method instead of re-implementing the use case.

6.Dependency Injection (pattern catalog entry)

Dependency Injection means a class receives the objects it depends on from outside (usually through its constructor) instead of creating them itself. ReportGenerator(logger) takes a logger; it never instantiates one internally.

Key features

  • The receiving class depends on an interface (anything with a .log() method), not a concrete class it constructs itself
  • Most common form in Python: constructor injection — pass the dependency as an __init__ argument
  • Makes unit testing straightforward — pass a fake/mock implementation instead of the real dependency
  • This entry is deliberately brief — the full treatment (dependency inversion, constructor vs. function injection, containers, FastAPI's Depends(), testing with injected dependencies) lives in the roadmap's own dedicated Dependency Injection section

Command

python
class ReportGenerator:
    def __init__(self, logger):   # injected, not created internally
        self.logger = logger

    def generate(self):
        self.logger.log("report generated")

ReportGenerator(ConsoleLogger()).generate()

Dependency Injection passes an object's dependencies in from outside (typically via the constructor) instead of letting it construct them itself — full treatment is the roadmap's dedicated Dependency Injection section, not here.

7.Observer

Observer lets any number of objects register interest in a subject and get notified when it changes. StockTicker.notify() calls update() on every attached observer, without knowing what each one does.

Key features

  • A subject keeps a list of observers and calls a shared method (here, update) on each one when state changes
  • attach() registers an observer; notify() (usually called from inside the subject after a state change) loops over all attached observers
  • The subject depends only on the observer interface (update), never on which concrete observer classes are attached
  • Adding a new reaction to an event means adding a new observer class and attaching it — the subject never changes
  • Python's standard library uses the same idea under different names — logging handlers, and any callback list, are Observer variants

Observer's two roles

Observer's two roles
RoleIn the exampleResponsibility
SubjectStockTickerOwns the state, keeps the observer list, calls notify() on change
ObserverPriceLogger, PriceAlertImplements update(event); reacts however it wants
One subject, any number of independent observers

StockTicker.set_price()

state changes, calls notify()

notify(event)

loops over every attached observer

PriceLogger, PriceAlert

each reacts independently to update()

  1. StockTicker.set_price() — state changes, calls notify()
  2. notify(event) — loops over every attached observer
  3. PriceLogger, PriceAlert — each reacts independently to update()

Command

python
class Subject:
    def __init__(self):
        self._observers = []

    def attach(self, observer):
        self._observers.append(observer)

    def notify(self, event):
        for observer in self._observers:
            observer.update(event)

A subject keeps a list of observers and calls a shared method on each when its state changes — attaching a new observer never requires changing the subject.

8.Command

Command wraps a request as an object with an execute() method (and usually undo()) instead of calling the action directly. RemoteControl.submit(TurnOnCommand(light)) stores the command, so it can be undone later.

Key features

  • A command object bundles a request into execute() (and often undo()), instead of the caller invoking the action directly
  • The invoker (RemoteControl) stores and runs commands without knowing what each one actually does
  • Storing executed commands in a list is what makes undo, redo, and replay possible — the action itself becomes data
  • Different from a plain function callback: a command instance can carry both the forward action AND the reverse action together
  • Common uses: undo/redo stacks, task queues, transactional operations, GUI button/menu actions

Command's three parts

Command's three parts
RoleIn the exampleResponsibility
ReceiverLightThe object that actually performs the work
CommandTurnOnCommandWraps one request against the receiver; implements execute()/undo()
InvokerRemoteControlStores and runs commands, without knowing what they do
The invoker runs commands without knowing what they do

RemoteControl.submit()

stores and calls command.execute()

TurnOnCommand

wraps the request against Light

Light.turn_on()

the receiver, doing the real work

  1. RemoteControl.submit() — stores and calls command.execute()
  2. TurnOnCommand — wraps the request against Light
  3. Light.turn_on() — the receiver, doing the real work

Command

python
class Command(ABC):
    @abstractmethod
    def execute(self): ...
    @abstractmethod
    def undo(self): ...

class TurnOnCommand(Command):
    def __init__(self, light):
        self.light = light

    def execute(self):
        return self.light.turn_on()

    def undo(self):
        return self.light.turn_off()

Command wraps a request as an object with execute() (and often undo()) — the invoker stores and runs commands without knowing what any of them actually do.

9.Builder

Builder constructs a complex object through a chain of method calls that each return self, ending in build(). HttpRequestBuilder().method("POST").url("/orders").build() reads as a sequence of decisions instead of one huge constructor call.

Key features

  • Each builder method sets one piece of configuration and returns self, which is what makes the calls chainable
  • Nothing is constructed until .build() is called — that is the one place required fields get validated
  • Builder replaces a constructor with many optional parameters, where remembering positional order becomes error-prone
  • The builder and the object it builds are usually two separate classes — HttpRequestBuilder builds an HttpRequest, it isn't one
  • Different from Factory: Factory picks WHICH class to instantiate; Builder assembles a complex instance of one class piece by piece

Builder vs. a many-argument constructor

Builder vs. a many-argument constructor
ApproachAdding one more optional fieldReadability at the call site
Constructor with many kwargsEvery caller's call site risks silently keeping old defaultsHttpRequest("POST", "/orders", {"Content-Type": "json"}, None, ...)
BuilderAdd one chainable method; existing calls are unaffected.method("POST").url("/orders").header(...).build()
Each chained call configures one piece, build() assembles the result

.method("POST")

returns self — chainable

.url(...).header(...)

each call sets one field, returns self

.build()

validates required fields, returns the finished object

  1. .method("POST") — returns self — chainable
  2. .url(...).header(...) — each call sets one field, returns self
  3. .build() — validates required fields, returns the finished object

Command

python
class HttpRequestBuilder:
    def __init__(self):
        self._request = HttpRequest()

    def method(self, method):
        self._request.method = method
        return self             # enables chaining

    def build(self):
        if self._request.url is None:
            raise ValueError("url is required")
        return self._request

Builder assembles a complex object through chained, self-returning method calls, validating required fields only at .build() — it replaces a constructor with many optional, order-dependent arguments.

10.Decorator (structural pattern)

The Decorator pattern wraps an object in another object implementing the same interface, adding behavior on top. WithMilk(Espresso()) still exposes cost() and description(), just with milk's price and text added in.

Key features

  • A decorator implements the SAME interface as the object it wraps, so wrapped and unwrapped objects are used identically by callers
  • Each decorator holds a reference to the object it wraps and delegates to it, adding its own behavior before or after
  • Decorators can be stacked — WithCaramel(WithMilk(Espresso())) — without a combinatorial explosion of classes for every combination
  • Different from Python's @decorator syntax: this pattern wraps an OBJECT at runtime; @decorator wraps a FUNCTION at definition time — related idea, different mechanism
  • Different from Adapter: Decorator keeps the interface the SAME and adds behavior; Adapter CHANGES the interface to match what callers expect

Decorator pattern vs. Python @decorator syntax

Decorator pattern vs. Python @decorator syntax
AspectDecorator pattern (this concept)Python @decorator syntax
WrapsAn object, at runtimeA function/method, at definition time
MechanismA class implementing the same interface, holding a reference@wraps applies a higher-order function to a def
StackingWithCaramel(WithMilk(Espresso()))@retry\n@cache\ndef fetch(): ...
Each layer wraps the one inside it, same interface throughout

Espresso

the base component — cost() = 2.50

WithMilk(Espresso())

wraps it, adds 0.50

WithCaramel(...)

wraps that, adds 0.75 — still just a Coffee

  1. Espresso — the base component — cost() = 2.50
  2. WithMilk(Espresso()) — wraps it, adds 0.50
  3. WithCaramel(...) — wraps that, adds 0.75 — still just a Coffee

Command

python
class CoffeeAddOn(Coffee):
    def __init__(self, coffee: Coffee):
        self._coffee = coffee

class WithMilk(CoffeeAddOn):
    def cost(self):
        return self._coffee.cost() + 0.50

    def description(self):
        return self._coffee.description() + " + milk"

Decorator wraps an object in another object implementing the same interface, adding behavior by delegating to the wrapped object — stack decorators to combine add-ons without a class per combination.

Remember

  • Factory centralizes "which class?"; Strategy centralizes "which algorithm?"; both replace an if/elif with an object.
  • Adapter changes an interface to match what callers expect; Decorator keeps the interface the same and adds behavior.
  • A repository exposes get/add, never a query — swap storage, or test in-memory, with zero change to callers.
  • Observer and Command both decouple "what happens" from "who triggered it" — one notifies a list, the other stores the request itself.

Pro tip

  • Every pattern here has a "know when not to use it" mistake in its own concept — a pattern applied where a plain function would do is a cargo cult, not craftsmanship.
  • Dependency Injection is covered in full depth in its own roadmap section — this entry is a brief pointer, not the deep treatment.

Next up

  • Dependency Injection (full section)

Sources

  • Refactoring.Guru — refactoring.guru/design-patterns
  • Martin Fowler (Repository, Service Layer) — martinfowler.com/eaaCatalog
  • Every pattern snippet run for real on Python 3.14.3 · checked 2026-08-22

Spotted a mistake on this page? Tell us.