Design Patterns in Depth

The second tier of must-know patterns — Factory, Builder, Adapter, Decorator, Composite, Command, Chain of Responsibility — each with the problem it kills and where you already use it.

LLDdesign-patternscreationalstructuralbehavioral

The real-world analogy: prefab assembly and adapter plugs

Imagine assembling a physical office space:

  1. Prefab Assembly (Factory & Builder): Instead of mixing cement and forging glass on-site, you order pre-fabricated walls (Factory Method) from a supplier who shields you from the fabrication details. If you're buying a custom luxury desk, you use a modular catalog builder to select standard drawers, tabletops, and cord inserts step-by-step (Builder).
  2. Outlet Adapter (Adapter): You travel to the UK and find a 3-prong square outlet. Your laptop charger has a 2-prong round plug. You don't rebuild the wall or re-wire your charger. You insert a UK plug adapter—the plug stays the same, the outlet stays the same, the adapter bridges the gap.
  3. Gift Wrap (Decorator): You buy a book. To make it a premium gift, you wrap it in foil paper. Then you add a ribbon. Then you affix a greeting card. Each wrapping conforms to the standard shape of a box (the interface), but adds a new layer of appearance or function dynamically.
  4. Matryoshka Nesting Doll (Composite): You open a large wooden nesting doll. Inside are smaller dolls, and inside those are even smaller dolls. You can paint the exterior of the doll without caring if you're holding a leaf-level solid wood doll or a hollow shell containing other dolls.

Creational Patterns

Factory Method — "don't make callers know concrete class names"

Pain: code littered with new PdfExporter(), new CsvExporter() behind if (format == ...) chains — every new format edits every call site.

Python

Python
class ExporterFactory:
    _registry = {"pdf": PdfExporter, "csv": CsvExporter}

    @classmethod
    def create(cls, fmt: str) -> Exporter:
        if fmt not in cls._registry:
            raise ValueError(f"unknown format: {fmt}")
        return cls._registry[fmt]()          # caller never names a class

exporter = ExporterFactory.create(request.format)
exporter.export(report)                      # polymorphism does the rest

Java

Java
import java.util.HashMap;
import java.util.Map;
import java.util.function.Supplier;

public interface Exporter { void export(String data); }
public class PdfExporter implements Exporter { public void export(String d) {} }
public class CsvExporter implements Exporter { public void export(String d) {} }

public class ExporterFactory {
    private static final Map<String, Supplier<Exporter>> registry = new HashMap<>();
    static {
        registry.put("pdf", PdfExporter::new);
        registry.put("csv", CsvExporter::new);
    }
    public static Exporter create(String format) {
        Supplier<Exporter> supplier = registry.get(format);
        if (supplier == null) throw new IllegalArgumentException("Unknown format: " + format);
        return supplier.get();
    }
}

C++

C++
#include <string>
#include <memory>
#include <unordered_map>
#include <stdexcept>

class Exporter {
public:
    virtual ~Exporter() = default;
    virtual void exportData(const std::string& data) = 0;
};

class PdfExporter : public Exporter {
public:
    void exportData(const std::string& d) override {}
};

class CsvExporter : public Exporter {
public:
    void exportData(const std::string& d) override {}
};

class ExporterFactory {
public:
    static std::unique_ptr<Exporter> create(const std::string& format) {
        if (format == "pdf") return std::make_unique<PdfExporter>();
        if (format == "csv") return std::make_unique<CsvExporter>();
        throw std::invalid_argument("Unknown format: " + format);
    }
};

Builder — "constructors with nine arguments are unreadable"

Pain: Pizza(true, false, null, 12, null, true) — what do those mean? Which are optional?

Python

In Python, native keyword arguments (Pizza(cheese=True, size=12)) make Builder mostly redundant. Pointing this out is a major interview bonus.

Python
class Pizza:
    def __init__(self, size: int, cheese: bool = False, pepperoni: bool = False):
        self.size = size
        self.cheese = cheese
        self.pepperoni = pepperoni

pizza = Pizza(size=12, cheese=True) # named parameters

Java

Java
HttpRequest req = HttpRequest.newBuilder()       // standard Java SDK example
    .uri(URI.create("https://api.example.com/orders"))
    .timeout(Duration.ofSeconds(5))
    .header("Authorization", token)
    .POST(BodyPublishers.ofString(json))
    .build();                                    // validate once, emit immutable object

C++

C++
#include <string>

class Pizza {
public:
    int size = 0;
    bool cheese = false;
    bool pepperoni = false;
};

