Skip to content
AyoKoding

Intermediate

Examples 29-57 cover intermediate software architecture concepts (40-75% coverage). These examples build on foundational patterns and introduce composite architectural styles, enterprise patterns, and domain-driven design building blocks. Each example is self-contained and uses object-oriented idioms — Java is the canonical language; Kotlin, C#, and TypeScript equivalents appear in the tabbed code blocks.

Hexagonal Architecture and Clean Architecture

Example 29: Hexagonal Architecture — Ports and Adapters

Hexagonal architecture (also called Ports and Adapters) separates the application core from external systems by defining explicit ports (interfaces) and adapters (implementations). The application core knows nothing about databases, HTTP, or messaging — it only communicates through port interfaces. This inversion allows you to swap infrastructure without touching business logic.

%% Color Palette: Blue #0173B2, Orange #DE8F05, Teal #029E73, Purple #CC78BC, Brown #CA9161
graph LR
    A["HTTP Adapter<br/>(Driving)"]
    B["CLI Adapter<br/>(Driving)"]
    C["Application Core<br/>+ Ports"]
    D["DB Adapter<br/>(Driven)"]
    E["Email Adapter<br/>(Driven)"]
 
    A -->|UserPort| C
    B -->|UserPort| C
    C -->|UserRepository| D
    C -->|NotifierPort| E
 
    style A fill:#DE8F05,stroke:#000,color:#fff
    style B fill:#DE8F05,stroke:#000,color:#fff
    style C fill:#0173B2,stroke:#000,color:#fff
    style D fill:#029E73,stroke:#000,color:#fff
    style E fill:#029E73,stroke:#000,color:#fff
import java.util.HashMap;
import java.util.Map;
 
// => PORT: defines what the application NEEDS from storage
// => The core depends on this abstraction, never on a real DB class
interface UserRepository {
    void save(User user);           // => persistence contract
    User findById(String userId);   // => retrieval contract; null if not found
}
 
// => PORT: defines what the application NEEDS from notifications
interface NotifierPort {
    void sendWelcome(String email); // => notification contract
}
 
// => ENTITY: pure domain data, no infrastructure knowledge
class User {
    final String id;     // => domain identity
    final String email;  // => entity attribute, not a DB column
    final String name;   // => owned by the application core
 
    User(String id, String email, String name) {
        // => constructor enforces required fields
        this.id = id;
        this.email = email;
        this.name = name;
    }
}
 
// => APPLICATION SERVICE: orchestrates domain logic using ports only
// => Never imports JDBC, JavaMail, or HTTP — only port interfaces
class UserService {
    private final UserRepository repo;
    // => injected port — could be DB or in-memory
    private final NotifierPort notifier;
    // => injected port — could be email or SMS
 
    UserService(UserRepository repo, NotifierPort notifier) {
        this.repo = repo;
        this.notifier = notifier;
    }
 
    User register(String userId, String email, String name) {
        User user = new User(userId, email, name); // => create domain object
        repo.save(user);                            // => persist via port
        notifier.sendWelcome(email);               // => notify via port
        return user;                               // => return domain object, not DTO
    }
}
 
// => ADAPTER (driven): concrete implementation of UserRepository port
// => Lives in infrastructure layer — swappable without changing UserService
class InMemoryUserRepository implements UserRepository {
    private final Map<String, User> store = new HashMap<>();
    // => in-memory store for tests/demos
 
    @Override
    public void save(User user) {
        store.put(user.id, user); // => stores by ID key
    }
 
    @Override
    public User findById(String userId) {
        return store.get(userId); // => returns null if not found
    }
}
 
// => ADAPTER (driven): concrete implementation of NotifierPort
class ConsoleNotifier implements NotifierPort {
    @Override
    public void sendWelcome(String email) {
        System.out.println("Welcome email sent to " + email);
        // => simulates email send; swap with SmtpNotifier in production
    }
}
 
// => ADAPTER (driving): CLI adapter calls the application core via UserService
// => Real apps may have an HTTP adapter alongside this CLI adapter
static void cliRegister(UserService service, String userId, String email, String name) {
    User user = service.register(userId, email, name); // => delegates to core
    System.out.println("Registered: " + user.name);    // => adapter formats output
}
 
// wire up adapters
InMemoryUserRepository repo = new InMemoryUserRepository();
// => swap to JpaUserRepository in production
ConsoleNotifier notifier = new ConsoleNotifier();
// => swap to SmtpNotifier in production
UserService service = new UserService(repo, notifier);
// => core receives ports via DI
 
cliRegister(service, "u1", "alice@example.com", "Alice");
// => Output: Welcome email sent to alice@example.com
// => Output: Registered: Alice
 
User found = repo.findById("u1");
System.out.println(found.email); // => Output: alice@example.com

Key Takeaway: Ports are interfaces owned by the application core; adapters are infrastructure implementations owned by outer layers. This boundary makes the core independently testable and infrastructure-swappable.

Why It Matters: Hexagonal architecture is the foundation behind major frameworks like Spring (ports as interfaces, adapters as repositories/controllers). The primary testing benefit is concrete: the core domain runs entirely with in-memory adapters — no database, no network, no flakiness. Test cycles are faster and deterministic because infrastructure is swapped out, not mocked at the boundary. The architecture also makes cloud migration straightforward: swap one adapter, not the entire codebase.


Example 30: Clean Architecture — Layer Separation with Dependency Rule

Clean Architecture organizes code into concentric rings (Entities → Use Cases → Interface Adapters → Frameworks). The dependency rule states that source code dependencies can only point inward — outer rings depend on inner rings, never the reverse. This keeps business rules free of framework and UI concerns.

%% Color Palette: Blue #0173B2, Orange #DE8F05, Teal #029E73, Purple #CC78BC, Brown #CA9161
graph TD
    A["Frameworks & Drivers<br/>Web, DB, UI"]
    B["Interface Adapters<br/>Controllers, Presenters"]
    C["Use Cases<br/>Application Business Rules"]
    D["Entities<br/>Enterprise Business Rules"]
 
    A -->|depends on| B
    B -->|depends on| C
    C -->|depends on| D
 
    style A fill:#CA9161,stroke:#000,color:#fff
    style B fill:#CC78BC,stroke:#000,color:#fff
    style C fill:#DE8F05,stroke:#000,color:#fff
    style D fill:#0173B2,stroke:#000,color:#fff
import java.util.HashMap;
import java.util.List;
import java.util.Map;
 
// ENTITIES LAYER — enterprise business rules, no imports from outer layers
// => Entity encapsulates core business rules independent of any framework
class OrderItem {
    final String productId; // => value by identity, not by object reference
    final double price;     // => snapshot price at order time
    final int qty;          // => quantity ordered
 
    OrderItem(String productId, double price, int qty) {
        this.productId = productId;
        this.price = price;
        this.qty = qty;
    }
}
 
class Order {
    final String id;           // => entity identity
    final List<OrderItem> items; // => domain data
    final String customerId;   // => relationship by ID, not object reference
 
    Order(String id, List<OrderItem> items, String customerId) {
        this.id = id;
        this.items = items;
        this.customerId = customerId;
    }
 
    // => business rule lives here, not in the use case or controller
    double getTotal() {
        return items.stream()
            .mapToDouble(i -> i.price * i.qty) // => price * qty per item
            .sum(); // => accumulates to final total
    }
}
 
// USE CASES LAYER — application business rules, depends only on entities
// => Repository interface belongs to use cases layer (dependency inversion)
// => Use case defines what it needs; infrastructure provides the implementation
interface OrderRepository {
    void save(Order order); // => persistence abstraction
}
 
// => Use case implementation — pure business orchestration, no HTTP/DB code
class PlaceOrderInteractor {
    private final OrderRepository repo;
    // => repo injected, satisfies dependency inversion
 
    PlaceOrderInteractor(OrderRepository repo) {
        this.repo = repo;
    }
 
    Order execute(String customerId, List<OrderItem> items) {
        String id = "ord-" + System.currentTimeMillis(); // => generate order ID
        Order order = new Order(id, items, customerId);   // => create entity
        repo.save(order); // => persist via repository port
        return order;     // => return entity, not DB row
    }
}
 
// INTERFACE ADAPTERS LAYER — converts data between use cases and frameworks
// => Controller translates HTTP request into use case input
class OrderController {
    private final PlaceOrderInteractor useCase;
    // => depends on use case class (or interface), not infrastructure
 
    OrderController(PlaceOrderInteractor useCase) {
        this.useCase = useCase;
    }
 
    Map<String, Object> handleRequest(String customerId, List<OrderItem> items) {
        Order order = useCase.execute(customerId, items);
        // => invoke use case with domain-shaped input
        return Map.of("orderId", order.id, "total", order.getTotal());
        // => presenter maps entity to HTTP-shaped output
    }
}
 
// FRAMEWORKS LAYER — in-memory adapter (stands in for a real DB adapter)
class InMemoryOrderRepo implements OrderRepository {
    private final Map<String, Order> store = new HashMap<>();
    // => storage detail hidden from use cases
 
    @Override
    public void save(Order order) {
        store.put(order.id, order); // => concrete persistence
    }
}
 
// wire layers together (this wiring itself lives in the frameworks layer)
InMemoryOrderRepo repo = new InMemoryOrderRepo();
// => outer layer provides implementation
PlaceOrderInteractor interactor = new PlaceOrderInteractor(repo);
// => use case receives repo
OrderController controller = new OrderController(interactor);
// => adapter receives use case
 
Map<String, Object> result = controller.handleRequest(
    "c1", List.of(new OrderItem("p1", 10.0, 2))
);
System.out.println(result); // => Output: {orderId=ord-..., total=20.0}

Key Takeaway: The dependency rule is the single most important rule in Clean Architecture — outer layers depend on inner layers, never the reverse. Enforce it by ensuring entities and use cases have zero imports from controllers or databases.

Why It Matters: Clean Architecture is the architecture behind many long-lived enterprise systems because it preserves the ability to change frameworks and databases independently of business logic. Uncle Bob's research across decades of software projects found that systems that violate the dependency rule accumulate coupling debt exponentially — a minor database change cascades into use case rewrites. Teams adopting Clean Architecture report business logic surviving 2-3 major framework migrations intact.


Example 31: Onion Architecture — Domain at the Center

Onion Architecture is a variant of Clean Architecture where the domain model sits at the very center, surrounded by domain services, then application services, then infrastructure. Unlike layered architecture, every layer depends only on layers closer to the center, and infrastructure is always the outermost layer.

import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
 
// => DOMAIN MODEL (innermost ring) — pure business objects, zero dependencies
final class Money {
    final double amount;  // => value in currency units
    final String currency; // => ISO 4217 code, e.g., "USD"
 
    Money(double amount, String currency) {
        this.amount = amount;
        this.currency = currency;
    }
 
    Money add(Money other) {
        if (!Objects.equals(this.currency, other.currency))
            throw new IllegalArgumentException("Currency mismatch");
            // => domain rule enforced here
        return new Money(this.amount + other.amount, this.currency);
        // => returns new Money (immutable pattern)
    }
}
 
// => ENTITY: product with a domain-typed price
class Product {
    final String id;    // => identity
    final String name;  // => display name
    final Money price;  // => price is a domain concept, not a raw double
 
    Product(String id, String name, Money price) {
        this.id = id;
        this.name = name;
        this.price = price;
    }
}
 
// => DOMAIN SERVICES (second ring) — stateless operations on domain objects
// => Domain services operate on domain entities; they have no infrastructure calls
class PricingService {
    Money applyDiscount(Money price, double discountPct) {
        double discounted = price.amount * (1 - discountPct / 100);
        // => computes discounted amount
        return new Money(Math.round(discounted * 100.0) / 100.0, price.currency);
        // => preserves currency, rounds to cents
    }
}
 
// => APPLICATION SERVICES (third ring) — orchestrates domain + domain services
// => Depends on domain model and domain services; not on infrastructure
class ProductCatalogService {
    private final PricingService pricer;
    // => domain service injected
 
    ProductCatalogService(PricingService pricer) {
        this.pricer = pricer;
    }
 
    Money getDiscountedPrice(Product product, double discountPct) {
        return pricer.applyDiscount(product.price, discountPct);
        // => application logic: which domain service to call and when
    }
}
 
// => REPOSITORY PORT (third ring, owned by application) — infrastructure contract
// => Defined here so infrastructure depends inward on this interface
interface ProductRepository {
    Product find(String productId); // => contract; null if not found
}
 
// => INFRASTRUCTURE (outermost ring) — implements repository port
class InMemoryProductRepository implements ProductRepository {
    private final Map<String, Product> data = new HashMap<>();
    // => in-memory store
 
    @Override
    public Product find(String productId) {
        return data.get(productId); // => concrete retrieval
    }
 
    void add(Product product) {
        data.put(product.id, product); // => concrete persistence
    }
}
 
// demo wiring — infrastructure created last, domain created first
InMemoryProductRepository repo = new InMemoryProductRepository();
repo.add(new Product("p1", "Widget", new Money(100.0, "USD")));
// => seed data
 
PricingService pricer = new PricingService();       // => domain service, no infra
ProductCatalogService catalog = new ProductCatalogService(pricer); // => app service
 
Product product = repo.find("p1");          // => retrieve from outer ring
Money discounted = catalog.getDiscountedPrice(product, 10);
// => applies 10% discount via domain service
System.out.println(discounted.currency + " " + discounted.amount);
// => Output: USD 90.0

Key Takeaway: Onion Architecture places the domain model at the center and makes infrastructure an outermost detail, ensuring business logic is the most stable and reusable part of the codebase.

Why It Matters: Onion Architecture gained popularity in enterprise .NET and Java communities because it naturally aligns with Domain-Driven Design — the domain model at the center corresponds to the Bounded Context's core. Keeping infrastructure as the outermost layer makes the domain model stable across service rewrites: switching from PostgreSQL to DynamoDB touches only the outermost ring, never the pricing or catalog logic. This also makes domain logic independently unit-testable without any infrastructure dependencies present.


Event-Driven Architecture

Example 32: Observer Pattern — Event Notification Without Coupling

The Observer pattern defines a one-to-many dependency so that when one object changes state, all its dependents are notified automatically. This decouples the event source from its handlers — the source doesn't know what handles its events.

%% Color Palette: Blue #0173B2, Orange #DE8F05, Teal #029E73, Purple #CC78BC, Brown #CA9161
graph LR
    A["EventBus<br/>(Subject)"]
    B["EmailHandler<br/>(Observer)"]
    C["AuditHandler<br/>(Observer)"]
    D["StatsHandler<br/>(Observer)"]
 
    A -->|notify| B
    A -->|notify| C
    A -->|notify| D
 
    style A fill:#0173B2,stroke:#000,color:#fff
    style B fill:#DE8F05,stroke:#000,color:#fff
    style C fill:#029E73,stroke:#000,color:#fff
    style D fill:#CC78BC,stroke:#000,color:#fff
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.function.Consumer;
 
// => EventBus: subject that holds observers and broadcasts events
// => Observers register themselves; the bus doesn't care what they do
// => Generic type T ensures type-safety per event kind
class EventBus<T> {
    private final List<Consumer<T>> handlers = new ArrayList<>();
    // => list grows as handlers subscribe; shrinks on unsubscribe
 
    void subscribe(Consumer<T> handler) {
        handlers.add(handler); // => add observer to list
    }
 
    void unsubscribe(Consumer<T> handler) {
        handlers.remove(handler);
        // => remove by reference equality; remaining handlers unaffected
    }
 
    void publish(T event) {
        for (Consumer<T> handler : handlers) {
            handler.accept(event); // => notify each observer in registration order
        }
        // => if one handler throws, subsequent handlers are skipped — add try/catch in production
    }
}
 
// => DOMAIN EVENT: named, immutable record of what happened
class UserRegistered {
    final String userId;    // => who registered
    final String email;     // => where to send welcome
    final Date timestamp;   // => when it happened
 
    UserRegistered(String userId, String email, Date timestamp) {
        this.userId = userId;
        this.email = email;
        this.timestamp = timestamp;
    }
}
 
// wire up: create bus and register observers
EventBus<UserRegistered> bus = new EventBus<>();
// => typed bus for UserRegistered events only
 
// => OBSERVER 1: sends welcome email
bus.subscribe(event -> {
    System.out.println("Sending welcome to " + event.email);
    // => in production: call email service API here
});
 
// => OBSERVER 2: writes audit log
bus.subscribe(event -> {
    System.out.println("Audit: user " + event.userId + " registered at " + event.timestamp);
    // => in production: write to append-only audit table
});
 
// => OBSERVER 3: increments stats counter
bus.subscribe(event -> {
    System.out.println("Stats: new user registered");
    // => in production: increment Prometheus counter
});
 
bus.publish(new UserRegistered("u1", "alice@example.com", new Date()));
// => Output: Sending welcome to alice@example.com
// => Output: Audit: user u1 registered at ...
// => Output: Stats: new user registered
// => all three handlers fired without the publisher knowing about any of them

Key Takeaway: The Observer pattern decouples event producers from event consumers — the source publishes events without knowing what handles them, and handlers register without knowing who triggers them.

Why It Matters: Observer is the foundation of virtually every UI framework (React's synthetic events, Vue's reactivity, browser DOM events) and server-side event systems (Node.js EventEmitter, Spring ApplicationEventPublisher). The pattern trades direct coupling for indirect coupling through an event contract, enabling teams to add handlers independently without coordinating with the event source. Fan-out to multiple subscribers—notification systems, analytics, audit logs—becomes a configuration change rather than a code change in the publisher.


Example 33: Domain Events — Signaling State Changes Within a Bounded Context

Domain Events capture the fact that something meaningful happened in the domain. Unlike technical events, domain events are named in business language and carry enough data for handlers to act without querying back. They enable reactive workflows within and across bounded contexts.

import java.time.Instant;
import java.util.*;
import java.util.function.Consumer;
 
// => DOMAIN EVENT BASE: every event has an occurredAt timestamp
abstract class DomainEvent {
    final Instant occurredAt = Instant.now();
    // => set at creation time so events are immutable records of the past
}
 
// => DOMAIN EVENTS: past-tense names (OrderPlaced, not PlaceOrder)
class OrderPlaced extends DomainEvent {
    final String orderId;     // => which order was placed
    final String customerId;  // => who placed it
    final double total;       // => enough data for handlers without DB lookups
 
    OrderPlaced(String orderId, String customerId, double total) {
        this.orderId = orderId;
        this.customerId = customerId;
        this.total = total;
    }
}
 
