The Blueprint
The Blueprint / design patterns

Design patterns, decoded.

The 23 Gang of Four patterns — the vocabulary every senior engineer already speaks. Each one explained in plain English, with a hand-drawn picture, a tiny Java snippet, and one honest sentence on when it actually helps.

01

How to read this page

A pattern is a name for a shape you'll write anyway. Learning the name lets you spot the shape in your teammate's code, argue about it in a design review, and skip re-inventing it in your own. Nothing here is magic — most patterns are ten to thirty lines of Java that you'll find yourself typing whether or not you know the label.

Three groups, one page: creational (how objects come into being), structural (how they fit together), and behavioral (how they talk). Jump around:

Group 1 of 3

Creational — how objects are born

These five answer the question: "who decides which concrete class to instantiate?" The answer is almost never new scattered across your code.

Creational

Singleton

One instance, one global handle. The "there can be only one" pattern.

Caller A Caller B Caller C Config the one and only

How it works

The class hides its constructor and hands out one shared instance through a static accessor. Every caller in the process gets the same object. Use for things that genuinely have one — a config loader, a connection pool, a logger.

Java

public final class Config {
  private static final Config INSTANCE = new Config();
  private Config() {}
  public static Config get() { return INSTANCE; }
  public String db() { return "postgres://..."; }
}
// Config.get().db()

Use when

Truly one-per-process resource. Not when you just want a global variable — that's how singletons become the most-hated pattern.

TradeoffGlobal state kills testability — you can't swap the instance for a fake. Prefer dependency injection unless the singleness is a real invariant.
Creational

Factory Method

A method whose job is to make the right subclass, so callers stop caring which one.

NotifierFactory EmailNotifier SmsNotifier PushNotifier

How it works

Instead of calling new EmailNotifier() everywhere, callers ask a factory method for a Notifier. The factory picks the concrete class from a config, a channel string, or the user's preference — one place to change when you add a new channel.

Java

interface Notifier { void send(String to, String msg); }
class EmailNotifier implements Notifier { public void send(String t,String m){ /*...*/ } }
class SmsNotifier   implements Notifier { public void send(String t,String m){ /*...*/ } }

class NotifierFactory {
  static Notifier of(String channel) {
    return switch (channel) {
      case "email" -> new EmailNotifier();
      case "sms"   -> new SmsNotifier();
      default -> throw new IllegalArgumentException(channel);
    };
  }
}

Use when

You have one caller and many possible concrete types keyed by something at runtime (channel, region, plan tier).

TradeoffAdds one indirection. Skip it if there's only ever one type — you don't need a factory to hide a single new.
Creational

Abstract Factory

A factory that makes families of related things — pick the family once, get compatible parts.

UiFactory MacFactory WinFactory MacButton + MacMenu WinButton + WinMenu

How it works

An abstract factory has multiple factory methods that all return matching objects. MacFactory makes MacButton and MacMenu; WinFactory makes WinButton and WinMenu. Pick the factory once, and everything you produce is from the same family.

Java

interface Button { void render(); }
interface Menu   { void render(); }
interface UiFactory { Button button(); Menu menu(); }

class MacFactory implements UiFactory {
  public Button button() { return new MacButton(); }
  public Menu   menu()   { return new MacMenu(); }
}
class WinFactory implements UiFactory { /* similarly */ }

UiFactory ui = System.getProperty("os").equals("mac") ? new MacFactory() : new WinFactory();
ui.button().render();
ui.menu().render();

Use when

Several products must vary together and stay compatible (cross-platform UI, SQL vs NoSQL DAO bundles).

TradeoffAdding a new product to the family means editing every concrete factory. Great when the family is stable, painful when it isn't.
Creational

Builder

Configure an object step by step, then .build(). The fix for constructors with fifteen arguments.

.title() .author() .pages() .build() → Book

How it works

Each setter returns the builder itself, so calls chain. The final build() validates and returns an immutable object. Beats a constructor with ten parameters where three are booleans no one can remember the order of.

Java

public class Book {
  private final String title, author;
  private final int pages;
  private Book(Builder b) { this.title=b.title; this.author=b.author; this.pages=b.pages; }

