Table of Contents

Overview & Classification

Design patterns are reusable solutions to common software design problems. They aren't code you copy-paste — they're templates for solving recurring structural and behavioral challenges. The "Gang of Four" (GoF) cataloged 23 patterns in 1994; many more have emerged since.

When NOT to use patterns
Patterns add indirection. If the problem is simple, use simple code. Don't introduce a Strategy pattern for two if branches. The cure shouldn't be worse than the disease.

GoF Catalog (23 Patterns)

CategoryPatternPurpose (one line)
CreationalSingletonEnsure exactly one instance
Factory MethodDefer instantiation to subclasses
Abstract FactoryCreate families of related objects
BuilderConstruct complex objects step-by-step
PrototypeClone existing objects
StructuralAdapterConvert one interface to another
BridgeSeparate abstraction from implementation
CompositeTree structures with uniform interface
DecoratorAdd behavior dynamically
FacadeSimplified interface to subsystem
FlyweightShare common state to save memory
ProxyControl access to another object
BehavioralChain of ResponsibilityPass request along handler chain
CommandEncapsulate request as object
IteratorSequential access without exposing internals
MediatorReduce direct dependencies between objects
MementoCapture/restore object state
ObserverNotify dependents of state changes
StateBehavior changes with internal state
StrategyInterchangeable algorithms
Template MethodAlgorithm skeleton, subclasses fill steps
VisitorAdd operations without modifying classes
InterpreterGrammar representation and evaluation

Beyond GoF Modern

Patterns that emerged from microservices, DDD, and cloud-native development:

Creational Patterns

Control how objects are created, hiding creation logic and making the system independent of how objects are composed.

Singleton

Intent: Ensure a class has exactly one instance and provide a global access point.

Java — Enum Singleton (recommended)

public enum DatabaseConnection {
    INSTANCE;

    private final Connection conn;

    DatabaseConnection() {
        conn = DriverManager.getConnection("jdbc:...");
    }

    public Connection getConnection() { return conn; }
}

// Usage
DatabaseConnection.INSTANCE.getConnection();
Java — Double-checked locking (legacy)
public class Singleton {
    private static volatile Singleton instance;

    private Singleton() {}

    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

Python — Module-level singleton

# db.py — the module IS the singleton
_connection = None

def get_connection():
    global _connection
    if _connection is None:
        _connection = create_connection("jdbc:...")
    return _connection

Go — sync.Once

var (
    instance *Database
    once     sync.Once
)

func GetDB() *Database {
    once.Do(func() {
        instance = &Database{conn: connect("...")}
    })
    return instance
}
Why Singleton is controversial
  • Hidden dependencies — callers don't declare what they need
  • Hard to test — can't substitute mock implementations easily
  • Thread safety complexity (Java double-checked locking)
Modern alternative: Use dependency injection. Pass the single instance via constructors.

Factory Method

Intent: Define an interface for creating objects, but let subclasses decide which class to instantiate.

// Product interface
interface Notification {
    void send(String message);
}

class EmailNotification implements Notification {
    public void send(String msg) { /* send email */ }
}

class SMSNotification implements Notification {
    public void send(String msg) { /* send SMS */ }
}

// Factory method
abstract class NotificationFactory {
    abstract Notification createNotification();

    public void notify(String msg) {
        Notification n = createNotification();
        n.send(msg);
    }
}

class EmailFactory extends NotificationFactory {
    Notification createNotification() { return new EmailNotification(); }
}

Python — Registry pattern (more Pythonic)

class Notification:
    _registry: dict[str, type] = {}

    def __init_subclass__(cls, type_key: str = "", **kwargs):
        super().__init_subclass__(**kwargs)
        if type_key:
            Notification._registry[type_key] = cls

    @classmethod
    def create(cls, type_key: str) -> "Notification":
        return cls._registry[type_key]()

class EmailNotification(Notification, type_key="email"):
    def send(self, msg): ...

class SMSNotification(Notification, type_key="sms"):
    def send(self, msg): ...

# Usage
notifier = Notification.create("email")

Abstract Factory

Intent: Create families of related objects without specifying concrete classes.

// Abstract products
interface Button { void render(); }
interface Checkbox { void render(); }

// Concrete products: macOS
class MacButton implements Button { public void render() { /* macOS button */ } }
class MacCheckbox implements Checkbox { public void render() { /* macOS checkbox */ } }

// Concrete products: Windows
class WinButton implements Button { public void render() { /* Windows button */ } }
class WinCheckbox implements Checkbox { public void render() { /* Windows checkbox */ } }

// Abstract factory
interface UIFactory {
    Button createButton();
    Checkbox createCheckbox();
}

class MacFactory implements UIFactory {
    public Button createButton() { return new MacButton(); }
    public Checkbox createCheckbox() { return new MacCheckbox(); }
}

class WinFactory implements UIFactory {
    public Button createButton() { return new WinButton(); }
    public Checkbox createCheckbox() { return new WinCheckbox(); }
}
AspectFactory MethodAbstract Factory
CreatesOne productFamily of products
MechanismInheritance (subclass overrides)Composition (factory object)
Use whenOne product variesMultiple related products vary together

Builder

Intent: Construct complex objects step-by-step, separating construction from representation.

Java — Fluent Builder

public class HttpRequest {
    private final String url;
    private final String method;
    private final Map<String, String> headers;
    private final String body;
    private final Duration timeout;