class OrderCancelled extends DomainEvent {
    final String orderId; // => which order was cancelled
    final String reason;  // => why — useful for analytics and customer comms
 
    OrderCancelled(String orderId, String reason) {
        this.orderId = orderId;
        this.reason = reason;
    }
}
 
// => AGGREGATE: raises domain events as part of state transitions
class Order {
    final String id;
    final String customerId;
    final double total;
    String status = "PENDING";                   // => initial state
    private final List<DomainEvent> events = new ArrayList<>();
    // => events collected here, NOT yet dispatched
 
    Order(String id, String customerId, double total) {
        this.id = id;
        this.customerId = customerId;
        this.total = total;
    }
 
    void place() {
        this.status = "PLACED";                   // => state change
        events.add(new OrderPlaced(id, customerId, total));
        // => record event for later dispatch
    }
 
    void cancel(String reason) {
        if (!"PLACED".equals(status))
            throw new IllegalStateException("Cannot cancel order in status " + status);
            // => business rule enforced before event is raised
        this.status = "CANCELLED";               // => state change
        events.add(new OrderCancelled(id, reason));
    }
 
    List<DomainEvent> popEvents() {
        List<DomainEvent> drained = new ArrayList<>(events);
        events.clear(); // => caller drains events; aggregate starts fresh
        return drained;
    }
}
 
// => EVENT DISPATCHER: collects events from aggregates and notifies handlers
class EventDispatcher {
    private final Map<Class<?>, List<Consumer<DomainEvent>>> handlers = new HashMap<>();
    // => maps event type to list of handlers
 
    @SuppressWarnings("unchecked")
    <T extends DomainEvent> void register(Class<T> type, Consumer<T> handler) {
        handlers.computeIfAbsent(type, k -> new ArrayList<>())
                .add(e -> handler.accept((T) e));
        // => multiple handlers per event type are supported
    }
 
    void dispatch(List<DomainEvent> events) {
        for (DomainEvent event : events) {
            for (Consumer<DomainEvent> h : handlers.getOrDefault(event.getClass(), List.of())) {
                h.accept(event); // => each handler called with the event
            }
        }
    }
}
 
// demo: place then cancel an order
EventDispatcher dispatcher = new EventDispatcher();
dispatcher.register(OrderPlaced.class,
    e -> System.out.println("Email: order " + e.orderId + " placed, total $" + e.total));
// => handler 1: send confirmation email
dispatcher.register(OrderCancelled.class,
    e -> System.out.println("Refund triggered: " + e.orderId + ", reason: " + e.reason));
// => handler 2: trigger refund workflow
 
Order order = new Order("o1", "c1", 49.99);
order.place();                        // => state: PENDING -> PLACED, event queued
dispatcher.dispatch(order.popEvents());
// => Output: Email: order o1 placed, total $49.99
 
order.cancel("Customer request");     // => state: PLACED -> CANCELLED, event queued
dispatcher.dispatch(order.popEvents());
// => Output: Refund triggered: o1, reason: Customer request

Key Takeaway: Domain events use past-tense business language, carry sufficient data for handlers, and are raised by aggregates as part of state transitions — not as raw technical notifications.

Why It Matters: Domain events are central to CQRS, Event Sourcing, and saga orchestration patterns. By naming events in business language (OrderPlaced, not UpdateOrderStatusEvent), the team's shared vocabulary aligns with the domain model. Handlers for domain events can trigger workflows — emails, refunds, inventory updates — without the aggregate knowing about them, which keeps the domain model focused and independently testable.


Example 34: Event-Driven Architecture — Async Message Passing Between Services

Event-driven architecture connects services through asynchronous messages on a message broker. Producers publish events without waiting for consumers; consumers process events at their own pace. This delivers temporal decoupling and horizontal scalability.

import java.time.Instant;
import java.util.*;
import java.util.function.Consumer;
 
// => MESSAGE BROKER SIMULATION: in-memory topic-based pub/sub
// => In production, replace with Kafka, RabbitMQ, or AWS SQS
class MessageBroker {
    private final Map<String, List<Consumer<Object>>> topics = new HashMap<>();
    // => each topic holds a list of consumer callbacks
 
    @SuppressWarnings("unchecked")
    <T> void subscribe(String topic, Consumer<T> consumer) {
        topics.computeIfAbsent(topic, k -> new ArrayList<>())
              .add(msg -> consumer.accept((T) msg));
        // => consumer registered for this topic
    }
 
    void publish(String topic, Object message) {
        List<Consumer<Object>> consumers = topics.getOrDefault(topic, List.of());
        // => get all consumers; empty list if topic has no subscribers
        for (Consumer<Object> c : consumers) {
            c.accept(message); // => deliver to each consumer
        }
        // => in production, broker delivers asynchronously via Kafka partition
    }
}
 
// => DOMAIN EVENT: something that happened in the Order service
class OrderShipped {
    final String orderId;      // => which order shipped
    final String trackingCode; // => courier tracking reference
    final String shippedAt;    // => ISO timestamp
 
    OrderShipped(String orderId, String trackingCode, String shippedAt) {
        this.orderId = orderId;
        this.trackingCode = trackingCode;
        this.shippedAt = shippedAt;
    }
}
 
// => PRODUCER SERVICE: publishes events, does not know about consumers
class OrderService {
    private final MessageBroker broker;
    // => broker injected; producer never references consumer classes directly
 
    OrderService(MessageBroker broker) {
        this.broker = broker;
    }
 
    void shipOrder(String orderId, String trackingCode) {
        OrderShipped event = new OrderShipped(orderId, trackingCode, Instant.now().toString());
        // => capture shipping time in event
        broker.publish("order.shipped", event);
        // => publishes to topic — does not call notification or warehouse services directly
        System.out.println("Order " + orderId + " shipped");
    }
}
 
// => CONSUMER 1: notification service — listens independently
class NotificationService {
    void onOrderShipped(OrderShipped event) {
        System.out.println("Notif: send tracking " + event.trackingCode + " to customer for order " + event.orderId);
        // => in production: call email/SMS provider API
    }
}
 
// => CONSUMER 2: warehouse service — listens independently
class WarehouseService {
    void onOrderShipped(OrderShipped event) {
        System.out.println("Warehouse: update inventory for order " + event.orderId);
        // => in production: decrement stock counts in warehouse DB
    }
}
 
// wire up
MessageBroker broker = new MessageBroker();         // => in production: Kafka client
NotificationService notifier = new NotificationService();
WarehouseService warehouse = new WarehouseService();
 
broker.subscribe("order.shipped", (OrderShipped e) -> notifier.onOrderShipped(e));
// => notification service subscribes to topic
broker.subscribe("order.shipped", (OrderShipped e) -> warehouse.onOrderShipped(e));
// => warehouse service subscribes to same topic — independent of notification
 
OrderService orderService = new OrderService(broker);
orderService.shipOrder("o1", "TRK-9876");
// => Output: Order o1 shipped
// => Output: Notif: send tracking TRK-9876 to customer for order o1
// => Output: Warehouse: update inventory for order o1
// => both consumers received the event independently

Key Takeaway: Event-driven architecture decouples producers from consumers through a message broker — the OrderService publishes once, and any number of consumers can independently react without the producer knowing about them.

Why It Matters: Event-driven architecture provides temporal decoupling that direct service calls cannot. The Order service ships an order without waiting for the Notification service to respond. If the Notification service is down, messages queue until it recovers. This fault isolation prevents cascading failures that would occur with synchronous direct calls, and enables adding new consumers without modifying producers.


Structural Design Patterns

Example 35: Strategy Pattern — Swappable Algorithms

The Strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable. The client selects the algorithm at runtime without changing the code that uses it. This eliminates conditional logic that switches between behaviors.

%% Color Palette: Blue #0173B2, Orange #DE8F05, Teal #029E73, Purple #CC78BC, Brown #CA9161
graph TD
    A["ShippingCalculator<br/>(Context)"]
    B["ShippingStrategy<br/>(Interface)"]
    C["FlatRateStrategy"]
    D["WeightBasedStrategy"]
    E["FreeShippingStrategy"]
 
    A -- uses --> B
    B -.-> C
    B -.-> D
    B -.-> E
 
    style A fill:#0173B2,stroke:#000,color:#fff
    style B fill:#DE8F05,stroke:#000,color:#fff
    style C fill:#029E73,stroke:#000,color:#fff
    style D fill:#029E73,stroke:#000,color:#fff
    style E fill:#029E73,stroke:#000,color:#fff
// => STRATEGY INTERFACE: defines the algorithm contract
interface ShippingStrategy {
    double calculate(double weightKg, double distanceKm);
    // => every strategy must implement this signature
}
 
// => CONCRETE STRATEGY 1: flat rate regardless of weight/distance
class FlatRateStrategy implements ShippingStrategy {
    private final double rate; // => configurable flat fee
 
    FlatRateStrategy(double rate) {
        this.rate = rate;
    }
 
    @Override
    public double calculate(double weightKg, double distanceKm) {
        return rate; // => ignores inputs — always same price
        // => Use when: subscription-based shipping or simple pricing tiers
    }
}
 
// => CONCRETE STRATEGY 2: price based on weight
class WeightBasedStrategy implements ShippingStrategy {
    private final double pricePerKg; // => cost per kilogram
 
    WeightBasedStrategy(double pricePerKg) {
        this.pricePerKg = pricePerKg;
    }
 
    @Override
    public double calculate(double weightKg, double distanceKm) {
        return weightKg * pricePerKg; // => heavier = more expensive
        // => distance ignored — Use when: local delivery with flat zone pricing
    }
}
 
// => CONCRETE STRATEGY 3: free shipping (zero cost always)
class FreeShippingStrategy implements ShippingStrategy {
    @Override
    public double calculate(double weightKg, double distanceKm) {
        return 0.0; // => promotional: customer pays nothing
        // => Use when: orders over threshold or loyalty program members
    }
}
 
// => CONTEXT: uses whatever strategy is injected
class ShippingCalculator {
    private ShippingStrategy strategy;
    // => accepts any ShippingStrategy implementation
 
    ShippingCalculator(ShippingStrategy strategy) {
        this.strategy = strategy;
    }
 
    void setStrategy(ShippingStrategy strategy) {
        this.strategy = strategy; // => swap strategy at runtime
    }
 
    double getCost(double weightKg, double distanceKm) {
        return strategy.calculate(weightKg, distanceKm);
        // => delegates entirely to strategy — no if/else here
    }
}
 
// runtime selection: choose strategy based on business conditions
double orderTotal = 120.0;
ShippingStrategy strategy;
if (orderTotal >= 100) {
    strategy = new FreeShippingStrategy();       // => orders over $100 ship free
} else if (orderTotal >= 50) {
    strategy = new FlatRateStrategy(5.0);        // => medium orders: flat $5
} else {
    strategy = new WeightBasedStrategy(2.5);     // => small orders: by weight
}
 
ShippingCalculator calculator = new ShippingCalculator(strategy);
double cost = calculator.getCost(2.0, 50.0);
System.out.printf("Shipping cost: $%.2f%n", cost);
// => Output: Shipping cost: $0.00 (free shipping applied)
 
// swap strategy at runtime — context code unchanged
calculator.setStrategy(new WeightBasedStrategy(2.5));
double cost2 = calculator.getCost(2.0, 50.0);
System.out.printf("Weight-based cost: $%.2f%n", cost2);
// => Output: Weight-based cost: $5.00

Key Takeaway: The Strategy pattern replaces conditional logic (if/elif/switch on algorithm type) with polymorphism — each algorithm lives in its own class and is selected by the client at runtime.

Why It Matters: Strategy is one of the most-applied GoF patterns in enterprise systems. Payment processors, shipping calculators, tax engines, and sorting algorithms all benefit from strategy isolation. Without it, a single class accumulates every algorithm variant behind if/else chains that grow unmaintainably and require retesting every variant when adding one new strategy. Adding a new strategy requires only a new class that implements the interface — zero changes to existing strategies or the calling code.


Example 36: Factory Pattern — Centralized Object Creation

The Factory pattern centralizes object creation logic, hiding which concrete class is instantiated from the caller. The caller specifies what it wants (by type, key, or configuration), and the factory decides how to build it. This decouples creation from use.

import java.util.HashMap;
import java.util.Map;
import java.util.function.Supplier;
 
// => PRODUCT INTERFACE: what all created objects share
// => Factory returns this type — callers depend only on this interface
interface PaymentProcessor {
    String process(double amount); // => returns transaction ID
    String getName();              // => processor name for logging
}
 
// => CONCRETE PRODUCT 1: Stripe payment implementation
// => Caller never imports this class directly — factory owns instantiation
class StripeProcessor implements PaymentProcessor {
    @Override
    public String getName() { return "Stripe"; }
 
    @Override
    public String process(double amount) {
        String txId = "stripe_" + System.currentTimeMillis();
        // => simulated transaction ID; real impl calls Stripe SDK
        System.out.printf("Stripe: charged $%.2f, tx=%s%n", amount, txId);
        return txId; // => return transaction reference to caller
    }
}
 
// => CONCRETE PRODUCT 2: PayPal payment implementation
class PayPalProcessor implements PaymentProcessor {
    @Override
    public String getName() { return "PayPal"; }
 
    @Override
    public String process(double amount) {
        String txId = "pp_" + System.currentTimeMillis();
        // => simulated PayPal transaction ID
        System.out.printf("PayPal: charged $%.2f, tx=%s%n", amount, txId);
        return txId;
    }
}
 
// => CONCRETE PRODUCT 3: bank transfer implementation
class BankTransferProcessor implements PaymentProcessor {
    @Override
    public String getName() { return "BankTransfer"; }
 
    @Override
    public String process(double amount) {
        String txId = "bt_" + System.currentTimeMillis();
        // => simulated bank reference number
        System.out.printf("BankTransfer: initiated $%.2f, ref=%s%n", amount, txId);
        return txId;
    }
}
 
// => FACTORY: registry maps type keys to factory functions (Suppliers)
// => Caller never calls new StripeProcessor() directly — factory owns creation
class PaymentProcessorFactory {
    private static final Map<String, Supplier<PaymentProcessor>> REGISTRY = new HashMap<>();
    // => static registry populated once at class load
 
    static {
        REGISTRY.put("stripe",        StripeProcessor::new);
        // => factory function per type — add new types here without if/else
        REGISTRY.put("paypal",        PayPalProcessor::new);
        REGISTRY.put("bank_transfer", BankTransferProcessor::new);
    }
 
    static PaymentProcessor create(String type) {
        Supplier<PaymentProcessor> factory = REGISTRY.get(type);
        // => look up factory function for the requested type
        if (factory == null) {
            throw new IllegalArgumentException("Unknown payment processor: " + type);
            // => fail early with useful error rather than returning null
        }
        return factory.get(); // => invoke factory function, return new instance
    }
}
 
// caller uses the factory — decoupled from concrete classes
String processorType = "stripe"; // => in practice: from config or HTTP request param
PaymentProcessor processor = PaymentProcessorFactory.create(processorType);
// => caller never imported StripeProcessor — depends only on PaymentProcessor
 
String txId = processor.process(99.99);
// => Output: Stripe: charged $99.99, tx=stripe_...
// => txId is "stripe_..." (type: String)
 
// adding a new processor: add one entry to REGISTRY + one new class
// => zero changes to checkout flow or any other caller

Key Takeaway: The Factory pattern centralizes object creation using a registry or conditional logic, so callers depend only on the product interface and never on concrete classes — adding a new product requires only updating the factory.

Why It Matters: Factories are ubiquitous in enterprise ecosystems — Java's BeanFactory and ObjectMapper, Python's logging handlers, and Node.js dependency injection containers all rely on the pattern. Without a factory, every caller must import and instantiate each concrete class directly, creating tight coupling that makes adding a new implementation a cross-cutting change across dozens of files. A factory centralises object creation so adding a new product type requires updating only one place.


Example 37: Builder Pattern — Constructing Complex Objects Step by Step

The Builder pattern separates the construction of a complex object from its representation, allowing the same construction process to create different representations. It eliminates telescoping constructors and makes object creation readable when many optional parameters exist.

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
 
// => PRODUCT: complex, immutable object with many optional fields
// => All fields set via builder — no public setters
final class HttpRequest {
    final String url;           // => required: endpoint URL
    final String method;        // => HTTP method (GET, POST, etc.)
    final Map<String, String> headers; // => request headers
    final String body;          // => optional request body; null if absent
    final double timeoutSeconds; // => request timeout
    final int retries;          // => retry count on transient failure
    final String authToken;     // => optional Bearer token; null if absent
 
    // => private constructor: only HttpRequestBuilder calls this
    private HttpRequest(Builder b) {
        this.url     = b.url;
        this.method  = b.method;
        this.headers = Collections.unmodifiableMap(new HashMap<>(b.headers));
        // => defensive copy + unmodifiable view
        this.body    = b.body;
        this.timeoutSeconds = b.timeoutSeconds;
        this.retries = b.retries;
        this.authToken = b.authToken;
    }
 
    // => BUILDER: inner static class accumulates configuration
    static class Builder {
        private final String url;              // => mandatory at construction time
        private String method = "GET";         // => defaults mirrored from product
        private final Map<String, String> headers = new HashMap<>();
        private String body = null;
        private double timeoutSeconds = 30.0;
        private int retries = 0;
        private String authToken = null;
 
        Builder(String url) {
            this.url = url; // => URL required — fail fast if null
        }
 
        Builder method(String method) {
            this.method = method.toUpperCase(); // => normalize to uppercase
            return this;                        // => return this enables chaining
        }
 
        Builder header(String key, String value) {
            this.headers.put(key, value); // => add individual header
            return this;                  // => fluent interface
        }
 
        Builder jsonBody(String body) {
            this.body = body;
            this.headers.put("Content-Type", "application/json");
            // => auto-set content-type when body is JSON
            return this;
        }
 
        Builder timeout(double seconds) {
            if (seconds <= 0) throw new IllegalArgumentException("Timeout must be positive");
            // => validate immediately; don't wait until execution
            this.timeoutSeconds = seconds;
            return this;
        }
 
        Builder withRetries(int count) {
            this.retries = count; // => retry count for transient failures
            return this;
        }
 
        Builder bearerToken(String token) {
            this.authToken = token;
            this.headers.put("Authorization", "Bearer " + token);
            // => set auth header alongside storing token
            return this;
        }
 