  public static class Builder {
    private String title, author; private int pages;
    public Builder title(String t)  { this.title = t; return this; }
    public Builder author(String a) { this.author = a; return this; }
    public Builder pages(int p)     { this.pages = p; return this; }
    public Book build() {
      if (title == null) throw new IllegalStateException("title required");
      return new Book(this);
    }
  }
}
// new Book.Builder().title("SDI").author("Alex").pages(300).build();

Use when

Object has many optional fields or the construction has order/validation rules.

TradeoffDoubles the code (class + builder). In Java 14+ prefer records with a validating static factory when the argument count is small.
Creational

Prototype

Make a new object by copying an existing one, instead of running expensive setup again.

Template expensive to build clone 1 clone 2 clone 3

How it works

Keep one fully-configured object as a template, then clone() it whenever you need a new one. Cheaper than re-running heavy setup — parsing a schema, loading defaults from disk, warming a cache.

Java

public class Report implements Cloneable {
  private String title;
  private List<String> sections = new ArrayList<>();
  public Report clone() {
    Report r = new Report();
    r.title = this.title;
    r.sections = new ArrayList<>(this.sections); // deep enough for strings
    return r;
  }
}
Report template = expensiveDefault();
Report a = template.clone(); a.title = "Q1";
Report b = template.clone(); b.title = "Q2";

Use when

Creation is expensive and most instances only differ in a handful of fields.

TradeoffShallow vs deep copy is a footgun — a cloned object that shares mutable state with the template is a bug waiting to happen.
Group 2 of 3

Structural — how objects fit together

These seven answer: "how do I compose these classes so I can change one without breaking the others?"

Structural

Adapter

Translate one interface into another — the plug converter for code.

Your app StripeAdapter .charge() Stripe SDK .createIntent()

How it works

Wrap the incompatible thing in a class that exposes the interface you actually wanted. Your code talks to the adapter; the adapter translates to the underlying API. Classic use: switching payment gateways, upgrading a library, isolating a third-party SDK behind your own boundary.

Java

interface PaymentGateway { void charge(long cents, String card); }

class StripeAdapter implements PaymentGateway {
  private final com.stripe.StripeClient stripe;
  StripeAdapter(com.stripe.StripeClient s) { this.stripe = s; }
  public void charge(long cents, String card) {
    stripe.paymentIntents().create(Map.of("amount", cents, "source", card));
  }
}
// PaymentGateway pg = new StripeAdapter(stripeClient);
// pg.charge(1999, "tok_visa");

Use when

Two things must talk but their interfaces don't match, and you don't own one of them.

TradeoffAdds a layer — worth it exactly when you might swap the underlying thing. Zero value if you'll never leave Stripe.
Structural

Bridge

Split "what" from "how" so both sides can vary without a class explosion.

RedShape BlueShape Shape (abstraction) uses SvgRenderer CanvasRenderer Renderer (impl)

How it works

You have two dimensions of variation — say shape × renderer — and you don't want RedSvgShape, RedCanvasShape, BlueSvgShape… (2 × 2, then 3 × 3, then M × N). Bridge holds the renderer as a field on the shape. Add a color: one class. Add a renderer: one class. Not M × N.

Java

interface Renderer { void drawCircle(int x, int y, int r); }
class SvgRenderer    implements Renderer { public void drawCircle(int x,int y,int r){ /* svg */ } }
class CanvasRenderer implements Renderer { public void drawCircle(int x,int y,int r){ /* canvas */ } }

abstract class Shape {
  protected final Renderer renderer;
  Shape(Renderer r) { this.renderer = r; }
  abstract void draw();
}
class Circle extends Shape {
  private final int x,y,r;
  Circle(Renderer rend, int x,int y,int r){ super(rend); this.x=x; this.y=y; this.r=r; }
  void draw() { renderer.drawCircle(x,y,r); }
}

Use when

Two orthogonal axes of change (message type × transport, format × storage, shape × renderer).

TradeoffConfused with Adapter constantly — Adapter fixes an existing mismatch; Bridge is a design decision made upfront to prevent one.
Structural

Composite

Treat a group of things the same way you treat one thing. Trees, in one word.