    private HttpRequest(Builder b) {
        this.url = b.url;
        this.method = b.method;
        this.headers = Map.copyOf(b.headers);
        this.body = b.body;
        this.timeout = b.timeout;
    }

    public static class Builder {
        private final String url;           // required
        private String method = "GET";      // optional with default
        private Map<String, String> headers = new HashMap<>();
        private String body;
        private Duration timeout = Duration.ofSeconds(30);

        public Builder(String url) { this.url = url; }

        public Builder method(String m)  { this.method = m; return this; }
        public Builder header(String k, String v) { headers.put(k, v); return this; }
        public Builder body(String b)    { this.body = b; return this; }
        public Builder timeout(Duration t) { this.timeout = t; return this; }

        public HttpRequest build() { return new HttpRequest(this); }
    }
}

// Usage
var req = new HttpRequest.Builder("https://api.example.com")
    .method("POST")
    .header("Content-Type", "application/json")
    .body("{\"key\": \"value\"}")
    .timeout(Duration.ofSeconds(10))
    .build();

Go — Functional Options (idiomatic)

type Server struct {
    host    string
    port    int
    timeout time.Duration
    maxConn int
}

type Option func(*Server)

func WithPort(p int) Option       { return func(s *Server) { s.port = p } }
func WithTimeout(t time.Duration) Option { return func(s *Server) { s.timeout = t } }
func WithMaxConn(n int) Option    { return func(s *Server) { s.maxConn = n } }

func NewServer(host string, opts ...Option) *Server {
    s := &Server{
        host:    host,
        port:    8080,        // defaults
        timeout: 30 * time.Second,
        maxConn: 100,
    }
    for _, opt := range opts {
        opt(s)
    }
    return s
}

// Usage
srv := NewServer("localhost",
    WithPort(9090),
    WithTimeout(10*time.Second),
)
Go's functional options = Builder pattern
This is the idiomatic Go equivalent of the Builder. No need for a separate Builder struct.

Prototype

Intent: Create new objects by cloning existing ones, avoiding expensive initialization.

public class GameUnit implements Cloneable {
    private String type;
    private int health;
    private List<String> abilities;

    @Override
    public GameUnit clone() {
        try {
            GameUnit copy = (GameUnit) super.clone();
            copy.abilities = new ArrayList<>(this.abilities); // deep copy
            return copy;
        } catch (CloneNotSupportedException e) {
            throw new AssertionError();
        }
    }
}

// Better: copy constructor
public GameUnit(GameUnit other) {
    this.type = other.type;
    this.health = other.health;
    this.abilities = new ArrayList<>(other.abilities);
}

Python

import copy

prototype = {"type": "warrior", "health": 100, "abilities": ["slash", "block"]}
unit = copy.deepcopy(prototype)  # full independent copy
unit["health"] = 80
Java Cloneable is broken
Object.clone() does a shallow copy. Prefer copy constructors or a static from() factory method.

Structural Patterns

Deal with object composition — how classes and objects are assembled to form larger structures.

Adapter

Intent: Convert one interface to another that clients expect.

// Legacy interface we can't change
class LegacyPrinter {
    void printDocument(String text) { System.out.println(text); }
}

// Target interface clients expect
interface ModernPrinter {
    void print(Document doc);
}

// Adapter
class PrinterAdapter implements ModernPrinter {
    private final LegacyPrinter legacy;

    PrinterAdapter(LegacyPrinter legacy) { this.legacy = legacy; }