        HttpRequest build() {
            return new HttpRequest(this);
            // => build() is the only place HttpRequest is constructed
        }
    }
}
 
// GOOD: builder reads like a sentence — far clearer than positional args
HttpRequest request = new HttpRequest.Builder("https://api.example.com/users")
    .method("POST")                      // => self-documenting
    .jsonBody("{\"name\":\"Alice\"}")    // => auto-sets Content-Type
    .timeout(10.0)                       // => named step, clear intent
    .withRetries(3)                      // => retry on transient failure
    .bearerToken("tok123")               // => auth token
    .build();                            // => produce immutable HttpRequest
 
System.out.println(request.method);          // => Output: POST
System.out.println(request.headers);         // => Output: {Content-Type=application/json, Authorization=Bearer tok123}
System.out.println(request.timeoutSeconds);  // => Output: 10.0
System.out.println(request.retries);         // => Output: 3

Key Takeaway: The Builder pattern makes complex object construction readable by providing a fluent API where each method name describes what it sets — far more maintainable than constructors with many positional parameters.

Why It Matters: The Builder pattern appears in almost every major library: Java's StringBuilder, Python's SQLAlchemy query builder, Kotlin's DSL builders, and Elasticsearch's QueryBuilder. It solves the "telescoping constructor" anti-pattern where objects with 8+ optional fields require 2^8 constructor overloads or a single unreadable constructor. Protocol buffer builders use this pattern for configuring complex API requests with dozens of optional fields, making the code self-documenting and less error-prone than positional arguments.


Example 38: Adapter Pattern — Bridging Incompatible Interfaces

The Adapter pattern converts the interface of a class into another interface that clients expect. It lets classes work together that could not otherwise because of incompatible interfaces. The adapter wraps the incompatible class and translates calls.

import java.util.Map;
 
// => EXISTING EXTERNAL LIBRARY: third-party analytics SDK with its own interface
// => We cannot change this class — it comes from an external package
class LegacyAnalyticsSDK {
    void trackPageView(String pageName, int userId, String platform) {
        System.out.printf("Legacy: page=%s, user=%d, platform=%s%n", pageName, userId, platform);
        // => legacy SDK uses integer userIds and positional args
    }
 
    void trackEvent(String eventName, String eventData) {
        System.out.printf("Legacy: event=%s, data=%s%n", eventName, eventData);
        // => eventData as raw JSON string — legacy string format
    }
}
 
// => OUR APPLICATION'S ANALYTICS PORT: the interface our code expects
// => Clean, typed interface aligned with our domain language
interface AnalyticsPort {
    void recordPageView(String page, String userId, String userName);
    // => our interface uses String IDs and named parameters
    void recordEvent(String eventName, Map<String, Object> properties);
    // => our interface accepts a typed map, not raw JSON string
}
 
// => ADAPTER: wraps the legacy SDK and exposes our AnalyticsPort interface
// => Adapter is the only place that knows both interfaces
class LegacyAnalyticsAdapter implements AnalyticsPort {
    private final LegacyAnalyticsSDK sdk;
    // => adapter holds reference to the adaptee (legacy SDK instance)
 
    LegacyAnalyticsAdapter(LegacyAnalyticsSDK sdk) {
        this.sdk = sdk;
    }
 
    @Override
    public void recordPageView(String page, String userId, String userName) {
        int numericUserId = Integer.parseInt(userId);
        // => TRANSLATE: our String ID → legacy integer userId
        sdk.trackPageView(page, numericUserId, "web");
        // => DELEGATE: call legacy SDK with translated args
        // => callers never know about the legacy integer ID format
    }
 
    @Override
    public void recordEvent(String eventName, Map<String, Object> properties) {
        String legacyData = properties.toString().replace("=", ":").replace(" ", "");
        // => TRANSLATE: our typed Map → legacy flat string representation
        sdk.trackEvent(eventName, legacyData);
        // => DELEGATE: call legacy SDK with translated format
    }
}
 
// => CLIENT: depends only on AnalyticsPort — zero knowledge of legacy SDK
static void trackCheckout(AnalyticsPort analytics, String userId, double total) {
    analytics.recordPageView("/checkout", userId, "Alice");
    // => calls our clean interface
    analytics.recordEvent("checkout_completed", Map.of("total", total, "currency", "USD"));
    // => calls our clean interface with typed Map
}
 
// wire up: inject adapter into client — adapter bridges the gap
LegacyAnalyticsSDK legacySDK = new LegacyAnalyticsSDK();
// => the incompatible adaptee
LegacyAnalyticsAdapter adapter = new LegacyAnalyticsAdapter(legacySDK);
// => adapter translates between our interface and legacy SDK
 
trackCheckout(adapter, "42", 99.99);
// => Output: Legacy: page=/checkout, user=42, platform=web
// => Output: Legacy: event=checkout_completed, data={total:99.99,currency:USD}
// => client code used clean interface; adapter handled translation silently

Key Takeaway: The Adapter wraps an incompatible class and translates its interface to match what callers expect — callers depend on the adapter's interface, never on the adaptee's interface.

Why It Matters: Adapters are essential when integrating third-party services, migrating legacy systems, or wrapping external APIs. Client libraries act as adapters between raw HTTP APIs and typed language interfaces. Teams can swap analytics or payment providers by writing a new adapter without touching any client code. The pattern also prevents third-party API changes from propagating across the codebase — only the adapter file changes.


Example 39: Decorator Pattern — Adding Behavior Without Subclassing

The Decorator pattern attaches additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality. Each decorator wraps a component and adds behavior before or after delegating to it.

%% Color Palette: Blue #0173B2, Orange #DE8F05, Teal #029E73, Purple #CC78BC, Brown #CA9161
graph LR
    A["LoggingDecorator"]
    B["CachingDecorator"]
    C["RetryDecorator"]
    D["UserRepository<br/>(core)"]
 
    A -->|wraps| B
    B -->|wraps| C
    C -->|wraps| D
 
    style A fill:#CC78BC,stroke:#000,color:#fff
    style B fill:#DE8F05,stroke:#000,color:#fff
    style C fill:#CA9161,stroke:#000,color:#fff
    style D fill:#0173B2,stroke:#000,color:#fff
import java.util.HashMap;
import java.util.Map;
 
// => COMPONENT INTERFACE: what all decorators and the core implement
// => Decorators and core all share this type — callers see only this
interface UserRepository {
    Map<String, String> findById(String userId);
    // => returns user map (id, name) or null if not found
}
 
// => CONCRETE COMPONENT: the real implementation being decorated
// => This is what decorators ultimately wrap and delegate to
class DatabaseUserRepository implements UserRepository {
    @Override
    public Map<String, String> findById(String userId) {
        // => simulate DB query — real impl queries JDBC/JPA here
        if ("u1".equals(userId)) {
            return Map.of("id", "u1", "name", "Alice"); // => found
        }
        return null; // => not found
    }
}
 
// => DECORATOR BASE: holds a wrapped component, delegates by default
// => Concrete decorators extend this and override to add behavior
abstract class UserRepositoryDecorator implements UserRepository {
    protected final UserRepository wrapped;
    // => the next layer in the chain — may be another decorator or core
 
    UserRepositoryDecorator(UserRepository wrapped) {
        this.wrapped = wrapped;
    }
 
    @Override
    public Map<String, String> findById(String userId) {
        return wrapped.findById(userId);
        // => default: pass through unchanged — subclasses override
    }
}
 
// => DECORATOR 1: adds structured logging around every call
class LoggingDecorator extends UserRepositoryDecorator {
    LoggingDecorator(UserRepository wrapped) { super(wrapped); }
 
    @Override
    public Map<String, String> findById(String userId) {
        System.out.println("[LOG] findById called with userId=" + userId);
        // => log before delegating
        Map<String, String> result = wrapped.findById(userId);
        // => delegate to next layer in chain
        System.out.println("[LOG] findById returned: " + result);
        // => log after delegation completes
        return result; // => return result unchanged
    }
}
 
// => DECORATOR 2: adds in-memory caching (cache-aside pattern)
class CachingDecorator extends UserRepositoryDecorator {
    private final Map<String, Map<String, String>> cache = new HashMap<>();
    // => in-memory cache; TTL/eviction omitted for clarity
 
    CachingDecorator(UserRepository wrapped) { super(wrapped); }
 
    @Override
    public Map<String, String> findById(String userId) {
        if (cache.containsKey(userId)) {
            System.out.println("[CACHE] hit for " + userId);
            return cache.get(userId); // => serve from cache, skip DB call
        }
        Map<String, String> result = wrapped.findById(userId);
        // => cache miss: delegate to next layer
        cache.put(userId, result); // => store in cache for future calls
        return result;
    }
}
 
// => DECORATOR 3: adds retry on exception (simplified for demo)
class RetryDecorator extends UserRepositoryDecorator {
    private final int maxRetries;
    // => configurable retry count; default 3
 
    RetryDecorator(UserRepository wrapped, int maxRetries) {
        super(wrapped);
        this.maxRetries = maxRetries;
    }
 
    @Override
    public Map<String, String> findById(String userId) {
        for (int attempt = 0; attempt < maxRetries; attempt++) {
            try {
                return wrapped.findById(userId); // => try the operation
            } catch (RuntimeException e) {
                if (attempt == maxRetries - 1) throw e;
                // => last attempt: re-throw
                System.out.println("[RETRY] attempt " + (attempt + 1) + " failed: " + e.getMessage());
            }
        }
        return null; // => unreachable — satisfies compiler
    }
}
 
// compose decorators: Logging → Caching → Retry → DB
DatabaseUserRepository dbRepo = new DatabaseUserRepository();
// => innermost: real DB implementation
UserRepository retryRepo   = new RetryDecorator(dbRepo, 3);
// => wrap DB with retry logic
UserRepository cachingRepo = new CachingDecorator(retryRepo);
// => wrap retry with caching
UserRepository loggingRepo = new LoggingDecorator(cachingRepo);
// => outermost: logging wraps entire chain
 
loggingRepo.findById("u1");
// => Output: [LOG] findById called with userId=u1
// => Output: [LOG] findById returned: {id=u1, name=Alice}
 
loggingRepo.findById("u1"); // => second call — cache hit
// => Output: [LOG] findById called with userId=u1
// => Output: [CACHE] hit for u1
// => Output: [LOG] findById returned: {id=u1, name=Alice}
// => DB was NOT called on second request (cache served it)

Key Takeaway: Decorators stack behaviors (logging, caching, retry) around a core component without modifying it — each decorator adds one concern and composes cleanly with others.

Why It Matters: Python's @functools.lru_cache, Java's Spring AOP (transaction, caching, security annotations), and gRPC interceptors all use the Decorator pattern. It addresses the cross-cutting concern problem: behaviors like logging and caching apply across many operations but belong in neither the domain model nor infrastructure. Decorator-style patterns (circuit breakers, retry logic) compose cleanly around service calls without modifying those calls. Adding a new cross-cutting concern requires writing one new decorator class — zero changes to existing decorators or the core component.


Example 40: Facade Pattern — Simplified Interface to a Subsystem

The Facade pattern provides a unified, simplified interface to a complex subsystem. The facade hides the complexity of coordinating multiple components and gives callers a single entry point. It does not add new behavior — it orchestrates existing behavior with a cleaner API.

// => SUBSYSTEM CLASS 1: manages inventory reservations
// => Has its own complex API; callers should not call this directly
class InventoryService {
    boolean reserve(String productId, int qty) {
        System.out.println("Inventory: reserved " + qty + "x " + productId);
        return true; // => simulated: always succeeds for demo
        // => in production: checks stock levels, applies reservations
    }
 
    void release(String productId, int qty) {
        System.out.println("Inventory: released " + qty + "x " + productId);
        // => in production: undoes reservation on order failure
    }
}
 
// => SUBSYSTEM CLASS 2: charges payment and issues refunds
class PaymentGateway {
    String charge(String customerId, double amount) {
        String authCode = "AUTH-" + System.currentTimeMillis();
        // => simulated authorization code; real impl calls payment provider
        System.out.printf("Payment: charged customer %s $%.2f, auth=%s%n", customerId, amount, authCode);
        return authCode; // => authorization code for records
    }
 
    void refund(String authCode) {
        System.out.println("Payment: refunded auth " + authCode);
        // => in production: reverse the charge via payment provider API
    }
}
 
// => SUBSYSTEM CLASS 3: schedules physical delivery
class ShippingService {
    String scheduleDelivery(String orderId, String address) {
        String trackingId = "TRK-" + orderId;
        // => simulated tracking ID; real impl calls courier API
        System.out.printf("Shipping: scheduled delivery for order %s to %s, tracking=%s%n", orderId, address, trackingId);
        return trackingId; // => tracking reference for customer
    }
}
 
// => SUBSYSTEM CLASS 4: sends customer notifications
class NotificationService {
    void sendConfirmation(String email, String orderId, String trackingId) {
        System.out.printf("Notification: sent order confirmation to %s, order=%s, tracking=%s%n", email, orderId, trackingId);
        // => in production: sends transactional email via SendGrid/SES
    }
}
 
// => RESULT: simple return type for facade callers
class OrderResult {
    final boolean success;
    final String trackingId; // => null if success is false
    OrderResult(boolean success, String trackingId) {
        this.success = success;
        this.trackingId = trackingId;
    }
}
 
// => FACADE: single entry point that orchestrates the four subsystem services
// => Callers invoke one method; facade owns the coordination sequence
class OrderFacade {
    private final InventoryService inventory;  // => subsystem dependency
    private final PaymentGateway payment;      // => subsystem dependency
    private final ShippingService shipping;    // => subsystem dependency
    private final NotificationService notification; // => subsystem dependency
 
    OrderFacade(InventoryService inventory, PaymentGateway payment,
                ShippingService shipping, NotificationService notification) {
        this.inventory    = inventory;
        this.payment      = payment;
        this.shipping     = shipping;
        this.notification = notification;
    }
 
    OrderResult placeOrder(String orderId, String customerId, String customerEmail,
                           String productId, int qty, double amount, String shippingAddress) {
        // => step 1: reserve inventory first — fail fast if out of stock
        boolean reserved = inventory.reserve(productId, qty);
        if (!reserved) {
            return new OrderResult(false, null);
            // => early return — no payment charged if inventory unavailable
        }
 
        // => step 2: charge payment
        String authCode = payment.charge(customerId, amount);
        // => authCode stored internally; facade hides this detail from caller
 
        // => step 3: schedule delivery
        String trackingId = shipping.scheduleDelivery(orderId, shippingAddress);
 
        // => step 4: notify customer
        notification.sendConfirmation(customerEmail, orderId, trackingId);
 
        return new OrderResult(true, trackingId);
        // => caller receives simple result; all subsystem coordination hidden
    }
}
 
// CALLER: uses only the facade — never touches subsystem classes directly
OrderFacade facade = new OrderFacade(
    new InventoryService(),
    new PaymentGateway(),
    new ShippingService(),
    new NotificationService()
);
 
OrderResult result = facade.placeOrder(
    "o1", "c1", "alice@example.com", "p1", 2, 49.99, "123 Main St"
);
// => Output: Inventory: reserved 2x p1
// => Output: Payment: charged customer c1 $49.99, auth=AUTH-...
// => Output: Shipping: scheduled delivery for order o1 to 123 Main St, tracking=TRK-o1
// => Output: Notification: sent order confirmation to alice@example.com, order=o1, tracking=TRK-o1
System.out.println("success=" + result.success + ", tracking=" + result.trackingId);
// => Output: success=true, tracking=TRK-o1

Key Takeaway: The Facade pattern gives callers a single, simplified method that hides the coordination complexity of multiple subsystem components — callers depend on the facade interface, not on the individual subsystem classes.

Why It Matters: Facades are the architecture of every SDK and client library — they hide dozens of internal calls, retry logic, and protocol details behind simple method calls. In microservices, API Gateways act as facades that route, aggregate, and transform calls across multiple backend services. Teams that adopt facade patterns for complex workflows reduce onboarding friction: new developers call orderFacade.placeOrder() without needing to understand inventory reservation, payment retry logic, and shipping provider APIs simultaneously.


Behavioral Patterns

Example 41: Command Pattern — Encapsulate Actions as Objects

The Command pattern encapsulates a request as an object, allowing you to parameterize clients with different requests, queue or log requests, and support undoable operations. Each command knows how to execute itself and optionally how to undo itself.

%% Color Palette: Blue #0173B2, Orange #DE8F05, Teal #029E73, Purple #CC78BC, Brown #CA9161
graph LR
    A["Invoker<br/>(CommandQueue)"]
    B["Command<br/>Interface"]
    C["CreateUserCmd"]
    D["DeleteUserCmd"]
    E["UpdateEmailCmd"]
    F["Receiver<br/>(UserService)"]
 
    A -->|executes| B
    B -.->|implements| C
    B -.->|implements| D
    B -.->|implements| E
    C -->|calls| F
    D -->|calls| F
    E -->|calls| F
 
    style A fill:#0173B2,stroke:#000,color:#fff
    style B fill:#DE8F05,stroke:#000,color:#fff
    style C fill:#029E73,stroke:#000,color:#fff
    style D fill:#029E73,stroke:#000,color:#fff
    style E fill:#029E73,stroke:#000,color:#fff
    style F fill:#CA9161,stroke:#000,color:#fff
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.HashMap;
import java.util.Map;
 
// => RECEIVER: the object that actually performs the work
// => Commands delegate to UserStore; store never knows about commands
class UserStore {
    private final Map<String, String> users = new HashMap<>();
    // => key: userId, value: email
 
    void create(String userId, String email) {
        users.put(userId, email); // => add user to store
        System.out.printf("Created user %s with email %s%n", userId, email);
    }
 
    String delete(String userId) {
        String email = users.remove(userId);
        // => remove and return email for undo support
        if (email != null) System.out.println("Deleted user " + userId);
        return email; // => null if user did not exist
    }
 
    String updateEmail(String userId, String newEmail) {
        if (!users.containsKey(userId)) return null;
        // => user not found
        String oldEmail = users.get(userId); // => save current email for undo
        users.put(userId, newEmail);          // => perform the update
        System.out.printf("Updated %s email to %s%n", userId, newEmail);
        return oldEmail; // => caller uses this to support undo
    }
 
    Map<String, String> all() {
        return Map.copyOf(users); // => immutable snapshot of current state
    }
}
 
