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.

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.