    @Override
    public void print(Document doc) {
        legacy.printDocument(doc.getText());
    }
}
Go interfaces = natural adapters
Go's implicit interfaces mean any type that has the right methods automatically satisfies the interface. No explicit adapter class needed in many cases.

Bridge

Intent: Decouple an abstraction from its implementation so both can vary independently.

// Implementation hierarchy
interface Renderer {
    void renderCircle(float radius);
}

class VectorRenderer implements Renderer {
    public void renderCircle(float r) { System.out.println("Vector circle r=" + r); }
}

class RasterRenderer implements Renderer {
    public void renderCircle(float r) { System.out.println("Raster circle r=" + r); }
}

// Abstraction hierarchy
abstract class Shape {
    protected Renderer renderer;
    Shape(Renderer r) { this.renderer = r; }
    abstract void draw();
}

class Circle extends Shape {
    private float radius;
    Circle(Renderer r, float radius) { super(r); this.radius = radius; }
    void draw() { renderer.renderCircle(radius); }
}

// Usage: mix and match
new Circle(new VectorRenderer(), 5).draw();
new Circle(new RasterRenderer(), 5).draw();

When to use: You'd otherwise have a cartesian explosion of subclasses (Shape x Renderer = too many classes).

Composite

Intent: Compose objects into tree structures and treat individual/composite objects uniformly.

from abc import ABC, abstractmethod

class FileSystemNode(ABC):
    def __init__(self, name: str):
        self.name = name

    @abstractmethod
    def size(self) -> int: ...

    @abstractmethod
    def display(self, indent: int = 0): ...

class File(FileSystemNode):
    def __init__(self, name: str, size: int):
        super().__init__(name)
        self._size = size

    def size(self) -> int:
        return self._size

    def display(self, indent=0):
        print(" " * indent + f"📄 {self.name} ({self._size}B)")

class Directory(FileSystemNode):
    def __init__(self, name: str):
        super().__init__(name)
        self.children: list[FileSystemNode] = []

    def add(self, node: FileSystemNode):
        self.children.append(node)
        return self

    def size(self) -> int:
        return sum(child.size() for child in self.children)

    def display(self, indent=0):
        print(" " * indent + f"📁 {self.name} ({self.size()}B)")
        for child in self.children:
            child.display(indent + 2)

Decorator

Intent: Attach additional behavior to objects dynamically, without altering their class.

Java — I/O Streams (classic example)

// Each wrapper adds behavior
InputStream in = new BufferedInputStream(     // adds buffering
    new GZIPInputStream(                       // adds decompression
        new FileInputStream("data.gz")         // base stream
    )
);

// Custom decorator
interface DataSource {
    String readData();
    void writeData(String data);
}

class EncryptionDecorator implements DataSource {
    private final DataSource wrapped;

    EncryptionDecorator(DataSource source) { this.wrapped = source; }

    public String readData() {
        return decrypt(wrapped.readData());
    }

    public void writeData(String data) {
        wrapped.writeData(encrypt(data));
    }
}

Python — Language-level decorators

import functools
import time

def retry(max_attempts=3, delay=1.0):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(max_attempts):
                try:
                    return func(*args, **kwargs)
                except Exception:
                    if attempt == max_attempts - 1:
                        raise
                    time.sleep(delay)
        return wrapper
    return decorator

@retry(max_attempts=3, delay=0.5)
def fetch_data(url: str):
    return requests.get(url).json()

Go — HTTP Middleware

func Logging(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        next.ServeHTTP(w, r)
        log.Printf("%s %s %v", r.Method, r.URL.Path, time.Since(start))
    })
}

func Auth(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        if !isAuthenticated(r) {
            http.Error(w, "unauthorized", 401)
            return
        }
        next.ServeHTTP(w, r)
    })
}

// Stack decorators
handler := Logging(Auth(myHandler))
AspectDecoratorInheritance
WhenRuntimeCompile time
CombinationsMix and match freelyClass explosion
TransparencySame interfaceNew class
ComplexityMany small objectsDeep hierarchies

Facade

Intent: Provide a simplified interface to a complex subsystem.

// Complex subsystem
class VideoCodec { /* ... */ }
class AudioMixer { /* ... */ }
class BitrateReader { /* ... */ }
class VideoFile { /* ... */ }

// Facade
class VideoConverter {
    public File convert(String filename, String format) {
        VideoFile file = new VideoFile(filename);
        VideoCodec codec = CodecFactory.extract(file);
        byte[] rawVideo = BitrateReader.read(file, codec);
        byte[] audio = AudioMixer.extract(file);
        return Muxer.combine(rawVideo, audio, format);
    }
}

// Client only needs this:
var converter = new VideoConverter();
File mp4 = converter.convert("video.ogg", "mp4");

Flyweight

Intent: Share common state across many objects to minimize memory usage.

// Flyweight: shared tree type data
class TreeType {
    final String name;
    final String color;
    final byte[] texture;  // expensive shared data

    TreeType(String name, String color, byte[] texture) {
        this.name = name; this.color = color; this.texture = texture;
    }

    void draw(int x, int y) { /* render at position */ }
}

// Factory ensures sharing
class TreeTypeFactory {
    private static Map<String, TreeType> cache = new HashMap<>();