// => COMMAND INTERFACE: every command must implement execute and undo
interface Command {
    void execute(); // => perform the action
    void undo();    // => reverse the action exactly
}
 
// => CONCRETE COMMAND 1: create a user — captures all params at construction
class CreateUserCommand implements Command {
    private final UserStore store; // => receiver
    private final String userId;   // => command param captured at construction
    private final String email;    // => not passed at execute() time
 
    CreateUserCommand(UserStore store, String userId, String email) {
        this.store  = store;
        this.userId = userId;
        this.email  = email;
    }
 
    @Override
    public void execute() {
        store.create(userId, email); // => delegate to receiver
    }
 
    @Override
    public void undo() {
        store.delete(userId);        // => reverse: remove the created user
        System.out.println("Undo: removed user " + userId);
    }
}
 
// => CONCRETE COMMAND 2: update user email with undo support
class UpdateEmailCommand implements Command {
    private final UserStore store;
    private final String userId;
    private final String newEmail;
    private String oldEmail; // => captured during execute() for undo
 
    UpdateEmailCommand(UserStore store, String userId, String newEmail) {
        this.store    = store;
        this.userId   = userId;
        this.newEmail = newEmail;
    }
 
    @Override
    public void execute() {
        oldEmail = store.updateEmail(userId, newEmail);
        // => save old email so undo can restore it exactly
    }
 
    @Override
    public void undo() {
        if (oldEmail != null) {
            store.updateEmail(userId, oldEmail);
            // => restore previous email
            System.out.println("Undo: restored email to " + oldEmail);
        }
    }
}
 
// => INVOKER: queues commands and maintains undo history
class CommandQueue {
    private final Deque<Command> history = new ArrayDeque<>();
    // => stack of executed commands; pop() gives most recent
 
    void execute(Command command) {
        command.execute();    // => execute the command on the receiver
        history.push(command); // => push onto history stack for potential undo
    }
 
    void undoLast() {
        if (!history.isEmpty()) {
            Command command = history.pop(); // => take most recent command
            command.undo();                  // => reverse it
        }
    }
}
 
UserStore store = new UserStore();
CommandQueue queue = new CommandQueue();
 
queue.execute(new CreateUserCommand(store, "u1", "alice@example.com"));
// => Output: Created user u1 with email alice@example.com
queue.execute(new UpdateEmailCommand(store, "u1", "newalice@example.com"));
// => Output: Updated u1 email to newalice@example.com
System.out.println(store.all()); // => Output: {u1=newalice@example.com}
 
queue.undoLast(); // => undo UpdateEmail command
// => Output: Updated u1 email to alice@example.com
// => Output: Undo: restored email to alice@example.com
System.out.println(store.all()); // => Output: {u1=alice@example.com}

Key Takeaway: The Command pattern encapsulates an action and its parameters as an object — this object can be stored in a queue, logged, replicated, or reversed, enabling features like undo/redo, job queues, and audit trails.

Why It Matters: The Command pattern powers undo/redo in text editors, task queues (Celery, Bull), and transactional outbox patterns. Git is fundamentally a command log — every commit is an immutable command that can be replayed or reverted. In financial systems, commands serve as audit trails where every action (CreateTransaction, UpdateBalance) is stored as an immutable record. Implementing command-sourced audit logs means compliance reporting becomes a query over the command log, requiring no additional instrumentation.


Example 42: Mediator Pattern — Centralized Component Coordination

The Mediator pattern defines an object that encapsulates how a set of objects interact. It promotes loose coupling by keeping objects from referring to each other explicitly and allows you to vary their interaction independently. The mediator becomes the hub; components become spokes.

import java.util.List;
 
// => MEDIATOR INTERFACE: defines how components talk to the mediator
// => Components call notify() instead of calling each other directly
interface Mediator {
    void notify(Component sender, String event, Object data);
    // => sender identifies which component fired; event names what happened
}
 
// => BASE COMPONENT: holds a reference to its mediator, uses it to emit events
// => Components never import each other — they only import Mediator
class Component {
    protected final String name; // => component identifier for logging
    protected Mediator mediator; // => set by mediator after construction
 
    Component(String name) {
        this.name = name;
    }
 
    void setMediator(Mediator mediator) {
        this.mediator = mediator; // => wired by the mediator at creation time
    }
 
    void emit(String event, Object data) {
        if (mediator != null) {
            mediator.notify(this, event, data);
            // => send event to mediator; mediator decides who else hears it
        }
    }
}
 
// => CONCRETE COMPONENTS: they emit events; never reference each other
class SearchBox extends Component {
    SearchBox() { super("search_box"); }
 
    void search(String query) {
        System.out.println("SearchBox: searching for '" + query + "'");
        emit("search_submitted", query);
        // => notifies mediator; does NOT call ResultsList directly
    }
}
 
class ResultsList extends Component {
    ResultsList() { super("results"); }
 
    void display(List<String> results) {
        System.out.println("ResultsList: displaying " + results.size() + " results: " + results);
        emit("results_displayed", results);
        // => notifies mediator; does NOT call StatusBar directly
    }
}
 
class StatusBar extends Component {
    StatusBar() { super("status"); }
 
    void update(String message) {
        System.out.println("StatusBar: " + message);
        // => StatusBar is a leaf — no events emitted
    }
}
 
class LoadingSpinner extends Component {
    LoadingSpinner() { super("spinner"); }
 
    void show() {
        System.out.println("Spinner: showing");
        emit("spinner_shown", null);
    }
 
    void hide() {
        System.out.println("Spinner: hidden");
        // => hide emits no event in this example
    }
}
 
// => CONCRETE MEDIATOR: knows all components and orchestrates their interactions
// => All coordination logic lives here — components stay simple
class SearchPageMediator implements Mediator {
    final SearchBox searchBox = new SearchBox();    // => create and own all components
    final ResultsList results  = new ResultsList();
    final StatusBar status     = new StatusBar();
    final LoadingSpinner spinner = new LoadingSpinner();
 
    SearchPageMediator() {
        // => wire self as mediator into every component
        for (Component c : List.of(searchBox, results, status, spinner)) {
            c.setMediator(this);
        }
        // => after wiring, all components know only this mediator
    }
 
    @Override
    public void notify(Component sender, String event, Object data) {
        // => ALL coordination logic is centralized here in the mediator
        if ("search_submitted".equals(event)) {
            spinner.show();                             // => start loading indicator
            status.update("Searching for '" + data + "'..."); // => update status text
            List<String> fakeResults = List.of(
                "Result for " + data + " #0",
                "Result for " + data + " #1",
                "Result for " + data + " #2"
            );
            // => simulate search results; real impl calls search service
            results.display(fakeResults);              // => populate results list
            spinner.hide();                            // => stop loading indicator
            status.update("Found " + fakeResults.size() + " results for '" + data + "'");
            // => update status with result count
        }
    }
}
 
// demo: triggering one component cascades through the mediator
SearchPageMediator mediator = new SearchPageMediator();
// => all components created and wired inside constructor
mediator.searchBox.search("architecture patterns");
// => Output: SearchBox: searching for 'architecture patterns'
// => Output: Spinner: showing
// => Output: StatusBar: Searching for 'architecture patterns'...
// => Output: ResultsList: displaying 3 results: [Result for architecture patterns #0, ...]
// => Output: Spinner: hidden
// => Output: StatusBar: Found 3 results for 'architecture patterns'
// => SearchBox never called ResultsList or Spinner directly

Key Takeaway: The Mediator centralizes all coordination logic — components emit events and receive instructions only through the mediator, preventing the web of direct cross-references that emerges when N components communicate peer-to-peer.

Why It Matters: Without a mediator, N components communicating directly create O(N²) coupling — every new component must know about every existing component. The Mediator reduces this to O(N): each component knows only the mediator. UI frameworks (React's event bubbling, Vue's event bus), air traffic control systems, and chat servers all use mediator-style coordination. Adding a new component to a mediator-based system requires wiring it only to the mediator, not to every existing peer—a linear cost versus the quadratic cost of direct coupling.


Example 43: State Pattern — Objects That Change Behavior Based on State

The State pattern allows an object to change its behavior when its internal state changes. The object will appear to change its class. Instead of using if/elif chains to check state, each state is a class that implements the behavior for that state.

// => STATE INTERFACE: defines behavior for every possible order state
interface OrderState {
    void pay();          // => attempt to pay
    void ship();         // => attempt to ship
    void cancel();       // => attempt to cancel
    String getStatus();  // => current status name
}
 
// => CONTEXT: the Order that delegates all behavior to its current state
class OrderContext {
    private OrderState state; // => current state — changes over lifecycle
 
    OrderContext() {
        this.state = new PendingState(this); // => always starts as Pending
        System.out.println("Order created in state: " + state.getStatus());
    }
 
    // => state transition: called by state objects, never by external callers
    void transitionTo(OrderState next) {
        System.out.println("  State: " + state.getStatus() + " -> " + next.getStatus());
        this.state = next; // => replace current state with new state object
    }
 
    // => delegate all behavior to the current state object
    void pay()    { state.pay(); }    // => behavior differs per state
    void ship()   { state.ship(); }   // => behavior differs per state
    void cancel() { state.cancel(); } // => behavior differs per state
    String status() { return state.getStatus(); }
}
 
// => CONCRETE STATE: Pending — awaiting payment
class PendingState implements OrderState {
    private final OrderContext order; // => back-reference to trigger transitions
    PendingState(OrderContext order) { this.order = order; }
 
    @Override public void pay() {
        System.out.println("Payment received");
        order.transitionTo(new PaidState(order));
        // => valid transition: Pending -> Paid
    }
 
    @Override public void ship() {
        System.out.println("Cannot ship: order not paid yet");
        // => invalid in this state — no transition occurs
    }
 
    @Override public void cancel() {
        System.out.println("Order cancelled before payment");
        order.transitionTo(new CancelledState(order));
        // => valid transition: Pending -> Cancelled
    }
 
    @Override public String getStatus() { return "PENDING"; }
}
 
// => CONCRETE STATE: Paid — payment received, awaiting shipment
class PaidState implements OrderState {
    private final OrderContext order;
    PaidState(OrderContext order) { this.order = order; }
 
    @Override public void pay() {
        System.out.println("Already paid");
        // => invalid: cannot pay twice — no transition
    }
 
    @Override public void ship() {
        System.out.println("Order shipped");
        order.transitionTo(new ShippedState(order));
        // => valid transition: Paid -> Shipped
    }
 
    @Override public void cancel() {
        System.out.println("Order cancelled, refund issued");
        order.transitionTo(new CancelledState(order));
        // => valid: refund required when cancelling after payment
    }
 
    @Override public String getStatus() { return "PAID"; }
}
 
// => CONCRETE STATE: Shipped — in transit, no backwards transitions
class ShippedState implements OrderState {
    private final OrderContext order;
    ShippedState(OrderContext order) { this.order = order; }
 
    @Override public void pay()    { System.out.println("Already paid"); }
    @Override public void ship()   { System.out.println("Already shipped"); }
    @Override public void cancel() { System.out.println("Cannot cancel: already shipped"); }
    // => all operations are no-ops or informational — shipped is near-terminal
 
    @Override public String getStatus() { return "SHIPPED"; }
}
 
// => CONCRETE STATE: Cancelled — terminal state, all operations rejected
class CancelledState implements OrderState {
    private final OrderContext order;
    CancelledState(OrderContext order) { this.order = order; }
 
    @Override public void pay()    { System.out.println("Cannot pay: order cancelled"); }
    @Override public void ship()   { System.out.println("Cannot ship: order cancelled"); }
    @Override public void cancel() { System.out.println("Already cancelled"); }
    // => no transitions from terminal state
 
    @Override public String getStatus() { return "CANCELLED"; }
}
 
// demo: valid order lifecycle
OrderContext order = new OrderContext();    // => Output: Order created in state: PENDING
order.ship();   // => Output: Cannot ship: order not paid yet
order.pay();    // => Output: Payment received
                // => Output:   State: PENDING -> PAID
order.ship();   // => Output: Order shipped
                // => Output:   State: PAID -> SHIPPED
order.cancel(); // => Output: Cannot cancel: already shipped
System.out.println(order.status()); // => Output: SHIPPED

Key Takeaway: The State pattern eliminates complex if/elif chains on state variables by giving each state its own class that implements the valid behaviors for that state — invalid transitions are handled within each state class, not scattered across conditional logic.

Why It Matters: Order lifecycle management, traffic lights, connection state machines, and CI/CD pipeline stages all require state-dependent behavior. Without the State pattern, every new state requires modifying every method that checks state — a classic open/closed principle violation. Complex state machines with many transitions become difficult to reason about when all logic is in one class; State pattern keeps each state's logic isolated and independently testable. State machines formalized this way also map directly to UML state diagrams for documentation, making the intended behavior verifiable against the implementation.


Example 44: Template Method — Define Algorithm Skeleton, Defer Steps to Subclasses

The Template Method pattern defines the skeleton of an algorithm in a base class, deferring some steps to subclasses. Subclasses can override specific steps without changing the algorithm's overall structure. This enforces a consistent process while allowing customization of individual steps.

import java.util.*;
 
// => ABSTRACT CLASS: defines the algorithm skeleton via a template method
// => Template method calls abstract hooks in a fixed sequence
abstract class DataExporter {
 
    // => TEMPLATE METHOD: invariant algorithm — validate -> transform -> serialize -> checksum
    // => Final prevents subclasses from reordering or skipping steps
    final String export(List<Map<String, Object>> data) {
        List<Map<String, Object>> validated   = validate(data);   // => step 1: filter invalid rows
        List<Map<String, Object>> transformed = transform(validated); // => step 2: shape the data
        String serialized = serialize(transformed);               // => step 3: produce string output
        String checksum   = computeChecksum(serialized);          // => step 4: invariant, not overridable
        return serialized + "\nChecksum:" + checksum;             // => step 5: invariant wrapper
    }
 
    // => ABSTRACT HOOKS: subclasses must fill in these steps
    protected abstract List<Map<String, Object>> validate(List<Map<String, Object>> data);
    protected abstract List<Map<String, Object>> transform(List<Map<String, Object>> data);
    protected abstract String serialize(List<Map<String, Object>> data);
 
    // => CONCRETE STEP: checksum is always the same — subclasses cannot override
    private String computeChecksum(String content) {
        return Integer.toHexString(content.hashCode()).substring(0, 8);
        // => simplified hash for demo; use SHA-256 in production
    }
}
 
// => CONCRETE SUBCLASS 1: CSV exporter
class CsvExporter extends DataExporter {
    @Override
    protected List<Map<String, Object>> validate(List<Map<String, Object>> data) {
        List<Map<String, Object>> result = new ArrayList<>();
        for (Map<String, Object> row : data) {
            if (!row.isEmpty()) result.add(row); // => filter empty rows
        }
        return result;
    }
 
    @Override
    protected List<Map<String, Object>> transform(List<Map<String, Object>> data) {
        List<Map<String, Object>> result = new ArrayList<>();
        for (Map<String, Object> row : data) {
            Map<String, Object> stringRow = new LinkedHashMap<>();
            for (Map.Entry<String, Object> e : row.entrySet()) {
                stringRow.put(e.getKey(), String.valueOf(e.getValue()));
                // => convert all values to strings for CSV compatibility
            }
            result.add(stringRow);
        }
        return result;
    }
 
    @Override
    protected String serialize(List<Map<String, Object>> data) {
        if (data.isEmpty()) return "";
        List<String> lines = new ArrayList<>();
        lines.add(String.join(",", data.get(0).keySet())); // => header row
        for (Map<String, Object> row : data) {
            lines.add(String.join(",", row.values().stream()
                .map(Object::toString).toArray(String[]::new)));
            // => data row: join all values with comma
        }
        return String.join("\n", lines); // => all lines joined
    }
}
 
// => CONCRETE SUBCLASS 2: JSON exporter
class JsonExporter extends DataExporter {
    @Override
    protected List<Map<String, Object>> validate(List<Map<String, Object>> data) {
        return new ArrayList<>(data); // => JSON handles any map structure natively
    }
 
    @Override
    protected List<Map<String, Object>> transform(List<Map<String, Object>> data) {
        return data; // => JSON handles types natively, no transformation needed
    }
 
    @Override
    protected String serialize(List<Map<String, Object>> data) {
        StringBuilder sb = new StringBuilder("[");
        for (int i = 0; i < data.size(); i++) {
            sb.append("{");
            data.get(i).forEach((k, v) -> sb.append("\"").append(k).append("\":\"").append(v).append("\","));
            if (sb.charAt(sb.length() - 1) == ',') sb.setLength(sb.length() - 1);
            sb.append("}");
            if (i < data.size() - 1) sb.append(","); // => separator between objects
        }
        return sb.append("]").toString(); // => close JSON array
    }
}
 
// demo: same export() call, different output format
List<Map<String, Object>> sampleData = List.of(
    new LinkedHashMap<>(Map.of("name", "Alice", "age", 30)),
    new LinkedHashMap<>(Map.of("name", "Bob",   "age", 25))
);
 
CsvExporter csv = new CsvExporter();
System.out.println("=== CSV ===");
System.out.println(csv.export(sampleData));
// => Output: name,age\nAlice,30\nBob,25\nChecksum:xxxxxxxx
 
JsonExporter json = new JsonExporter();
System.out.println("=== JSON ===");
System.out.println(json.export(sampleData));
// => Output: [{"name":"Alice","age":"30"},{"name":"Bob","age":"25"}]\nChecksum:yyyyyyyy
// => Both use identical validate->transform->serialize->checksum sequence
// => Only the format-specific steps differ between subclasses

Key Takeaway: The Template Method pattern enforces a consistent algorithm structure (validate → transform → serialize → checksum) while letting subclasses customize individual steps — the base class owns the workflow, subclasses own the variations.

Why It Matters: Template Method is foundational to frameworks where the framework defines the lifecycle and applications fill in the steps. Java's servlet lifecycle (init, service, destroy), Spring's JdbcTemplate (connection, statement, mapping, cleanup), and Django's class-based views (get, post, put, delete) all use Template Method. The pattern prevents scattered code where each exporter reimplements checksum and validation logic independently, leading to divergence. When you need to change checksum algorithm globally, you change one place in the base class.


Domain-Driven Design Building Blocks

Example 45: Value Objects — Immutable Domain Concepts Without Identity

Paradigm Note: Immutable, equality-by-value types are the default in FP — every record in F#/Haskell/Clojure is a value object without ceremony. Wlaschin (Domain Modeling Made Functional): value objects are the natural unit. In OOP they require deliberate equals/hashCode/final discipline. See the FP framing.