class PizzaBuilder {
private:
    Pizza pizza;
public:
    PizzaBuilder& setSize(int s) { pizza.size = s; return *this; }
    PizzaBuilder& addCheese() { pizza.cheese = true; return *this; }
    PizzaBuilder& addPepperoni() { pizza.pepperoni = true; return *this; }
    Pizza build() { return pizza; }
};

Structural Patterns

Adapter — "right behavior, wrong interface"

Pain: your code expects PaymentGateway.charge(amount); the new provider's SDK exposes submit_transaction(cents, currency, ref). You can't change their SDK, and you shouldn't change your 40 call sites.

Python

Python
class StripeAdapter(PaymentGateway):            # wraps theirs, speaks yours
    def __init__(self, sdk: StripeSDK):
        self._sdk = sdk

    def charge(self, amount: Money) -> Receipt:
        result = self._sdk.submit_transaction(
            cents=amount.to_cents(), currency=amount.currency, ref=new_ref()
        )
        return Receipt.from_stripe(result)       # translate the answer too

Java

Java
public interface PaymentGateway { void charge(double amount); }

public class StripeSDK {
    public void submitTransaction(int cents) {}
}

public class StripeAdapter implements PaymentGateway {
    private final StripeSDK sdk;
    
    public StripeAdapter(StripeSDK sdk) { this.sdk = sdk; }
    
    @Override
    public void charge(double amount) {
        sdk.submitTransaction((int)(amount * 100)); // convert dollars to cents
    }
}

C++

C++
class PaymentGateway {
public:
    virtual ~PaymentGateway() = default;
    virtual void charge(double amount) = 0;
};

class StripeSDK {
public:
    void submitTransaction(int cents) {}
};

class StripeAdapter : public PaymentGateway {
private:
    StripeSDK sdk;
public:
    StripeAdapter(StripeSDK s) : sdk(s) {}
    void charge(double amount) override {
        sdk.submitTransaction(static_cast<int>(amount * 100));
    }
};

Decorator — "add behavior without touching the class"

Pain: you need retries on the payment gateway. And logging. And metrics. Subclassing gives RetryingLoggingMeteredGateway — a combinatorial explosion.

Python

Python
class RetryingGateway(PaymentGateway):          # wraps ANY gateway
    def __init__(self, inner: PaymentGateway, attempts=3):
        self._inner, self._attempts = inner, attempts

    def charge(self, amount):
        for i in range(self._attempts):
            try:
                return self._inner.charge(amount)     # delegate
            except TransientError:
                backoff(i)
        raise

gateway = MeteredGateway(RetryingGateway(StripeAdapter(sdk)))   # stack at runtime

Java

Java
public interface PaymentGateway { void charge(double amount); }

public class LoggingGateway implements PaymentGateway {
    private final PaymentGateway inner;
    
    public LoggingGateway(PaymentGateway inner) { this.inner = inner; }
    
    @Override
    public void charge(double amount) {
        System.out.println("Starting transaction of " + amount);
        inner.charge(amount);
        System.out.println("Finished transaction.");
    }
}

C++

C++
#include <iostream>
#include <memory>

class PaymentGateway {
public:
    virtual ~PaymentGateway() = default;
    virtual void charge(double amount) = 0;
};

class LoggingGateway : public PaymentGateway {
private:
    std::shared_ptr<PaymentGateway> inner;
public:
    LoggingGateway(std::shared_ptr<PaymentGateway> in) : inner(in) {}
    void charge(double amount) override {
        std::cout << "Starting charge of " << amount << "\n";
        inner->charge(amount);
        std::cout << "Finished charge.\n";
    }
};

Same interface in, same interface out — so decorators stack in any order, chosen at runtime.


Composite — "treat one and many uniformly"

Pain: a folder contains files and folders; your size-calculator shouldn't care which it's holding.

Python

Python
class Node(ABC):
    @abstractmethod
    def size(self) -> int: ...

class File(Node):
    def size(self): return self._bytes

class Folder(Node):
    def __init__(self): self.children: list[Node] = []
    def size(self): return sum(c.size() for c in self.children)   # recursion!

Java

Java
import java.util.ArrayList;
import java.util.List;

public interface Node { int size(); }

public class FileNode implements Node {
    private final int bytes;
    public FileNode(int bytes) { this.bytes = bytes; }
    
    @Override
    public int size() { return bytes; }
}

public class FolderNode implements Node {
    private final List<Node> children = new ArrayList<>();
    
    public void add(Node node) { children.add(node); }
    
    @Override
    public int size() {
        return children.stream().mapToInt(Node::size).sum();
    }
}

C++

C++
#include <vector>
#include <memory>
#include <numeric>

class Node {
public:
    virtual ~Node() = default;
    virtual int size() const = 0;
};