Folder Folder File a.txt File b.txt file file

How it works

Leaves and containers share an interface, so calling size() on a file just returns its bytes; calling it on a folder recursively sums its children. The caller doesn't need to know which is which.

Java

interface Node { long size(); }
record File(String name, long bytes) implements Node {
  public long size() { return bytes; }
}
class Folder implements Node {
  private final List<Node> children = new ArrayList<>();
  public void add(Node n) { children.add(n); }
  public long size() { return children.stream().mapToLong(Node::size).sum(); }
}

Use when

Tree-shaped data: filesystems, UI widgets, org charts, expression ASTs.

TradeoffOne interface for leaves and groups means either leaves get useless methods (add()?) or the interface is minimal and callers need casts.
Structural

Decorator

Wrap an object to add behaviour without changing its class. Middleware, essentially.

Core Coffee + milk milk(coffee) + sugar sugar(…)

How it works

Both wrapper and wrapped implement the same interface. The wrapper forwards to the inner object, doing extra work before or after. Stack as many wrappers as you like — logging → auth → rate-limit → handler is exactly this pattern.

Java

interface Coffee { int cost(); }
class Espresso implements Coffee { public int cost() { return 3; } }

abstract class CoffeeDecorator implements Coffee {
  protected final Coffee inner;
  CoffeeDecorator(Coffee c) { this.inner = c; }
}
class Milk  extends CoffeeDecorator { Milk(Coffee c){super(c);}  public int cost(){ return inner.cost() + 1; } }
class Sugar extends CoffeeDecorator { Sugar(Coffee c){super(c);} public int cost(){ return inner.cost() + 1; } }

Coffee order = new Sugar(new Milk(new Espresso())); // cost = 5

Use when

You want to add optional, stackable behaviour without touching the original class or writing 2ⁿ subclasses.

TradeoffDebugging a 5-deep wrapper stack is annoying. Ordering matters — sugar-before-milk is not the same as milk-before-sugar.
Structural

Facade

Hide a messy subsystem behind one simple method. The "just do it" pattern.

Client OrderFacade Inventory Payment Shipping

How it works

Bundle a common workflow across many classes into one method with an obvious name. Callers stop coordinating a dozen collaborators; the facade does. Every SDK's client.doThing() is a facade over dozens of moving parts.

Java

class OrderFacade {
  private final Inventory inv;
  private final Payment pay;
  private final Shipping ship;
  OrderFacade(Inventory i, Payment p, Shipping s) { inv=i; pay=p; ship=s; }

  public String placeOrder(String userId, String sku, String card) {
    inv.reserve(sku);
    String txn = pay.charge(userId, card, inv.price(sku));
    return ship.dispatch(userId, sku, txn);
  }
}
// new OrderFacade(inv, pay, ship).placeOrder("u1","widget","tok_visa");

Use when

The 80% workflow through a subsystem is always the same and callers shouldn't have to memorise it.

TradeoffFacades can hide too much. Keep the underlying classes usable directly — the facade is a shortcut, not a jail.
Structural

Flyweight

Share the parts that don't change, so a million objects fit in the memory of ten.

GlyphPool "A" "B" "C"… pos(2,3) 'A' pos(8,3) 'A' pos(4,5) 'B' 1M chars · 26 glyphs

How it works

Split state into intrinsic (shared, immutable — the font glyph) and extrinsic (per-instance — where it sits on the page). Cache the intrinsic part and pass the extrinsic part in at use time. Renders a million-character document with 26 glyph objects instead of a million.

Java

class Glyph { final char c; Glyph(char c){ this.c = c; } void draw(int x, int y){ /*...*/ } }

class GlyphPool {
  private final Map<Character,Glyph> pool = new HashMap<>();
  Glyph of(char c) { return pool.computeIfAbsent(c, Glyph::new); }
}
// GlyphPool p = new GlyphPool();
// for (Char ch : doc) p.of(ch.value).draw(ch.x, ch.y); // 26 glyphs, N draws

Use when

Millions of objects that mostly repeat a small alphabet of underlying values (glyphs, tree species in a forest sim, particle sprites).