    static TreeType get(String name, String color) {
        String key = name + ":" + color;
        return cache.computeIfAbsent(key,
            k -> new TreeType(name, color, loadTexture(name)));
    }
}

// Context: unique per-tree data
class Tree {
    int x, y;          // extrinsic (unique) state
    TreeType type;      // intrinsic (shared) state
}
Flyweight in the wild
  • Java Integer.valueOf() caches -128 to 127
  • Python caches small ints (-5 to 256) and interned strings
  • String pools in Java / Go

Proxy

Intent: Provide a surrogate or placeholder to control access to another object.

TypePurposeExample
VirtualLazy initializationLoad image only when displayed
ProtectionAccess controlCheck permissions before operation
RemoteNetwork transparencyRPC stub, gRPC client
CachingCache resultsMemoize expensive computation
LoggingRecord operationsAudit trail
class CachingProxy:
    def __init__(self, service):
        self._service = service
        self._cache: dict[str, Any] = {}

    def fetch(self, key: str):
        if key not in self._cache:
            self._cache[key] = self._service.fetch(key)
        return self._cache[key]

Behavioral Patterns

Define how objects communicate and distribute responsibility.

Chain of Responsibility

Intent: Pass a request along a chain of handlers; each decides to process or pass it on.

abstract class Handler {
    private Handler next;

    Handler setNext(Handler h) { this.next = h; return h; }

    void handle(Request req) {
        if (next != null) next.handle(req);
    }
}

class AuthHandler extends Handler {
    void handle(Request req) {
        if (!req.isAuthenticated()) {
            throw new UnauthorizedException();
        }
        super.handle(req);  // pass to next
    }
}

class RateLimitHandler extends Handler {
    void handle(Request req) {
        if (isRateLimited(req.getIp())) {
            throw new TooManyRequestsException();
        }
        super.handle(req);
    }
}

class LoggingHandler extends Handler {
    void handle(Request req) {
        log.info("Processing: {}", req);
        super.handle(req);
    }
}

// Build chain
Handler chain = new LoggingHandler();
chain.setNext(new AuthHandler())
     .setNext(new RateLimitHandler());
Real-world Chain of Responsibility
Servlet filters, Express.js middleware, Go chi/Echo middleware, Python Django middleware — all are this pattern.

Command

Intent: Encapsulate a request as an object, enabling undo/redo, queueing, and logging.

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

class AddTextCommand implements Command {
    private final Document doc;
    private final String text;
    private final int position;

    AddTextCommand(Document doc, String text, int position) {
        this.doc = doc; this.text = text; this.position = position;
    }

    public void execute() { doc.insert(position, text); }
    public void undo()    { doc.delete(position, text.length()); }
}

// Command history for undo/redo
class CommandHistory {
    private final Deque<Command> history = new ArrayDeque<>();

    void execute(Command cmd) {
        cmd.execute();
        history.push(cmd);
    }

    void undo() {
        if (!history.isEmpty()) {
            history.pop().undo();
        }
    }
}

Iterator

Intent: Provide sequential access to elements without exposing internal representation.

LanguageMechanismExample
JavaIterable<T> / Iterator<T>Enhanced for-loop
Python__iter__ / __next__for x in collection
Gorange, closures, channelsfor k, v := range m
JavaScriptSymbol.iteratorfor...of

Python — Generator as Iterator

def fibonacci(limit: int):
    a, b = 0, 1
    while a < limit:
        yield a
        a, b = b, a + b

for num in fibonacci(100):
    print(num)  # 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89

Mediator

Intent: Reduce chaotic many-to-many dependencies by introducing a central coordinator.

class EventBus:
    def __init__(self):
        self._handlers: dict[str, list[Callable]] = {}

    def subscribe(self, event: str, handler: Callable):
        self._handlers.setdefault(event, []).append(handler)

    def publish(self, event: str, data=None):
        for handler in self._handlers.get(event, []):
            handler(data)

# Components don't know about each other
bus = EventBus()
bus.subscribe("order_placed", inventory.reserve)
bus.subscribe("order_placed", email.send_confirmation)
bus.subscribe("order_placed", analytics.track)

Memento

Intent: Capture and externalize an object's state for later restoration, without violating encapsulation.

class Editor {
    private String content;
    private int cursorPos;

    // Creates memento
    Snapshot save() {
        return new Snapshot(content, cursorPos);
    }

    // Restores from memento
    void restore(Snapshot s) {
        this.content = s.content();
        this.cursorPos = s.cursorPos();
    }

    record Snapshot(String content, int cursorPos) {}
}

