Design Pattern Guide
Design
Patterns
A focused learning guide to the 23 Gang of Four patterns — one clear lesson at a time.
Harsh Sharma · Sys Titan, Live LLD Weekday 2.0 Batch
Pradeep Kumar · Senior Backend Engineer, ex-NetApp & Qualcomm
What Is a Design Pattern?
No prior experience needed. We start from scratch — with a real-world story before a single line of code.
A software design pattern is exactly this: a documented, proven solution to a recurring problem in software design. It is a template — not actual code — for how to tackle a situation that skilled developers have faced thousands of times before.
An algorithm is a specific step-by-step recipe with a definite input and output. "Sort these numbers using quicksort" — there is one correct implementation. A design pattern, by contrast, is a general strategy. Two developers using the same pattern might write completely different code.
A library or framework is actual code you import and run. A pattern is a concept — a shared vocabulary. When a senior dev says "we need a Facade here," that is a communication shortcut, not a library call.
Without patterns, every developer who faces "how do I notify many parts of my app when data changes?" must discover the solution independently. With patterns, they can say "that is the Observer pattern" — apply a proven solution in minutes instead of days, and avoid all the pitfalls others discovered the hard way.
- Shared vocabulary between developers
- Proven solutions to known problems
- A way to think at a higher level of abstraction
- Faster onboarding: a new developer who knows patterns can understand a codebase faster
What Does a Pattern Consist Of?
Every well-documented pattern has four essential parts.
A brief, precise statement of what the pattern does and what problem it solves. Usually one or two sentences. Example: "Define an interface for creating objects, letting subclasses decide which class to instantiate."
A scenario that illustrates the design problem and why an ordinary approach fails. This is the story that makes the pattern click intuitively.
A diagram showing the classes/objects involved and how they relate. Usually a UML class diagram. Gives you the "shape" of the solution.
A concrete implementation showing the pattern in action. Makes the abstract tangible and actionable.
History of Design Patterns
Patterns did not originate in software. They started in architecture — then crossed disciplines in a way that changed how software is written forever.
Why Should You Learn Patterns?
Beyond the theoretical — here is what patterns actually do for your career and your code.
"We need a Factory here" communicates more in 5 words than 5 paragraphs of explanation. Patterns are vocabulary — they let teams think and talk faster.
Every pattern comes with hard-won knowledge about what goes wrong. Knowing patterns means you inherit decades of collective experience — including failure modes.
Patterns let you think about structure instead of syntax. Senior engineers think in patterns before they think in code.
System design interviews, architecture roles, and code reviews all expect fluency in patterns. It is a professional signal of seniority.
Before & After: Without vs With Pattern
Criticism of Patterns — The Honest Truth
This guide will not cheerlead for patterns. They are tools — and like all tools, they can be misused.
Some critics — especially Peter Norvig — have pointed out that several GoF patterns are only needed because most languages lack certain features. The Visitor pattern exists primarily because languages like Java do not support multiple dispatch. In a language like Common Lisp that does have multiple dispatch, Visitor is simply not needed. The Iterator pattern is built into Python via __iter__ and for loops. As languages evolve, some patterns become obsolete — which is healthy.
A junior developer who just learned Abstract Factory might restructure a simple object creation into five new classes, two interfaces, and three packages — for a problem that a plain constructor would have solved in three lines. Patterns should reduce complexity, not add it. If applying a pattern makes your code harder to read than the original mess, you have made it worse.
Sometimes a 5-line function is the right answer, and a pattern is the wrong one. The right question is never "which pattern should I use?" but "does this problem actually need a pattern, or is there a simpler solution?"
Classification of the 23 GoF Patterns
The Gang of Four organised their 23 patterns into three families based on the fundamental type of problem each solves.
How objects are born. These patterns control the creation process, giving you flexibility over what gets created, who creates it, and how it comes to exist.
How objects are assembled. These patterns describe how classes and objects are composed into larger structures.
How objects communicate. These patterns define how responsibility is distributed and how objects interact.
All 23 Patterns at a Glance
Object Creation
These patterns are all about HOW objects are born into existence — and giving you control over that birth. Instead of simply calling new SomeClass() everywhere, creational patterns let you decide what gets created, when, and by whom.
Factory Method defines a method for creating objects in a superclass, allowing subclasses to alter the type of objects created. Instead of calling a constructor directly (new Dog()), you call a factory method (createAnimal()) which subclasses override to return different types.
You are building a UI framework. You have a Dialog class with a render() method that creates buttons. On Windows it should create WindowsButton; on Mac it should create MacButton. If you hardcode new WindowsButton() inside Dialog.render(), you can never use the Dialog on Mac without modifying its source code. You have tightly coupled the creator to a specific product type.
Move the object creation into a dedicated factory method. The base Dialog class calls createButton(). WindowsDialog overrides it to return a WindowsButton. MacDialog overrides it to return a MacButton. The render() logic is written once in the base class — the what to create changes per subclass.
Java's Calendar.getInstance() is a classic Factory Method — it returns a Calendar subclass appropriate for your locale (GregorianCalendar for most, BuddhistCalendar in Thailand). Android's LayoutInflater.inflate() creates View objects (Button, TextView, ImageView) from XML — a factory method that reads the XML tag and instantiates the correct View subclass automatically.
| ✓ Benefit | ⚠ Cost | 🚫 When It Hurts |
|---|---|---|
| Open/Closed: add new products without changing existing code | Requires a new subclass for each product type — class explosion | When you only ever need one product type |
| Decouples creator from concrete products — easier to test | Code becomes more complex and harder to trace | When product types rarely change |
- You do not know which class to instantiate until runtime
- You want subclasses to control which objects they create
- You are building a framework that should be extensible
- You have related classes that all implement the same interface
Abstract Factory provides an interface for creating families of related or dependent objects without specifying their concrete classes. Where Factory Method creates one kind of product, Abstract Factory creates multiple kinds designed to work together.
You are building a cross-platform UI library with Button and Checkbox widgets. On Windows, both must be Windows-styled. On Mac, both must be Mac-styled. Using separate Factory Methods for each widget type, nothing prevents a developer from accidentally mixing Windows buttons with Mac checkboxes — creating visual inconsistency.
Create a GUIFactory interface with createButton() and createCheckbox(). WindowsFactory always returns Windows-styled widgets; MacFactory returns Mac-styled widgets. The client talks only to GUIFactory — and gets a guaranteed consistent family.
Flutter's theming system is an Abstract Factory: a ThemeData object defines a complete family of coordinated UI elements — colors, text styles, button styles — guaranteed to be visually consistent. Qt's style system has QStyle subclasses (WindowsStyle, MacStyle, FusionStyle) that act as Abstract Factories producing matching widget appearances. Hibernate's SessionFactory abstracts over different databases, returning properly configured sessions for each.
| ✓ Benefit | ⚠ Cost | 🚫 When It Hurts |
|---|---|---|
| Guarantees consistency among products in a family | Adding new product types requires changing all factory interfaces and implementations | When product families are unstable — constantly adding new product types |
| Isolates concrete product classes from client code | More complex than Factory Method — significant boilerplate | When you only have one product variant — massive overkill |
- You need to ensure that products from a family are used together
- You want to support multiple platform families (Windows/Mac, MySQL/PostgreSQL)
- You anticipate switching between entire product families at runtime
Builder separates the construction of a complex object from its representation. It solves the "telescoping constructor" problem — where constructors accumulate so many parameters that you end up passing null for most of them.
You need to construct an object with many optional fields. A single constructor either forces dozens of parameters (most null) or explodes into overloaded variants that are hard to read and easy to misuse.
Put construction steps on a Builder that returns itself after each call. A Director can orchestrate a fixed recipe, while clients can also call steps fluently and finish with build().
SQLAlchemy's Query API is a textbook Builder: session.query(User).filter(User.age > 18).order_by(User.name).limit(10).all() — each method adds a step, .all() triggers final execution. OkHttp's Request.Builder similarly constructs HTTP requests step by step. Lombok's @Builder annotation in Java auto-generates Builder code for any class.
| ✓ Benefit | ⚠ Cost | 🚫 When It Hurts |
|---|---|---|
| Eliminates telescoping constructor problem | Requires a separate Builder class (doubles the code) | When objects are simple — a 3-parameter constructor is fine |
| Highly readable fluent API — reads like a sentence | Builder must be synchronised with product — maintenance burden | When the product has very few optional fields |
- Your constructor needs more than 4-5 parameters, many optional
- You want a readable, fluent API for object creation
- You need to produce different representations of the same object
Prototype lets you copy existing objects without making your code dependent on their classes. You define a clone() method — the object knows how to copy itself — so any object can be a template for creating new ones.
Creating objects from scratch is expensive (DB loads, complex config). You need many similar instances that differ only slightly, but copying by hand duplicates setup code and risks incomplete copies.
Give objects a clone() (or copy) operation so they copy themselves — including private state — and return a new instance the client can tweak.
Unity's Instantiate() function is Prototype in action — you create a "prefab" (template object), then clone it to spawn many instances. JavaScript's entire inheritance model is prototype-based: every object has a prototype it inherits from, and Object.create(proto) creates a new object that clones the prototype's structure. Python's copy.deepcopy() implements Prototype for any object.
| ✓ Benefit | ⚠ Cost | 🚫 When It Hurts |
|---|---|---|
| Clone complex objects without knowing their class | Deep cloning objects with circular references is tricky | When objects contain non-copyable resources like file handles |
| Avoid expensive initialisation — clone from a known-good state | Cloning complex hierarchies can be error-prone | When creation is cheap anyway — cloning adds no value |
- Object creation is expensive (many API calls or database reads)
- You need many similar objects with minor variations
- You do not know the concrete class of objects you need to copy
Singleton solves two problems: (1) guaranteeing a class has only one instance, and (2) providing a global access point. Note: solving two problems in one pattern is itself a design smell — which is why Singleton is the most controversial GoF pattern.
Some resources must exist exactly once (config, logger, connection pool). Scattered `new` calls create multiple instances, conflicting state, and hard-to-test globals.
Hide the constructor and expose a single static accessor (getInstance) that creates the object lazily (or eagerly) and always returns the same instance.
Spring's beans are Singleton by default — the container creates one instance of each bean and reuses it throughout the application. java.lang.Runtime.getRuntime() is a classic Singleton — one JVM runtime per process. Node.js's require() system implements Singleton via module caching: the first require('module') creates and caches it; all subsequent calls return the same object.
| ✓ Benefit | ⚠ Cost | 🚫 When It Hurts |
|---|---|---|
| Guaranteed single instance — prevents resource duplication | Violates Single Responsibility Principle — handles both instance control and core logic | Unit tests — Singletons are notoriously hard to mock |
| Global access point — convenient | Introduces global state — the enemy of testability | Multi-threaded environments — requires careful synchronisation |
- There genuinely must be exactly one instance (OS-level resources, hardware device access)
- You need global access and have considered the testability implications
- You need lazy initialisation of an expensive shared resource
Object Composition
These patterns are about HOW objects and classes are assembled into larger structures — like Lego bricks snapping together. Each pattern defines a way of connecting pieces so the larger system is flexible, maintainable, and clean.
The Adapter pattern allows classes with incompatible interfaces to work together by wrapping an object in an adapter that translates calls from one interface to another. It is the go-to pattern when integrating third-party code or legacy systems.
You need a class that speaks interface A, but the useful library only exposes incompatible interface B. Rewriting either side is costly or impossible.
Write an Adapter that implements the target interface and translates calls to the adaptee, so clients stay coupled only to the interface they expect.
Java's InputStreamReader is an Adapter: it wraps a byte-stream (InputStream) and adapts it to a character-stream interface (Reader). E-commerce platforms (Shopify, WooCommerce) use adapters for every payment gateway — Stripe, PayPal, Razorpay all have different APIs, all adapted to a common PaymentGateway interface. Spring Data's repository abstraction adapts between your domain model and various database engines.
| ✓ Benefit | ⚠ Cost | 🚫 When It Hurts |
|---|---|---|
| Integrate third-party code without modifying it | Adds an extra layer of indirection — harder to trace execution | When you control both interfaces and can simply unify them |
| Single Responsibility: adaptation logic is isolated | Impedance mismatch can lead to data loss or translation errors | When interface differences are too large — translation becomes a liability |
- You want to use an existing class but its interface does not match what you need
- You are integrating legacy or third-party code into a new system
- You need to reuse existing subclasses that lack a common interface
Without Bridge, adding new shapes to a drawing program that supports multiple renderers (Raster, Vector) would require a class for every combination: RasterCircle, VectorCircle, RasterSquare, VectorSquare. With Bridge, Shape and Renderer are separate hierarchies — add a new shape or renderer without touching the other.
You need to vary an abstraction (e.g. shape) and its implementation (e.g. renderer) independently. Inheritance couples them into an explosion of subclasses (CircleOpenGL, CircleVulkan…).
Split into two hierarchies: Abstraction holds a reference to Implementor. Clients compose any abstraction with any implementor at runtime.
Java's JDBC is one of the most famous examples of Bridge in production. Your code uses the java.sql.Connection abstraction — always the same API. The driver (MySQL, PostgreSQL, Oracle) is the implementation. Switching databases means swapping the driver, not rewriting your SQL code. This decoupling has enabled Java to work with every database engine ever created without changing application code.
| ✓ Benefit | ⚠ Cost | 🚫 When It Hurts |
|---|---|---|
| Platform-independent code — swap implementations without changing abstractions | Increases overall code complexity — two hierarchies instead of one | When you only have one implementation — unnecessary overhead |
| Open/Closed in both dimensions — add shapes OR renderers independently | Harder to understand — the indirect call path is non-obvious | When abstraction and implementation are unlikely to vary independently |
- You want to divide a monolithic class with several variants into separate hierarchies
- You need to extend a class in two independent dimensions
- You want to switch implementations at runtime
Composite organises objects into tree structures. Both simple (leaf) objects and complex (container) objects implement the same interface. This lets you write code that works on a single item and a collection of items without knowing which it is.
Clients must treat individual objects and groups of objects differently, flooding call sites with type checks whenever a tree of parts grows.
Define a shared Component interface. Leaves and Composites both implement it; composites hold children and forward operations — clients talk to one interface.
The browser's Document Object Model is the most-used Composite in the world. A div can contain other divs and text nodes. element.innerHTML and element.querySelectorAll() work on any node — leaf or container — uniformly. React's component tree is identical: a Page contains Sections which contain Cards which contain Text. React's virtual DOM reconciliation traverses this tree treating all components uniformly.
| ✓ Benefit | ⚠ Cost | 🚫 When It Hurts |
|---|---|---|
| Work with complex tree structures polymorphically | Hard to restrict which types can be added to a composite | When data is not genuinely hierarchical |
| Open/Closed — add new component types without changing existing code | Overly general design — making every component both leaf and container can be confusing | When you need type-specific operations — the uniform interface hides distinctions |
- You need to represent part-whole hierarchies (file systems, org charts, menus)
- You want client code to treat individual and composite objects uniformly
- You are building a tree-structured data model
Decorator lets you add behaviors to an object at runtime by wrapping it in a decorator object that has the same interface. Decorators can be stacked. Unlike inheritance (which adds behaviour to an entire class at compile time), Decorator adds behaviour dynamically to individual objects.
You need optional behaviors (logging, compression, borders) in many combinations. Subclassing every mix creates a combinatorial explosion.
Wrap the object with decorators that implement the same interface, forward calls, and add behavior before/after — stackable at runtime.
Java's I/O library is Decorator throughout: new BufferedReader(new InputStreamReader(new FileInputStream("file.txt"))). Each wrapper adds buffering, character encoding, or file reading to the base stream. Express.js middleware is Decorator applied to HTTP handling — app.use(cors()), app.use(bodyParser()), app.use(auth()) each wraps the request/response pipeline. Python's @functools.wraps decorator is the language-level version of this pattern.
| ✓ Benefit | ⚠ Cost | 🚫 When It Hurts |
|---|---|---|
| Add behaviors without subclassing — no inheritance needed | Many small wrapper objects — hard to debug | When decoration order matters but is not enforced |
| Mix and match behaviors at runtime | Removing a decorator from the middle of a stack is hard | When code needs to interact with specific decorators — wrapping hides identity |
- You want to add behavior to individual objects without affecting others of the same class
- Extension by subclassing is impractical due to combinatorial explosion
- You want to add responsibilities that can be withdrawn later
Facade provides a high-level, simplified interface to a complex subsystem. It does not hide the subsystem — you can still use it directly if needed — but it provides a convenient shortcut for common operations and reduces coupling between client code and the subsystem.
Clients depend on many subsystem classes and their wiring. Every feature change ripples through call sites that should not know those details.
Introduce a Facade with a small, high-level API that orchestrates the subsystem. Clients depend on the facade, not the internals.
The AWS SDK is a massive Facade: rather than making raw HTTP requests to AWS endpoints, signing them with AWS Signature V4, and parsing XML responses — you call s3.putObject({Bucket, Key, Body}). Spring Boot's auto-configuration is a Facade over hundreds of configuration decisions. Redux's createStore() is a Facade that sets up the store, middleware, and devtools in a single call.
| ✓ Benefit | ⚠ Cost | 🚫 When It Hurts |
|---|---|---|
| Dramatically reduces cognitive complexity for clients | Can become a God Object if given too much responsibility | When you need full subsystem access regularly |
| Decouples client from subsystem — easier to evolve subsystem | Does not prevent direct subsystem access — clients can bypass facade | When the facade interface hardens and cannot evolve |
- You have a complex subsystem and want a simple interface for common use cases
- You want to layer your system — facade defines boundaries between layers
- You are writing a library and want a concise API for clients
Flyweight splits object state into intrinsic (shared, immutable) and extrinsic (unique per instance). The Flyweight stores only intrinsic state and is shared. Extrinsic state is passed in by the caller.
You create huge numbers of similar objects; most of their data is identical. Memory balloons because every instance stores the shared bits again.
Split intrinsic (shareable) state into Flyweight objects cached by a factory; keep extrinsic state outside and pass it into operations.
Java's string pool is Flyweight built into the language: "hello" == "hello" is true because Java reuses the same string object from a shared pool. Python interns short strings similarly. Game engines (Unity, Unreal) use Flyweight for particle systems — thousands of particles sharing one sprite/texture object. Modern text rendering engines (HarfBuzz, FreeType) cache glyph outlines using Flyweight.
| ✓ Benefit | ⚠ Cost | 🚫 When It Hurts |
|---|---|---|
| Massive memory savings when many similar objects exist | CPU overhead — you spend computation to look up shared flyweights | When the number of shared objects is small |
| Enables scenarios impossible with full per-object state | Code becomes harder to understand — intrinsic/extrinsic split is non-obvious | When objects have mostly unique state — no shared state to exploit |
- Your application needs to support huge numbers of objects (thousands to millions)
- Most object state can be made extrinsic (passed in, not stored)
- Memory is the bottleneck, not CPU
Proxy creates a class that has the same interface as the real service object. A Proxy object controls access to the real object — adding logging, caching, access control, lazy initialization, or remote communication — without the client knowing it is talking to a proxy.
Clients should not access a real object directly — creation is costly, access needs checks, or you need caching/logging around every call.
Provide a Proxy with the same interface as the real subject. The proxy controls access, then forwards to the real object when appropriate.
Spring's AOP creates proxy objects around your beans — when you annotate a method with @Transactional or @Cacheable, Spring wraps the bean in a proxy that intercepts calls and adds transaction or caching behavior before and after. Nginx is a server-level proxy: clients think they are talking to your application; Nginx intercepts, adds TLS, load-balancing, rate limiting, and caching. Hibernate's lazy-loaded entities are proxy objects — accessing a field triggers the actual database query.
| ✓ Benefit | ⚠ Cost | 🚫 When It Hurts |
|---|---|---|
| Control service access without client changes | Response delayed — a proxy adds latency | When added indirection creates more confusion than value |
| Lazy init — do not pay the cost until needed | More complex code — extra class, extra layer | High-performance systems where every millisecond matters |
- Lazy initialization — do not create expensive objects until needed
- Access control — restrict who can use the real service
- Logging — log calls without modifying the service
- Caching — cache results and return them without hitting the service again
Object Communication
These patterns define HOW objects talk to each other — who is responsible for what, how work flows between objects, and how to keep that communication clean and decoupled.
Chain of Responsibility lets you pass requests along a chain of handlers. Each handler either processes the request or passes it to the next handler. The sender does not know which handler will ultimately process it — or even if any will.
A request might be handled by several possible processors. Hard-coding who handles what couples senders to receivers and resists runtime reordering.
Link handlers into a chain. Each handler processes the request or passes it to the next — the sender only knows the first link.
Express.js middleware is Chain of Responsibility: app.use(cors()), app.use(helmet()), app.use(bodyParser()) each is a handler that either modifies the request/response and calls next(), or short-circuits with an error. Java's javax.servlet.Filter chain works identically. Django's middleware stack, ASP.NET Core's pipeline, and AWS API Gateway's request processing are all Chain of Responsibility implementations.
| ✓ Benefit | ⚠ Cost | 🚫 When It Hurts |
|---|---|---|
| Decouple sender from receiver — sender does not know who handles | No guarantee a request gets handled — it might fall through | When you need guaranteed handling — use a catch-all at the end |
| Add/remove handlers dynamically — flexible pipeline | Debugging is harder — hard to trace where a request was dropped | Simple conditional logic — a switch statement may be cleaner |
- Multiple objects may handle a request but you do not know which until runtime
- You want to issue a request to a pipeline of handlers without coupling to a specific one
- The set of handlers or their order should be configurable at runtime
Command encapsulates a request as an object, letting you parameterise methods with different requests, delay or queue their execution, and support undoable operations. The Command object stores everything needed to execute the action later.
UI buttons, macros, and queues need to invoke actions later, undo them, or log them — but binding the invoker directly to concrete methods makes that impossible.
Wrap each request as a Command object with execute() (and often undo()). Invokers store and run commands without knowing the receiver’s details.
Git commits are Commands: each stores exactly what changed, who changed it, and when — enabling perfect undo (git revert) and replay (git cherry-pick). Redux actions are Commands: plain objects with {type, payload} that encapsulate every state change. The Redux store's history enables time-travel debugging — replaying all Commands from any checkpoint. Database transactions are Commands encapsulated in ACID-safe wrappers with rollback (undo) support.
| ✓ Benefit | ⚠ Cost | 🚫 When It Hurts |
|---|---|---|
| Full undo/redo — store and reverse any action | Lots of small command classes — one per operation | Simple operations where undo is not needed |
| Queue, schedule, or defer actions | Undo logic can be complex — especially for operations with side effects | When function references achieve the same result more simply |
- You need undo/redo functionality
- You want to queue operations, schedule them, or execute them remotely
- You want to implement transaction-style operations with rollback
Iterator defines an interface for accessing and traversing elements of a collection sequentially without exposing its underlying structure. The pattern separates the traversal algorithm from the collection, enabling multiple simultaneous traversal strategies.
Clients that walk a collection must know whether it is a list, tree, or graph. Exposing internals couples them to the data structure.
Provide an Iterator with hasNext/next (or equivalent). The collection creates the iterator; clients traverse without seeing storage details.
Python's generators (yield keyword) are Iterator pattern as a first-class language feature. Java's Iterable interface and for-each loop implement Iterator — any class implementing Iterable can be traversed with for (Item item : collection). JavaScript's Symbol.iterator protocol does the same. Database cursors are iterators over query result sets.
| ✓ Benefit | ⚠ Cost | 🚫 When It Hurts |
|---|---|---|
| Single Responsibility — move traversal out of collection | Overkill for simple collections — adds classes for no benefit | When your language already handles iteration natively |
| Multiple iterators can traverse the same collection simultaneously | Less efficient than direct access if you need random access by index | When random access by index is the primary use case |
- You need to traverse a complex data structure (tree, graph) without exposing its internals
- You want multiple traversal strategies for the same collection
- Your language does not have built-in iterator support
true AND (false OR true) is a sentence. Terminals are the words true/false; non-terminals are operators like AND/OR that combine smaller expressions. The interpreter walks that tree and returns a value.Interpreter represents grammar rules as classes. Each rule knows how to interpret itself in a context — ideal for small DSLs, filters, and configuration languages where a full parser generator would be overkill.
You need to evaluate many sentences in a simple domain language (boolean filters, math snippets, route rules). Hard-coding every sentence as custom code does not scale, and shipping a full compiler is too heavy for a tiny grammar.
Map each grammar rule to a class implementing interpret(context). Terminals return literal values; non-terminals combine child expressions. Clients build (or parse into) an abstract syntax tree and call interpret on the root.
Many ORMs and policy engines compile filter strings into expression trees — each predicate and boolean operator is a node that can evaluate against a row or request context. Regex engines and simple config DSLs use the same idea at smaller scale: grammar as objects, evaluate by walking the tree.
| ✓ Benefit | ⚠ Cost | 🚫 When It Hurts |
|---|---|---|
| Easy to extend the grammar with new expression types | Class-per-rule can explode for rich languages | When you need a full programming language — use a real parser |
| Clear mapping from grammar to code | Interpreting deep trees can be slow | Hot paths that need compiled bytecode |
- You have a small, stable grammar that users or configs express repeatedly
- Sentences should be stored, combined, or evaluated in different contexts
- A full parser generator would be disproportionate to the language size
Mediator centralises complex communications between many objects. Instead of objects knowing about each other directly (creating a web of dependencies), they all know only about the mediator. The mediator handles all cross-component communication.
Many components talk to each other directly. The web of references becomes brittle — every new colleague needs updates in several places.
Route interactions through a Mediator. Components notify the mediator; the mediator coordinates colleagues so they stay decoupled.
Slack is literally a Mediator: instead of every team member messaging every other directly, they communicate through Slack channels (the mediator). MVC's Controller is a Mediator: the View does not talk to the Model directly — all communication flows through the Controller. React Redux's store acts as a Mediator: components dispatch actions to the store, and the store notifies relevant components of state changes.
| ✓ Benefit | ⚠ Cost | 🚫 When It Hurts |
|---|---|---|
| Dramatically reduces coupling between components | Mediator can become a God Object — knowing too much about too many things | When it grows to 1000+ lines — split into multiple mediators |
| Reuse components without dependent coupling | Centralises control — introduces single point of failure/congestion | When components have simple, direct relationships |
- Many objects communicate in complex, tightly coupled ways
- You cannot reuse a component in a different context because it depends on others
- You are building complex UI forms where UI elements affect each other
Memento allows you to save and restore snapshots of an object's state without violating encapsulation. The originator creates and restores mementos; the caretaker stores them. The caretaker never inspects the contents of a memento — the originator's private state stays private.
You need undo/restore, but exposing internal fields to outsiders breaks encapsulation — or you end up with messy getters just for history.
The Originator creates opaque Memento objects capturing state. A Caretaker stores them and never peeks inside; only the Originator can restore.
Microsoft Word's undo history is Memento: each keystroke saves a snapshot; Ctrl+Z restores the previous one. Database transactions use Memento in savepoints — you can ROLLBACK TO SAVEPOINT to restore state to any checkpoint without undoing the entire transaction. Git's stash feature is Memento applied to your working directory state.
| ✓ Benefit | ⚠ Cost | 🚫 When It Hurts |
|---|---|---|
| Save and restore state without exposing internals | Memory cost — each snapshot stores full state | When objects have large state — memory pressure is significant |
| Enables complete undo/redo and checkpointing | Caretakers must manage the memento lifecycle | When state is immutable anyway — no need for snapshots |
- You need to produce snapshots of an object's state for undo/redo
- Direct access to the object's private fields violates encapsulation
- You need checkpoint and rollback functionality
Observer defines a one-to-many dependency: when one object (Subject/Publisher) changes state, all its dependents (Observers/Subscribers) are notified and updated automatically. This is the most widely used pattern in all of modern software.
When one object changes, many others must react. Hard-wiring dependents creates tight coupling and missed updates.
Subjects keep a list of Observers and notify them on change. Observers subscribe/unsubscribe without the subject knowing concrete types.
React's useState hook is Observer: components "observe" state and re-render when it changes. Vue's reactivity system uses Proxy-based observation — access a property and Vue registers a dependency; mutate it and Vue re-renders all dependents. Apache Kafka is Observer at infrastructure scale: producers publish events to topics, and multiple consumer groups independently subscribe and process them. DOM's addEventListener is Observer built into the browser.
| ✓ Benefit | ⚠ Cost | 🚫 When It Hurts |
|---|---|---|
| Open/Closed — add new observers without modifying the subject | Observers notified in random order — do not rely on sequence | When notification chains create cascading updates (event storms) |
| Loose coupling — publisher knows nothing about subscribers | Memory leaks if observers are not properly removed | When observers need to know about each other's updates |
- Changes to one object require notifying an unknown number of other objects
- You want loose coupling between related objects
- You are building event systems, reactive UIs, or pub/sub architectures
State allows an object to change its behaviour when its internal state changes. Instead of one large class with conditional logic (if state == "locked" do X, else if state == "unlocked" do Y), each state is a separate class. Transitioning from one state to another means swapping the state object.
Behavior depends on internal state, so methods fill with giant switch/if chains that grow with every new state.
Extract each state into a State object. Context delegates to the current state; states may trigger transitions to other states.
Uber's driver app is a State machine: drivers transition through Offline → Available → Matched → OnTrip → Returning. Each state has strict allowed transitions — you cannot go from OnTrip directly back to Offline. TCP connection protocol is one of the most famous State machines in computing: CLOSED → LISTEN → SYN_SENT → ESTABLISHED → TIME_WAIT — each state dictates exactly which packets can be sent and received.
| ✓ Benefit | ⚠ Cost | 🚫 When It Hurts |
|---|---|---|
| Eliminates massive if-else or switch chains for state-dependent behaviour | Overkill for few states — a simple enum + switch might suffice | When states have very little unique behaviour |
| Single Responsibility — each state class has one job | States may need to know about each other to manage transitions | When states share extensive behaviour — lots of duplication |
- An object's behaviour depends heavily on its state and must change at runtime
- You have massive conditionals that choose behaviour based on state
- State transitions need to be explicit and enforced (invalid transitions should be impossible)
Strategy defines a family of algorithms, encapsulates each one, and makes them interchangeable. The Strategy pattern lets you change the algorithm used by an object at runtime without changing the object itself.
You need interchangeable algorithms (sorts, pricing, routing). Embedding them as conditionals or subclasses locks clients to one approach.
Define a Strategy interface; concrete strategies implement variants. Context holds a strategy and delegates — swap anytime.
E-commerce checkout is textbook Strategy. The PaymentService context accepts any payment strategy: CreditCardStrategy, UPIStrategy, WalletStrategy, NetBankingStrategy. All implement the same pay(amount) interface; the checkout process never changes. Java's Arrays.sort() accepts a Comparator — this is Strategy applied to comparison logic. Spring Security's AuthenticationProvider is a Strategy: swap out LDAP, database, or OAuth authentication without changing the security framework.
| ✓ Benefit | ⚠ Cost | 🚫 When It Hurts |
|---|---|---|
| Swap algorithms at runtime — eliminates conditional logic | Clients must know which strategy to choose | When you only have one algorithm — over-engineering |
| Each strategy is independently testable | More classes — one per strategy | In functional languages — first-class functions make this unnecessary |
- You want to switch between different variants of an algorithm at runtime
- You have multiple classes that only differ in how they execute a certain behaviour
- A class has massive conditionals that switch between algorithm variants
Template Method defines the skeleton of an algorithm in the base class, deferring some steps to subclasses. Subclasses can override certain steps without changing the algorithm's overall structure. It is inheritance applied to algorithms.
Several classes share the same algorithm outline but differ in a few steps. Copy-pasting the outline causes drift; forcing one concrete class loses flexibility.
Put the skeleton in a base-class template method that calls overridable steps. Subclasses implement only the varying pieces.
JUnit's test lifecycle is Template Method: the framework calls @BeforeEach, then @Test, then @AfterEach — the skeleton is fixed; you fill in the test methods. Spring's JdbcTemplate abstracts the template: get connection → prepare statement → execute → handle results → release. You provide only the SQL and result mapping. Django's class-based views (ListView, DetailView) are Template Method — override get_queryset() to customise; the HTTP handling skeleton is inherited.
| ✓ Benefit | ⚠ Cost | 🚫 When It Hurts |
|---|---|---|
| Eliminates code duplication in algorithms with the same skeleton | Clients bound to the template — cannot change the skeleton | When subclasses need to radically alter the algorithm — too rigid |
| Easy to extend — just override the variable steps | Inheritance-based — tightly coupled hierarchy | When "composition over inheritance" is your design principle |
- Multiple classes share the same algorithm structure but differ in specific steps
- You want to eliminate code duplication in algorithm skeletons
- You want to control extension points — only certain steps can be overridden
Visitor separates an algorithm from the object structure it operates on. New operations can be added without modifying the classes. Core trade-off: adding new operations is easy (just write a new Visitor), but adding new element types requires updating all existing Visitors.
You need many operations over a stable object structure. Adding each operation into every element class pollutes the model and forces constant edits.
Elements accept a Visitor; the visitor implements visitX for each element type (double dispatch). New operations become new visitors.
Babel's plugin system is Visitor at scale. The compiler parses JavaScript into an AST. Each Babel plugin is a Visitor that traverses the tree — converting JSX to function calls, transpiling arrow functions — without modifying the core AST node classes. TypeScript's type checker similarly visits AST nodes. OpenDocument's export visitors generate PDF, HTML, and DOCX from the same document AST without modifying the document model.
| ✓ Benefit | ⚠ Cost | 🚫 When It Hurts |
|---|---|---|
| Add new operations without modifying element classes | Adding a new element type requires updating ALL visitors | When element types change frequently |
| All related operations in one Visitor — easy to find | Complex double-dispatch mechanism — hard to understand at first | When the hierarchy is simple — overkill |
- You need to perform many distinct and unrelated operations on an object structure
- You do not want to pollute classes with unrelated operations
- New operations are added frequently but the element class hierarchy is stable
Find Your Pattern
One guided path — answer what you are trying to solve, and land on the right pattern. Knowing when to use a pattern is the skill that separates junior from architect.
5.1 Decision Path
5.2 Pattern Combinations
Model notifies View via Observer. Controller applies Strategy for different actions. View is a Composite tree.
Events published via Observer. Processed through Chain of Responsibility filters. Actions as Commands for replay/audit.
Gateway is a Proxy. Middleware is Chain of Responsibility. Aggregation is Facade. Load balancing is Strategy.
Factory creates plugins. Strategy makes them swappable. Decorators add cross-cutting concerns. Composite chains them.
5.3 Anti-Patterns
What it is: One class that knows everything and does everything. How it happens: "Let me just add this method here." Repeated 500 times. Fix: Extract distinct responsibilities into focused classes. Apply Single Responsibility Principle.
What it is: Applying the same familiar pattern to every problem. ("We always use Singleton.") How it happens: Expertise in one pattern, ignorance of alternatives. Fix: Learn the full pattern vocabulary. Match patterns to problems, not comfort zones.
What it is: Dead code nobody dares remove because nobody understands what it does. How it happens: Fear of breaking things, no tests, no documentation. Fix: Write tests first, then refactor. Delete code aggressively. If it is not tested, assume it is dead.
What it is: Keeping unused components "just in case." How it happens: Over-engineering, speculative generality. Fix: YAGNI — You Aren't Gonna Need It. Version control is your safety net.
Compare Patterns
Pick two patterns and see intent, structure, when to use, and when to avoid — side by side.
Select two different patterns to compare them side by side.
Industry Standards & Professional Context
How patterns manifest in real, named production systems used by billions of people.
6.1 Patterns in Famous Systems
- Observer: Microservices subscribe to topic events via Apache Kafka
- Strategy: Adaptive bitrate streaming selects encoding strategy (4K, 1080p, 480p) at runtime
- Proxy: API Gateway proxies all internal services — adds auth, rate limiting transparently
- Facade: The API Gateway hides hundreds of microservices behind a single consumer API
- Flyweight: Search index stores billions of posting list entries with shared data structures
- Composite: Protocol Buffers messages compose other messages — recursive composite structures
- Template Method: MapReduce — the framework defines the skeleton; devs fill in map/reduce
- Proxy: API Gateway wraps all internal services with auth, logging, and rate limiting
- Command: Trip lifecycle — each action (request, accept, start, complete, cancel) is a Command
- State: Driver state machine — Offline → Available → OnTrip → Returning with strict transitions
- Strategy: Surge pricing algorithm selected by demand/supply ratio at runtime
- Chain of Responsibility: Payment pipeline tries methods in order until one succeeds
- Observer: React's core reactivity — state changes trigger re-renders in observing components
- Composite: Component tree from root to leaf — uniform interface for all components
- Proxy: Spring AOP wraps beans — @Transactional, @Cacheable work via transparent proxying
- Template Method: Spring's JdbcTemplate, RestTemplate — fixed skeleton with customisable steps
6.2 Patterns in System Design Interviews
| Interview Scenario | Expected Patterns | What to Say |
|---|---|---|
| "Design a notification system" | Observer + Chain of Responsibility | "Observer so subscribers register for events. Chain for notification rules (email → SMS fallback)." |
| "Design undo/redo" | Command + Memento | "Commands encapsulate each action with undo(). Memento stores snapshots for complex rollbacks." |
| "Design a payment gateway" | Strategy + Adapter + Facade | "Each gateway (Stripe, PayPal) is an Adapter. Strategy selects which to use. Facade for clients." |
| "Design a logging system" | Singleton + Chain + Decorator | "Logger is Singleton. Log levels form a Chain. Formatters are Decorators on the pipeline." |
6.3 SOLID Principles & Their Relationship to Patterns
Click any SOLID principle to see which patterns enforce it.
Patterns: Facade (separates subsystem from client), Decorator (separates concerns from core class), Command (separates invocation from execution), Iterator (separates traversal from collection).
Patterns: Strategy (add strategies without changing context), Observer (add observers without modifying subject), Decorator (add wrappers without changing core), Factory Method (add creators without changing base), Visitor (add operations without modifying elements).
Patterns: Factory Method (client uses Creator interface — any subclass is substitutable), Composite (leaf and composite share an interface), Template Method (subclasses must honour the algorithm contract).
Patterns: Facade (provides a narrow interface tailored to client needs), Adapter (exposes only the interface the client needs), Abstract Factory (each factory method is a narrow, specific creation interface).
Patterns: Factory Method (client depends on Creator interface, not concrete classes), Abstract Factory (client never references concrete factory), Bridge (abstraction depends on implementation interface), Strategy (context depends on strategy interface).
6.4 From Patterns to Architecture
GoF patterns operate at class level. Architectural patterns operate at system level — but they are built from GoF patterns:
Separates read (Query) and write (Command) models. Directly applies the Command pattern at architectural level — every write is an explicit Command through a pipeline of handlers.
Store the full history of Commands (events) instead of current state. Current state computed by replaying events — Memento at architectural scale. The event log is an immutable Command history.
Manages distributed transactions. Each step is a Command; on failure, compensating Commands execute in reverse — Chain of Responsibility meets Command meets Memento at microservices scale.
MVC evolved because Observer coupling between Model and View was too tight for testing. MVP introduced a Presenter (Mediator). MVVM added data binding (Observer at property level) for declarative UI.
Master Quiz
15 randomised questions across all patterns. Get a knowledge-level badge at the end.
Acknowledgements
Where this guide's content, patterns, and tooling come from.
Course & Content
Built by Harsh Sharma (Sys Titan, Live LLD Weekday 2.0 Batch — LinkedIn →) and Pradeep Kumar (Senior Backend Engineer, ex-NetApp & Qualcomm — full bio →) for the Live LLD & HLD Cohorts.
Thanks a lot to Harsh Sharma for your contribution in getting this page ready and helping learners quickly revise and understand the design patterns.
Original Patterns
The 23 patterns covered here were first catalogued by the "Gang of Four" — Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides — in Design Patterns: Elements of Reusable Object-Oriented Software (1994). This guide re-explains their work with modern, real-world examples.
Tools & Technology
Typography — Syne, DM Sans & IBM Plex Mono (Google Fonts). 3D background — Three.js.