TradeoffOnly helps when instances share a lot. Immutable intrinsic state is non-negotiable — a mutation ripples to every user.
Structural

Proxy

A stand-in that looks like the real object but adds a check, a cache, or a lazy load.

Client ImageProxy check + cache RealImage

How it works

Proxy implements the same interface as the real object. Callers can't tell them apart. The proxy decides when — or whether — to touch the real thing. Four common flavours: virtual (lazy-load heavy resources), protection (auth check), remote (network stub), caching (memoise expensive calls).

Java

interface Image { void render(); }

class RealImage implements Image {
  private final byte[] pixels;
  RealImage(String path) { this.pixels = loadFromDisk(path); /* heavy */ }
  public void render() { /* paint */ }
}

class ImageProxy implements Image {
  private final String path;
  private RealImage real;
  ImageProxy(String path) { this.path = path; }
  public void render() {
    if (real == null) real = new RealImage(path); // lazy
    real.render();
  }
}

Use when

Real object is expensive, restricted, or remote — and you want the substitution to be transparent.

TradeoffAdds indirection. Callers can't tell why something is slow — is it the real object, or the proxy's check?
Group 3 of 3

Behavioral — how objects talk

These eleven answer: "who calls whom, in what order, and how do they stay decoupled?"

Behavioral

Chain of Responsibility

Pass a request down a line of handlers until one of them handles it.

Auth Rate limit Cache Handler

How it works

Each handler either processes the request or passes it to the next. Order the chain to your policy: auth first, cache last, handler at the end. Adding a step doesn't touch the others.

Java

abstract class Handler {
  protected Handler next;
  public Handler chain(Handler n) { this.next = n; return n; }
  public void handle(Request r) {
    if (canHandle(r)) process(r);
    else if (next != null) next.handle(r);
  }
  abstract boolean canHandle(Request r);
  abstract void process(Request r);
}
// new AuthHandler().chain(new RateLimitHandler()).chain(new CacheHandler()).chain(new AppHandler());
// entry.handle(request);

Use when

Preprocessing pipelines: web middleware, logging filters, event routing where any handler may claim the event.

TradeoffNobody handles the request? Silent drop. Always end the chain with a default handler that logs and fails loudly.
Behavioral

Command

Wrap an action as an object so you can queue it, log it, undo it.

User DeleteCommand execute() / undo() Document

How it works

Each user action becomes an object with execute() and (optionally) undo(). Push executed commands onto a stack — Ctrl+Z pops and undoes. Serialise them and you have a job queue.

Java

interface Command { void execute(); void undo(); }

class DeleteText implements Command {
  private final Document doc; private final int from, to;
  private String removed;
  DeleteText(Document d, int f, int t){ doc=d; from=f; to=t; }
  public void execute() { removed = doc.cut(from, to); }
  public void undo()    { doc.insert(from, removed); }
}
// history.push(cmd); cmd.execute();
// on Ctrl+Z: history.pop().undo();

Use when

Undo/redo, task queues, macro recording, transactional workflows.

TradeoffUndo requires capturing enough state to reverse the action — that state grows fast in a heavy editor.
Behavioral

Interpreter

Build a tiny language: parse expressions into a tree, then evaluate the tree.

Plus Num 3 Times Num 4 Num 5 "3 + 4 * 5" = 23

How it works

Every grammar rule is a class. Numbers, variables, operators — each implements eval(). Parse the input into a tree of these nodes, then call eval() on the root. This is how the tiny DSL for feature flags, search filters, or spreadsheet formulas gets built.

Java

interface Expr { int eval(Map<String,Integer> ctx); }
record Num(int v)            implements Expr { public int eval(Map<String,Integer> c){ return v; } }
record Var(String name)      implements Expr { public int eval(Map<String,Integer> c){ return c.get(name); } }
record Plus(Expr a, Expr b)  implements Expr { public int eval(Map<String,Integer> c){ return a.eval(c) + b.eval(c); } }
record Times(Expr a, Expr b) implements Expr { public int eval(Map<String,Integer> c){ return a.eval(c) * b.eval(c); } }
// 3 + 4 * 5 → new Plus(new Num(3), new Times(new Num(4), new Num(5))).eval(Map.of()) == 23

