Guide

Software design patterns explained

Every mid-size codebase eventually grows the same awkward shape: payment logic tangled in HTTP handlers, three slightly different ways to send email, and a switch statement that nobody wants to touch. Design patterns are named, reusable solutions to those recurring problems — catalogued most famously in the 1994 Gang of Four book and still referenced daily in code review. They are not a style guide and not an excuse for abstraction layers nobody asked for. This guide explains the patterns that still earn their keep in modern web and backend development, how they relate to service architecture and API design, and when the best pattern is no pattern at all.

What a design pattern is (and is not)

A design pattern describes a structure of objects and responsibilities that solves a problem in a particular context. It is language-agnostic: the Strategy pattern works in TypeScript, Java, Python, and Rust. It is not a library — you implement it with your own types. It is also not a mandate. Patterns trade complexity for flexibility; applying them before you feel the pain produces "pattern theater" — interfaces with one implementation and factories that always return the same class.

The classic taxonomy groups patterns into three families:

  • Creational — how objects are instantiated (Factory, Builder, Singleton).
  • Structural — how objects are composed (Adapter, Facade, Decorator).
  • Behavioral — how objects communicate and divide work (Strategy, Observer, Command).

Modern teams also talk about architectural patterns — MVC, repository, event sourcing, CQRS — that span modules or services. Those sit above the Gang of Four catalog but use the same idea: a proven shape that experienced developers recognize instantly.

Creational patterns: controlling construction

Factory Method and Abstract Factory

A factory centralizes object creation behind an interface so calling code depends on PaymentProcessor, not StripePaymentProcessor. Use a simple factory function when you have two or three implementations and one decision point ("if region is EU, use Adyen; else Stripe"). Use an abstract factory when you need families of related objects — a UI toolkit that must ship consistent button, dialog, and theme classes per platform.

Builder

The builder pattern constructs complex objects step by step — SQL query builders, HTTP client configs with dozens of optional fields, test fixture setup. It shines when constructors would need twelve parameters or when construction order matters. In many languages, fluent builders or named-argument structs replace hand-rolled Builder classes; the pattern's idea (separate construction from representation) remains valuable even when you skip the UML ceremony.

Singleton — usually an anti-pattern

The singleton guarantees one instance globally. Database connection pools and logger registries sometimes need that — but global mutable state makes unit tests painful and hides dependencies. Prefer dependency injection: construct one instance at the application root and pass it explicitly. If you reach for a singleton, ask whether you actually need a scoped service in your framework's container instead.

Structural patterns: composing systems

Adapter

An adapter wraps a third-party API or legacy module so it matches the interface your application expects. You integrate a vendor's webhook payload into your internal OrderEvent type without polluting every caller with vendor-specific field names. Adapters are the glue in microservice boundaries and anti-corruption layers.

Facade

A facade exposes a simple API over a messy subsystem. Your NotificationService.send(userId, message) might orchestrate email, push, SMS, preference lookups, and retry logic — but the checkout flow only calls one method. Facades reduce cognitive load; they do not remove the subsystem, they hide it behind a stable entry point.

Decorator

A decorator wraps an object to add behavior without subclassing. Middleware stacks are decorators: logging wraps caching wraps authentication wraps the handler. In TypeScript, higher-order functions often replace explicit Decorator classes. Use decorators when behavior combinations explode if you subclass for every permutation (LoggedCachedAuthenticatedService vs CachedLoggedService…).

Composite

The composite pattern treats individual objects and groups uniformly — a folder and a file both implement getSize(); a UI component tree where containers and leaves share a render() interface. It simplifies tree traversal and recursive operations at the cost of type precision (not every node supports every operation).

Behavioral patterns: algorithms and collaboration

Strategy

Strategy swaps interchangeable algorithms behind a common interface. Tax calculation by country, compression codec selection, pricing rules for subscription tiers — each strategy implements the same method; context picks one at runtime. This is the clean alternative to a 400-line switch. Strategies pair naturally with dependency injection: register all implementations, select by key or config.

Observer

The observer pattern notifies dependents when state changes. DOM event listeners, React's state updates, and pub/sub buses are observer variants. In backend systems, domain events ("OrderPlaced") decouple producers from consumers — inventory, analytics, and email each subscribe without the order service knowing them. Watch for memory leaks (forgotten unsubscribe) and ordering assumptions; observers should not depend on delivery sequence unless you enforce it.

Command