// Caretaker
Deque<Editor.Snapshot> history = new ArrayDeque<>();
history.push(editor.save());
// ... user makes changes ...
editor.restore(history.pop());  // undo

Observer

Intent: Define a one-to-many dependency so that when one object changes state, all dependents are notified.

interface EventListener {
    void update(String event, Object data);
}

class EventManager {
    private final Map<String, List<EventListener>> listeners = new HashMap<>();

    void subscribe(String event, EventListener listener) {
        listeners.computeIfAbsent(event, k -> new ArrayList<>()).add(listener);
    }

    void unsubscribe(String event, EventListener listener) {
        listeners.getOrDefault(event, List.of()).remove(listener);
    }

    void notify(String event, Object data) {
        for (var listener : listeners.getOrDefault(event, List.of())) {
            listener.update(event, data);
        }
    }
}

Go — Channels as observers

type Broker struct {
    subscribers map[string][]chan Event
    mu          sync.RWMutex
}

func (b *Broker) Subscribe(topic string) <-chan Event {
    ch := make(chan Event, 10)
    b.mu.Lock()
    b.subscribers[topic] = append(b.subscribers[topic], ch)
    b.mu.Unlock()
    return ch
}

func (b *Broker) Publish(topic string, e Event) {
    b.mu.RLock()
    defer b.mu.RUnlock()
    for _, ch := range b.subscribers[topic] {
        select {
        case ch <- e:
        default: // drop if full
        }
    }
}
PatternCouplingDeliveryUse case
ObserverSubject knows observersSynchronousUI events, in-process
Pub/SubDecoupled via brokerAsync possibleMicroservices, Kafka
Event BusCentral dispatcherUsually syncIn-app event system

State

Intent: Allow an object to alter its behavior when its internal state changes.

interface OrderState {
    void next(Order order);
    void cancel(Order order);
    String status();
}

class PendingState implements OrderState {
    public void next(Order o) { o.setState(new ProcessingState()); }
    public void cancel(Order o) { o.setState(new CancelledState()); }
    public String status() { return "PENDING"; }
}

class ProcessingState implements OrderState {
    public void next(Order o) { o.setState(new ShippedState()); }
    public void cancel(Order o) { throw new IllegalStateException("Cannot cancel"); }
    public String status() { return "PROCESSING"; }
}

class ShippedState implements OrderState {
    public void next(Order o) { o.setState(new DeliveredState()); }
    public void cancel(Order o) { throw new IllegalStateException("Already shipped"); }
    public String status() { return "SHIPPED"; }
}

class Order {
    private OrderState state = new PendingState();
    void setState(OrderState s) { this.state = s; }
    void next() { state.next(this); }
    void cancel() { state.cancel(this); }
    String status() { return state.status(); }
}

Strategy

Intent: Define a family of interchangeable algorithms encapsulated behind a common interface.

Java

interface CompressionStrategy {
    byte[] compress(byte[] data);
}

class ZipStrategy implements CompressionStrategy {
    public byte[] compress(byte[] data) { /* zip */ return data; }
}

class GzipStrategy implements CompressionStrategy {
    public byte[] compress(byte[] data) { /* gzip */ return data; }
}

class Compressor {
    private CompressionStrategy strategy;

    Compressor(CompressionStrategy s) { this.strategy = s; }
    void setStrategy(CompressionStrategy s) { this.strategy = s; }

    byte[] compress(byte[] data) { return strategy.compress(data); }
}

// Java shortcut: lambdas ARE strategies
Compressor c = new Compressor(data -> Arrays.copyOf(data, data.length));

Python — Functions as strategies

from typing import Callable

def sort_by_price(items: list[Product]) -> list[Product]:
    return sorted(items, key=lambda p: p.price)

def sort_by_rating(items: list[Product]) -> list[Product]:
    return sorted(items, key=lambda p: -p.rating)

def display_products(items: list[Product], strategy: Callable):
    for product in strategy(items):
        print(product)
First-class functions replace Strategy
In Python, Go, and modern Java (lambdas), you often don't need the full Strategy interface. A function type suffices.

Template Method

Intent: Define the skeleton of an algorithm, deferring specific steps to subclasses.

abstract class DataMiner {
    // Template method — final to prevent override
    public final void mine(String path) {
        var file = openFile(path);
        var data = extractData(file);
        var parsed = parseData(data);
        analyzeData(parsed);
        generateReport(parsed);
        closeFile(file);
    }

    abstract Object openFile(String path);
    abstract String extractData(Object file);
    abstract List<Record> parseData(String data);

    // Hook methods — optional override
    void analyzeData(List<Record> data) { /* default: no-op */ }
    void generateReport(List<Record> data) { /* default report */ }
    void closeFile(Object file) { /* default close */ }
}