Value Objects represent domain concepts that are defined entirely by their attributes — two value objects with the same attributes are considered equal. They have no identity (no ID), are immutable, and encapsulate domain validation and behavior around the concept they represent.

%% Color Palette: Blue #0173B2, Orange #DE8F05, Teal #029E73, Purple #CC78BC, Brown #CA9161
graph TD
    A["Money<br/>amount + currency<br/>(Value Object)"]
    B["Email<br/>address string<br/>(Value Object)"]
    C["Address<br/>street, city, zip<br/>(Value Object)"]
    D["Order<br/>(Entity)"]
 
    D -->|has a| A
    D -->|has a| B
    D -->|has a| C
 
    style A fill:#029E73,stroke:#000,color:#fff
    style B fill:#029E73,stroke:#000,color:#fff
    style C fill:#029E73,stroke:#000,color:#fff
    style D fill:#0173B2,stroke:#000,color:#fff
import java.util.Objects;
 
// => VALUE OBJECT: Money — defined by amount + currency together
// => No setters: once constructed, values never change (immutable)
final class Money {
    private final double amount;   // => monetary amount
    private final String currency; // => ISO 4217 currency code, normalized to uppercase
 
    Money(double amount, String currency) {
        // => validation at construction — invalid Money cannot exist
        if (amount < 0)
            throw new IllegalArgumentException("Money amount cannot be negative: " + amount);
        if (currency == null || currency.length() != 3)
            throw new IllegalArgumentException("Currency must be a 3-letter ISO code: " + currency);
        this.amount   = amount;
        this.currency = currency.toUpperCase(); // => normalize to uppercase at construction
    }
 
    Money add(Money other) {
        if (!this.currency.equals(other.currency))
            throw new IllegalArgumentException("Cannot add " + currency + " and " + other.currency);
        return new Money(this.amount + other.amount, this.currency);
        // => returns NEW Money — does not mutate self (immutable value semantics)
    }
 
    Money multiply(double factor) {
        return new Money(Math.round(this.amount * factor * 100.0) / 100.0, this.currency);
        // => rounds to 2 decimal places (cents precision)
    }
 
    // => EQUALITY BY VALUE: two Money objects with same amount+currency are equal
    @Override public boolean equals(Object o) {
        if (!(o instanceof Money)) return false;
        Money m = (Money) o;
        return Double.compare(amount, m.amount) == 0 && currency.equals(m.currency);
        // => reference identity irrelevant — only values matter
    }
 
    @Override public int hashCode() { return Objects.hash(amount, currency); }
 
    @Override public String toString() {
        return String.format("%s %.2f", currency, amount); // => "USD 10.00"
    }
 
    double getAmount()   { return amount; }
    String getCurrency() { return currency; }
}
 
// => VALUE OBJECT: Email — self-validating, immutable
final class Email {
    private final String address; // => always lowercase after construction
 
    Email(String raw) {
        // => validation at construction — invalid Email cannot exist
        if (raw == null || !raw.contains("@") || !raw.contains("."))
            throw new IllegalArgumentException("Invalid email address: " + raw);
        this.address = raw.toLowerCase(); // => normalize to lowercase
    }
 
    String domain() {
        return address.split("@")[1]; // => extract domain part
    }
 
    @Override public boolean equals(Object o) {
        return o instanceof Email && address.equals(((Email) o).address);
        // => equality by normalized address value
    }
 
    @Override public int hashCode() { return address.hashCode(); }
    @Override public String toString() { return address; }
}
 
// demo: value object equality and immutability
Money price1 = new Money(10.0, "USD"); // => USD 10.00
Money price2 = new Money(10.0, "USD"); // => USD 10.00 — same attributes
Money price3 = new Money(20.0, "USD"); // => USD 20.00 — different amount
 
System.out.println(price1.equals(price2)); // => Output: true  (same values = equal)
System.out.println(price1.equals(price3)); // => Output: false (different amount)
System.out.println(price1 == price2);      // => Output: false (different object references)
// => reference identity differs; equality is determined by values, not identity
 
Money total = price1.add(price3);
System.out.println(total); // => Output: USD 30.00
 
try {
    new Money(-5.0, "USD"); // => attempt negative amount
} catch (IllegalArgumentException e) {
    System.out.println(e.getMessage()); // => Output: Money amount cannot be negative: -5.0
}
 
Email email1 = new Email("Alice@Example.COM"); // => mixed case input
Email email2 = new Email("alice@example.com"); // => lowercase input
System.out.println(email1.equals(email2)); // => Output: true (both normalized to lowercase)
System.out.println(email1.domain());       // => Output: example.com

Key Takeaway: Value objects are immutable, equality-by-value domain concepts that validate themselves at construction — they are safer than primitives (validated, meaningful type) and simpler than entities (no identity, no lifecycle).

Why It Matters: Using primitives for domain concepts (float for money, string for email) leads to bugs: comparing "$10 USD" with "10.0" requires scattered null checks, currency mismatches go undetected, and invalid values propagate silently. Value objects eliminate entire categories of bugs by making invalid states unrepresentable—a Money value object that carries its currency alongside its amount prevents currency confusion at the type level, turning what would be a silent runtime error into a compile-time failure. In financial systems, this class of enforcement is especially critical because currency mismatches carry regulatory and financial consequences.


Example 46: Aggregate Roots — Consistency Boundaries in DDD

An Aggregate is a cluster of domain objects treated as a single unit. The Aggregate Root is the only member that external objects can hold references to. All modifications to objects inside the aggregate must go through the root, which enforces invariants across the entire cluster.

import java.util.*;
 
// => VALUE OBJECT: line item inside the aggregate
// => Immutable snapshot — values fixed at creation time
final class LineItem {
    final String productId;   // => identifies the product
    final String productName; // => snapshot name at order time (not live)
    final double unitPrice;   // => snapshot price at order time (not live)
    final int    quantity;    // => how many units
 
    LineItem(String productId, String productName, double unitPrice, int quantity) {
        this.productId   = productId;
        this.productName = productName;
        this.unitPrice   = unitPrice;
        this.quantity    = quantity;
    }
 
    double subtotal() { return unitPrice * quantity; } // => line total
}
 
// => AGGREGATE ROOT: Order — the consistency boundary
// => All modifications to order contents MUST go through Order methods
class Order {
    final String id;          // => aggregate identity
    final String customerId;  // => relationship by ID, not by object reference
    private String status = "DRAFT";
    // => status only changes through root methods
    private final List<LineItem> items = new ArrayList<>();
    // => internal — external code accesses via items() accessor only
 
    private static final double MAX_ORDER_VALUE = 10_000.0;
    // => BUSINESS INVARIANT enforced by the aggregate root
 
    Order(String id, String customerId) {
        this.id         = id;
        this.customerId = customerId;
    }
 
    void addItem(String productId, String name, double price, int qty) {
        // => GUARD: can only add items to DRAFT orders
        if (!"DRAFT".equals(status))
            throw new IllegalStateException("Cannot add items to order in status " + status);
 
        LineItem item = new LineItem(productId, name, price, qty);
        // => INVARIANT CHECK: compute candidate total before committing
        double candidateTotal = items.stream().mapToDouble(LineItem::subtotal).sum() + item.subtotal();
        if (candidateTotal > MAX_ORDER_VALUE)
            throw new IllegalStateException("Order total would exceed maximum $" + MAX_ORDER_VALUE);
 
        items.add(item); // => commit: invariant satisfied
    }
 
    void removeItem(String productId) {
        if (!"DRAFT".equals(status))
            throw new IllegalStateException("Cannot remove items from non-draft order");
        boolean removed = items.removeIf(i -> i.productId.equals(productId));
        // => removeIf returns true if any element was removed
        if (!removed)
            throw new NoSuchElementException("Product " + productId + " not in order");
    }
 
    void confirm() {
        // => INVARIANT: cannot confirm an empty order
        if (items.isEmpty())
            throw new IllegalStateException("Cannot confirm empty order");
        status = "CONFIRMED"; // => state transition through root — never set directly
    }
 
    double total() {
        return items.stream().mapToDouble(LineItem::subtotal).sum();
        // => aggregate total computed from child line items
    }
 
    List<LineItem> items() {
        return Collections.unmodifiableList(items);
        // => return unmodifiable view — external code cannot mutate the internal list
    }
 
    String status() { return status; }
}
 
// demo: all modifications go through the aggregate root
Order order = new Order("o1", "c1"); // => starts as DRAFT
 
order.addItem("p1", "Widget", 50.0, 2); // => subtotal $100
order.addItem("p2", "Gadget", 30.0, 1); // => subtotal $30
System.out.println("Total: $" + order.total()); // => Output: Total: $130.0
 
try {
    order.addItem("p3", "BigItem", 9_900.0, 1); // => would exceed $10,000
} catch (IllegalStateException e) {
    System.out.println(e.getMessage()); // => Output: Order total would exceed maximum $10000.0
}
 
order.confirm();                         // => transition: DRAFT -> CONFIRMED
System.out.println(order.status());      // => Output: CONFIRMED
 
try {
    order.addItem("p4", "Extra", 10.0, 1); // => cannot add to CONFIRMED
} catch (IllegalStateException e) {
    System.out.println(e.getMessage()); // => Output: Cannot add items to order in status CONFIRMED
}

Key Takeaway: The Aggregate Root is the sole entry point for all modifications to a cluster of related objects — this concentrates business rule enforcement in one place and ensures the cluster is always in a valid state.

Why It Matters: Without aggregate roots, invariants (max order value, minimum item count) get scattered across services, controllers, and repositories — each enforcing them inconsistently. DDD's aggregate design aligns with how databases enforce consistency: a transaction is a consistency boundary just as an aggregate is. Aggregate roots centralize enforcement so that invariants cannot be bypassed by any caller, regardless of which code path initiates the modification. Repositories save and load complete aggregates, not partial graphs, ensuring the consistency boundary is respected across the persistence layer.


Example 47: Bounded Contexts — Separating Domain Models by Responsibility

A Bounded Context defines the scope within which a particular domain model applies. The same concept (e.g., "Customer") can mean different things in different bounded contexts — the Sales context cares about purchase history, the Shipping context cares about delivery address. Each context has its own model.

import java.util.*;
 
// === BOUNDED CONTEXT 1: Sales — Customer means loyalty + purchase history ===
 
// => Sales-specific Customer model: buying behavior, loyalty, discounts
// => Does NOT know about delivery addresses or shipment preferences
class SalesCustomer {
    final String customerId;         // => shared identifier across contexts
    final String loyaltyTier;        // => "bronze" | "silver" | "gold" — Sales concept
    final double totalSpend;         // => cumulative purchase total — Sales concept
    final List<String> preferredCategories; // => for recommendations — Sales concept
 
    SalesCustomer(String customerId, String loyaltyTier, double totalSpend, List<String> preferred) {
        this.customerId          = customerId;
        this.loyaltyTier         = loyaltyTier;
        this.totalSpend          = totalSpend;
        this.preferredCategories = preferred;
    }
}
 
// => Sales context owns its own domain operations
class SalesService {
    double calculateDiscount(SalesCustomer customer, double orderTotal) {
        Map<String, Double> rates = Map.of("bronze", 0.0, "silver", 0.05, "gold", 0.1);
        double rate = rates.getOrDefault(customer.loyaltyTier, 0.0);
        return orderTotal * rate;
        // => discount logic: Sales context owns this, not Shipping
    }
}
 
// === BOUNDED CONTEXT 2: Shipping — Customer means delivery logistics ===
 
// => Shipping-specific Address value object — meaningless in Sales context
class Address {
    final String street;    // => delivery street
    final String city;      // => delivery city
    final String country;   // => ISO country code
    final String postalCode; // => postal code
 
    Address(String street, String city, String country, String postalCode) {
        this.street     = street;
        this.city       = city;
        this.country    = country;
        this.postalCode = postalCode;
    }
}
 
// => Shipping-specific Customer model: delivery details, preferences
// => Same conceptual customer, completely different attributes
class ShippingCustomer {
    final String  customerId;          // => same shared ID, different attributes
    final Address defaultAddress;      // => shipping-specific concept
    final String  deliveryPreferences; // => "standard" | "express" — meaningless in Sales
 
    ShippingCustomer(String customerId, Address defaultAddress, String deliveryPreferences) {
        this.customerId          = customerId;
        this.defaultAddress      = defaultAddress;
        this.deliveryPreferences = deliveryPreferences;
    }
}
 
// => Shipping context owns its own domain operations
class ShippingService {
    double calculateDeliveryFee(ShippingCustomer customer, double weightKg) {
        double baseRate = "express".equals(customer.deliveryPreferences) ? 15.0 : 5.0;
        return baseRate + weightKg * 0.5;
        // => Shipping context owns delivery fee logic, not Sales
    }
}
 
// => TRANSLATION LAYER: maps shared customer ID + address to ShippingCustomer
// => Prevents Sales model from importing ShippingCustomer directly
class CustomerContextTranslator {
    ShippingCustomer toShippingCustomer(String sharedId, Address address) {
        return new ShippingCustomer(
            sharedId,   // => same ID links contexts without coupling models
            address,    // => translate address from shared data
            "standard"  // => default when preference unknown
        );
    }
}
 
// demo: same conceptual customer, two distinct models
SalesCustomer salesCustomer = new SalesCustomer(
    "c1", "gold", 1500.0, List.of("electronics", "books")
);
 
CustomerContextTranslator translator = new CustomerContextTranslator();
ShippingCustomer shippingCustomer = translator.toShippingCustomer(
    "c1", new Address("123 Main St", "Berlin", "DE", "10115")
);
 
SalesService    salesSvc    = new SalesService();
ShippingService shippingSvc = new ShippingService();
 
double discount = salesSvc.calculateDiscount(salesCustomer, 200.0);
System.out.println("Discount: $" + discount);
// => Output: Discount: $20.0 (10% gold tier)
 
double delivery = shippingSvc.calculateDeliveryFee(shippingCustomer, 2.5);
System.out.println("Delivery: $" + delivery);
// => Output: Delivery: $6.25 (standard + weight)
// => Each context operates on its own Customer model — no shared model contamination

Key Takeaway: Each Bounded Context maintains its own model of shared concepts — "Customer" means different attributes in Sales vs Shipping, and each context owns the operations relevant to its responsibility.

Why It Matters: Attempting to create a single unified Customer model that satisfies Sales, Shipping, Billing, and Support contexts creates a god object that grows unboundedly and becomes impossible to change without breaking all consumers. Each context needs different attributes, different validation rules, and different operations—what "Customer" means to a recommendations engine differs fundamentally from what it means to a logistics system. Bounded Contexts map directly to microservice boundaries: each service owns one context's model and evolves it independently, eliminating the cross-team coordination overhead imposed by a shared universal model.


Example 48: Anti-Corruption Layer — Protecting the Domain from External Models

The Anti-Corruption Layer (ACL) is a translation layer between two bounded contexts or between the domain model and an external system. It prevents the external system's model, terminology, and concepts from leaking into the domain model. The ACL translates both directions.

%% Color Palette: Blue #0173B2, Orange #DE8F05, Teal #029E73, Purple #CC78BC, Brown #CA9161
graph LR
    A["Domain Model<br/>(Internal)"]
    B["Anti-Corruption<br/>Layer"]
    C["Legacy System<br/>(External)"]
 
    A -->|domain concepts| B
    B -->|legacy format| C
    C -->|legacy format| B
    B -->|domain concepts| A
 
    style A fill:#0173B2,stroke:#000,color:#fff
    style B fill:#CC78BC,stroke:#000,color:#fff
    style C fill:#CA9161,stroke:#000,color:#fff
import java.util.Map;
 
// => DOMAIN MODEL: clean internal representation
// => All field names, types, and conventions are domain-decided — not legacy-dictated
final class DomainProduct {
    final String  id;        // => internal product ID (stripped of ERP padding)
    final String  name;      // => human-readable name
    final double  priceUsd;  // => always in USD (domain decision)
    final String  category;  // => normalized category name
    final boolean inStock;   // => simple boolean availability
 
    DomainProduct(String id, String name, double priceUsd, String category, boolean inStock) {
        this.id       = id;
        this.name     = name;
        this.priceUsd = priceUsd;
        this.category = category;
        this.inStock  = inStock;
    }
 
    @Override public String toString() {
        return "DomainProduct{id='" + id + "', name='" + name + "', priceUsd=" + priceUsd
            + ", category='" + category + "', inStock=" + inStock + "}";
    }
}
 
// => LEGACY EXTERNAL SYSTEM: different field names, types, and conventions
// => This mirrors data returned from a legacy ERP or third-party catalog API
class LegacyProductData {
    String PROD_CODE  = "";   // => ERP internal code with leading zeros (uppercase convention)
    String PROD_DESC  = "";   // => description field (NOT 'name')
    double UNIT_PRICE = 0.0;  // => price in EUR (legacy system is EU-based)
    String CAT_CODE   = "";   // => numeric category code (NOT a name)
    int    STOCK_QTY  = 0;    // => quantity on hand (NOT a boolean)
}
 
// => ANTI-CORRUPTION LAYER: translates between legacy ERP and domain models
// => Domain code calls fromLegacy/toLegacy — it never directly touches ERP field names
class ProductACL {
    // => category code mapping: legacy numeric codes -> domain category names
    private static final Map<String, String> CATEGORY_MAP = Map.of(
        "001", "electronics",
        "002", "clothing",
        "003", "books",
        "999", "uncategorized"
    );
    private static final double EUR_TO_USD = 1.08;
    // => in production: fetch live exchange rate from a FX service
 
    DomainProduct fromLegacy(LegacyProductData legacy) {
        String domainId = legacy.PROD_CODE.replaceAll("^0+", "");
        // => TRANSLATE: strip leading zeros — ERP "00042" becomes domain "42"
 
        double priceUsd = Math.round(legacy.UNIT_PRICE * EUR_TO_USD * 100.0) / 100.0;
        // => TRANSLATE: EUR price -> USD price (currency conversion)
 
        String category = CATEGORY_MAP.getOrDefault(legacy.CAT_CODE, "uncategorized");
        // => TRANSLATE: numeric code -> domain category name
 
        boolean inStock = legacy.STOCK_QTY > 0;
        // => TRANSLATE: integer quantity -> boolean availability
 
        return new DomainProduct(domainId, legacy.PROD_DESC, priceUsd, category, inStock);
        // => domain model never sees PROD_CODE, UNIT_PRICE, CAT_CODE, or STOCK_QTY
    }
 