A command encapsulates a request as an object — enabling undo stacks, job queues, and audit logs. "Place order" becomes a serializable command with execute() and rollback(). CQRS and event sourcing extend this idea: commands change state; events record what happened. Commands also make retries explicit — critical when combined with idempotency keys on APIs.

Iterator and Template Method

Iterator abstracts traversal — language for…of loops and database cursors are iterators. Template Method defines a skeleton algorithm in a base class with hooks for subclasses (e.g. parseHeader() fixed, parseBody() overridden). Template methods age poorly in favor of composition and strategy injection, but they still appear in framework lifecycles (hook methods in test runners and build pipelines).

Architectural patterns in production codebases

Model–View–Controller (MVC) and variants

MVC separates data (model), presentation (view), and input handling (controller). Web frameworks stretch the labels — Rails "controllers" are thick; React blurs view and controller into components. The durable lesson is separation of concerns: business rules should not live in JSX or SQL strings. MVVM and MVP are regional dialects of the same idea.

Repository

The repository pattern hides persistence behind a collection-like interface — users.findByEmail() instead of raw SQL in handlers. It simplifies testing (swap in memory repo) and centralizes query logic. Do not repository-wrap every table one-to-one; aggregate roots that match your domain boundaries matter more than CRUD symmetry.

Dependency Injection (DI)

Dependency injection supplies dependencies from outside rather than constructing them inside a class. Framework containers (Spring, NestJS, .NET) wire interfaces to implementations. Manual constructor injection works fine in smaller services. DI is the enabler for Strategy, testing with mocks, and configuration-driven behavior without new scattered everywhere.

Event-driven and saga patterns

At service scale, patterns become message contracts. Sagas coordinate multi-step transactions across services with compensating actions. Outbox tables ensure events publish reliably with database commits. These are distributed cousins of Observer and Command — same forces, harder failure modes.

When patterns help — and when they hurt

Reach for a pattern when you have proven variation or proven integration pain: multiple payment providers, multiple notification channels, a third legacy API. Delay until the second or third copy of similar logic appears (the Rule of Three). Red flags that you over-patterned:

  • Interfaces with exactly one implementation and no test double need.
  • Abstract factories for objects you never swap at runtime.
  • Deep inheritance trees where composition would be two functions.
  • Pattern names in class names (UserFactoryFactory) without team consensus on why.

Functional languages often replace patterns with first-class functions — Strategy becomes a function parameter; Decorator becomes function composition. That is still the pattern's intent, just with less boilerplate. Pick the shape your team can read and maintain, not the one that looks most like a textbook diagram.

Patterns and testing

Well-chosen patterns improve testability. Strategy lets you inject a fake pricing engine. Repository lets integration tests hit SQLite while production uses Postgres. Observer tested in isolation needs contract tests on event payloads. Command objects are trivially unit-tested: construct, execute, assert side effects. Pair pattern choices with the pyramid in our software testing fundamentals guide — patterns are not a substitute for tests; they make tests cheaper to write.

Common mistakes

  • Pattern-first design — drawing UML before understanding requirements.
  • Leaky abstraction — repository methods that expose ORM entities with lazy-load surprises.
  • God singleton — global config object mutated from everywhere.
  • Observer spaghetti — circular event chains with no documented flow.
  • Strategy explosion — forty strategy classes where a data table and one function suffice.
  • Ignoring framework conventions — fighting Rails, Django, or NestJS idioms to "be pure MVC."

Production checklist

  • Document which patterns appear at module boundaries (adapter, facade, repository).
  • Prefer composition and injection over inheritance for new code.
  • Name interfaces after roles (PaymentProcessor), not patterns (IPaymentStrategy).
  • Keep factories thin — creation logic should not contain business rules.
  • Version event and command schemas; observers break silently on shape changes.
  • Limit observer depth; use explicit workflow engines for multi-step processes.
  • Review new singletons in PR checklist; default to scoped services.
  • Align pattern vocabulary in code review so the team shares mental models.
  • Refactor toward patterns when duplication hurts, not preemptively.
  • Measure complexity (file count, dependency graph) after introducing abstraction layers.

Key takeaways

  • Design patterns are named solutions to recurring structure problems — not mandatory architecture.
  • Strategy, Observer, Factory, Adapter, and Repository appear constantly in real web and backend code.
  • Singleton and deep inheritance are often replaced by dependency injection and composition.
  • Architectural patterns (MVC, event sourcing, saga) scale the same ideas across services.
  • Apply patterns after duplication or integration pain is real; avoid abstraction for its own sake.
  • Good patterns improve testability and team communication — the name is shorthand for a shape everyone knows.

Related reading