class CSVDataMiner extends DataMiner {
    Object openFile(String path) { return new FileReader(path); }
    String extractData(Object file) { /* read CSV */ return "..."; }
    List<Record> parseData(String data) { /* parse CSV rows */ return List.of(); }
}

Visitor

Intent: Add new operations to existing object structures without modifying them (double dispatch).

// Element hierarchy
interface Shape {
    void accept(ShapeVisitor visitor);
}

record Circle(double radius) implements Shape {
    public void accept(ShapeVisitor v) { v.visit(this); }
}

record Rectangle(double w, double h) implements Shape {
    public void accept(ShapeVisitor v) { v.visit(this); }
}

// Visitor — add operations without modifying shapes
interface ShapeVisitor {
    void visit(Circle c);
    void visit(Rectangle r);
}

class AreaCalculator implements ShapeVisitor {
    double total = 0;
    public void visit(Circle c) { total += Math.PI * c.radius() * c.radius(); }
    public void visit(Rectangle r) { total += r.w() * r.h(); }
}

class SvgExporter implements ShapeVisitor {
    StringBuilder svg = new StringBuilder();
    public void visit(Circle c) { svg.append("<circle r='" + c.radius() + "'/>"); }
    public void visit(Rectangle r) { svg.append("<rect w='" + r.w() + "' h='" + r.h() + "'/>"); }
}
Python — functools.singledispatch alternative
from functools import singledispatch

@singledispatch
def area(shape):
    raise NotImplementedError

@area.register
def _(c: Circle):
    return math.pi * c.radius ** 2

@area.register
def _(r: Rectangle):
    return r.width * r.height

Modern & Architectural Patterns Modern

Patterns that emerged from DDD, microservices, and cloud-native architecture.

Dependency Injection

Intent: Invert control of object creation — pass dependencies in instead of creating them internally.

// WITHOUT DI — tightly coupled
class OrderService {
    private final PaymentGateway gateway = new StripeGateway();  // hard-coded!
}

// WITH DI — constructor injection
class OrderService {
    private final PaymentGateway gateway;

    OrderService(PaymentGateway gateway) {  // injected
        this.gateway = gateway;
    }
}

// In tests: inject mock
var service = new OrderService(new MockPaymentGateway());

Go — Constructor injection (no framework needed)

type OrderService struct {
    gateway PaymentGateway  // interface
    repo    OrderRepository // interface
}

func NewOrderService(gw PaymentGateway, repo OrderRepository) *OrderService {
    return &OrderService{gateway: gw, repo: repo}
}

// Production
svc := NewOrderService(stripe.New(), postgres.NewOrderRepo(db))
// Test
svc := NewOrderService(&mockGateway{}, &mockRepo{})
DI Frameworks
  • Java: Spring (@Autowired), Guice, Dagger
  • Python: dependency-injector, or just pass args
  • Go: wire (code-gen), or manual constructors (common)

Repository Pattern

Intent: Abstract data access behind a collection-like interface. Business logic never touches SQL/ORM directly.

// Repository interface
interface UserRepository {
    Optional<User> findById(UUID id);
    List<User> findByEmail(String email);
    User save(User user);
    void delete(UUID id);
}

// Production implementation
class JpaUserRepository implements UserRepository {
    private final EntityManager em;
    // ... JPA queries
}

// Test implementation
class InMemoryUserRepository implements UserRepository {
    private final Map<UUID, User> store = new HashMap<>();
    // ... map operations
}

Circuit Breaker Resilience

Intent: Prevent cascading failures by wrapping calls to external services with a fail-fast mechanism.

States: Closed (normal) → Open (failures hit threshold, fast-fail) → Half-Open (try one request to test recovery)

import time
from enum import Enum

class State(Enum):
    CLOSED = "closed"
    OPEN = "open"
    HALF_OPEN = "half_open"

class CircuitBreaker:
    def __init__(self, failure_threshold=5, recovery_timeout=30):
        self.state = State.CLOSED
        self.failures = 0
        self.threshold = failure_threshold
        self.recovery_timeout = recovery_timeout
        self.last_failure_time = 0

    def call(self, func, *args, **kwargs):
        if self.state == State.OPEN:
            if time.time() - self.last_failure_time > self.recovery_timeout:
                self.state = State.HALF_OPEN
            else:
                raise CircuitOpenError("Circuit is open")

        try:
            result = func(*args, **kwargs)
            self._on_success()
            return result
        except Exception as e:
            self._on_failure()
            raise

    def _on_success(self):
        self.failures = 0
        self.state = State.CLOSED

    def _on_failure(self):
        self.failures += 1
        self.last_failure_time = time.time()
        if self.failures >= self.threshold:
            self.state = State.OPEN
