Design Patterns, Explained Simply · Modern Patterns and Choosing Well

Choosing the Right Pattern

A practical cheat sheet: start from the symptom in your code, find the matching pattern, and tell the look-alike patterns apart.

Create a free account to save this page, take notes and track your progress. Sign in

The problem

You know 23 patterns and several modern ones. In a real code review, you see a growing switch, a third-party class that doesn't fit, or a feature that needs undo, and you need to pick a tool quickly and explain why. Many patterns also look the same in code (a wrapper with the same interface could be a Decorator, a Proxy or an Adapter), so you need to know how to tell them apart.

Real-world analogy

A doctor doesn't start from "which medicine shall I prescribe today?". They start from the symptoms, narrow down the cause, and then choose a treatment, including "no medicine; rest". Pick patterns the same way: symptom first.

The idea

Start from the symptom:

Symptom in your code Consider
if/else choosing between algorithms or rules Strategy (or a lookup table of functions)
switch on a status field in many methods State
Constructor with many optional parameters Builder (or named arguments)
Creating objects whose class depends on config/input Factory Method / simple factory
Several related objects must match (theme, vendor) Abstract Factory
A class with the wrong interface for your code Adapter
Need logging, caching or retries around an object Decorator (or middleware)
Need lazy loading, access control or a remote stand-in Proxy
Callers repeat a complicated sequence of subsystem calls Facade
Tree structures (folders, menus, UI) Composite
Several things should react when something happens Observer (in-process) or Pub/Sub (across services)
Need undo/redo, queues or retries of actions Command (or Memento for snapshots)
Same steps in the same order, a few vary Template Method (or Strategy for the varying step)
Request goes through ordered checks that can stop it Chain of Responsibility / middleware
Business logic full of SQL Repository (+ Unit of Work)
null checks everywhere for an optional collaborator Null Object (or explicit optionals)

Tell the look-alikes apart (they differ by intent, not by code shape):

Pattern Wraps an object with... Intent
Adapter a different interface make an incompatible class fit
Decorator the same interface add behaviour, stackable
Proxy the same interface control access (lazy, permissions, remote)
Facade a new, simpler interface over many classes simplify a subsystem
Pattern Who picks the variant? Do variants know each other?
Strategy the client / configuration no
State the states themselves (transitions) yes, they switch to each other
Pattern Creates...
Factory Method one product, chosen by a subclass
Abstract Factory a matching family of products
Builder one complex product, step by step
Prototype a copy of an existing configured object

The code

The three "same interface" wrappers side by side, around one key-value store, so you can see that the shape is the same and only the intent differs.

Adapter, Decorator and Proxy around the same storeTry it yourself
class LegacyStore:  # incompatible interface
    def fetch_value(self, key):
        return {"theme": "dark", "lang": "en"}.get(key, "?")


class StoreAdapter:  # ADAPTER: different interface -> our get()
    def __init__(self, legacy):
        self.legacy = legacy

    def get(self, key):
        return self.legacy.fetch_value(key)


class LoggingStore:  # DECORATOR: same get(), adds behaviour
    def __init__(self, inner):
        self.inner = inner

    def get(self, key):
        value = self.inner.get(key)
        print(f"  log: get({key}) = {value}")
        return value


class AdminOnlyStore:  # PROXY: same get(), controls access
    def __init__(self, inner, role):
        self.inner, self.role = inner, role

    def get(self, key):
        if self.role != "admin":
            return "denied"
        return self.inner.get(key)


store = StoreAdapter(LegacyStore())
print("adapter:  ", store.get("theme"))
print("decorator:", LoggingStore(store).get("lang"))
print("proxy:    ", AdminOnlyStore(store, "viewer").get("theme"))
print("stacked:  ", AdminOnlyStore(LoggingStore(store), "admin").get("theme"))

Output

adapter:   dark
  log: get(lang) = en
decorator: en
proxy:     denied
  log: get(theme) = dark
stacked:   dark

All three wrappers have nearly identical code. What makes each one an Adapter, a Decorator or a Proxy is the reason it exists, which is also what its name should tell the next reader.

Common mistakes

  • Choosing by shape instead of intent. Name and document wrappers by why they exist.
  • Reaching for a pattern before simpler options: a function, a dictionary of functions, named arguments or an enum often solve the problem with less code.
  • Not combining patterns. Real designs mix them: Strategy objects created by a factory, decorated with logging, triggered by commands from an event bus.
  • Forgetting to explain the choice. In review, say which symptom the pattern addresses ("Strategy, because the shipping rules keep growing").

Key points

  • Pick patterns from symptoms: growing conditionals, wrong interfaces, cross-cutting concerns, many optional parameters, trees, events, undo.
  • Adapter, Decorator, Proxy and Facade look alike; they differ by intent: fit, add, control, simplify.
  • Strategy vs State: the client picks a strategy; states pick their successors.
  • Prefer the simplest thing that works, and refactor to a pattern when the variation is real.
  • Further reading: Refactoring Guru: Design patterns catalog.

Practice questions

How do Adapter, Decorator, Proxy and Facade differ?

Answer

All wrap something. Adapter gives an existing object a different interface so it fits. Decorator keeps the same interface and adds behaviour (stackable). Proxy keeps the same interface and controls access (lazy, permissions, remote). Facade offers a new, simpler interface over a whole subsystem.

Which pattern would you consider for a `switch` on an order's status repeated across many methods, and why?

Answer

State: each status becomes a class implementing the allowed actions and transitions, removing the repeated switch and making illegal transitions fail by default.

Which pattern would you consider for a constructor with ten optional parameters?

Answer

Builder (named, chainable steps and a validating build()), or simply named/keyword arguments with defaults in languages that have them (Python, PHP 8, TypeScript object parameters).

How should you justify using a pattern in a code review?

Answer

Name the symptom it fixes and the trade-off: "Strategy, because shipping rules change every month and each rule can now be tested alone; the cost is one interface and a class per rule." If you can't name the symptom, the pattern probably isn't needed.

Open in the app to mark lessons complete, take notes and track your progress.

Was this page helpful?