    LegacyProductData toLegacy(DomainProduct product) {
        LegacyProductData legacy = new LegacyProductData();
        legacy.PROD_CODE  = String.format("%05d", Integer.parseInt(product.id));
        // => TRANSLATE: pad to 5 digits — domain "42" becomes ERP "00042"
        legacy.PROD_DESC  = product.name;
        // => TRANSLATE: map field name back
        legacy.UNIT_PRICE = Math.round(product.priceUsd / EUR_TO_USD * 100.0) / 100.0;
        // => TRANSLATE: USD price -> EUR price
        legacy.CAT_CODE   = CATEGORY_MAP.entrySet().stream()
            .filter(e -> e.getValue().equals(product.category))
            .map(Map.Entry::getKey).findFirst().orElse("999");
        // => TRANSLATE: domain category name -> numeric code
        legacy.STOCK_QTY  = product.inStock ? 1 : 0;
        // => TRANSLATE: boolean -> integer (1 or 0)
        return legacy;
    }
}
 
// demo: fetch from legacy, use in domain, write back
ProductACL acl = new ProductACL();
 
// simulate: legacy ERP returns this raw structure
LegacyProductData raw = new LegacyProductData();
raw.PROD_CODE  = "00042";
raw.PROD_DESC  = "Wireless Headphones";
raw.UNIT_PRICE = 92.59; // => EUR
raw.CAT_CODE   = "001";
raw.STOCK_QTY  = 15;
 
DomainProduct domain = acl.fromLegacy(raw);
System.out.println(domain);
// => Output: DomainProduct{id='42', name='Wireless Headphones', priceUsd=99.99, category='electronics', inStock=true}
// => domain code works with clean names; never sees PROD_CODE or STOCK_QTY
 
// modify in domain, write back through ACL
DomainProduct updated = new DomainProduct("42", "Wireless Headphones Pro", 109.99, "electronics", true);
LegacyProductData legacyWrite = acl.toLegacy(updated);
System.out.println(legacyWrite.PROD_CODE);   // => Output: 00042 (padded back)
System.out.println(legacyWrite.UNIT_PRICE);  // => Output: 101.84 (converted to EUR)

Key Takeaway: The Anti-Corruption Layer translates external system models into domain concepts at the boundary — the domain model never directly touches external field names, data types, or conventions.

Why It Matters: Without an ACL, legacy system concepts (numeric codes, currency-specific prices, quantity-as-boolean flags) leak into the domain model and spread throughout the codebase. When the legacy system changes (new field names, different currency representation), every domain file that imported legacy concepts breaks. The ACL pattern is essential for modernization projects where new systems must coexist with legacy ERPs—it contains the blast radius of legacy changes to the translation layer and keeps the domain model expressing concepts in terms that match the new system's understanding of the world.


CQRS and Advanced Patterns

Example 49: CQRS Pattern — Separate Read and Write Models

Paradigm Note: CQRS maps naturally to FP — write side = command (effectful), read side = pure projection over immutable data (Fowler, CQRS). OOP CQRS often retains mutable aggregate framing on the write side. See the FP framing.

Command Query Responsibility Segregation (CQRS) separates the model used to update information (Commands) from the model used to read information (Queries). The write model enforces business rules; the read model is optimized for query performance. They can evolve independently.

%% Color Palette: Blue #0173B2, Orange #DE8F05, Teal #029E73, Purple #CC78BC, Brown #CA9161
graph TD
    A["Client"]
    B["Command Handler<br/>(Write Side)"]
    C["Write Store<br/>(Normalized DB)"]
    D["Query Handler<br/>(Read Side)"]
    E["Read Store<br/>(Denormalized / Cache)"]
 
    A -- Command --> B
    B -- persist --> C
    C -- sync event --> E
    A -- Query --> D
    D -- read --> E
 
    style A fill:#CA9161,stroke:#000,color:#fff
    style B fill:#0173B2,stroke:#000,color:#fff
    style C fill:#029E73,stroke:#000,color:#fff
    style D fill:#CC78BC,stroke:#000,color:#fff
    style E fill:#DE8F05,stroke:#000,color:#fff
import java.util.*;
 
// === WRITE SIDE: normalized model optimized for business rule enforcement ===
 
// => WRITE MODEL: domain entity with business rules — not tailored to UI
class WriteProduct {
    String id;         // => product identity
    String name;       // => canonical product name
    double priceUsd;   // => normalized: actual price in USD
    int    stockQty;   // => normalized: actual quantity (not pre-computed label)
    String categoryId; // => normalized: foreign key to category table
 
    WriteProduct(String id, String name, double priceUsd, int stockQty, String categoryId) {
        this.id         = id;
        this.name       = name;
        this.priceUsd   = priceUsd;
        this.stockQty   = stockQty;
        this.categoryId = categoryId;
    }
}
 
// => COMMAND: an intent to change state (imperative, present tense)
class AddStockCommand {
    final String productId; // => which product
    final int    qtyToAdd;  // => how much to add
    final String reason;    // => why (audit trail)
 
    AddStockCommand(String productId, int qtyToAdd, String reason) {
        this.productId = productId;
        this.qtyToAdd  = qtyToAdd;
        this.reason    = reason;
    }
}
 
// => COMMAND HANDLER: validates command, enforces business rules, updates write store
class ProductCommandHandler {
    private final Map<String, WriteProduct> writeStore = new HashMap<>();
    // => separate from read store — write side owns normalized data
 
    void seed(WriteProduct product) {
        writeStore.put(product.id, product); // => initial data
    }
 
    void handleAddStock(AddStockCommand cmd) {
        WriteProduct product = writeStore.get(cmd.productId);
        if (product == null)
            throw new NoSuchElementException("Product " + cmd.productId + " not found");
        // => BUSINESS RULE: cannot add non-positive stock
        if (cmd.qtyToAdd <= 0)
            throw new IllegalArgumentException("qtyToAdd must be positive");
 
        product.stockQty += cmd.qtyToAdd;
        // => update write model — triggers sync to read store in production (via event)
        writeStore.put(product.id, product);
        System.out.println("Write: added " + cmd.qtyToAdd + " units to " + product.name
            + " (reason: " + cmd.reason + ")");
    }
}
 
// === READ SIDE: denormalized model optimized for query performance ===
 
// => READ MODEL: flat, pre-joined view tailored to UI needs
// => Everything pre-computed so the query handler just reads, never computes
class ProductListItem {
    final String id;           // => product identity
    final String name;         // => product name
    final String displayPrice; // => pre-formatted: "$9.99" — no client-side formatting
    final String category;     // => pre-joined category name, not ID
    final String availability; // => pre-computed: "in_stock" | "low_stock" | "out_of_stock"
 
    ProductListItem(String id, String name, String displayPrice, String category, String availability) {
        this.id           = id;
        this.name         = name;
        this.displayPrice = displayPrice;
        this.category     = category;
        this.availability = availability;
    }
}
 
// => QUERY HANDLER: reads from read store — can be cache, read replica, Elasticsearch
class ProductQueryHandler {
    private List<ProductListItem> readStore = new ArrayList<>();
    // => separate from write store — read side owns denormalized projections
 
    void sync(List<ProductListItem> products) {
        this.readStore = products; // => updated by event from write side
    }
 
    List<ProductListItem> listAll() {
        return Collections.unmodifiableList(readStore);
        // => no computation needed — all data pre-projected
    }
 
    Optional<ProductListItem> findById(String id) {
        return readStore.stream().filter(p -> p.id.equals(id)).findFirst();
        // => in production: key lookup in Redis or Elasticsearch — O(1) not O(n)
    }
}
 
// wire up: simulate CQRS with two separate stores
ProductCommandHandler commandHandler = new ProductCommandHandler();
ProductQueryHandler   queryHandler   = new ProductQueryHandler();
 
// seed write store with normalized domain data
commandHandler.seed(new WriteProduct("p1", "Widget", 9.99, 5, "cat1"));
 
// seed read store with denormalized projection (in production: built from write events)
queryHandler.sync(List.of(
    new ProductListItem("p1", "Widget", "$9.99", "Electronics", "low_stock")
));
 
// COMMAND: add stock via write side
commandHandler.handleAddStock(new AddStockCommand("p1", 100, "restocking"));
// => Output: Write: added 100 units to Widget (reason: restocking)
 
// QUERY: read from read side (in production: read store synced via event after command)
queryHandler.findById("p1").ifPresent(item -> {
    System.out.println(item.availability); // => Output: low_stock (read model not yet updated in demo)
    // => in production: after write event, read model rebuilds projection: "in_stock"
    System.out.println(item.displayPrice); // => Output: $9.99 (pre-formatted, no client logic needed)
});

Key Takeaway: CQRS separates the write model (enforcing business rules, normalized) from the read model (optimized for display, denormalized) — they evolve independently and can be scaled separately.

Why It Matters: CQRS addresses the fundamental tension between write consistency and read performance. Systems like e-commerce product catalogs need fast, join-free reads at scale but require strict business rule enforcement on writes. Read replicas handle the majority of traffic while write masters focus on consistency — a common scaling strategy that reduces database read load by orders of magnitude. CQRS makes this separation explicit in the codebase, preventing the commands and queries from drifting back into a single tangled model.


Example 50: Middleware Pattern — Processing Pipeline for Cross-Cutting Concerns

The Middleware pattern processes requests through a chain of handlers, where each handler can modify the request or response, pass control to the next handler, or short-circuit the chain. Cross-cutting concerns (auth, logging, rate limiting) are implemented as middleware, not scattered in business logic.

import java.util.*;
import java.util.function.*;
 
// => REQUEST: carries HTTP method, path, headers, and optional body through the pipeline
record Request(String method, String path, Map<String, String> headers, Object body) {
    // => immutable value object — no setters, middleware must return new Response
    Request(String method, String path, Map<String, String> headers) {
        this(method, path, headers, null); // => convenience constructor, body optional
    }
}
 
// => RESPONSE: result produced by a handler at any point in the pipeline
record Response(int status, Object body) {
    // => immutable value — middleware that short-circuits returns its own Response
}
 
// => HANDLER FUNCTIONAL INTERFACE: takes a Request, returns a Response
@FunctionalInterface
interface Handler {
    Response handle(Request request); // => core contract for every handler in the chain
}
 
// => MIDDLEWARE FUNCTIONAL INTERFACE: wraps an existing Handler, returns a new Handler
@FunctionalInterface
interface Middleware {
    Handler apply(Handler next); // => receives next handler, returns wrapping handler
}
 
// => LOGGING MIDDLEWARE: records method, path, status, and elapsed time for every request
class LoggingMiddleware implements Middleware {
    @Override
    public Handler apply(Handler next) {
        return request -> {
            long start = System.currentTimeMillis(); // => capture start time before delegation
            System.out.printf("[LOG] %s %s started%n", request.method(), request.path());
            Response response = next.handle(request); // => delegate to next handler in chain
            long elapsed = System.currentTimeMillis() - start; // => compute elapsed after return
            System.out.printf("[LOG] %s %s → %d (%dms)%n",
                request.method(), request.path(), response.status(), elapsed);
            return response; // => return response unchanged; logging is a side effect only
        };
    }
}
 
// => AUTH MIDDLEWARE: validates Bearer token; short-circuits with 401 if missing
class AuthMiddleware implements Middleware {
    @Override
    public Handler apply(Handler next) {
        return request -> {
            String token = request.headers().getOrDefault("Authorization", "");
            // => getOrDefault avoids NPE if Authorization header absent
            if (!token.startsWith("Bearer ")) {
                System.out.println("[AUTH] rejected: missing bearer token");
                return new Response(401, Map.of("error", "Unauthorized"));
                // => short-circuit: next handler is NEVER called; chain stops here
            }
            System.out.println("[AUTH] accepted token");
            return next.handle(request); // => token valid: delegate to next middleware
        };
    }
}
 
// => RATE LIMIT MIDDLEWARE: tracks per-path request count; blocks when limit exceeded
class RateLimitMiddleware implements Middleware {
    private final int limitPerMinute;                   // => max requests per path per window
    private final Map<String, Integer> counts = new HashMap<>(); // => simple counter store
 
    RateLimitMiddleware(int limitPerMinute) {
        this.limitPerMinute = limitPerMinute; // => configurable threshold
    }
 
    @Override
    public Handler apply(Handler next) {
        return request -> {
            String key = request.path();
            int current = counts.merge(key, 1, Integer::sum); // => atomically increment counter
            if (current > limitPerMinute) {
                System.out.printf("[RATE] limit exceeded for %s%n", key);
                return new Response(429, Map.of("error", "Too Many Requests"));
                // => short-circuit: next handler skipped when rate exceeded
            }
            return next.handle(request); // => within limit: continue to next middleware
        };
    }
}
 
// => CORE BUSINESS HANDLER: pure business logic, zero cross-cutting concerns
class UserHandler implements Handler {
    @Override
    public Response handle(Request request) {
        // => no logging, no auth checks, no rate limiting — all handled upstream
        return new Response(200, Map.of("users", List.of("Alice", "Bob")));
    }
}
 
// => PIPELINE BUILDER: composes middlewares right-to-left around the core handler
static Handler buildPipeline(Handler core, Middleware... middlewares) {
    Handler result = core;
    for (int i = middlewares.length - 1; i >= 0; i--) {
        result = middlewares[i].apply(result); // => each middleware wraps the previous
    }
    return result; // => outermost middleware runs first, core handler runs last
}
 
// demo: assemble and exercise the pipeline
Handler pipeline = buildPipeline(
    new UserHandler(),                          // => innermost: business logic
    new RateLimitMiddleware(10),                // => third: rate check before auth
    new AuthMiddleware(),                       // => second: auth check
    new LoggingMiddleware()                     // => outermost: logs every request
);
 
// GOOD request: valid Bearer token present
Response resp = pipeline.handle(
    new Request("GET", "/api/users", Map.of("Authorization", "Bearer tok123")));
// => Output: [LOG] GET /api/users started
// => Output: [AUTH] accepted token
// => Output: [LOG] GET /api/users → 200 (0ms)
System.out.println(resp.status()); // => Output: 200
 
// BAD request: missing Authorization header
Response resp2 = pipeline.handle(new Request("GET", "/api/users", Map.of()));
// => Output: [LOG] GET /api/users started
// => Output: [AUTH] rejected: missing bearer token
// => Output: [LOG] GET /api/users → 401 (0ms)
System.out.println(resp2.status()); // => Output: 401

Key Takeaway: The Middleware pattern processes requests through a composable pipeline where each middleware handles one cross-cutting concern — auth, logging, rate limiting — independently, without mixing into business logic.

Why It Matters: Express.js, Django's middleware system, ASP.NET Core's pipeline, and gRPC interceptors all implement the Middleware pattern. It solves the cross-cutting concern problem more composably than Decorator (which requires class hierarchies) by using function composition. Cloudflare Workers uses a middleware pipeline for every HTTP request, applying DDoS protection, rate limiting, and authentication before business logic runs. Adding a new cross-cutting concern (e.g., request tracing) requires writing one middleware function and adding it to the pipeline — zero changes to existing middleware or business handlers.


Example 51: Plugin Architecture — Extending Systems Without Modifying Core

A Plugin Architecture defines a stable core with extension points (hooks, interfaces) that external plugins implement. The core discovers and loads plugins at startup or runtime. This enables third parties to add capabilities without modifying the core codebase.

import java.util.*;
import java.util.function.*;
 
// => CORE API: the controlled surface area the core exposes to plugins
// => deliberately narrow — plugins cannot reach core internals beyond this interface
interface CoreAPI {
    void registerRoute(String path, Function<Object, Object> handler);
    // => plugin registers an HTTP handler under path; core stores and dispatches
    void on(String event, Consumer<Object> handler);
    // => plugin subscribes to a named event; multiple plugins can listen to same event
    void emit(String event, Object data);
    // => plugin broadcasts an event; core fans out to all subscribers
}
 
// => PLUGIN: every plugin must implement this contract
interface Plugin {
    String getName();    // => unique identifier; duplicate names are rejected
    String getVersion(); // => semantic version string; recorded for diagnostics
    void initialize(CoreAPI core); // => called once at load time with the CoreAPI
    default void shutdown() {}     // => optional cleanup; called on unload
}
 
// => PLUGIN MANAGER: loads, stores, and unloads plugins; owns the CoreAPI implementation
class PluginManager {
    private final Map<String, Plugin> plugins = new LinkedHashMap<>();
    // => load-order-preserving map; iteration order matters for shutdown
 
    private final Map<String, Function<Object, Object>> routes = new HashMap<>();
    // => routes registered by plugins; dispatched by manager
 
    private final Map<String, List<Consumer<Object>>> eventHandlers = new HashMap<>();
    // => per-event list of handlers; plugins accumulate here via on()
 
    // => CORE API IMPLEMENTATION: the only channel plugins may use to affect the core
    private final CoreAPI coreAPI = new CoreAPI() {
        @Override
        public void registerRoute(String path, Function<Object, Object> handler) {
            routes.put(path, handler); // => store handler; overwrite if path already registered
            System.out.printf("Core: route registered — %s%n", path);
        }
 
        @Override
        public void on(String event, Consumer<Object> handler) {
            eventHandlers.computeIfAbsent(event, k -> new ArrayList<>()).add(handler);
            // => computeIfAbsent initializes list on first subscriber for this event
        }
 
        @Override
        public void emit(String event, Object data) {
            List<Consumer<Object>> handlers = eventHandlers.getOrDefault(event, List.of());
            handlers.forEach(h -> h.accept(data)); // => fan out to all subscribers
        }
    };
 
    // => LOAD: registers plugin and gives it access to the CoreAPI
    public void load(Plugin plugin) {
        if (plugins.containsKey(plugin.getName())) {
            throw new IllegalStateException("Plugin already loaded: " + plugin.getName());
        }
        plugin.initialize(coreAPI); // => plugin registers its routes and event handlers
        plugins.put(plugin.getName(), plugin);
        System.out.printf("Core: plugin loaded — %s v%s%n", plugin.getName(), plugin.getVersion());
    }
 
    // => UNLOAD: calls optional shutdown hook then removes plugin
    public void unload(String name) {
        Plugin plugin = plugins.remove(name);
        if (plugin != null) {
            plugin.shutdown(); // => default no-op unless plugin overrides
            System.out.printf("Core: plugin unloaded — %s%n", name);
        }
    }
 