class FileNode : public Node {
private:
    int bytes;
public:
    FileNode(int b) : bytes(b) {}
    int size() const override { return bytes; }
};

class FolderNode : public Node {
private:
    std::vector<std::shared_ptr<Node>> children;
public:
    void add(std::shared_ptr<Node> child) { children.push_back(child); }
    int size() const override {
        int total = 0;
        for (const auto& child : children) {
            total += child->size();
        }
        return total;
    }
};

Behavioral Patterns

Command — "turn an action into an object"

Pain: undo/redo, job queues, macros — anything where actions must be stored, not just executed immediately.

Python

Python
class Command(ABC):
    @abstractmethod
    def execute(self): ...
    @abstractmethod
    def undo(self): ...

class AddItemCommand(Command):
    def __init__(self, cart, item): self.cart, self.item = cart, item
    def execute(self): self.cart.add(self.item)
    def undo(self): self.cart.remove(self.item)

history.push(cmd); cmd.execute()        # undo = history.pop().undo()

Java

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

public class Cart {
    public void add(String item) {}
    public void remove(String item) {}
}

public class AddItemCommand implements Command {
    private final Cart cart;
    private final String item;
    
    public AddItemCommand(Cart cart, String item) {
        this.cart = cart;
        this.item = item;
    }
    
    @Override
    public void execute() { cart.add(item); }
    
    @Override
    public void undo() { cart.remove(item); }
}

C++

C++
#include <string>

class Command {
public:
    virtual ~Command() = default;
    virtual void execute() = 0;
    virtual void undo() = 0;
};

class Cart {
public:
    void add(const std::string& item) {}
    void remove(const std::string& item) {}
};

class AddItemCommand : public Command {
private:
    Cart& cart;
    std::string item;
public:
    AddItemCommand(Cart& c, const std::string& i) : cart(c), item(i) {}
    void execute() override { cart.add(item); }
    void undo() override { cart.remove(item); }
};

Chain of Responsibility — "let a pipeline decide who handles it"

Pain: a request must pass auth, then rate limiting, then validation, then the handler — without one god-function knowing all steps.

Python

Python
class Handler(ABC):
    def __init__(self): self._next = None
    def set_next(self, h): self._next = h; return h
    def handle(self, req):
        if self._next: return self._next.handle(req)

class AuthHandler(Handler):
    def handle(self, req):
        if not verify(req.token): raise Unauthorized()
        return super().handle(req)          # pass it on

Java

Java
public abstract class Handler {
    protected Handler next;
    
    public Handler setNext(Handler next) {
        this.next = next;
        return next;
    }
    
    public void handle(String request) {
        if (next != null) next.handle(request);
    }
}

public class AuthHandler extends Handler {
    @Override
    public void handle(String request) {
        if (request.contains("valid-token")) {
            super.handle(request);
        } else {
            throw new RuntimeException("Unauthorized");
        }
    }
}

C++

C++
#include <memory>
#include <string>
#include <stdexcept>

class Handler {
protected:
    std::shared_ptr<Handler> next;
public:
    virtual ~Handler() = default;
    std::shared_ptr<Handler> setNext(std::shared_ptr<Handler> n) {
        next = n;
        return next;
    }
    virtual void handle(const std::string& request) {
        if (next) next->handle(request);
    }
};

class AuthHandler : public Handler {
public:
    void handle(const std::string& request) override {
        if (request.find("valid-token") != std::string::npos) {
            Handler::handle(request);
        } else {
            throw std::runtime_error("Unauthorized");
        }
    }
};

Interactive Quiz

Check yourself0/3 answered

1.

2.

3.


The honest meta-lesson

Patterns are vocabulary, not virtue. The failure mode interviews screen for isn't ignorance — it's pattern fever: a Builder for a two-field object, an AbstractFactoryFactory. Every pattern adds indirection, and indirection must pay rent (the simplicity rule). Strong answers name the pain first, the pattern second, and the simpler alternative they rejected third. ("Keyword args would do, but this is Java, so: Builder.")


Practice — level up

You learn a pattern by building its shape, not memorizing its name. Each drill below is one of the second-tier patterns in disguise — spot it as you solve.

Practice ladder: Patterns in practice0/4 solved

Climb in order — every rung assumes the one above it. Solve on LeetCode, then tick it here; progress is saved on this device.

Warm-up — Adapter

  1. Expose one interface (stack) backed by another (queue) — Adapter in three methods.

Core — Composite & Command

Trees of objects; reversible operations.
  1. Treat leaves and branches uniformly — Composite plus an Iterator.

  2. Reversible forward/back operations over state — the Command / Memento shape.

Stretch — recursive structure behind a clean API

  1. A recursive Composite (trie) hidden behind add / search — structure the caller never sees.