Skip to content
aipatterns.dev

Guide

Choosing a Pattern

A decision guide that maps concrete code symptoms to the pattern that fixes them, and tells you when plain code beats any pattern.

Also known as
Pattern selection, Pattern catalog

Start from the symptom, not the pattern

Pick a pattern only after you can name the concrete problem in the current code that it removes. "This would be more flexible" is not a problem. "Adding a fourth export format means editing the same switch in five files" is.

Work in this order:

  1. Name the pain in one sentence, with a file and function.
  2. Check the "Prefer no pattern when" list below. Most pains have a smaller fix.
  3. Find the symptom in the table. Read the candidate pattern's ## Do not use it when section before anything else.
  4. If two patterns fit, compare their ## Relations sections. They state the difference directly.

Symptom to pattern

Symptom in the codeCandidateCheck first
new ConcreteThing() scattered in code that should not know which concrete type it getsFactory MethodA plain factory function is often enough
Several related objects must be created as a matched set (all AWS or all GCP clients)Abstract FactoryOnly worth it with two or more real families
Constructor with many optional parameters, or objects built in several stepsBuilderNamed or object parameters may do it
Creating an object is expensive and you need many near-identical copiesPrototypestructuredClone or a copy constructor
Exactly one instance must exist and be reachable process-wideSingletonUsually pass the instance in instead
A third-party or legacy class has the wrong interface for your codeAdapterA wrapper function for one call
Two independent dimensions of variation multiply into many subclassesBridgeOnly if both dimensions really vary
Tree of items where leaves and groups must be treated the sameCompositeA recursive function over plain data
You need to add behavior (logging, caching, retries) around an object without changing itDecoratorHigher-order function or middleware
Callers wire together many subsystem objects to do one common taskFacadeA single module-level function
Memory blows up from millions of objects that share most of their dataFlyweightMeasure first
Access to an object needs lazy loading, access control or remote callsProxyLanguage-level Proxy or a lazy getter
A request passes through a sequence of handlers, any of which may handle or reject itChain of ResponsibilityAn array of functions in a loop
Actions must be queued, logged, retried or undoneCommandA closure plus a record of inputs
Callers depend on a collection's internal layout to traverse itIteratorBuilt-in iterators and generators
Many components call each other directly, forming a tangleMediatorAn event bus you already have
You need snapshots for undo without exposing internal fieldsMementoImmutable state makes this free
Other objects must react when something changesObserverBuilt-in events, signals or streams
Behavior depends on an object's mode, with switch (this.status) in many methodsStateA transition table if behavior is small
A switch picks between algorithms that compute the same result in different waysStrategyPass a function
Subclasses copy the same workflow and differ in a few stepsTemplate MethodHigher-order function with hooks
You keep adding operations over a stable set of node types (AST, document tree)VisitorPattern matching over a union type

Prefer no pattern when

  • There are only one or two variants. An if is honest. Wait for the third case. See Speculative Generality.
  • The language already has the feature. First-class functions replace most single-method Strategy, Command and Template Method uses. Generators replace Iterator. Discriminated unions with exhaustive switch replace many Visitor uses. Module scope replaces Singleton.
  • A refactoring technique removes the smell. Extract Method, Introduce Parameter Object and Decompose Conditional fix more code than any pattern.
  • The code is not going to change. Patterns pay off on the next change. Stable code gets no return.
  • The team or codebase does not use the pattern elsewhere. One lone Abstract Factory in a codebase of plain functions is a reading tax for every future contributor.
  • You cannot write a test that the pattern makes easier. If the test is the same before and after, the pattern bought nothing measurable.
  • The framework already owns that concern. Dependency injection containers, ORMs and UI frameworks implement Factory, Proxy and Observer for you. Use theirs.

Selection flowchart

The first chart decides whether you need a pattern at all and which family to look in. Families are explained in Classification of patterns.

Diagram source (Mermaid)
flowchart TD
  A{"Can you name a concrete pain in current code?"}
  A -->|No| N["Use no pattern"]
  A -->|Yes| B{"Would a function, map or refactoring fix it?"}
  B -->|Yes| N
  B -->|No| C{"Is the pain about creating objects?"}
  C -->|Yes| CR["Creational patterns"]
  C -->|No| D{"Is it about fitting or wrapping objects?"}
  D -->|Yes| ST["Structural patterns"]
  D -->|No| BH["Behavioral patterns"]

Behavioral is the largest family and the most common destination. The second chart covers its frequent cases.

Diagram source (Mermaid)
flowchart TD
  A{"Does a switch pick an algorithm per call?"}
  A -->|Yes| S["Strategy, or pass a function"]
  A -->|No| B{"Does behavior depend on a mode that changes over time?"}
  B -->|Yes| ST["State"]
  B -->|No| C{"Must others react when something changes?"}
  C -->|Yes| O["Observer"]
  C -->|No| T["Use the symptom table"]

Telling look-alikes apart

Several patterns share a class diagram. Decide by intent:

  • Strategy vs State: strategies are chosen from outside and ignore each other; states switch themselves.
  • Decorator vs Proxy: a decorator adds behavior and stacks; a proxy controls access and usually manages the real object's lifecycle.
  • Adapter vs Facade: an adapter makes one existing interface fit another; a facade invents a simpler interface over many objects.
  • Factory Method vs Abstract Factory: one product vs a family of products. See Factory comparison.
  • Command vs Strategy: a command represents a request to run later; a strategy represents a way to compute now.

Agent checklist

  • You can state the pain in one sentence that names real code.
  • You checked the "Prefer no pattern when" list and none of it applies.
  • You read the chosen pattern's ## Do not use it when section and none of it applies.
  • At least three variants exist, or a third is concretely planned.
  • A test exists, or will exist, that is easier to write after the change.
  • Existing tests still pass after you introduce the pattern.

See also

Original writing and examples. Topic coverage follows Refactoring.Guru, which explains this topic with its own illustrations.