    // => DISPATCH: routes an incoming request to the plugin-registered handler
    public Object dispatch(String path, Object req) {
        Function<Object, Object> handler = routes.get(path);
        if (handler == null) throw new IllegalArgumentException("No handler for: " + path);
        return handler.apply(req); // => delegate to whichever plugin owns this route
    }
}
 
// => PLUGIN 1: auth — registers a login route and emits user.login events
class AuthPlugin implements Plugin {
    @Override public String getName() { return "auth"; }
    @Override public String getVersion() { return "1.0.0"; }
 
    @Override
    public void initialize(CoreAPI core) {
        core.registerRoute("/auth/login", req -> {
            core.emit("user.login", Map.of("timestamp", System.currentTimeMillis()));
            // => emit event so other plugins (e.g., audit) can react
            return Map.of("token", "jwt-token-here"); // => simulated login response
        });
        core.on("user.login", data -> {
            System.out.println("Auth plugin: login event received " + data);
            // => auth plugin can also subscribe to its own event for logging
        });
    }
}
 
// => PLUGIN 2: audit — subscribes to user.login and records each login event
class AuditPlugin implements Plugin {
    @Override public String getName() { return "audit"; }
    @Override public String getVersion() { return "2.1.0"; }
 
    @Override
    public void initialize(CoreAPI core) {
        core.on("user.login", data -> {
            System.out.println("Audit plugin: recording login event " + data);
            // => cross-plugin communication via events; audit has no direct dep on auth
        });
    }
}
 
// assemble and exercise the plugin system
PluginManager manager = new PluginManager();
manager.load(new AuthPlugin());   // => Output: Core: plugin loaded — auth v1.0.0
manager.load(new AuditPlugin());  // => Output: Core: plugin loaded — audit v2.1.0
 
Object result = manager.dispatch("/auth/login", Map.of("username", "alice"));
// => Output: Auth plugin: login event received {timestamp=...}
// => Output: Audit plugin: recording login event {timestamp=...}
System.out.println(result); // => Output: {token=jwt-token-here}
// => PluginManager never imported AuthPlugin or AuditPlugin as concrete deps — loaded dynamically

Key Takeaway: Plugin architecture exposes a stable CoreAPI for plugins to register behavior (routes, event handlers) — the core stays unchanged as new capabilities are added through plugins loaded at runtime.

Why It Matters: Plugin architectures enable organizations to extend platforms without access to core source code and allow independent release cycles — the core team ships the platform while plugin authors release independently. Tools like VS Code, Webpack, and Jenkins demonstrate this at scale: each extension point defined by a stable plugin interface allows an ecosystem to grow without coupling to core internals. The CoreAPI surface area is deliberately limited to prevent plugins from accessing internals that would create tight coupling to core implementation details.


Example 52: Repository Pattern — Abstracting Data Access

The Repository pattern provides a collection-like interface for accessing domain objects, hiding the persistence mechanism behind an abstraction. The domain layer interacts with repositories as if they were in-memory collections, while the implementation handles SQL, HTTP, or file storage.

import java.util.*;
 
// => DOMAIN ENTITY: pure value object; no persistence annotations
record Product(String id, String name, double price, String category) {
    // => immutable record — JPA annotations would go here in a real project
}
 
// => REPOSITORY INTERFACE: collection-like API that the domain layer depends on
// => domain code never calls SQL or HTTP — only these method signatures
interface ProductRepository {
    Optional<Product> findById(String productId);
    // => returns Optional to force callers to handle the "not found" case
    List<Product> findByCategory(String category);
    // => returns all products matching the category — collection semantics
    void save(Product product); // => upsert: insert new or update existing
    void delete(String productId); // => remove from the collection
}
 
// => IMPLEMENTATION 1: in-memory — used in unit tests; no DB required
class InMemoryProductRepository implements ProductRepository {
    private final Map<String, Product> store = new LinkedHashMap<>();
    // => LinkedHashMap preserves insertion order for predictable test output
 
    @Override
    public Optional<Product> findById(String productId) {
        return Optional.ofNullable(store.get(productId)); // => O(1) lookup; absent → empty
    }
 
    @Override
    public List<Product> findByCategory(String category) {
        return store.values().stream()
            .filter(p -> p.category().equals(category)) // => O(n) scan; acceptable in tests
            .toList(); // => immutable list copy
    }
 
    @Override
    public void save(Product product) {
        store.put(product.id(), product); // => upsert: put overwrites existing key
    }
 
    @Override
    public void delete(String productId) {
        store.remove(productId); // => no-op if key absent; mirrors SQL DELETE safety
    }
}
 
// => IMPLEMENTATION 2: simulated SQL — shows the shape of a real JDBC repository
class SqlProductRepository implements ProductRepository {
    private final String connectionString; // => real impl would open a JDBC Connection
    private final Map<String, Product> simulated = new HashMap<>(); // => simulation only
 
    SqlProductRepository(String connectionString) {
        this.connectionString = connectionString; // => stored for real usage; unused in demo
    }
 
    @Override
    public Optional<Product> findById(String productId) {
        System.out.printf("SQL: SELECT * FROM products WHERE id = '%s'%n", productId);
        // => production: PreparedStatement with '?' parameter to prevent SQL injection
        return Optional.ofNullable(simulated.get(productId));
    }
 
    @Override
    public List<Product> findByCategory(String category) {
        System.out.printf("SQL: SELECT * FROM products WHERE category = '%s'%n", category);
        // => production: indexed query; O(log n) with a B-tree index on category column
        return simulated.values().stream()
            .filter(p -> p.category().equals(category)).toList();
    }
 
    @Override
    public void save(Product product) {
        System.out.printf("SQL: UPSERT product id='%s' name='%s'%n", product.id(), product.name());
        // => production: INSERT ... ON CONFLICT DO UPDATE SET ... (PostgreSQL syntax)
        simulated.put(product.id(), product);
    }
 
    @Override
    public void delete(String productId) {
        System.out.printf("SQL: DELETE FROM products WHERE id = '%s'%n", productId);
        // => production: DELETE FROM products WHERE id = ?
        simulated.remove(productId);
    }
}
 
// => DOMAIN SERVICE: depends only on the interface — same code works with any repo
class ProductCatalogService {
    private final ProductRepository repo; // => injected; could be in-memory or SQL
 
    ProductCatalogService(ProductRepository repo) {
        this.repo = repo; // => dependency injection; no new SqlProductRepository() here
    }
 
    public List<Product> getElectronics() {
        return repo.findByCategory("electronics"); // => no SQL here; reads via abstraction
    }
 
    public void addProduct(Product product) {
        repo.save(product); // => no SQL here; persists via abstraction
    }
}
 
// demo: swap implementations without touching service code
ProductRepository inMemoryRepo = new InMemoryProductRepository();
ProductCatalogService service = new ProductCatalogService(inMemoryRepo);
 
service.addProduct(new Product("p1", "Laptop", 999.99, "electronics"));
service.addProduct(new Product("p2", "Phone", 599.99, "electronics"));
List<Product> results = service.getElectronics();
System.out.println(results.stream().map(Product::name).toList());
// => Output: [Laptop, Phone]
 
// swap to SQL repo — service code is UNCHANGED
ProductRepository sqlRepo = new SqlProductRepository("jdbc:postgresql://localhost/shop");
ProductCatalogService sqlService = new ProductCatalogService(sqlRepo);
sqlService.addProduct(new Product("p3", "Tablet", 449.99, "electronics"));
// => Output: SQL: UPSERT product id='p3' name='Tablet'

Key Takeaway: The Repository pattern gives the domain layer a collection-like interface for persistence — the domain calls save() and findByCategory(), not SQL or HTTP, making it easy to swap implementations and test with in-memory repositories.

Why It Matters: Repository is the standard persistence abstraction in DDD, Spring Data (JpaRepository), Entity Framework (DbSet), and Active Record. The testing benefit is structural: unit tests run against in-memory repositories with no database connections, no migrations, and no test data cleanup. This eliminates an entire category of test flakiness and infrastructure dependency. The pattern also enables blue-green database migrations: implement a new SqlProductRepository pointing at the new schema, run side-by-side, then switch over by changing one DI binding.


Example 53: Unit of Work Pattern — Grouping Operations into Atomic Transactions

The Unit of Work pattern tracks all changes made during a business transaction and commits them atomically at the end. It prevents partial writes — if any operation fails, all changes roll back. This is the foundation behind database transaction management.

import java.util.*;
 
// => DOMAIN ENTITY: immutable record representing a bank account
record Account(String id, String ownerId, double balance) {
    // => record auto-generates equals(), hashCode(), toString(), and accessors
    Account withBalance(double newBalance) {
        return new Account(id, ownerId, newBalance);
        // => functional update — creates new Account rather than mutating in place
    }
}
 
// => CHANGE SET: accumulates all mutations within one UoW boundary
record ChangeSet(List<Account> inserts, List<Account> updates, List<String> deletes) {
    static ChangeSet empty() {
        return new ChangeSet(new ArrayList<>(), new ArrayList<>(), new ArrayList<>());
        // => factory method returns a fresh, mutable ChangeSet
    }
}
 
// => UNIT OF WORK: tracks every insert/update/delete; commits or rolls back atomically
class AccountUnitOfWork {
    private final Map<String, Account> accounts = new LinkedHashMap<>();
    // => simulated DB store; LinkedHashMap preserves insertion order for predictable output
    private ChangeSet changeSet = ChangeSet.empty();
    // => starts empty; accumulates changes until commit() or rollback()
 
    public void seed(List<Account> initial) {
        initial.forEach(a -> accounts.put(a.id(), a));
        // => load initial data into the UoW scope (simulates SELECT into unit of work)
    }
 
    public Optional<Account> getAccount(String id) {
        return Optional.ofNullable(accounts.get(id)); // => read within UoW scope; no SQL
    }
 
    public void registerNew(Account account) {
        accounts.put(account.id(), account);         // => track new entity in memory
        changeSet.inserts().add(account);             // => mark for INSERT on commit
    }
 
    public void registerModified(Account account) {
        accounts.put(account.id(), account);          // => update in-memory view
        boolean alreadyTracked = changeSet.updates().stream()
            .anyMatch(a -> a.id().equals(account.id()));
        if (!alreadyTracked) changeSet.updates().add(account);
        // => guard against duplicate entries for the same account
    }
 
    public void registerDeleted(String accountId) {
        accounts.remove(accountId);              // => remove from in-memory view
        changeSet.deletes().add(accountId);      // => mark for DELETE on commit
    }
 
    public void commit() {
        System.out.printf("UoW commit: %d inserts, %d updates, %d deletes%n",
            changeSet.inserts().size(), changeSet.updates().size(), changeSet.deletes().size());
        // => production: wrap everything below in BEGIN / COMMIT transaction block
        changeSet.inserts().forEach(a ->
            System.out.printf("  INSERT account id=%s balance=%.0f%n", a.id(), a.balance()));
        changeSet.updates().forEach(a ->
            System.out.printf("  UPDATE account id=%s balance=%.0f%n", a.id(), a.balance()));
        changeSet.deletes().forEach(id ->
            System.out.printf("  DELETE account id=%s%n", id));
        changeSet = ChangeSet.empty(); // => reset after commit; ready for next operation
    }
 
    public void rollback() {
        System.out.println("UoW rollback: all changes discarded");
        changeSet = ChangeSet.empty(); // => discard all pending changes
        // => production: issue ROLLBACK to DB; in-memory store not restored here for brevity
    }
}
 
// => BUSINESS OPERATION: transfers money atomically via UoW
static void transfer(AccountUnitOfWork uow, String fromId, String toId, double amount) {
    Account from = uow.getAccount(fromId).orElseThrow(() -> new RuntimeException("Account not found: " + fromId));
    Account to   = uow.getAccount(toId).orElseThrow(() -> new RuntimeException("Account not found: " + toId));
    // => load both accounts within same UoW scope before any mutation
 
    if (from.balance() < amount) throw new RuntimeException("Insufficient funds");
    // => business rule enforced BEFORE any modification is registered
 
    uow.registerModified(from.withBalance(from.balance() - amount));
    // => debit: tracked as dirty; not yet persisted
    uow.registerModified(to.withBalance(to.balance() + amount));
    // => credit: tracked as dirty; ATOMICITY — both commit or neither does
}
 
// demo: successful transfer
AccountUnitOfWork uow = new AccountUnitOfWork();
uow.seed(List.of(
    new Account("acc1", "u1", 500),
    new Account("acc2", "u2", 200)
));
 
try {
    transfer(uow, "acc1", "acc2", 150); // => debit acc1 by 150, credit acc2 by 150
    uow.commit();
    // => Output: UoW commit: 0 inserts, 2 updates, 0 deletes
    // => Output:   UPDATE account id=acc1 balance=350
    // => Output:   UPDATE account id=acc2 balance=350
} catch (RuntimeException e) {
    uow.rollback(); // => if transfer throws (e.g., insufficient funds), rollback both
    System.out.println("Transfer failed: " + e.getMessage());
}

Key Takeaway: The Unit of Work collects all changes made during a business operation and commits them atomically — if any step fails, all tracked changes roll back, preventing data inconsistency from partial writes.

Why It Matters: Every ORM implements Unit of Work: SQLAlchemy's Session, Entity Framework's DbContext, and Hibernate's Session all track dirty objects and flush them in a single transaction. Without Unit of Work, a money transfer that debits Account A but crashes before crediting Account B leaves the database in an inconsistent state — money is lost. Financial systems, inventory management, and order processing rely on Unit of Work to guarantee that related changes either all succeed or all fail. This pattern directly corresponds to database ACID atomicity.


Example 54: Specification Pattern — Composable Business Rules

The Specification pattern encapsulates a business rule as an object that can evaluate whether a candidate satisfies the rule. Specifications can be combined with AND, OR, and NOT operators to build complex rules from simple ones — without modifying the candidate class.

import java.util.*;
import java.util.stream.*;
 
// => SPECIFICATION BASE: generic interface every spec must implement
interface Specification<T> {
    boolean isSatisfiedBy(T candidate); // => true if candidate meets this rule
 
    // => AND: both this and other must be satisfied
    default Specification<T> and(Specification<T> other) {
        return candidate -> this.isSatisfiedBy(candidate) && other.isSatisfiedBy(candidate);
        // => lambda creates an AndSpecification inline; short-circuits on first false
    }
 
    // => OR: either this or other must be satisfied
    default Specification<T> or(Specification<T> other) {
        return candidate -> this.isSatisfiedBy(candidate) || other.isSatisfiedBy(candidate);
        // => short-circuits on first true; combines rules without a new class
    }
 
    // => NOT: negates this specification
    default Specification<T> not() {
        return candidate -> !this.isSatisfiedBy(candidate);
        // => wraps this spec and inverts the result
    }
}
 
// => DOMAIN ENTITY: pure data class; Specification operates on this without modifying it
record Product(String id, String name, double price, String category, boolean inStock) {}
 
// => CONCRETE SPECIFICATIONS: each encapsulates exactly one business rule
class InStockSpec implements Specification<Product> {
    @Override
    public boolean isSatisfiedBy(Product p) {
        return p.inStock(); // => rule: product must be in stock
    }
}
 
class PriceRangeSpec implements Specification<Product> {
    private final double min, max; // => inclusive bounds for the price range rule
 
    PriceRangeSpec(double min, double max) {
        this.min = min; // => lower price bound (inclusive)
        this.max = max; // => upper price bound (inclusive)
    }
 
    @Override
    public boolean isSatisfiedBy(Product p) {
        return p.price() >= min && p.price() <= max; // => must fall within [min, max]
    }
}
 
class CategorySpec implements Specification<Product> {
    private final String category; // => exact category string required
 
    CategorySpec(String category) {
        this.category = category; // => store for comparison in isSatisfiedBy
    }
 
    @Override
    public boolean isSatisfiedBy(Product p) {
        return category.equals(p.category()); // => exact category match
    }
}
 
// COMPOSE: build complex rules from simple specifications via and() / or() / not()
Specification<Product> electronicsInStock =
    new CategorySpec("electronics").and(new InStockSpec());
// => rule: category == "electronics" AND inStock == true
 
Specification<Product> affordableElectronics =
    electronicsInStock.and(new PriceRangeSpec(0, 500));
// => rule: electronics AND in_stock AND price <= $500
 
List<Product> products = List.of(
    new Product("p1", "Budget Phone",   199.99, "electronics", true),  // => matches all three
    new Product("p2", "Premium Laptop", 1499.99, "electronics", true), // => too expensive
    new Product("p3", "T-Shirt",        29.99, "clothing", true),      // => wrong category
    new Product("p4", "Smart TV",       349.99, "electronics", false)  // => out of stock
);
 
List<String> matching = products.stream()
    .filter(affordableElectronics::isSatisfiedBy) // => method reference passes each product
    .map(Product::name)
    .collect(Collectors.toList());
System.out.println(matching); // => Output: [Budget Phone]
// => only Budget Phone: electronics + in_stock + price <= 500
 
// combine differently without touching Product class
Specification<Product> overpricedOrOutOfStock =
    new PriceRangeSpec(500, 9999).or(new InStockSpec().not());
List<String> excluded = products.stream()
    .filter(overpricedOrOutOfStock::isSatisfiedBy)
    .map(Product::name)
    .collect(Collectors.toList());
System.out.println(excluded); // => Output: [Premium Laptop, Smart TV]

Key Takeaway: The Specification pattern encapsulates each business rule as a composable object — complex filtering logic is built by combining simple specifications with AND, OR, and NOT without modifying the candidate class or scattering if/else chains across the codebase.

Why It Matters: Specification is the architecture behind Elasticsearch query composition, Spring Data's Specification interface, and rule engines used in fraud detection and underwriting. Without it, every new filter combination requires a new repository method or service method with hardcoded conditional logic — exponential growth as rule combinations multiply. Stripe uses specification-like composition in their fraud detection rules: a transaction is declined if it satisfies (HighRisk AND NewMerchant) OR (UnusualAmount AND ForeignCard) — each rule is isolated, auditable, and composable.


Example 55: CQRS with Event Sourcing — State as a Sequence of Events

Paradigm Note: "Aggregate state = foldl apply init events" is FP-native by construction (Greg Young; Haskell ES community). OOP encodes the same fold semantics as a mutable rehydration loop on aggregate objects. See the FP framing.