Use when

Small, stable grammar you own — search predicates, targeting rules, calculator engines.

TradeoffReal languages need real parsers (ANTLR, hand-rolled). Interpreter is the OOP form for tiny DSLs — don't reach for it to reimplement SQL.
Behavioral

Iterator

Walk a collection without exposing how it's stored.

a b c d it.next() it.hasNext()

How it works

The collection hands out an iterator object that knows how to walk itself — array, linked list, tree, database cursor, all the same to the caller. for (x : xs) in Java desugars to exactly this.

Java

class Range implements Iterable<Integer> {
  private final int from, to;
  Range(int f, int t) { from=f; to=t; }
  public Iterator<Integer> iterator() {
    return new Iterator<>() {
      int i = from;
      public boolean hasNext() { return i < to; }
      public Integer  next()   { return i++; }
    };
  }
}
for (int n : new Range(0, 5)) System.out.println(n); // 0..4

Use when

Custom collection or lazy sequence (paged results, DB rows, infinite streams).

TradeoffIn Java, streams and Iterable already exist — reach for a hand-rolled iterator only for lazy or paged sequences.
Behavioral

Mediator

Objects talk to a central hub instead of each other. N² wires become N.

ChatRoom Alice Bob Carol Dan Eve Frank

How it works

Each participant knows only the mediator. To broadcast, send to the mediator; it fans out. Removes the mesh of direct references — add a new participant without editing the others.

Java

class ChatRoom {
  private final List<User> users = new ArrayList<>();
  public void join(User u) { users.add(u); u.room = this; }
  public void send(User from, String msg) {
    for (User u : users) if (u != from) u.receive(from.name, msg);
  }
}
class User {
  final String name; ChatRoom room;
  User(String n) { this.name = n; }
  void say(String m) { room.send(this, m); }
  void receive(String from, String m) { System.out.println(from + ": " + m); }
}

Use when

Many-to-many communication (UI form fields validating each other, chat, event bus in a small scope).

TradeoffMediator becomes a god object if the protocol grows. Split it or move to event bus + typed events.
Behavioral

Memento

Snapshot an object's state, tuck it away, restore it later. Undo without exposing internals.

Editor save() Snapshot t0 Snapshot t1 Snapshot t2 restore() Editor

How it works

The object emits an opaque memento (its state, no methods). A caretaker stores mementos. Later, hand one back to the object to restore. The caretaker never peeks inside — encapsulation preserved.

Java

class Editor {
  private String text = "";
  public record Memento(String snapshot) {}
  public Memento save() { return new Memento(text); }
  public void restore(Memento m) { this.text = m.snapshot; }
  public void type(String s) { text += s; }
}
Editor e = new Editor();
e.type("hello ");
Editor.Memento snap = e.save();
e.type("world!");
e.restore(snap); // back to "hello "

Use when

Undo/redo with rich state; checkpoint/rollback semantics.

TradeoffEvery snapshot costs memory. Big documents: store diffs instead of full copies.
Behavioral

Observer

Publish a change once, and everyone who cares gets notified. The pub/sub of OOP.

Subject notify Logger EmailSender Metrics

How it works

Observers register with a subject. When the subject's state changes, it calls each observer's update(). Loose coupling — the subject has no idea who listens or what they'll do.

Java

interface Observer<T> { void update(T event); }

class OrderEvents {
  private final List<Observer<String>> subs = new ArrayList<>();
  public void subscribe(Observer<String> o) { subs.add(o); }
  public void publish(String event) { for (Observer<String> o : subs) o.update(event); }
}
OrderEvents bus = new OrderEvents();
bus.subscribe(e -> log.info(e));
bus.subscribe(e -> email.send("ops@co", e));
bus.publish("order.placed:1234");

Use when

One event, many reactions; UI models notifying views; in-process event bus.

TradeoffCascading notifications can loop; a slow observer stalls the whole notify. For cross-process events, use a real queue.
Behavioral

State

The object's behaviour changes with its state — because state is its own class.

Draft submit Review approve Published

How it works

