Skip to main content

Design patterns

A design pattern is a named solution to a problem that recurs. Name the problem first, then pick the pattern. If you cannot state the problem in one sentence, you do not need the pattern yet (KISS and YAGNI).

Patterns are most useful as shared vocabulary: "this is an adapter" tells a reviewer the intent faster than a paragraph. They are not templates to copy, and a language feature often replaces them. Python modules are already singletons, and first-class functions often replace a Strategy class.

Before using a pattern, pass the abstraction decision test. Patterns specific to this architecture (repository, use case, DTO and mapper, composition root) are covered in Clean Architecture, not here.

Creational​

These patterns control how objects are created.

PatternIntentUse whenAvoid when
Factory MethodOne function or method decides which concrete type to createThe caller should depend on a port, not on the implementationOnly one implementation exists; call the constructor
Abstract FactoryCreate a family of related objects that must be used togetherSeveral products must stay consistent, such as one provider's client, signer, and parserThere is one product, or the products are independent
BuilderBuild a complex object step by stepA constructor has many optional parts, or construction needs validation between stepsA dataclass or keyword arguments already read clearly
SingletonGuarantee a single shared instanceRarely. See belowAlmost always

Singleton​

Treat Singleton as a last resort. A global instance hides a dependency, makes tests share state, and makes order of initialization matter. Create the instance once at the composition root and pass it in. If a project already uses a module-level settings object, keep it read-only and set once at startup.

Factory function returning a port​

The handbook's factories return the port type, never the implementation (FastAPI structure). Callers stay unaware of which adapter is in use.

def build_file_storage(settings: Settings) -> FileStorage:
if settings.storage_backend == "s3":
return S3FileStorage(settings.storage_bucket)
return LocalFileStorage(settings.storage_path)

Structural​

These patterns control how objects and modules are composed.

PatternIntentUse whenAvoid when
AdapterConvert one interface into the one the caller expectsWrapping a database, SDK, or third-party API behind a portThe two interfaces already match
DecoratorAdd behavior around an object without changing itCross-cutting concerns such as logging, caching, retry, or authorizationThe extra behavior belongs to the core logic
FacadeOffer one simple entry point over several collaboratorsA caller would otherwise coordinate many objectsIt only forwards calls one to one; that is a pass-through wrapper
ProxyStand in for an object to control access to itLazy loading, access checks, or remote calls behind the same interfaceA plain function call is enough

Behavioral​

These patterns control how objects communicate and share responsibility.

PatternIntentUse whenAvoid when
StrategySwap interchangeable algorithms behind one interfaceTwo or more real variants exist and are chosen at runtimeOne variant exists; pass a function instead of a class
ObserverNotify subscribers when something happensProducers must not know their consumers, such as domain eventsA direct call is clearer and there is one consumer
CommandRepresent a request as an objectQueuing, retrying, undoing, or auditing operationsA function call is enough
Template MethodFix the steps of an algorithm and let subclasses fill in someSeveral variants share a stable sequenceComposition (pass in the varying step) would do

Strategy replacing a branch​

Introduce a strategy when real variants exist and keep growing. Two cases that rarely change do not justify it.

class ShippingCalculator(Protocol):
def cost(self, order: Order) -> Decimal: ...


class FlatRateShipping:
def cost(self, order: Order) -> Decimal:
return Decimal("5.00")


class WeightBasedShipping:
def cost(self, order: Order) -> Decimal:
return order.total_weight * Decimal("0.80")

If only the flat rate exists today, keep a function and add the second variant when it is required.

Patterns already in this architecture​

Two patterns are already part of the layered structure. Recognizing them avoids adding a second one for the same job.

PatternWhere it appears
AdapterInfrastructure repositories and HTTP clients implementing ports
FactoryFactories in the composition root that return port types, not implementations

Warning signs​

  • A pattern name appears in a class name (UserRepositoryFactoryManager) but not in the problem it solves.
  • A factory, builder, or strategy exists with a single implementation.
  • A Singleton holds mutable state shared across requests.
  • A Decorator or Proxy changes the behavior callers rely on instead of adding to it, which breaks substitution.

More signs and fixes: Design principles.