Event Sourcing stores the history of all state-changing events rather than the current state. The current state is derived by replaying events from the beginning. Combined with CQRS, the write side appends events to an event store; the read side builds projections by processing those events.

import java.time.Instant;
import java.util.*;
 
// => DOMAIN EVENTS: sealed hierarchy of immutable records (past-tense names)
sealed interface BankEvent permits AccountOpened, MoneyDeposited, MoneyWithdrawn {
    String accountId(); // => every event belongs to a specific account
    Instant occurredAt(); // => timestamp for audit trail and time-travel queries
}
 
// => ACCOUNT OPENED: first event in every account's stream
record AccountOpened(String accountId, String ownerId, Instant occurredAt) implements BankEvent {
    AccountOpened(String accountId, String ownerId) {
        this(accountId, ownerId, Instant.now()); // => convenience constructor captures time
    }
}
 
// => MONEY DEPOSITED: records each deposit; balance is never stored directly
record MoneyDeposited(String accountId, double amount, Instant occurredAt) implements BankEvent {
    MoneyDeposited(String accountId, double amount) {
        this(accountId, amount, Instant.now());
    }
}
 
// => MONEY WITHDRAWN: records each withdrawal; state derived by replaying all events
record MoneyWithdrawn(String accountId, double amount, Instant occurredAt) implements BankEvent {
    MoneyWithdrawn(String accountId, double amount) {
        this(accountId, amount, Instant.now());
    }
}
 
// => EVENT STORE: append-only log; never updates or deletes existing events
class EventStore {
    private final Map<String, List<BankEvent>> streams = new HashMap<>();
    // => key: account_id; value: ordered list of events (the event stream)
 
    public void append(String accountId, BankEvent event) {
        streams.computeIfAbsent(accountId, k -> new ArrayList<>()).add(event);
        // => computeIfAbsent initializes list on first event; append to existing stream
        // => IMMUTABILITY INVARIANT: previously appended events are never modified
    }
 
    public List<BankEvent> load(String accountId) {
        return List.copyOf(streams.getOrDefault(accountId, List.of()));
        // => defensive copy: caller cannot mutate the internal event stream
    }
}
 
// => BANK ACCOUNT AGGREGATE (WRITE SIDE): reconstructs state by replaying events
class BankAccount {
    private final String id;      // => aggregate identity
    private String ownerId = "";  // => derived by replaying AccountOpened
    private double balance = 0.0; // => derived by summing deposits minus withdrawals
    private boolean isOpen = false;
 
    private BankAccount(String id) { this.id = id; }
 
    // => FACTORY: replay all events to build current state (no DB read of balance)
    public static BankAccount fromEvents(String accountId, List<BankEvent> events) {
        BankAccount account = new BankAccount(accountId); // => start with blank state
        for (BankEvent event : events) account.apply(event); // => replay each event in order
        return account; // => final state equals sum of all events
    }
 
    // => APPLY: each case updates derived state from the event's data
    private void apply(BankEvent event) {
        switch (event) {
            case AccountOpened e -> { ownerId = e.ownerId(); isOpen = true; }
            // => extract owner from event; set open flag
            case MoneyDeposited e -> balance += e.amount();
            // => accumulate deposit into running balance
            case MoneyWithdrawn e -> balance -= e.amount();
            // => subtract withdrawal from running balance
        }
    }
 
    // => COMMAND: deposit creates event, appends to store, updates in-memory state
    public MoneyDeposited deposit(EventStore store, double amount) {
        if (amount <= 0) throw new IllegalArgumentException("Deposit must be positive");
        MoneyDeposited event = new MoneyDeposited(id, amount);
        store.append(id, event); // => persist event — NOT the new balance
        apply(event);            // => update in-memory state to stay consistent
        return event;
    }
 
    // => COMMAND: withdraw creates event only if sufficient funds
    public MoneyWithdrawn withdraw(EventStore store, double amount) {
        if (amount > balance) throw new IllegalArgumentException("Insufficient funds");
        MoneyWithdrawn event = new MoneyWithdrawn(id, amount);
        store.append(id, event); // => persist event — balance is never written to store
        apply(event);
        return event;
    }
 
    public double getBalance() { return balance; } // => derived state; no DB read
}
 
// demo: open account, transact, replay from scratch
EventStore store = new EventStore();
store.append("acc1", new AccountOpened("acc1", "u1")); // => first event in stream
 
BankAccount account = BankAccount.fromEvents("acc1", store.load("acc1"));
account.deposit(store, 500.0);  // => appends MoneyDeposited(500)
account.deposit(store, 200.0);  // => appends MoneyDeposited(200)
account.withdraw(store, 150.0); // => appends MoneyWithdrawn(150)
 
System.out.printf("Current balance: $%.1f%n", account.getBalance());
// => Output: Current balance: $550.0
 
// replay from scratch — deterministic: same events → same state
BankAccount replayed = BankAccount.fromEvents("acc1", store.load("acc1"));
System.out.printf("Replayed balance: $%.1f%n", replayed.getBalance());
// => Output: Replayed balance: $550.0
 
// audit trail: complete history; nothing ever deleted
store.load("acc1").forEach(e -> {
    String amount = (e instanceof MoneyDeposited d) ? " " + d.amount()
                  : (e instanceof MoneyWithdrawn w) ? " " + w.amount() : "";
    System.out.println(e.getClass().getSimpleName() + amount);
});
// => Output: AccountOpened
// => Output: MoneyDeposited 500.0
// => Output: MoneyDeposited 200.0
// => Output: MoneyWithdrawn 150.0

Key Takeaway: Event Sourcing stores the full sequence of events rather than current state — current state is derived by replaying events, giving you complete audit history and the ability to time-travel to any past state.

Why It Matters: Event Sourcing is the architecture behind git (commits are events, file state is derived by replay), banking ledgers (debits and credits are events, balance is derived), and financial audit systems. The pattern eliminates the "why" gap — traditional systems store only current state, losing the history of how it was reached. With Event Sourcing, debugging means replaying events to the point of failure, and time-travel queries ("what was the state at time T?") become natural. The tradeoff is complexity: eventual consistency between write and read models, and snapshot strategies needed for streams with millions of events.


Example 56: Saga Pattern — Managing Distributed Transactions

A Saga is a sequence of local transactions where each transaction publishes an event triggering the next step. If a step fails, compensating transactions undo the preceding steps. Sagas replace distributed ACID transactions (which require 2PC and don't scale) with eventual consistency and explicit rollback logic.

import java.util.*;
 
// => SAGA STEP: each step encapsulates one local transaction and its compensating action
interface SagaStep {
    String name();           // => human-readable identifier for logging
    void execute();          // => forward: perform the local transaction
    void compensate();       // => backward: undo this step's action if saga fails later
}
 
// => SAGA ORCHESTRATOR: executes steps in sequence; compensates completed steps on failure
class SagaOrchestrator {
    private final Deque<SagaStep> executedSteps = new ArrayDeque<>();
    // => Deque used as a stack; LIFO traversal for compensation (reverse order)
 
    public void execute(List<SagaStep> steps) {
        for (SagaStep step : steps) {
            System.out.printf("Saga: executing step — %s%n", step.name());
            try {
                step.execute();              // => run the forward transaction
                executedSteps.push(step);    // => record as completed; pushed to LIFO stack
            } catch (Exception e) {
                System.out.printf("Saga: step %s failed: %s%n", step.name(), e.getMessage());
                compensate();                // => rollback all completed steps before re-throw
                throw new RuntimeException("Saga failed at step: " + step.name(), e);
            }
        }
        System.out.println("Saga: all steps completed successfully");
    }
 
    private void compensate() {
        System.out.println("Saga: starting compensation (rolling back completed steps)");
        // => LIFO: compensate most recent step first — critical for data consistency
        while (!executedSteps.isEmpty()) {
            SagaStep step = executedSteps.pop();
            System.out.printf("Saga: compensating step — %s%n", step.name());
            try {
                step.compensate(); // => undo this step's local transaction
            } catch (Exception e) {
                System.out.printf("Saga: compensation failed for %s — manual intervention needed%n",
                    step.name());
                // => compensation failure goes to dead-letter queue in production
            }
        }
    }
}
 
// => INVENTORY SERVICE: owns step 1 of the order saga
class InventoryService {
    private boolean reserved = false; // => simplified reservation state
 
    public void reserve(String productId, int qty) {
        System.out.printf("  Inventory: reserving %dx %s%n", qty, productId);
        reserved = true; // => local transaction: mark as reserved
    }
 
    public void release(String productId, int qty) {
        System.out.printf("  Inventory: releasing %dx %s (compensation)%n", qty, productId);
        reserved = false; // => compensation: undo reservation
    }
}
 
// => PAYMENT SERVICE: owns step 2 of the order saga
class PaymentService {
    private String authCode = ""; // => holds authorization code after charge
 
    public void charge(double amount) {
        System.out.printf("  Payment: charging $%.2f%n", amount);
        authCode = "AUTH-" + System.currentTimeMillis(); // => simulated authorization
    }
 
    public void refund() {
        System.out.printf("  Payment: refunding auth %s (compensation)%n", authCode);
        authCode = ""; // => compensation: void the charge
    }
}
 
// => SHIPPING SERVICE: owns step 3; simulates failure to trigger compensation
class ShippingService {
    public void schedule(String orderId) {
        throw new RuntimeException("Shipping service unavailable");
        // => failure here triggers compensation of steps 2 (payment) and 1 (inventory)
    }
 
    public void cancel(String orderId) {
        System.out.printf("  Shipping: cancelling order %s (compensation)%n", orderId);
        // => compensation for shipping (would run if schedule() had succeeded)
    }
}
 
// assemble and run the saga
InventoryService inventory = new InventoryService();
PaymentService payment     = new PaymentService();
ShippingService shipping   = new ShippingService();
String orderId = "o1";
 
List<SagaStep> steps = List.of(
    new SagaStep() {
        public String name() { return "ReserveInventory"; }
        public void execute()   { inventory.reserve("p1", 2); }
        public void compensate() { inventory.release("p1", 2); }
    },
    new SagaStep() {
        public String name() { return "ChargePayment"; }
        public void execute()   { payment.charge(49.99); }
        public void compensate() { payment.refund(); }
    },
    new SagaStep() {
        public String name() { return "ScheduleShipping"; }
        public void execute()   { shipping.schedule(orderId); } // => throws
        public void compensate() { shipping.cancel(orderId); }
    }
);
 
try {
    new SagaOrchestrator().execute(steps);
} catch (RuntimeException e) {
    System.out.println("Order placement failed: " + e.getMessage());
}
// => Saga: executing step — ReserveInventory
// =>   Inventory: reserving 2x p1
// => Saga: executing step — ChargePayment
// =>   Payment: charging $49.99
// => Saga: executing step — ScheduleShipping
// => Saga: step ScheduleShipping failed: Shipping service unavailable
// => Saga: starting compensation (rolling back completed steps)
// => Saga: compensating step — ChargePayment
// =>   Payment: refunding auth AUTH-...
// => Saga: compensating step — ReserveInventory
// =>   Inventory: releasing 2x p1 (compensation)
// => Order placement failed: Saga failed at step: ScheduleShipping

Key Takeaway: The Saga pattern manages distributed transactions through a sequence of local transactions with compensating rollbacks — each service commits its local change and compensations undo completed steps on failure, achieving eventual consistency without 2PC.

Why It Matters: Two-phase commit (2PC) distributed transactions require all participating services to be available simultaneously and lock resources across services for the duration — catastrophic for microservices at scale. Workflows involving inventory reservation, payment processing, and notification services cannot be locked together without unacceptable performance and availability consequences. Sagas trade strict atomicity for availability: the system remains operational even if one service is temporarily down, with compensation transactions eventually restoring consistency when the failed step resolves.


Example 57: Circuit Breaker Pattern — Preventing Cascade Failures

The Circuit Breaker pattern wraps calls to remote services and monitors for failures. When failures exceed a threshold, the circuit "opens" and subsequent calls fail immediately without attempting the remote call. After a timeout, the circuit enters "half-open" state and probes whether the service has recovered.

%% Color Palette: Blue #0173B2, Orange #DE8F05, Teal #029E73, Purple #CC78BC, Brown #CA9161
stateDiagram-v2
    [*] --> Closed
    Closed --> Open : failures >= threshold
    Open --> HalfOpen : timeout elapsed
    HalfOpen --> Closed : probe succeeds
    HalfOpen --> Open : probe fails
 
    classDef closed fill:#029E73,color:#fff
    classDef open fill:#DE8F05,color:#fff
    classDef halfopen fill:#CC78BC,color:#fff
import java.util.concurrent.atomic.*;
import java.util.function.Supplier;
 
// => CIRCUIT STATE: three-state machine tracks health of the downstream service
enum CircuitState {
    CLOSED,    // => normal operation: every call passes through to the service
    OPEN,      // => failure mode: calls blocked immediately without touching the service
    HALF_OPEN  // => recovery probe: one call allowed through to test service health
}
 
// => CIRCUIT BREAKER: wraps calls to a remote service and monitors failure rates
class CircuitBreaker {
    private volatile CircuitState state = CircuitState.CLOSED;
    // => volatile: state transitions visible across threads without locking
 
    private final int failureThreshold;   // => open after this many consecutive failures
    private final long recoveryTimeoutMs; // => milliseconds before transitioning OPEN -> HALF_OPEN
    private final int successThreshold;   // => successes needed in HALF_OPEN before closing
 
    private int failureCount = 0;   // => consecutive failures in CLOSED state
    private int successCount = 0;   // => successes accumulated in HALF_OPEN state
    private long openedAt = 0L;     // => timestamp (ms) when circuit entered OPEN state
 
    CircuitBreaker(int failureThreshold, long recoveryTimeoutMs, int successThreshold) {
        this.failureThreshold   = failureThreshold;   // => e.g. 3 failures → OPEN
        this.recoveryTimeoutMs  = recoveryTimeoutMs;  // => e.g. 5000ms before probe
        this.successThreshold   = successThreshold;   // => e.g. 1 success → CLOSED
    }
 
    public <T> T call(Supplier<T> fn) {
        if (state == CircuitState.OPEN) {
            long elapsed = System.currentTimeMillis() - openedAt;
            if (elapsed >= recoveryTimeoutMs) {
                transition(CircuitState.HALF_OPEN); // => timeout elapsed; allow probe
                System.out.printf("Circuit: transitioning to HALF_OPEN after %dms%n", elapsed);
            } else {
                throw new RuntimeException(String.format(
                    "Circuit OPEN — call blocked (retry in %dms)", recoveryTimeoutMs - elapsed));
                // => fail fast: no attempt made; caller gets exception immediately
            }
        }
 
        try {
            T result = fn.get(); // => attempt the downstream call
            onSuccess();         // => record success; may close circuit
            return result;
        } catch (Exception e) {
            onFailure();         // => record failure; may open circuit
            throw e;             // => re-throw original exception unchanged
        }
    }
 
    private void onSuccess() {
        if (state == CircuitState.HALF_OPEN) {
            successCount++;
            if (successCount >= successThreshold) {
                transition(CircuitState.CLOSED); // => service healthy; resume normal operation
                System.out.println("Circuit: CLOSED — service recovered");
            }
        } else {
            failureCount = 0; // => reset consecutive failure count on any success in CLOSED
        }
    }
 
    private void onFailure() {
        failureCount++;
        System.out.printf("Circuit: failure #%d%n", failureCount);
        if (state == CircuitState.HALF_OPEN || failureCount >= failureThreshold) {
            transition(CircuitState.OPEN); // => probe failed or threshold hit; open circuit
            System.out.printf("Circuit: OPEN — blocking calls for %dms%n", recoveryTimeoutMs);
        }
    }
 
    private void transition(CircuitState newState) {
        state = newState;
        if (newState == CircuitState.OPEN) {
            openedAt = System.currentTimeMillis(); // => record when opened for timeout calc
            failureCount = 0;
        } else if (newState == CircuitState.CLOSED) {
            failureCount = 0;
            successCount = 0;
        } else if (newState == CircuitState.HALF_OPEN) {
            successCount = 0; // => reset probe counter when entering HALF_OPEN
        }
    }
 
    public CircuitState getState() { return state; }
}
 
// simulate: flaky service that fails 3 times then recovers
int[] callCount = {0}; // => array trick to mutate from lambda
CircuitBreaker breaker = new CircuitBreaker(3, 100, 1);
 
// calls 1-3: consecutive failures; circuit opens on 3rd failure
for (int i = 0; i < 3; i++) {
    try {
        breaker.call(() -> {
            callCount[0]++;
            if (callCount[0] <= 3) throw new RuntimeException("Service unavailable");
            return "OK (call #" + callCount[0] + ")";
        });
    } catch (Exception e) {
        System.out.println("Call failed: " + e.getMessage());
    }
}
// => Circuit: failure #1 ... #2 ... #3
// => Circuit: OPEN — blocking calls for 100ms
 
// immediate call after opening: blocked without touching service
try {
    breaker.call(() -> "never reached");
} catch (Exception e) {
    System.out.println("Blocked: " + e.getMessage()); // => Circuit OPEN — call blocked
}
 
// wait for recovery timeout, then allow probe
Thread.sleep(120); // => wait > recoveryTimeoutMs to allow HALF_OPEN transition
String result = breaker.call(() -> {
    callCount[0]++;
    return "OK (call #" + callCount[0] + ")"; // => call #5: service has recovered
});
// => Circuit: transitioning to HALF_OPEN after 120ms
// => Circuit: CLOSED — service recovered
System.out.println(result);           // => Output: OK (call #5)
System.out.println(breaker.getState()); // => Output: CLOSED

Key Takeaway: The Circuit Breaker prevents cascade failures by monitoring call success rates and blocking calls when a remote service is failing — this protects the caller from timeout storms and gives the failing service time to recover.

Why It Matters: Without Circuit Breakers, a failing downstream service causes callers to wait for timeouts on every request — threads accumulate, memory fills, and the entire system cascades into failure within seconds. Netflix's Hystrix popularized circuit breakers in microservices; Hystrix is now in maintenance mode and Netflix recommends Resilience4j (a separate, independently maintained library inspired by Hystrix) for new projects. The pattern itself was formalized by Michael Nygard in "Release It!" (2007). Today, service meshes (Istio, Linkerd) implement circuit breaking at the infrastructure layer — but application-level circuit breakers provide more fine-grained control per operation.

Last updated March 19, 2026

Command Palette

Search for a command to run...