Libraries
Java: Resilience4j, Sentinel • Python: pybreaker, tenacity • Go: sony/gobreaker

CQRS Architecture

Intent: Separate the read model from the write model. Queries and commands have different data representations.

AspectSimple CRUDCQRS
ModelSingle model for read/writeSeparate read + write models
ConsistencyStrongEventual (usually)
ComplexityLowHigh
ScalingScale togetherScale independently
Best forSimple domainsComplex domains, high-read systems
// Command side — optimized for writes
class CreateOrderCommand {
    UUID customerId;
    List<OrderItem> items;
}

class OrderCommandHandler {
    void handle(CreateOrderCommand cmd) {
        var order = Order.create(cmd.customerId, cmd.items);
        orderRepo.save(order);
        eventBus.publish(new OrderCreatedEvent(order.getId()));
    }
}

// Query side — optimized for reads (denormalized)
class OrderSummaryQuery {
    UUID orderId;
}

class OrderQueryHandler {
    OrderSummaryDTO handle(OrderSummaryQuery q) {
        return readDb.findOrderSummary(q.orderId);  // flat, pre-joined view
    }
}

Event Sourcing Architecture

Intent: Store state changes as an immutable sequence of events rather than mutable current state.

// Events (immutable facts)
sealed interface AccountEvent {
    record Opened(UUID id, String owner, Instant at) implements AccountEvent {}
    record Deposited(UUID id, BigDecimal amount, Instant at) implements AccountEvent {}
    record Withdrawn(UUID id, BigDecimal amount, Instant at) implements AccountEvent {}
}

// Rebuild state by replaying events
class Account {
    UUID id;
    BigDecimal balance = BigDecimal.ZERO;

    static Account replay(List<AccountEvent> events) {
        var account = new Account();
        for (var event : events) {
            account.apply(event);
        }
        return account;
    }

    void apply(AccountEvent event) {
        switch (event) {
            case AccountEvent.Opened e -> { this.id = e.id(); }
            case AccountEvent.Deposited e -> { this.balance = balance.add(e.amount()); }
            case AccountEvent.Withdrawn e -> { this.balance = balance.subtract(e.amount()); }
        }
    }
}
Event Sourcing challenges
  • Schema evolution — old events must remain parseable as the schema changes
  • Replay performance — use snapshots for entities with many events
  • Eventual consistency — projections may lag behind writes

Saga Pattern Distributed

Intent: Manage distributed transactions across microservices using a sequence of local transactions with compensating actions.

StyleChoreographyOrchestration
ControlDecentralized (events)Central coordinator
CouplingLowOrchestrator knows all steps
ComplexityHard to traceEasier to understand
Best forSimple flows, few stepsComplex flows, many steps
# Orchestration saga
class OrderSaga:
    def execute(self, order):
        try:
            payment_id = payment_service.charge(order.total)
        except PaymentError:
            raise  # nothing to compensate

        try:
            inventory_service.reserve(order.items)
        except InventoryError:
            payment_service.refund(payment_id)   # compensate step 1
            raise

        try:
            shipping_service.schedule(order)
        except ShippingError:
            inventory_service.release(order.items)  # compensate step 2
            payment_service.refund(payment_id)      # compensate step 1
            raise

Sidecar / Ambassador / Anti-corruption Layer

PatternPurposeExample
SidecarAttach helper alongside main serviceEnvoy proxy for mTLS, logging agent, Dapr
AmbassadorProxy for remote service callsConnection pooling, retry logic, circuit breaking
Anti-corruption LayerIsolate legacy system interfacesTranslate between old/new domain models at boundary

Concurrency Patterns

Producer-Consumer

Intent: Decouple production of data from consumption using a buffer/queue.

Go — Channels

func producer(ch chan<- int) {
    for i := 0; i < 100; i++ {
        ch <- i
    }
    close(ch)
}

func consumer(id int, ch <-chan int, wg *sync.WaitGroup) {
    defer wg.Done()
    for item := range ch {
        process(item)
    }
}

func main() {
    ch := make(chan int, 10) // buffered
    var wg sync.WaitGroup

    go producer(ch)

    for i := 0; i < 4; i++ {
        wg.Add(1)
        go consumer(i, ch, &wg)
    }
    wg.Wait()
}

Java — BlockingQueue

BlockingQueue<Task> queue = new ArrayBlockingQueue<>(100);

// Producer thread
executor.submit(() -> {
    while (running) {
        queue.put(generateTask());  // blocks if full
    }
});

// Consumer threads
for (int i = 0; i < 4; i++) {
    executor.submit(() -> {
        while (running) {
            Task task = queue.take();  // blocks if empty
            task.process();
        }
    });
}