Instead of a fat if (state == ...) ladder in every method, each state is a class implementing the same interface. The context delegates to its current state object; transitions swap the object out.

Java

interface State { State submit(Post p); State approve(Post p); }
class Draft     implements State { public State submit(Post p){ return new Review(); }   public State approve(Post p){ throw new IllegalStateException(); } }
class Review    implements State { public State submit(Post p){ throw new IllegalStateException(); } public State approve(Post p){ return new Published(); } }
class Published implements State { public State submit(Post p){ throw new IllegalStateException(); } public State approve(Post p){ throw new IllegalStateException(); } }

class Post {
  private State state = new Draft();
  public void submit()  { state = state.submit(this); }
  public void approve() { state = state.approve(this); }
}

Use when

An object has a clear lifecycle with rules on which transitions are allowed (order, subscription, document workflow).

TradeoffEach state = a class. Small state machines are cleaner as a switch on an enum.
Behavioral

Strategy

Pluggable algorithm. Swap the "how" without changing the "what".

SortContext QuickSort MergeSort RadixSort picks one

How it works

Family of algorithms, all behind one interface. The context holds a reference to one and calls it; the caller can swap. This is the pattern once you learn it — validators, pricing rules, ranking, compression, retry policies all fit.

Java

interface PricingStrategy { int price(int cents, User u); }
class FullPrice   implements PricingStrategy { public int price(int c, User u){ return c; } }
class MemberPrice implements PricingStrategy { public int price(int c, User u){ return c * 90 / 100; } }
class BlackFriday implements PricingStrategy { public int price(int c, User u){ return c / 2; } }

class Checkout {
  private PricingStrategy strategy;
  public void setStrategy(PricingStrategy s) { this.strategy = s; }
  public int total(int c, User u) { return strategy.price(c, u); }
}

Use when

Multiple interchangeable ways to do the same thing, chosen at runtime.

TradeoffAdds an interface + a class per strategy. For two branches, a lambda or enum is enough.
Behavioral

Template Method

A base class fixes the skeleton; subclasses fill in the interesting bits.

run(): open() → work() → close() open() [common] work() [override] close() [common] subclass overrides only the middle step

How it works

Abstract class implements the algorithm as a final method calling smaller steps. Steps that vary are abstract; subclasses override them. Framework code, essentially — Spring's JdbcTemplate, HttpServlet.service(), JUnit's setUp/tearDown.

Java

abstract class Report {
  public final String render() {
    return header() + body() + footer();
  }
  protected String header() { return "==== Report ====\n"; }
  protected String footer() { return "\n==== End ====\n"; }
  protected abstract String body();
}
class SalesReport extends Report {
  protected String body() { return "Sales: $1,234"; }
}
new SalesReport().render();

Use when

Fixed workflow with a few hot spots that vary. Inversion of control — base calls sub, not the other way around.

TradeoffLocks you into inheritance. Composition (Strategy) is more flexible when the varying step is more than a hook.
Behavioral

Visitor

Add a new operation across a class hierarchy without editing the classes.

Circle Square Triangle AreaVisitor SvgExportVisitor CollisionVisitor

How it works

Each element in the hierarchy has an accept(Visitor v) method that calls back v.visit(this). Add a new operation = one new visitor class, no edits to the elements. The trick: double-dispatch picks the right visit() based on the concrete element type.

Java

interface Shape { double accept(ShapeVisitor v); }
record Circle(double r) implements Shape { public double accept(ShapeVisitor v){ return v.visit(this); } }
record Square(double s) implements Shape { public double accept(ShapeVisitor v){ return v.visit(this); } }

interface ShapeVisitor { double visit(Circle c); double visit(Square s); }

class Area implements ShapeVisitor {
  public double visit(Circle c) { return Math.PI * c.r() * c.r(); }
  public double visit(Square s) { return s.s() * s.s(); }
}
// new Circle(3).accept(new Area()); // 28.27
// Add PerimeterVisitor: no touching Circle/Square.

Use when

Stable set of element classes, growing set of operations (ASTs, IR passes in a compiler).

TradeoffInverse: adding a new element type forces every visitor to change. Pick Visitor only when the element hierarchy is closed.