Worker Pool

Intent: Fixed number of workers processing tasks from a shared queue.

func workerPool(jobs <-chan Job, results chan<- Result, numWorkers int) {
    var wg sync.WaitGroup
    for i := 0; i < numWorkers; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            for job := range jobs {
                results <- job.Process()
            }
        }()
    }
    wg.Wait()
    close(results)
}

Python — concurrent.futures

from concurrent.futures import ThreadPoolExecutor, as_completed

def process_url(url: str) -> dict:
    return requests.get(url).json()

urls = ["https://api.example.com/1", "https://api.example.com/2", ...]

with ThreadPoolExecutor(max_workers=10) as pool:
    futures = {pool.submit(process_url, url): url for url in urls}
    for future in as_completed(futures):
        url = futures[future]
        result = future.result()  # raises if task failed

Fan-Out / Fan-In

Intent: Distribute work across multiple workers (fan-out), then merge results (fan-in).

func fanOut(input <-chan int, workers int) []<-chan int {
    channels := make([]<-chan int, workers)
    for i := 0; i < workers; i++ {
        channels[i] = worker(input)
    }
    return channels
}

func fanIn(channels ...<-chan int) <-chan int {
    merged := make(chan int)
    var wg sync.WaitGroup
    for _, ch := range channels {
        wg.Add(1)
        go func(c <-chan int) {
            defer wg.Done()
            for v := range c {
                merged <- v
            }
        }(ch)
    }
    go func() { wg.Wait(); close(merged) }()
    return merged
}

Reactor / Event Loop

Intent: Handle many concurrent I/O operations with a single thread dispatching events.

ImplementationLanguageModel
asyncioPythonSingle-threaded event loop + coroutines
Node.jsJavaScriptlibuv event loop + callbacks/promises
Java NIOJavaSelector + channels (Netty wraps this)
Go runtimeGoM:N scheduler (goroutines on OS threads)

Anti-Patterns

Patterns to recognize and avoid.

Anti-PatternSymptomsFix
God ObjectOne class does everything, 1000+ lines, imported everywhereSingle Responsibility Principle, extract classes
Singleton AbuseGlobal state everywhere, untestable, hidden dependenciesUse dependency injection
Premature AbstractionInterfaces with one implementation, "what if" abstractionsRule of three: wait until 3 uses to abstract
Golden HammerUsing the same pattern/tool for every problemChoose patterns based on the actual problem
Lava FlowDead code nobody dares delete, mysterious utilsDelete it. Git has history. Tests catch breakage.
Cargo CultCopying patterns without understanding whyUnderstand the problem before applying a pattern
Anemic DomainEntities are just data bags, all logic in servicesRich domain model: behavior belongs on the entity
Leaky AbstractionCallers must understand internals to use correctlyBetter encapsulation, expose clean contracts
The Pattern Trap
The most common anti-pattern is applying patterns where they aren't needed. Three lines of if/else don't need a Strategy. A simple class doesn't need a Builder. Use the simplest thing that works.

Pattern Selection Guide

Decision Table

I need to...Consider
Create objects without specifying exact classFactory Method, Abstract Factory
Build complex objects with many optionsBuilder
Ensure only one instance existsSingleton (or just DI)
Convert an incompatible interfaceAdapter
Add behavior without modifying existing codeDecorator, Visitor
Simplify a complex subsystem APIFacade
Represent tree/hierarchy structuresComposite
Swap algorithms at runtimeStrategy
React to state changes in another objectObserver, Event Bus
Implement undo/redoCommand + Memento
Handle requests through a pipelineChain of Responsibility
Change behavior based on stateState
Traverse collections uniformlyIterator
Handle distributed transactionsSaga
Prevent cascading failuresCircuit Breaker
Separate read/write concernsCQRS
Keep complete audit historyEvent Sourcing

Language Idioms That Replace Patterns

Some GoF patterns exist because of language limitations. Modern languages often have built-in support:

GoF PatternLanguage Feature That Replaces It
StrategyFirst-class functions (Python, Go, JS, Java lambdas)
IteratorGenerators (Python), range (Go), for...of (JS)
DecoratorPython @decorator, Go middleware, JS decorators
ObserverGo channels, JS EventEmitter, Python signals
SingletonPython modules, Go sync.Once
BuilderGo functional options, Python kwargs + dataclasses
AdapterGo implicit interfaces (structural typing)
CommandFirst-class functions / closures in any language
Template MethodPython ABC, Go interfaces + embedding
VisitorPython singledispatch, Java sealed + switch (21+)
When in doubt
Start without the pattern. Add it when the code tells you it's needed — not when you imagine it might be. The best code is the simplest code that solves the problem.