Skip to content
AyoKoding

Advanced

This section covers advanced software architecture patterns for distributed systems, microservices, resilience, and expert-level architectural decisions. Examples 58-85 assume familiarity with foundational and intermediate architecture concepts covered earlier in this series.

Decomposition and Service Design

Example 58: Microservices Decomposition by Business Capability

Decomposing a monolith into microservices by business capability aligns service boundaries with organizational units and business domains. Each capability — such as Orders, Inventory, or Billing — becomes an independently deployable service with its own data store, reducing inter-team coupling and enabling autonomous delivery.

graph TD
    A["Monolith<br/>All capabilities"]
    B["Orders Service<br/>Place, track, cancel"]
    C["Inventory Service<br/>Stock, reservations"]
    D["Billing Service<br/>Payments, invoices"]
    E["Notification Service<br/>Email, SMS"]
 
    A -->|"decompose by<br/>business capability"| B
    A --> C
    A --> D
    A --> 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:#DE8F05,stroke:#000,color:#fff
    style E fill:#CC78BC,stroke:#000,color:#fff
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
 
// => Value object: immutable identifier scoped to the Orders domain
record OrderId(String value) {
    // => Static factory generates a UUID-backed id — stable across network hops
    static OrderId generate() {
        return new OrderId(UUID.randomUUID().toString());
        // => Immutable record — value cannot be mutated after construction
    }
}
 
// => Value object for product identity — ties order line to inventory SKU
record ProductId(String value) {}
 
// => Interface defines the Orders service contract
// => Real service, HTTP adapter, and in-memory stub all implement this
interface OrdersService {
    OrderId placeOrder(ProductId productId, int qty);
    // => Returns OrderId so callers never depend on internal order structure
    boolean cancelOrder(OrderId orderId);
}
 
// => Concrete in-memory implementation — used in tests and local dev
class InMemoryOrdersService implements OrdersService {
    // => order map: orderId -> {productId, qty, status}
    private final Map<String, Map<String, Object>> orders = new HashMap<>();
 
    @Override
    public OrderId placeOrder(ProductId productId, int qty) {
        var oid = OrderId.generate();  // => Fresh id per call — no collision risk
        orders.put(oid.value(), Map.of(
            "productId", productId.value(),
            "qty", qty,
            "status", "PLACED"
        ));
        // => Stores minimal state; real impl persists to DB via JPA or JDBC
        return oid;  // => Caller receives id to reference the order later
    }
 
    @Override
    public boolean cancelOrder(OrderId orderId) {
        var order = orders.get(orderId.value());
        if (order == null) return false;  // => Not found — idempotent cancel is safe
        // => Real impl issues UPDATE orders SET status='CANCELLED' WHERE id=?
        orders.put(orderId.value(), Map.of("status", "CANCELLED"));
        return true;  // => Caller knows the cancel succeeded
    }
}
 
// => Usage: swap InMemoryOrdersService for HttpOrdersService without changing callers
var svc = new InMemoryOrdersService();
var pid = new ProductId("SKU-001");
var oid = svc.placeOrder(pid, 3);     // => Returns OrderId("some-uuid")
System.out.println(oid.value());      // => Output: <uuid string>
System.out.println(svc.cancelOrder(oid)); // => Output: true

Key Takeaway: Decompose by business capability using interfaces to enforce service boundaries in code, making each capability independently replaceable.

Why It Matters: Business-capability decomposition aligns service ownership with Conway's Law — the team owning "Orders" controls its full stack without coordinating schema changes with the "Inventory" team. Misaligned decomposition (for example, by technical tier) creates distributed monoliths where every feature requires cross-team merges, eliminating the delivery speed advantage of microservices. Autonomous teams shipping to independently deployable services is the central premise that business-capability boundaries make possible.


Example 59: Strangler Fig Pattern

The Strangler Fig pattern migrates a monolith incrementally by routing traffic through a proxy that gradually redirects requests to new microservices as they are built, leaving untouched paths on the legacy system. The monolith is "strangled" until all routes are migrated and it can be retired.

graph LR
    Client["Client"] --> Proxy["Strangler Proxy<br/>(router)"]
    Proxy -->|"migrated routes"| New["New Services"]
    Proxy -->|"legacy routes"| Old["Legacy Monolith"]
 
    style Client fill:#CA9161,stroke:#000,color:#fff
    style Proxy fill:#DE8F05,stroke:#000,color:#fff
    style New fill:#029E73,stroke:#000,color:#fff
    style Old fill:#CC78BC,stroke:#000,color:#fff
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.function.BiFunction;
 
// => RouteHandler: takes (path, payload) and returns a response map
// => Implemented by real HTTP adapters or in-process stubs
@FunctionalInterface
interface RouteHandler {
    Map<String, Object> handle(String path, Map<String, Object> payload);
}
 
class StranglerProxy {
    // => LinkedHashMap preserves insertion order — longer prefixes registered first win
    private final Map<String, RouteHandler> newRoutes = new LinkedHashMap<>();
    // => Legacy handler is the single catch-all for all unmigrated paths
    private RouteHandler legacyHandler;
 
    public void registerNew(String prefix, RouteHandler handler) {
        newRoutes.put(prefix, handler);
        // => Each call represents one migrated service; order of registration matters
    }
 
    public void setLegacy(RouteHandler handler) {
        legacyHandler = handler;  // => Monolith becomes the fallback until fully retired
    }
 
    public Map<String, Object> route(String path, Map<String, Object> payload) {
        for (var entry : newRoutes.entrySet()) {
            if (path.startsWith(entry.getKey())) {
                return entry.getValue().handle(path, payload);
                // => New service handles this path — monolith not involved
                // => Return immediately; no fallthrough to legacy
            }
        }
        // => No migrated handler matched — delegate to legacy monolith
        if (legacyHandler != null) return legacyHandler.handle(path, payload);
        throw new IllegalArgumentException("No handler for path: " + path);
        // => Should not occur in production if legacy handler is always set
    }
}
 
// => Simulated migrated Orders service
RouteHandler newOrdersHandler = (path, payload) ->
    Map.of("source", "new_service", "path", path, "data", payload);
    // => New service responds; monolith is bypassed entirely
 
// => Simulated legacy monolith catch-all
RouteHandler legacyHandler = (path, payload) ->
    Map.of("source", "legacy_monolith", "path", path, "data", payload);
 
var proxy = new StranglerProxy();
proxy.setLegacy(legacyHandler);
proxy.registerNew("/api/orders", newOrdersHandler);  // => Orders route migrated
 
System.out.println(proxy.route("/api/orders/123", Map.of("action", "get")));
// => Output: {source=new_service, path=/api/orders/123, data={action=get}}
System.out.println(proxy.route("/api/products/abc", Map.of("action", "list")));
// => Output: {source=legacy_monolith, path=/api/products/abc, data={action=list}}

Key Takeaway: The Strangler Fig pattern allows zero-downtime incremental migration by routing at the proxy level; each migrated route is an isolated, low-risk step.

Why It Matters: Big-bang rewrites fail at a high rate because they require running two systems simultaneously, training all users at once, and accepting rollback as all-or-nothing. The Strangler Fig pattern, documented by Martin Fowler, allows teams to migrate one route at a time, roll back individual services, and retire the legacy system only after every path is covered — dramatically reducing migration risk by making each step small, reversible, and independently verifiable.


Distributed Coordination

Example 60: Saga Orchestration

Saga orchestration uses a central orchestrator that issues commands to participants and reacts to their replies, making the saga's flow explicit and observable. When any step fails, the orchestrator drives compensating transactions in reverse order to restore consistency across services.

sequenceDiagram
    participant O as Orchestrator
    participant I as Inventory
    participant P as Payment
    participant S as Shipping
 
    O->>I: ReserveStock
    I-->>O: StockReserved
    O->>P: ChargePayment
    P-->>O: PaymentFailed
    O->>I: ReleaseStock (compensation)
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.List;
 
// => SagaStep: pairs a forward action with its compensating action
record SagaStep(
    String name,
    java.util.function.BooleanSupplier execute,   // => Returns true on success, false on failure
    Runnable compensate                            // => Undoes the execute action
) {}
 
class SagaOrchestrator {
    private final List<SagaStep> steps;  // => Steps executed in list order
    SagaOrchestrator(List<SagaStep> steps) { this.steps = steps; }
 
    boolean run() {
        Deque<SagaStep> completed = new ArrayDeque<>();
        // => Stack (LIFO) tracks successful steps so compensation runs in reverse
        for (var step : steps) {
            boolean success = step.execute().getAsBoolean();  // => Drive the participant
            if (success) {
                completed.push(step);  // => Push onto stack — will compensate in reverse if needed
            } else {
                System.out.println(step.name() + " failed — compensating");
                // => Step failed: compensate all completed steps in LIFO order
                completed.forEach(done -> done.compensate().run());
                return false;  // => Saga failed; distributed state is back to consistent
            }
        }
        return true;  // => All steps succeeded — saga complete
    }
}
 
// => Simulated participants with in-memory state
boolean[] stockReserved = {false};  // => Array used so lambda can mutate it
 
var reserveStock = (java.util.function.BooleanSupplier) () -> {
    stockReserved[0] = true;  // => Reserve 1 unit
    System.out.println("Stock reserved");  // => Output: Stock reserved
    return true;
};
var releaseStock = (Runnable) () -> {
    stockReserved[0] = false;  // => Compensation: return unit to inventory
    System.out.println("Stock released (compensation)");  // => Output: Stock released (compensation)
};
var chargePayment = (java.util.function.BooleanSupplier) () -> {
    System.out.println("Payment failed");  // => Output: Payment failed
    return false;  // => Simulate payment processor rejection
};
var refundPayment = (Runnable) () ->
    System.out.println("Payment refunded (compensation)");
 
var steps = List.of(
    new SagaStep("reserve", reserveStock, releaseStock),
    new SagaStep("payment", chargePayment, refundPayment)
);
boolean result = new SagaOrchestrator(steps).run();
System.out.println("Saga succeeded: " + result);          // => Output: Saga succeeded: false
System.out.println("Stock released: " + !stockReserved[0]); // => Output: Stock released: true

Key Takeaway: Orchestration keeps saga logic in one place; the orchestrator compensates completed steps when any forward step fails, maintaining distributed consistency.

Why It Matters: Distributed transactions using 2PC are impractical in microservices because they require all participants to lock resources simultaneously, reducing availability. Sagas replace locks with compensating transactions, enabling each service to commit locally. Orchestration (vs. choreography) is preferred when saga complexity grows beyond 3-4 steps, because the flow is visible in a single class rather than scattered across event handlers — critical for debugging production incidents where understanding what happened in what order is essential.


Example 61: Saga Choreography

Saga choreography removes the central orchestrator; instead, each service reacts to domain events and emits new events to trigger the next step. Services are decoupled because no service calls another directly, but the saga's flow is implicit across multiple event handlers.

Happy path then compensation:

graph LR
    A["OrderPlaced<br/>event"] --> B["Inventory Service<br/>reserves stock"]
    B --> C["StockReserved<br/>event"]
    C --> D["Payment Service<br/>charges card"]
 
    style A fill:#0173B2,stroke:#000,color:#fff
    style B fill:#029E73,stroke:#000,color:#fff
    style C fill:#0173B2,stroke:#000,color:#fff
    style D fill:#029E73,stroke:#000,color:#fff
graph LR
    D["Payment Service<br/>charges card"] --> E["PaymentFailed<br/>event"]
    E --> F["Inventory Service<br/>releases stock (compensation)"]
 
    style D fill:#029E73,stroke:#000,color:#fff
    style E fill:#DE8F05,stroke:#000,color:#fff
    style F fill:#CC78BC,stroke:#000,color:#fff
import java.util.*;
import java.util.function.Consumer;
 
// => Minimal in-process event bus — models a message broker (Kafka, RabbitMQ)
class EventBus {
    // => Maps event type -> list of subscriber lambdas
    private final Map<String, List<Consumer<Map<String, Object>>>> handlers = new HashMap<>();
 
    public void subscribe(String eventType, Consumer<Map<String, Object>> handler) {
        handlers.computeIfAbsent(eventType, k -> new ArrayList<>()).add(handler);
        // => computeIfAbsent creates the list on first subscribe — avoids null checks
    }
 
    public void publish(String eventType, Map<String, Object> payload) {
        handlers.getOrDefault(eventType, List.of()).forEach(h -> h.accept(payload));
        // => Synchronous here; real Kafka consumer group is async per partition
    }
}
 
var bus = new EventBus();
boolean[] stockHeld = {false};  // => Simulates DB row — array so lambda can mutate it
 
// => Inventory service: listens for OrderPlaced (reserve) and PaymentFailed (compensate)
bus.subscribe("OrderPlaced", event -> {
    stockHeld[0] = true;  // => Reserve stock optimistically
    System.out.println("Inventory: reserved stock for order " + event.get("orderId"));
    bus.publish("StockReserved", Map.of("orderId", event.get("orderId")));
    // => Emits next event; Payment reacts without Inventory knowing about it
});
 
bus.subscribe("PaymentFailed", event -> {
    stockHeld[0] = false;  // => Compensation: release reserved stock
    System.out.println("Inventory: released stock for order " + event.get("orderId") + " (compensation)");
});
 
// => Payment service: listens for StockReserved; does NOT call Inventory directly
bus.subscribe("StockReserved", event -> {
    System.out.println("Payment: charging for order " + event.get("orderId"));
    bus.publish("PaymentFailed", Map.of("orderId", event.get("orderId")));
    // => Publishes PaymentFailed — Inventory listens and compensates autonomously
});
 
bus.publish("OrderPlaced", Map.of("orderId", "ORD-42"));
// => Output: Inventory: reserved stock for order ORD-42
// => Output: Payment: charging for order ORD-42
// => Output: Inventory: released stock for order ORD-42 (compensation)
System.out.println("Stock held after failure: " + stockHeld[0]); // => Output: Stock held after failure: false

Key Takeaway: Choreography achieves loose coupling through events, but requires careful event schema design because the saga flow is distributed across multiple handlers.

Why It Matters: Choreography eliminates the orchestrator as a single point of failure and a coordination bottleneck, making each service independently deployable without versioning the orchestrator. However, tracing a saga through event logs is harder than reading a single orchestrator class. Choreography suits high-throughput flows where independent deployability and minimal coupling matter most; orchestration is preferable when business rules require centralized audit trails or when saga logic changes frequently enough that a single source of truth simplifies maintenance.


API Design

Example 62: API Versioning Strategies

API versioning prevents breaking changes from disrupting existing consumers when a service evolves its contract. The three dominant strategies — URI path versioning, Accept header versioning, and query-parameter versioning — make different trade-offs between cacheability, client simplicity, and routing ease.

URI path versioning (most common, most cacheable):

import java.util.Map;
import java.util.function.Supplier;
import java.util.List;
 
// => Route map keyed by (method, path) — path includes version prefix
// => Supplier<Map> defers construction until dispatched — lazy evaluation
Map<String, Supplier<Map<String, Object>>> routes = Map.of(
    "GET:/v1/users", () -> Map.of("version", 1, "users", List.of("alice")),
    // => v1 contract: flat list of usernames — never change this response shape
    "GET:/v2/users", () -> Map.of("version", 2, "users",
        List.of(Map.of("name", "alice", "email", "alice@example.com")))
    // => v2 contract: richer user objects with email field added
);
 
// => Dispatcher routes requests to versioned handlers
Map<String, Object> dispatch(String method, String path) {
    var handler = routes.get(method + ":" + path);
    if (handler == null) return Map.of("error", "Not found");  // => 404 equivalent
    return handler.get();  // => Execute matched route handler
}
 
System.out.println(dispatch("GET", "/v1/users"));
// => Output: {version=1, users=[alice]}
System.out.println(dispatch("GET", "/v2/users"));
// => Output: {version=2, users=[{name=alice, email=alice@example.com}]}

URI versioning: CDN caches /v1/users and /v2/users independently; path is visible in logs and browser history; old clients never break when a new version is added.

Accept header versioning (REST-purist, less cacheable):

import java.util.Map;
import java.util.List;
 
// => Version is negotiated through the Accept header, keeping URLs clean
Map<String, Object> dispatchHeader(String method, String path, String accept) {
    // => Parse version from Accept: application/vnd.myapi.v2+json
    String version = accept.contains("v2") ? "v2" : "v1";
    // => Default to v1 when header absent or unversioned — conservative fallback
 
    if ("/users".equals(path)) {
        if ("v2".equals(version)) {
            return Map.of("version", 2, "users",
                List.of(Map.of("name", "alice", "email", "alice@example.com")));
        }
        return Map.of("version", 1, "users", List.of("alice"));
        // => Same URL, different response shape based on negotiated version
    }
    return Map.of("error", "Not found");
}
 
System.out.println(dispatchHeader("GET", "/users", "application/json"));
// => Output: {version=1, users=[alice]}
System.out.println(dispatchHeader("GET", "/users", "application/vnd.myapi.v2+json"));
// => Output: {version=2, users=[{name=alice, email=alice@example.com}]}

Header versioning: same URL makes REST purists happy; however, CDNs cannot cache different versions of /users by default — requires Vary: Accept header which many CDNs handle poorly.

Key Takeaway: Use URI path versioning for public APIs where CDN cacheability and operational simplicity outweigh URL aesthetics; use header versioning for internal APIs with strict semantic versioning requirements.

Why It Matters: Breaking API changes without a versioning strategy are among the leading causes of production incidents when microservices are upgraded. URI path versioning is explicit in logs, debuggable in browsers, and cache-friendly — benefits that outweigh the "impurity" of embedding version in the URL. Choosing the wrong strategy early forces a painful migration later when consumer count is large and backward compatibility cannot be broken without coordinating across many teams.


Example 63: Backend for Frontend (BFF) Pattern

The Backend for Frontend pattern creates a dedicated aggregation layer for each client type — mobile, web, and third-party — each shaped to that client's data requirements. BFFs eliminate over-fetching, reduce round trips, and prevent a single general-purpose API from being designed around the least-common denominator of all clients.

graph TD
    Mobile["Mobile Client"] --> BFFM["Mobile BFF<br/>(lightweight payloads)"]
    Web["Web Client"] --> BFFW["Web BFF<br/>(rich aggregates)"]
    Third["Third-party API"] --> BFFT["Partner BFF<br/>(versioned, stable)"]
    BFFM --> DS["Downstream Services"]
    BFFW --> DS
    BFFT --> DS
 
    style Mobile fill:#CA9161,stroke:#000,color:#fff
    style Web fill:#CA9161,stroke:#000,color:#fff
    style Third fill:#CA9161,stroke:#000,color:#fff
    style BFFM fill:#0173B2,stroke:#000,color:#fff
    style BFFW fill:#029E73,stroke:#000,color:#fff
    style BFFT fill:#CC78BC,stroke:#000,color:#fff
    style DS fill:#DE8F05,stroke:#000,color:#fff
import java.util.List;
import java.util.Map;
 
// => Downstream service: returns full user profile — downstream owns all fields
static Map<String, Object> getUserProfile(String userId) {
    return Map.of(
        "id", userId,
        "name", "Alice",
        "email", "alice@example.com",
        "preferences", Map.of("theme", "dark")
    );
    // => Downstream service is not shaped for any client — it exposes everything
}
 
// => Downstream service: returns full order list — may be large for heavy users
static List<Map<String, Object>> getUserOrders(String userId) {
    return List.of(
        Map.of("id", "ORD-1", "total", 99.99, "status", "shipped"),
        Map.of("id", "ORD-2", "total", 14.50, "status", "pending")
    );
    // => Full order objects — BFF decides what to expose to each client
}
 
// => Mobile BFF: strips unnecessary fields to reduce bandwidth on cellular connections
static Map<String, Object> mobileBffDashboard(String userId) {
    var profile = getUserProfile(userId);
    var orders = getUserOrders(userId);
    long pendingCount = orders.stream().filter(o -> "pending".equals(o.get("status"))).count();
    // => Aggregates count server-side — mobile shows one number, not a table
    return Map.of(
        "name", profile.get("name"),   // => Only name; email and preferences omitted
        "pendingOrders", pendingCount  // => Pre-computed: saves mobile from parsing a list
    );
}
 
// => Web BFF: returns richer aggregate with full order list and preferences
static Map<String, Object> webBffDashboard(String userId) {
    var profile = getUserProfile(userId);
    var orders = getUserOrders(userId);
    return Map.of(
        "profile", profile,            // => Full profile including email and preferences
        "orders", orders,              // => Full order objects — web renders a sortable table
        "orderCount", orders.size()    // => Pre-computed convenience field for the header
    );
}
 
System.out.println(mobileBffDashboard("u1"));
// => Output: {name=Alice, pendingOrders=1}
System.out.println(webBffDashboard("u1").get("orderCount"));
// => Output: 2

Key Takeaway: Each BFF owns its own aggregation logic so client teams can evolve their API without negotiating with teams serving other clients.

Why It Matters: A single general-purpose API must satisfy every client's needs simultaneously, leading to bloated responses, API design by committee, and tight release coupling between mobile and web teams. BFFs give each client team autonomy to evolve their aggregation layer independently while downstream services remain focused on their own domain. The pattern directly resolves the tension between mobile clients needing lightweight payloads and web clients needing rich aggregates.


Resilience Patterns

Example 64: Circuit Breaker with Fallback

A circuit breaker monitors failure rates on calls to an external dependency and trips open when failures exceed a threshold, stopping further calls and returning a fallback immediately. After a timeout, it enters a half-open state to probe whether the dependency has recovered.

stateDiagram-v2
    [*] --> Closed
    Closed --> Open : failures >= threshold
    Open --> HalfOpen : probe timeout elapsed
    HalfOpen --> Closed : probe succeeds
    HalfOpen --> Open : probe fails
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Supplier;
 
// => Circuit breaker states — three-state machine matching Resilience4j semantics
enum CBState { CLOSED, OPEN, HALF_OPEN }
// => CLOSED: calls pass through normally
// => OPEN: calls fast-fail; dependency gets breathing room
// => HALF_OPEN: one probe call allowed through to test recovery
 
class CircuitBreaker {
    private final int threshold;        // => Trip after this many consecutive failures
    private final long probeTimeoutMs;  // => Milliseconds before attempting probe
    private volatile CBState state = CBState.CLOSED;  // => volatile: visible across threads
    private final AtomicInteger failures = new AtomicInteger(0);  // => Thread-safe counter
    private final AtomicLong openedAt = new AtomicLong(0);        // => Timestamp when tripped
 
    CircuitBreaker(int threshold, long probeTimeoutMs) {
        this.threshold = threshold;
        this.probeTimeoutMs = probeTimeoutMs;
    }
 
    String call(Supplier<String> func, Supplier<String> fallback) {
        if (state == CBState.OPEN) {
            if (System.currentTimeMillis() - openedAt.get() >= probeTimeoutMs) {
                state = CBState.HALF_OPEN;  // => Allow one probe attempt
                System.out.println("Circuit: half-open (probing)");
            } else {
                return fallback.get();  // => Fast-fail: no timeout; dependency rests
            }
        }
        try {
            String result = func.get();  // => Attempt the real call
            onSuccess();                 // => Reset failure count and close breaker
            return result;
        } catch (Exception e) {
            onFailure();         // => Increment counter; may trip breaker
            return fallback.get(); // => Degraded response instead of propagating error
        }
    }
 
    private void onSuccess() {
        failures.set(0);          // => Reset consecutive failure streak
        state = CBState.CLOSED;   // => Close breaker — works for HALF_OPEN probe too
    }
 
    private void onFailure() {
        int count = failures.incrementAndGet();  // => Atomic increment
        if (count >= threshold) {
            state = CBState.OPEN;
            openedAt.set(System.currentTimeMillis());  // => Record trip time for probe timeout
            System.out.println("Circuit: tripped OPEN after " + count + " failures");
        }
    }
}
 
// => Simulate a flaky downstream service recovering after 3 failures
int[] callCount = {0};
Supplier<String> flakyService = () -> {
    if (++callCount[0] <= 3) throw new RuntimeException("service down");
    // => First 3 calls fail — simulates intermittent dependency outage
    return "fresh data";  // => Recovers on 4th call
};
Supplier<String> fallback = () -> "cached data";  // => Stale but acceptable response
 
var cb = new CircuitBreaker(3, 0);  // => probeTimeout=0 so half-open triggers immediately
for (int i = 1; i <= 6; i++) {
    System.out.println("Call " + i + ": " + cb.call(flakyService, fallback));
}
// => Output: Call 1: cached data  (failure 1)
// => Output: Call 2: cached data  (failure 2)
// => Output: Circuit: tripped OPEN after 3 failures
// => Output: Call 3: cached data  (failure 3, tripped)
// => Output: Circuit: half-open (probing)
// => Output: Call 4: fresh data   (probe succeeds, breaker closes)
// => Output: Call 5: fresh data
// => Output: Call 6: fresh data

Key Takeaway: Circuit breakers prevent cascading failures by fast-failing requests when a dependency is unhealthy, giving it time to recover while callers receive fallback responses.

Why It Matters: Without circuit breakers, a slow or failing downstream service causes thread pools to fill with waiting requests, blocking the entire caller — the cascade failure pattern that turns a partial dependency failure into a full service outage. Resilience libraries across all major OOP runtimes embed this pattern as production standard because properly configured breakers allow a system to shed load during incidents without full outages, giving the failing dependency time to recover while callers receive fast fallback responses.


Example 65: Bulkhead Pattern

The bulkhead pattern isolates failures within one pool of resources so they do not exhaust shared resources needed by other operations. By separating thread pools (or semaphores) for different dependency calls, a slow third-party service starves only its own pool, not the entire application.

graph TD
    App["Application Thread Pool<br/>(shared — vulnerable)"]
    BH1["Bulkhead: Payments<br/>(isolated pool, 5 threads)"]
    BH2["Bulkhead: Inventory<br/>(isolated pool, 10 threads)"]
    P["Payment Service"]
    I["Inventory Service"]
 
    App -->|"without bulkhead:<br/>all threads go here"| P
    BH1 --> P
    BH2 --> I
 
    style App fill:#DE8F05,stroke:#000,color:#fff
    style BH1 fill:#0173B2,stroke:#000,color:#fff
    style BH2 fill:#029E73,stroke:#000,color:#fff
    style P fill:#CC78BC,stroke:#000,color:#fff
    style I fill:#CA9161,stroke:#000,color:#fff
import java.util.concurrent.Semaphore;
 
// => Bulkhead: limits concurrent calls to a downstream dependency via a semaphore
class Bulkhead {
    private final String name;           // => Name used in error messages and metrics
    private final Semaphore semaphore;   // => Semaphore permits = max concurrent slots
    private int rejected = 0;            // => Count of calls rejected due to full pool
 
    Bulkhead(String name, int maxConcurrent) {
        this.name = name;
        this.semaphore = new Semaphore(maxConcurrent);
        // => Fair=false (default): no queuing — callers fail fast if pool full
    }
 
    // => Executes func inside a semaphore permit; rejects immediately if no permit available
    <T> T execute(java.util.function.Supplier<T> func) {
        boolean acquired = semaphore.tryAcquire();
        // => tryAcquire: non-blocking — returns false if all permits are taken
        if (!acquired) {
            rejected++;
            throw new RuntimeException("Bulkhead '" + name + "' full — call rejected");
            // => Fail fast: caller gets immediate error; downstream not contacted
        }
        try {
            return func.get();  // => Caller's work executes under semaphore protection
        } finally {
            semaphore.release();  // => Always release permit — even on exception
        }
    }
 
    int getRejected() { return rejected; }  // => Expose for metrics dashboard
}
 
// => Two separate bulkheads — slow Payments cannot exhaust Inventory's pool
var paymentsBulkhead = new Bulkhead("payments", 2);
// => Max 2 concurrent payment calls — third caller is rejected immediately
var inventoryBulkhead = new Bulkhead("inventory", 5);
// => Inventory has its own independent pool of 5 slots
 
// => Simulate: drain all payment permits, then show inventory still works
paymentsBulkhead.execute(() -> "hold-slot-1");  // => Permit 1 taken (synthetic hold)
 
try {
    // => Manually exhaust remaining permit to simulate "pool full" scenario
    boolean p2 = paymentsBulkhead.semaphore.tryAcquire();  // => Takes permit 2
    try {
        paymentsBulkhead.execute(() -> "ORD-1");  // => Should throw: pool full
    } finally {
        if (p2) paymentsBulkhead.semaphore.release();  // => Restore permit 2
    }
} catch (RuntimeException e) {
    System.out.println(e.getMessage());
    // => Output: Bulkhead 'payments' full — call rejected
}
 
String invResult = inventoryBulkhead.execute(() -> "stock_ok:SKU-1");
System.out.println(invResult);  // => Output: stock_ok:SKU-1 (unaffected by payments)

Key Takeaway: Assign separate resource pools (semaphores or thread pools) to each downstream dependency so one slow service can only exhaust its own pool, not the shared application pool.

Why It Matters: The bulkhead pattern is named after ship compartments that prevent flooding from spreading. In services, a payment processor slowdown that fills a shared thread pool starves inventory checks and health endpoints — a total service outage caused by a partial dependency failure. Bulkhead isolation in resilience libraries enforces this separation in production, allowing services to degrade gracefully (payments fail, site keeps working) rather than failing completely because one slow dependency exhausted the shared resource pool.


Example 66: Retry with Exponential Backoff and Jitter

Retrying transient failures is essential in distributed systems, but naive fixed-interval retries cause thundering herds when many clients retry simultaneously. Exponential backoff with jitter spreads retries over time, reducing collision probability and letting overloaded services recover.

import java.util.Random;
import java.util.function.Supplier;
 
// => Computes exponential delay with full jitter: delay = random(0, min(base*2^attempt, cap))
// => Resilience4j uses the same formula internally — this models its behaviour
static double exponentialBackoff(int attempt, double baseMs, double capMs) {
    double ceiling = Math.min(baseMs * Math.pow(2, attempt), capMs);
    // => ceiling grows exponentially: 100, 200, 400, 800 ... capped at capMs
    return new Random().nextDouble() * ceiling;
    // => Full jitter: random between 0 and ceiling — avoids thundering herd
}
 
// => Generic retry loop: up to maxAttempts, sleeping exponential backoff between failures
static <T> T retry(Supplier<T> func, int maxAttempts) throws Exception {
    Exception last = null;
    for (int attempt = 0; attempt < maxAttempts; attempt++) {
        try {
            return func.get();  // => Attempt the operation
        } catch (Exception e) {
            last = e;
            if (attempt == maxAttempts - 1) break;  // => Last attempt; skip sleep
            long delayMs = (long) exponentialBackoff(attempt, 100.0, 30_000.0);
            // => delayMs varies per attempt and per client — jitter prevents synchronised retry bursts
            System.out.println("Attempt " + (attempt + 1) + " failed: " + e.getMessage()
                + ". Retrying in " + delayMs + "ms");
            Thread.sleep(0);  // => Simulation: skip actual sleep; prod uses Thread.sleep(delayMs)
        }
    }
    throw new RuntimeException("All " + maxAttempts + " attempts failed", last);
    // => Exhausted retries — propagate last error to caller
}
 
// => Simulate a service that fails twice then succeeds on the third call
int[] callCount = {0};
Supplier<String> unstableCall = () -> {
    callCount[0]++;
    if (callCount[0] < 3) throw new RuntimeException("timeout on attempt " + callCount[0]);
    // => First two calls throw transient error — retry handles these transparently
    return "success";  // => Third call returns result normally
};
 
try {
    String result = retry(unstableCall, 5);
    System.out.println("Result: " + result);  // => Output: Result: success (after 2 retried failures)
    System.out.println("Total attempts: " + callCount[0]);  // => Output: Total attempts: 3
} catch (Exception e) {
    System.out.println("All retries exhausted: " + e.getMessage());
}

Key Takeaway: Use exponential backoff to avoid retry storms, and add jitter to spread load across time when many clients retry the same failing service concurrently.

Why It Matters: Naive fixed-interval retries cause cascading failures when many services retry simultaneously during an availability event, amplifying load on already-stressed infrastructure. Exponential backoff with jitter — the "Full Jitter" strategy — reduces collision probability dramatically compared to fixed intervals by spreading retries over time, enabling overloaded services to shed excess load and recover within seconds instead of minutes.


Observability Patterns

Example 67: Distributed Tracing Architecture

Distributed tracing tracks a request as it propagates through multiple services by injecting a trace ID into every outbound call. Each service creates a child span attached to the parent trace, enabling engineers to reconstruct the full request timeline across service boundaries.

sequenceDiagram
    participant A as API Gateway<br/>trace_id=abc, span=1
    participant B as Orders Service<br/>trace_id=abc, span=2
    participant C as Inventory Service<br/>trace_id=abc, span=3
 
    A->>B: request + trace headers
    B->>C: request + trace headers
    C-->>B: response (span 3 ends)
    B-->>A: response (span 2 ends)
    Note over A,C: All spans share trace_id=abc
import java.util.UUID;
 
// => Span: represents one timed operation within a distributed trace tree
record Span(
    String traceId,        // => Same across all spans from the same root request
    String spanId,         // => Unique per operation — used as parentSpanId by children
    String parentSpanId,   // => null for root span; links child to parent in trace tree
    String operation,      // => Human-readable name for this span (e.g. "orders-svc:place_order")
    long startNs           // => Nanosecond timestamp — nanoseconds give sub-millisecond precision
) {
    // => finish: compute duration and emit — in production, send to Jaeger/Zipkin, not stdout
    void finish() {
        long durationMs = (System.nanoTime() - startNs) / 1_000_000;
        System.out.printf("[TRACE] trace=%s span=%s parent=%s op=%s duration=%dms%n",
            traceId, spanId, parentSpanId, operation, durationMs);
        // => In production: opentelemetry-sdk exports this to the configured backend
    }
}
 
// => Tracer: creates spans for one named service; propagates trace context across calls
class Tracer {
    private final String service;  // => Service name prefixed to every operation name
    Tracer(String service) { this.service = service; }
 
    // => startSpan: traceId=null creates a root span; non-null continues an existing trace
    Span startSpan(String operation, String traceId, String parentSpanId) {
        String tid = traceId != null ? traceId : UUID.randomUUID().toString();
        // => Root span: generates new traceId; child span: inherits caller's traceId
        String sid = UUID.randomUUID().toString().substring(0, 8);
        // => spanId is short for readability; real OpenTelemetry uses 16-byte hex
        return new Span(tid, sid, parentSpanId, service + ":" + operation, System.nanoTime());
    }
}
 
// => Three services each create spans under the same trace
var gateway = new Tracer("api-gateway");
var ordersSvc = new Tracer("orders-svc");
var inventorySvc = new Tracer("inventory-svc");
 
// => Step 1: API Gateway starts the root span — traceId generated here
var root = gateway.startSpan("handle_request", null, null);
// => root.traceId is propagated to all downstream services via W3C traceparent header
 
// => Step 2: Orders service starts child span with gateway's traceId
var ordersSpan = ordersSvc.startSpan("place_order", root.traceId(), root.spanId());
// => ordersSpan.parentSpanId == root.spanId — links child to parent in trace tree
 
// => Step 3: Inventory service starts grandchild span
var invSpan = inventorySvc.startSpan("reserve_stock", root.traceId(), ordersSpan.spanId());
invSpan.finish();    // => Inventory finishes first (innermost call)
// => Output: [TRACE] trace=<id> span=<id> parent=<orders-span-id> op=inventory-svc:reserve_stock duration=0ms
 
ordersSpan.finish(); // => Orders finishes after Inventory returns
// => Output: [TRACE] trace=<id> span=<id> parent=<root-span-id> op=orders-svc:place_order duration=0ms
 
root.finish();       // => Gateway finishes last — outermost span spans the entire request
// => Output: [TRACE] trace=<id> span=<id> parent=null op=api-gateway:handle_request duration=0ms
// => All three spans share the same traceId — Jaeger/Zipkin links them into a flame graph

Key Takeaway: Propagate trace_id in every outbound request header (W3C traceparent) so that all spans generated by a single user request share the same root identifier.

Why It Matters: Without distributed tracing, debugging latency in a chain of ten microservices requires correlating timestamps across ten separate log files — a manual task that takes hours. Tracing tools like Jaeger, Zipkin, and Datadog APM reduce this to a single flame graph, enabling engineers to identify which service or database query caused a P99 latency spike in minutes. The value of distributed tracing scales with the number of services in the call chain: the more microservices, the more essential a shared trace context becomes for diagnosing production incidents.


Deployment Patterns

Example 68: Sidecar Pattern

The sidecar pattern deploys a secondary container alongside the primary application container in the same pod or VM, sharing the same network namespace. The sidecar handles cross-cutting concerns — TLS termination, logging, metrics scraping, service discovery — so the application code remains free of infrastructure concerns.

graph LR
    subgraph Pod["Pod / VM"]
        App["Application Container<br/>(business logic)"]
        Side["Sidecar Container<br/>(logging, TLS, metrics)"]
    end
    Side --> Collector["Log Collector<br/>/ Prometheus"]
 
    style App fill:#0173B2,stroke:#000,color:#fff
    style Side fill:#029E73,stroke:#000,color:#fff
    style Collector fill:#DE8F05,stroke:#000,color:#fff
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
 
// => SidecarProxy: wraps the application handler and adds cross-cutting concerns
// => App never touches TLS, metrics, or request-ID generation — sidecar owns all of that
class SidecarProxy {
    private final Function<Map<String, Object>, Map<String, Object>> app;
    // => app: the real application handler — pure business logic
 
    SidecarProxy(Function<Map<String, Object>, Map<String, Object>> app) {
        this.app = app;
        // => app is injected; sidecar can wrap any handler without changing it
    }
 
    Map<String, Object> handle(Map<String, Object> request) {
        long startNs = System.nanoTime();  // => Start timing before any sidecar work
 
        // => Sidecar concern 1: enforce mutual TLS — block unauthenticated callers
        if (!Boolean.TRUE.equals(request.get("mtlsVerified"))) {
            return Map.of("error", "TLS handshake failed", "status", 401);
            // => App never sees this request — sidecar short-circuits before delegating
        }
 
        // => Sidecar concern 2: inject request metadata — app reads requestId from map
        var enriched = new HashMap<>(request);
        enriched.put("requestId", "req-" + (startNs % 10_000));
        // => App can log requestId without implementing header parsing itself
 
        // => Delegate to application logic — app knows nothing about sidecar
        var response = app.apply(enriched);
 
        // => Sidecar concern 3: emit latency metric to Prometheus (stdout here)
        long durationMs = (System.nanoTime() - startNs) / 1_000_000;
        System.out.printf("[SIDECAR] method=%s path=%s status=%s duration=%dms%n",
            request.get("method"), request.get("path"),
            response.getOrDefault("status", 200), durationMs);
        // => Application code never calls a metrics client; sidecar owns all instrumentation
 
        return response;
    }
}
 
// => Application handler: pure business logic — no TLS, metrics, or log formatting
Function<Map<String, Object>, Map<String, Object>> orderApp = req ->
    Map.of("status", 200, "data", "Order " + req.getOrDefault("orderId", "unknown") + " retrieved");
// => orderApp is completely unaware that a sidecar wraps it
 
var proxy = new SidecarProxy(orderApp);
 
// => Blocked request: no mTLS certificate — sidecar rejects before app sees it
var blockedResp = proxy.handle(Map.of("method", "GET", "path", "/orders/1",
    "orderId", "1", "mtlsVerified", false));
System.out.println(blockedResp);
// => Output: {error=TLS handshake failed, status=401}
 
// => Allowed request: mTLS verified — sidecar enriches, delegates, emits metrics
var okResp = proxy.handle(Map.of("method", "GET", "path", "/orders/1",
    "orderId", "1", "mtlsVerified", true));
// => Output: [SIDECAR] method=GET path=/orders/1 status=200 duration=0ms
System.out.println(okResp);
// => Output: {status=200, data=Order 1 retrieved}

Key Takeaway: Sidecars let application developers focus on business logic while a separate deployable unit evolves independently to handle observability, security, and traffic management.

Why It Matters: Kubernetes service mesh implementations (Istio, Linkerd) use the sidecar pattern to inject Envoy proxies alongside every pod transparently — the application team deploys business code unchanged, while the platform team rotates TLS certificates, controls traffic splits, and collects distributed traces through the sidecar. This separation allows infrastructure upgrades without coordinating with application teams, enabling cluster-wide policy rollouts without modifying a single application binary.


Example 69: Ambassador Pattern

The ambassador pattern places a proxy between an application and a remote service to handle concerns specific to that client's relationship with the service: protocol translation, retry policy, credential injection, and connection pooling. Unlike the sidecar (which handles all outbound traffic), an ambassador is purpose-built for one specific remote.

import java.util.List;
import java.util.Map;
 
// => DatabaseAmbassador: encapsulates all complexity of calling the database
// => Application code never sees retries, DSN, pool management, or credential injection
class DatabaseAmbassador {
    private final String dsn;         // => Connection string — app never accesses this directly
    private final int maxRetries;     // => How many transient errors to absorb before propagating
    private int callCount = 0;        // => Internal counter for metrics reporting
 
    DatabaseAmbassador(String dsn, int maxRetries) {
        this.dsn = dsn;
        this.maxRetries = maxRetries;
        // => Real ambassador would initialise a connection pool here (HikariCP, c3p0)
    }
 
    // => query: public API for the application — hides all retry and pool complexity
    List<Map<String, Object>> query(String sql, Object... params) {
        for (int attempt = 0; attempt < maxRetries; attempt++) {
            try {
                return execute(sql, params);  // => Attempt via connection pool
            } catch (RuntimeException e) {
                if (attempt == maxRetries - 1) throw e;  // => Exhausted retries; propagate
                long backoffMs = (long)(100 * Math.pow(2, attempt));
                // => Exponential backoff between retries — DB gets breathing room
                System.out.println("Transient error on attempt " + (attempt + 1) + ": " + e.getMessage());
                // => App never sees this retry loop; it transparently hides transient errors
            }
        }
        throw new RuntimeException("Unreachable");  // => Loop always throws or returns
    }
 
    // => execute: simulates a real DB call through the connection pool
    private List<Map<String, Object>> execute(String sql, Object[] params) {
        callCount++;
        // => In production: use a pooled Connection from HikariDataSource; apply query timeout
        if (callCount == 1) throw new RuntimeException("transient connection loss");
        // => Simulate flaky first call — ambassador absorbs and retries this transparently
        return List.of(
            Map.of("id", 1, "name", "Alice"),
            Map.of("id", 2, "name", "Bob")
        );
        // => Returns clean result list; app sees this as if no failure occurred
    }
 
    int getCallCount() { return callCount; }  // => Expose for monitoring/testing
}
 
// => Application code: calls ambassador — knows nothing about retries, DSN, or pool size
var db = new DatabaseAmbassador("jdbc:postgresql://localhost/mydb", 3);
var rows = db.query("SELECT id, name FROM users WHERE active = ?", true);
System.out.println(rows);
// => Output: [{id=1, name=Alice}, {id=2, name=Bob}]
// => (First call failed internally; ambassador retried transparently)
System.out.println("Total calls made (including retries): " + db.getCallCount());
// => Output: Total calls made (including retries): 2

Key Takeaway: The ambassador externalises connection management, retry logic, and credential handling from application code, keeping business logic free of infrastructure concerns.

Why It Matters: Without an ambassador, retry logic, connection pool configuration, and timeout handling are duplicated across every service that calls the same downstream. When the retry policy needs changing — for example, after a database upgrade changes failure characteristics — a single ambassador change affects all consumers. Proxy-based ambassadors demonstrate this pattern at scale: a single proxy configuration update changes connection pool behaviour for all microservices without requiring individual code deployments.


Event-Driven Architecture

Example 70: Event Sourcing Implementation

Paradigm Note: Same fold-over-immutable-log semantics as Example 55. The pure function (state, event) -> state is the heart of the pattern; OOP wraps it in aggregate-method ceremony. See the FP framing.

Event sourcing stores state as an append-only sequence of domain events rather than as current mutable state. The current state is derived by replaying events. This enables complete audit trails, temporal queries ("what was the account balance at 3 PM yesterday?"), and event-driven integration.

graph TD
    E1["AccountOpened<br/>{balance: 0}"]
    E2["MoneyDeposited<br/>{amount: 500}"]
    E3["MoneyWithdrawn<br/>{amount: 200}"]
    E4["Current State<br/>balance=300"]
 
    E1 --> E2 --> E3 -->|replay| E4
 
    style E1 fill:#0173B2,stroke:#000,color:#fff
    style E2 fill:#029E73,stroke:#000,color:#fff
    style E3 fill:#DE8F05,stroke:#000,color:#fff
    style E4 fill:#CC78BC,stroke:#000,color:#fff
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
 
// => DomainEvent: immutable record of a state change — never updated or deleted after appending
record DomainEvent(
    String eventType,     // => e.g. "AccountOpened", "MoneyDeposited", "MoneyWithdrawn"
    Map<String, Object> payload  // => Event-specific data — always serialisable to JSON
) {}
 
// => EventStore: append-only log — the source of truth for all state changes
class EventStore {
    private final List<DomainEvent> events = new ArrayList<>();
    // => Append-only: events never updated or deleted — immutable history
 
    void append(DomainEvent event) {
        events.add(event);  // => Append only; production uses INSERT with sequence number
        // => In production: rows in PostgreSQL events table, or EventStoreDB streams
    }
 
    List<DomainEvent> getEvents() {
        return Collections.unmodifiableList(events);
        // => Returns read-only view — callers cannot mutate the store
    }
}
 
// => BankAccount: state is always derived by replaying events, never stored directly
class BankAccount {
    String accountId = "";  // => Set by AccountOpened event
    double balance = 0.0;   // => Recomputed by replay; never stored in a "balance" column
 
    // => replay: rebuild current state by applying each event in order
    static BankAccount replay(List<DomainEvent> events) {
        var account = new BankAccount();
        for (var e : events) account.apply(e);
        // => Each event is applied in sequence order — order matters for financial ledgers
        return account;  // => Account is now in the state it was after all given events
    }
 
    private void apply(DomainEvent e) {
        switch (e.eventType()) {
            case "AccountOpened" -> {
                accountId = (String) e.payload().get("accountId");
                balance = 0.0;  // => Initial balance always 0 at account opening
            }
            case "MoneyDeposited" -> balance += (double) e.payload().get("amount");
            // => Increase balance by deposited amount
            case "MoneyWithdrawn" -> balance -= (double) e.payload().get("amount");
            // => Decrease balance by withdrawn amount
        }
    }
}
 
// => Simulation: three events recorded over an account's lifetime
var store = new EventStore();
store.append(new DomainEvent("AccountOpened", Map.of("accountId", "ACC-001")));
store.append(new DomainEvent("MoneyDeposited", Map.of("amount", 500.0)));
store.append(new DomainEvent("MoneyWithdrawn", Map.of("amount", 200.0)));
 
// => Replay all events to derive current state
var account = BankAccount.replay(store.getEvents());
System.out.println("Account: " + account.accountId + ", Balance: " + account.balance);
// => Output: Account: ACC-001, Balance: 300.0
 
// => Temporal query: state after only first two events (deposit not yet withdrawn)
var accountAtT2 = BankAccount.replay(store.getEvents().subList(0, 2));
System.out.println("Balance after deposit only: " + accountAtT2.balance);
// => Output: Balance after deposit only: 500.0

Key Takeaway: Store domain events as the source of truth; derive read-state by replaying events so the full history is preserved for audit, debugging, and temporal queries.

Why It Matters: Traditional CRUD databases overwrite state on every update, losing historical information. Financial services (banks, trading platforms), healthcare systems, and compliance-heavy domains require complete audit trails mandated by regulations such as GDPR, SOX, and PCI DSS. Event sourcing satisfies these requirements by design: every state transition is recorded with its cause. CQRS (Command Query Responsibility Segregation) pairs naturally with event sourcing because read models can be rebuilt from events when requirements change, without migrating data.


Structural Patterns

Example 71: Modular Monolith

A modular monolith deploys as a single process but enforces strict module boundaries in code, preventing cross-module dependencies at the wrong level of abstraction. Each module owns its domain model, service layer, and repository — but shares the process and database, making local calls cheap and distributed tracing unnecessary.

import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
 
// ============================================================
// Module: Orders — owns its own model and service interface
// ============================================================
// => Order: domain object owned exclusively by the Orders module
record Order(String orderId, String customerId, double total) {}
 
// => OrderRepository: interface owned by Orders module — implementations injected from outside
interface OrderRepository {
    void save(Order order);
    // => Module specifies what it needs; infrastructure provides the implementation
    Optional<Order> find(String orderId);
}
 
// => OrderService: Orders module's public API — other modules call this, never the repo directly
class OrderService {
    private final OrderRepository repo;
    // => Injected at composition root — module owns the interface, not the implementation
    OrderService(OrderRepository repo) { this.repo = repo; }
 
    Order place(String customerId, double total) {
        var order = new Order(UUID.randomUUID().toString().substring(0, 8), customerId, total);
        repo.save(order);  // => Persists via injected repo — module doesn't know which DB
        return order;
        // => Returns domain object; Billing receives this via service call, not direct DB access
    }
}
 
// ============================================================
// Module: Billing — separate domain; communicates via Order domain object only
// ============================================================
// => Invoice: domain object owned exclusively by the Billing module
// => Billing imports Order (shared kernel type) but NOT OrderRepository — boundary enforced
record Invoice(String invoiceId, String orderId, double amount, boolean paid) {}
 
// => BillingService: Billing module's public API
class BillingService {
    private final Map<String, Invoice> invoices = new HashMap<>();
    // => Billing owns its own in-memory store — real impl uses its own DB schema
 
    Invoice issueInvoice(Order order) {
        var inv = new Invoice(UUID.randomUUID().toString().substring(0, 8),
            order.orderId(), order.total(), false);
        invoices.put(inv.invoiceId(), inv);  // => Billing persists in its own storage
        return inv;
        // => Returns Billing's Invoice type — not Orders' domain object
    }
}
 
// ============================================================
// Composition Root — wires both modules together at application startup
// ============================================================
// => InMemoryOrderRepo: infrastructure implementation injected into Orders module
class InMemoryOrderRepo implements OrderRepository {
    private final Map<String, Order> store = new HashMap<>();
 
    @Override public void save(Order o) { store.put(o.orderId(), o); }
    // => In production: JPA repository or JDBC template
    @Override public Optional<Order> find(String id) { return Optional.ofNullable(store.get(id)); }
    // => Returns Optional — avoids null; caller handles missing case explicitly
}
 
// => Composition Root wires modules — neither module knows about the other's internals
var orderSvc = new OrderService(new InMemoryOrderRepo());
var billingSvc = new BillingService();
 
var order = orderSvc.place("cust-1", 149.99);  // => Orders module creates and persists order
var invoice = billingSvc.issueInvoice(order);   // => Billing receives Order domain object
System.out.println("Order: " + order.orderId() + ", Invoice: " + invoice.invoiceId()
    + ", Amount: " + invoice.amount());
// => Output: Order: <id>, Invoice: <id>, Amount: 149.99

Key Takeaway: Enforce module boundaries through interfaces and dependency injection rather than package-access modifiers, making the modular monolith easier to later split into services if needed.

Why It Matters: Microservices introduce distributed systems complexity (network failures, data consistency, distributed tracing) that many teams are not ready for. A modular monolith provides the domain boundary discipline of microservices while retaining the operational simplicity of a single deployable unit. Well-designed modular monoliths handle high traffic volumes without the overhead of distributed coordination, and their strict internal boundaries enable selective service extraction later without the coupling problems of an unstructured monolith.


Example 72: Vertical Slice Architecture

Vertical slice architecture organises code by feature rather than by technical layer (Controller, Service, Repository). Each slice contains all layers needed for that feature in one cohesive unit, reducing cross-slice coupling and making it easy to find, understand, and change a complete feature.

import java.util.*;
 
// => PlaceOrder slice: request + handler + response in one cohesive unit
// => No shared service layer — each slice owns its full vertical stack
record PlaceOrderRequest(String customerId, String productId, int quantity) {}
// => Immutable record; all fields set at construction, no setters needed
 
record PlaceOrderResponse(String orderId, String status, double total) {}
// => Immutable response; caller reads fields, never mutates them
 
class PlaceOrderHandler {
    // => Price and storage are private implementation details of this slice
    private final double pricePerUnit;
    // => In prod: injected OrderRepository + PriceService via constructor DI
    private final List<Map<String, Object>> orders = new ArrayList<>();
 
    PlaceOrderHandler(double pricePerUnit) {
        this.pricePerUnit = pricePerUnit;
        // => Price injected at construction — easy to stub in unit tests
    }
 
    PlaceOrderResponse handle(PlaceOrderRequest req) {
        double total = pricePerUnit * req.quantity();
        // => Business rule: total = price * qty — lives here, not in a shared service
        String orderId = String.format("ORD-%04d", orders.size() + 1);
        // => Simple sequential id; prod uses UUID or DB sequence
        orders.add(Map.of("id", orderId, "customer", req.customerId(), "total", total));
        // => Persists order; prod uses Unit of Work + transaction boundary
        return new PlaceOrderResponse(orderId, "placed", total);
        // => Slice-specific response; no generic Result envelope needed
    }
 
    List<Map<String, Object>> getOrders() { return orders; }
    // => Exposed only for GetOrder slice to share in-memory list in this demo
}
 
// => GetOrder slice: separate unit, no dependency on PlaceOrder internals
record GetOrderRequest(String orderId) {}
record GetOrderResponse(String orderId, Double total, boolean found) {}
// => Nullable Double signals "not found" without a separate Maybe type
 
class GetOrderHandler {
    private final List<Map<String, Object>> orders;
    // => In prod: separate read-side repository (CQRS read model)
 
    GetOrderHandler(List<Map<String, Object>> orders) { this.orders = orders; }
 
    GetOrderResponse handle(GetOrderRequest req) {
        return orders.stream()
            // => Linear scan for demo; prod uses indexed DB query by order_id
            .filter(o -> req.orderId().equals(o.get("id")))
            .findFirst()
            .map(o -> new GetOrderResponse(req.orderId(), (Double) o.get("total"), true))
            // => Found: wrap total and flag as found
            .orElse(new GetOrderResponse(req.orderId(), null, false));
            // => Not found: null total, found=false
    }
}
 
PlaceOrderHandler placer = new PlaceOrderHandler(12.50);
PlaceOrderResponse resp = placer.handle(new PlaceOrderRequest("C1", "P1", 4));
System.out.println(resp);
// => Output: PlaceOrderResponse[orderId=ORD-0001, status=placed, total=50.0]
 
GetOrderHandler getter = new GetOrderHandler(placer.getOrders());
GetOrderResponse got = getter.handle(new GetOrderRequest("ORD-0001"));
System.out.println(got);
// => Output: GetOrderResponse[orderId=ORD-0001, total=50.0, found=true]

Key Takeaway: One feature, one folder, all layers — a developer should be able to read a single file to understand, change, and test a feature end to end.

Why It Matters: Traditional layered architecture (Controller/Service/Repository) scatters a feature across three folders, requiring developers to navigate multiple files to understand one user story. Vertical slice architecture collocates request, handler, and response in one unit, reducing cognitive load and making the scope of a change immediately visible to anyone reading the code.


Example 73: Shared Kernel

The Shared Kernel is a bounded context pattern where two related domains share a small, deliberately chosen subset of their domain model — typically value objects and domain events — without sharing full application or infrastructure code. Both teams must agree on changes to the kernel.

import java.util.Objects;
 
// => Shared Kernel: only value objects and domain events live here
// => Never: repositories, services, application logic, database schemas
record Money(double amount, String currency) {
    // => Immutable value object — amount and currency never change after construction
    // => ISO 4217 currency code e.g. "USD", "EUR"
 
    Money add(Money other) {
        if (!Objects.equals(currency, other.currency))
            throw new IllegalArgumentException("Cannot add " + currency + " and " + other.currency);
        // => Type safety: prevent accidental USD + EUR addition at compile-adjacent runtime
        return new Money(amount + other.amount, currency);
        // => Returns new Money; immutable — no mutation of existing instances
    }
 
    @Override public String toString() {
        return String.format("%s %.2f", currency, amount);
        // => e.g. "USD 99.50" — consistent across both Orders and Billing domains
    }
}
 
record OrderId(String value) {
    // => Shared identifier type; both Orders and Billing reference the same type
    // => Wrapping String in a record prevents accidental use of raw strings as IDs
}
 
// => Orders Domain: uses shared kernel types without any conversion layer
record OrderLine(String productId, Money price, int qty) {
    // => Money from shared kernel — no translation needed when passing to Billing
 
    Money subtotal() {
        return new Money(price.amount() * qty, price.currency());
        // => Computes subtotal using shared kernel Money; result is also a shared kernel type
    }
}
 
// => Billing Domain: uses the same shared kernel types independently
record Invoice(OrderId orderId, Money total) {
    // => orderId from shared kernel — both domains reference the exact same type
    // => total is shared kernel Money; no DTO translation needed at domain boundary
 
    boolean isOverdue(int daysOutstanding) {
        return daysOutstanding > 30;
        // => Billing's own rule; this logic does NOT belong in the shared kernel
    }
}
 
// => Usage: both domains speak the same Money and OrderId language
OrderLine line = new OrderLine("P1", new Money(10.0, "USD"), 3);
Invoice invoice = new Invoice(new OrderId("ORD-42"), line.subtotal());
System.out.println("Invoice total: " + invoice.total());
// => Output: Invoice total: USD 30.00
System.out.println("Overdue (35 days): " + invoice.isOverdue(35));
// => Output: Overdue (35 days): true

Key Takeaway: Keep the shared kernel minimal — value objects and events only — and require both teams to agree on changes through a formal RFC or PR review, treating the kernel as a public API.

Why It Matters: Without a shared kernel, teams independently define Money — one with Decimal, one with float — leading to precision mismatches and bugs when billing calculates differently from orders. Domain-Driven Design's Shared Kernel pattern establishes a formal contract between bounded contexts, preventing the implicit coupling that occurs when teams copy-paste shared types. Eric Evans documented this pattern after observing that teams with clear, small shared kernels resolve inter-team conflicts faster and have fewer integration-layer bugs.


Design Patterns at Architecture Scale

Example 74: Specification Pattern

The specification pattern encapsulates a business rule as a composable object with a single is_satisfied_by(candidate) method. Specifications compose via and_, or_, and not_, enabling complex business rules to be expressed as readable, testable combinations.

// => Base Specification: composable business rule abstraction
// => Each subclass implements one focused predicate; combinations built via and/or/not
abstract class Specification<T> {
    abstract boolean isSatisfiedBy(T candidate);
    // => Subclasses implement the specific predicate — single responsibility per spec
 
    Specification<T> and(Specification<T> other) {
        return candidate -> isSatisfiedBy(candidate) && other.isSatisfiedBy(candidate);
        // => Lambda: both specs must pass; short-circuits if left is false
    }
 
    Specification<T> or(Specification<T> other) {
        return candidate -> isSatisfiedBy(candidate) || other.isSatisfiedBy(candidate);
        // => Lambda: either spec passing is sufficient; short-circuits if left is true
    }
 
    Specification<T> not() {
        return candidate -> !isSatisfiedBy(candidate);
        // => Lambda: inverts result — useful for "not a gold customer" type rules
    }
}
 
// => Domain: Order eligibility for discount
record Order(double total, String customerTier, int itemCount) {}
// => "gold", "silver", "bronze" — tier drives discount eligibility
 
class HighValueOrder extends Specification<Order> {
    boolean isSatisfiedBy(Order o) { return o.total() >= 100.0; }
    // => Orders over $100 qualify as high-value — business threshold, easy to change
}
 
class GoldCustomer extends Specification<Order> {
    boolean isSatisfiedBy(Order o) { return "gold".equals(o.customerTier()); }
    // => Only gold-tier customers — tier checked here, not scattered in service
}
 
class BulkOrder extends Specification<Order> {
    boolean isSatisfiedBy(Order o) { return o.itemCount() >= 10; }
    // => 10+ items qualify as bulk — threshold expressed once, tested independently
}
 
// => Business rule: gold customer OR (high value AND bulk)
// => Reads almost like English; each rule independently unit-testable
Specification<Order> discountEligible =
    new GoldCustomer().or(new HighValueOrder().and(new BulkOrder()));
 
Order o1 = new Order(150.0, "gold", 3);
Order o2 = new Order(150.0, "silver", 12);
Order o3 = new Order(50.0, "bronze", 5);
 
System.out.println(discountEligible.isSatisfiedBy(o1)); // => Output: true (gold customer)
System.out.println(discountEligible.isSatisfiedBy(o2)); // => Output: true (high value AND bulk)
System.out.println(discountEligible.isSatisfiedBy(o3)); // => Output: false (no criteria met)

Key Takeaway: Express business rules as composable specification objects so rules can be independently tested, combined, and reused without scattering if statements across the codebase.

Why It Matters: Discount eligibility, loan approval criteria, fraud detection rules, and compliance checks are business rules that change frequently and involve multiple conditions. Encoding them as if chains in service methods makes rules impossible to find, test independently, or reuse. The Specification pattern from Eric Evans' DDD textbook externalises these rules as first-class objects, enabling business analysts to read specification class names like sentences (GoldCustomer().and_(BulkOrder())) and developers to unit test each rule in isolation.


Example 75: Chain of Responsibility

The chain of responsibility pattern passes a request through a linked sequence of handlers, where each handler either processes the request or forwards it to the next handler in the chain. In architecture this maps to middleware pipelines, request validation chains, and plugin systems.

graph LR
    Req["Request"] --> H1["AuthHandler"]
    H1 -->|"authenticated"| H2["RateLimitHandler"]
    H2 -->|"within limit"| H3["BusinessHandler"]
    H1 -->|"rejected"| Resp1["401 Unauthorized"]
    H2 -->|"exceeded"| Resp2["429 Too Many Requests"]
    H3 --> Resp3["200 OK"]
 
    style Req fill:#CA9161,stroke:#000,color:#fff
    style H1 fill:#0173B2,stroke:#000,color:#fff
    style H2 fill:#029E73,stroke:#000,color:#fff
    style H3 fill:#DE8F05,stroke:#000,color:#fff
    style Resp1 fill:#CC78BC,stroke:#000,color:#fff
    style Resp2 fill:#CC78BC,stroke:#000,color:#fff
    style Resp3 fill:#029E73,stroke:#000,color:#fff
import java.util.*;
import java.util.function.*;
 
// => Request/response types for the middleware chain
record HttpRequest(String path, String apiKey, String callerId) {}
// => apiKey may be null — auth middleware checks nullness
 
record HttpResponse(int status, String body) {}
// => status + body model HTTP semantics; each middleware either sets status or forwards
 
// => Middleware type: receives request + next handler, returns response
// => Each middleware decides whether to short-circuit or call next
@FunctionalInterface
interface Middleware {
    HttpResponse apply(HttpRequest req, UnaryOperator<HttpRequest> next,
                       Function<HttpRequest, HttpResponse> finalHandler);
}
 
// => Builder composes middlewares into a single callable handler
class MiddlewareChain {
    private final List<BiFunction<HttpRequest, Function<HttpRequest, HttpResponse>,
                                 HttpResponse>> layers = new ArrayList<>();
    // => Each layer: (request, nextFn) -> response; nextFn drives remaining chain
 
    MiddlewareChain use(BiFunction<HttpRequest,
                                   Function<HttpRequest, HttpResponse>,
                                   HttpResponse> mw) {
        layers.add(mw);
        return this; // => Fluent builder — chain .use() calls together
    }
 
    Function<HttpRequest, HttpResponse> build(Function<HttpRequest, HttpResponse> finalHandler) {
        Function<HttpRequest, HttpResponse> handler = finalHandler;
        // => Start from innermost handler and wrap outward
        for (int i = layers.size() - 1; i >= 0; i--) {
            final Function<HttpRequest, HttpResponse> next = handler;
            final var mw = layers.get(i);
            handler = req -> mw.apply(req, next);
            // => Wrap current handler; each layer gets a reference to the layer below it
        }
        return handler; // => Outermost handler drives the whole chain
    }
}
 
// => Auth middleware: rejects requests without valid API key
Set<String> validKeys = Set.of("key-abc", "key-xyz");
// => Simplified key store; prod uses DB lookup or JWT validation
 
BiFunction<HttpRequest, Function<HttpRequest, HttpResponse>, HttpResponse> authMiddleware =
    (req, next) -> {
        if (!validKeys.contains(req.apiKey()))
            return new HttpResponse(401, "Unauthorized");
            // => Chain terminates; next never called for invalid keys
        return next.apply(req); // => Valid key: forward to next middleware
    };
 
// => Rate limit middleware: max 2 calls per callerId (demo counter, not thread-safe)
Map<String, Integer> callCounts = new HashMap<>();
BiFunction<HttpRequest, Function<HttpRequest, HttpResponse>, HttpResponse> rateLimitMiddleware =
    (req, next) -> {
        int count = callCounts.merge(req.callerId(), 1, Integer::sum);
        // => Atomically increment call count for this caller
        if (count > 2) return new HttpResponse(429, "Too Many Requests");
        // => Chain terminates; caller exceeded limit, downstream protected
        return next.apply(req); // => Within limit: continue to business handler
    };
 
Function<HttpRequest, HttpResponse> orderHandler =
    req -> new HttpResponse(200, "Orders for " + req.callerId());
// => Final business handler — only reached if auth + rate limit both pass
 
var chain = new MiddlewareChain()
    .use(authMiddleware)
    .use(rateLimitMiddleware)
    .build(orderHandler);
 
System.out.println(chain.apply(new HttpRequest("/orders", null, "caller-1")));
// => Output: HttpResponse[status=401, body=Unauthorized]
System.out.println(chain.apply(new HttpRequest("/orders", "key-abc", "caller-1")));
// => Output: HttpResponse[status=200, body=Orders for caller-1]
System.out.println(chain.apply(new HttpRequest("/orders", "key-abc", "caller-1")));
// => Output: HttpResponse[status=200, body=Orders for caller-1]
System.out.println(chain.apply(new HttpRequest("/orders", "key-abc", "caller-1")));
// => Output: HttpResponse[status=429, body=Too Many Requests]

Key Takeaway: Build middleware chains with clear early-termination semantics so each handler has one responsibility and can be inserted, removed, or reordered independently.

Why It Matters: Web frameworks (Express.js, FastAPI, Django, ASP.NET Core) are built on chain of responsibility middleware stacks because adding cross-cutting concerns (authentication, CORS, compression, caching) as separate middlewares is far safer than embedding them in business handlers. Adding a new security control is a one-line middleware registration, not a cross-cutting change to every endpoint. AWS API Gateway, Kong, and Nginx implement the same pattern at the infrastructure level for the same reason.


Example 76: Visitor Pattern in Architecture

The visitor pattern separates algorithms from the objects they operate on by defining a visitor class per algorithm. Each domain object accepts a visitor and calls the appropriate visit method, enabling new operations to be added without modifying domain classes — valuable when adding reporting, serialisation, or transformation rules to a stable object hierarchy.

import java.util.*;
 
// => Stable domain hierarchy — these classes do NOT change when new operations are added
// => Visitor enables open/closed principle: open for extension, closed for modification
interface ComponentVisitor {
    void visitService(Service svc);
    // => Called by Service.accept(); visitor implements the algorithm for services
    void visitDatabase(Database db);
    // => Called by Database.accept(); visitor implements the algorithm for databases
    void visitArchitecture(Architecture arch);
    // => Called by Architecture.accept(); visitor gets the top-level context first
}
 
interface Component {
    void accept(ComponentVisitor visitor);
    // => Double dispatch: component calls the right visitXxx method on visitor
}
 
record Service(String name, int replicas, int cpuMillicores) implements Component {
    // => cpuMillicores: e.g. 500 = 0.5 CPU cores
    public void accept(ComponentVisitor v) { v.visitService(this); }
    // => Dispatches to visitService — visitor handles service-specific logic
}
 
record Database(String name, int storageGb, boolean multiAz) implements Component {
    // => multiAz: true = multi-region deployment for high availability
    public void accept(ComponentVisitor v) { v.visitDatabase(this); }
    // => Dispatches to visitDatabase — visitor handles database-specific logic
}
 
record Architecture(List<Component> components) implements Component {
    public void accept(ComponentVisitor v) {
        v.visitArchitecture(this); // => Visitor gets top-level context first
        components.forEach(c -> c.accept(v));
        // => Each component dispatches to its own visit method — recursive traversal
    }
}
 
// => Concrete visitor 1: Cost estimation — new operation, zero domain changes
class CostEstimator implements ComponentVisitor {
    double totalMonthlyUsd = 0.0; // => Accumulates cost across all components
 
    public void visitArchitecture(Architecture arch) {
        System.out.println("Estimating cost for " + arch.components().size() + " components");
    }
 
    public void visitService(Service svc) {
        double cost = svc.replicas() * (svc.cpuMillicores() / 1000.0) * 30 * 0.05;
        // => $0.05 per vCPU per hour * 30 days; simplified cloud pricing model
        totalMonthlyUsd += cost;
        System.out.printf("  Service %s: $%.2f/month%n", svc.name(), cost);
    }
 
    public void visitDatabase(Database db) {
        double cost = db.storageGb() * 0.10 * (db.multiAz() ? 2 : 1);
        // => $0.10 per GB/month, doubled for multi-AZ redundancy
        totalMonthlyUsd += cost;
        System.out.printf("  Database %s: $%.2f/month%n", db.name(), cost);
    }
}
 
// => Concrete visitor 2: Compliance check — another new operation, still no domain changes
class ComplianceChecker implements ComponentVisitor {
    List<String> violations = new ArrayList<>(); // => Accumulates all violations
 
    public void visitArchitecture(Architecture arch) {} // => No arch-level rules here
 
    public void visitService(Service svc) {
        if (svc.replicas() < 2)
            violations.add("Service '" + svc.name() + "' has only " + svc.replicas() + " replica (min 2 for HA)");
            // => Single replica violates high-availability requirement
    }
 
    public void visitDatabase(Database db) {
        if (!db.multiAz())
            violations.add("Database '" + db.name() + "' is not multi-AZ (compliance requirement)");
            // => Single-AZ database violates disaster-recovery policy
    }
}
 
var arch = new Architecture(List.of(
    new Service("orders-api", 3, 500),
    new Service("worker", 1, 1000),       // => Only 1 replica — will fail compliance
    new Database("orders-db", 100, true),
    new Database("cache-db", 20, false)   // => Not multi-AZ — will fail compliance
));
 
var cost = new CostEstimator();
arch.accept(cost);
System.out.printf("Total: $%.2f/month%n", cost.totalMonthlyUsd);
// => Output: Estimating cost for 4 components
// => Output:   Service orders-api: $2.25/month
// => Output:   Service worker: $1.50/month
// => Output:   Database orders-db: $20.00/month
// => Output:   Database cache-db: $2.00/month
// => Output: Total: $25.75/month
 
var compliance = new ComplianceChecker();
arch.accept(compliance);
System.out.println("Violations: " + compliance.violations);
// => Output: Violations: [Service 'worker' has only 1 replica..., Database 'cache-db' is not multi-AZ...]

Key Takeaway: Use the visitor pattern when you need to add operations to a stable class hierarchy without modifying the classes themselves — especially valuable for architecture tools that analyse, transform, or validate object graphs.

Why It Matters: Architecture tooling (cost estimators, compliance checkers, diagram generators, security scanners) must traverse the same infrastructure object graph with different algorithms. Without visitor, each tool either subclasses domain objects or adds methods directly, creating maintenance coupling. The visitor pattern — used in AWS CDK, Terraform's internal AST traversal, and compiler front ends — cleanly separates the object hierarchy (what the architecture IS) from operations (what tools DO to the architecture), enabling teams to add new analysis passes without touching core infrastructure models.


Advanced Resilience and Scalability

Example 77: Database per Service Pattern

The database-per-service pattern assigns each microservice an exclusive database it fully controls, preventing schema coupling and enabling independent scaling and technology selection. Cross-service data access uses APIs — never direct database joins — ensuring services remain independently deployable.

import java.util.*;
 
// => Orders Service — owns its own database; no other service queries orders_db directly
record OrderRecord(String orderId, String customerId, double total) {
    // => customerId stored as opaque ID, not a FK join key; no customer schema knowledge
    // => Orders DB: only order data; customer name/email are never stored here
}
 
class OrdersDatabase {
    private final Map<String, OrderRecord> rows = new HashMap<>();
    // => Orders service's exclusive database; only this service reads and writes it
 
    void insert(OrderRecord record) {
        rows.put(record.orderId(), record);
        // => Persists order; only Orders service writes here — no shared schema
    }
 
    List<OrderRecord> findByCustomer(String customerId) {
        return rows.values().stream()
            .filter(r -> customerId.equals(r.customerId())).toList();
        // => Orders service's own read model — no JOIN to customer table needed
    }
}
 
// => Customers Service — owns customers_db; never touches orders_db directly
record CustomerRecord(String customerId, String name, String email) {}
// => Private to Customers service; Orders service has no visibility into this schema
 
class CustomersDatabase {
    private final Map<String, CustomerRecord> rows = new HashMap<>();
    // => Customers service's exclusive database — independently scalable and evolvable
 
    void insert(CustomerRecord record) { rows.put(record.customerId(), record); }
 
    Optional<CustomerRecord> find(String customerId) {
        return Optional.ofNullable(rows.get(customerId));
        // => Returns Optional.empty() if customer not found; caller handles absence
    }
}
 
// => API Aggregation Layer — composes data from both services via API calls, not DB joins
record OrderWithCustomerDTO(String orderId, double total, String customerName, String email) {}
// => DTO owned by the aggregation layer; neither service owns this shape
 
List<OrderWithCustomerDTO> getOrdersWithCustomer(
    String customerId, OrdersDatabase ordersDb, CustomersDatabase customersDb) {
    var orders = ordersDb.findByCustomer(customerId);   // => Call Orders service (simulated)
    var customer = customersDb.find(customerId);         // => Call Customers service (simulated)
    // => Aggregation done here (BFF or API gateway), NOT via cross-service DB join
    return orders.stream()
        .map(o -> new OrderWithCustomerDTO(
            o.orderId(), o.total(),
            customer.map(CustomerRecord::name).orElse("Unknown"),
            // => Denormalise customer name at aggregation layer, not in Orders DB
            customer.map(CustomerRecord::email).orElse("N/A")))
        .toList();
}
 
var ordersDb = new OrdersDatabase();
var customersDb = new CustomersDatabase();
customersDb.insert(new CustomerRecord("CUST-1", "Alice", "alice@example.com"));
ordersDb.insert(new OrderRecord("ORD-1", "CUST-1", 99.99));
ordersDb.insert(new OrderRecord("ORD-2", "CUST-1", 49.50));
 
getOrdersWithCustomer("CUST-1", ordersDb, customersDb).forEach(r ->
    System.out.println("Order " + r.orderId() + ": $" + r.total()
        + " — Customer: " + r.customerName() + " (" + r.email() + ")"));
// => Output: Order ORD-1: $99.99 — Customer: Alice (alice@example.com)
// => Output: Order ORD-2: $49.5 — Customer: Alice (alice@example.com)

Key Takeaway: Each service's database is a private implementation detail; all cross-service data access must go through APIs so services remain independently deployable and scalable.

Why It Matters: Shared databases are the most common reason microservices fail to deliver on their independence promise: a schema change in one service breaks another service's queries at runtime. Database-per-service enables independent scaling (Orders at 100 replicas, Billing at 5) and technology selection (relational for transactions, document store for recommendations) — neither is possible with a shared schema, because any schema change requires coordinating all teams that query the shared tables.


Example 78: Feature Toggle Architecture

Feature toggles (feature flags) allow code for new features to be deployed to production but remain inactive for most users, enabling trunk-based development, A/B testing, canary releases, and kill-switch controls without re-deploying. The toggle system decouples deployment from release.

graph LR
    Code["Deployed Code<br/>(all features)"]
    FS["Feature Store<br/>(toggles config)"]
    User1["User Segment A<br/>(flag=ON)"]
    User2["User Segment B<br/>(flag=OFF)"]
    F1["New Feature Path"]
    F2["Old Feature Path"]
 
    Code --> FS
    FS -->|"segment A"| User1
    FS -->|"segment B"| User2
    User1 --> F1
    User2 --> F2
 
    style Code fill:#0173B2,stroke:#000,color:#fff
    style FS fill:#DE8F05,stroke:#000,color:#fff
    style User1 fill:#029E73,stroke:#000,color:#fff
    style User2 fill:#CA9161,stroke:#000,color:#fff
    style F1 fill:#029E73,stroke:#000,color:#fff
    style F2 fill:#CA9161,stroke:#000,color:#fff
import java.util.*;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
 
// => Toggle: configuration for one feature flag with global switch + rollout % + allowlist
record Toggle(String name, boolean enabled, int rolloutPercentage, Set<String> allowedUserIds) {
    // => enabled: global kill-switch; when false, no user ever sees the feature
    // => rolloutPercentage: 0-100; controls the fraction of users who see the feature
    // => allowedUserIds: explicit allowlist; these users bypass rollout percentage
}
 
class FeatureStore {
    private final Map<String, Toggle> toggles = new HashMap<>();
    // => Toggle registry; populated at startup from config file or remote service
 
    void register(Toggle toggle) {
        toggles.put(toggle.name(), toggle);
        // => Register at startup; prod typically hot-reloads from remote config store
    }
 
    boolean isEnabled(String name, String userId) {
        var toggle = toggles.get(name);
        if (toggle == null || !toggle.enabled()) return false;
        // => Unknown toggle or globally disabled: deny — fail-closed safety model
 
        if (toggle.allowedUserIds().contains(userId)) return true;
        // => Explicit allowlist wins; beta testers always see the feature
 
        // => Deterministic rollout: same user always gets the same decision
        int bucket = deterministicBucket(name + ":" + userId);
        // => Bucket 0-99; consistent across calls — prevents flickering UX
        return bucket < toggle.rolloutPercentage();
        // => If bucket < percentage, user is in rollout cohort — consistent per user
    }
 
    private int deterministicBucket(String key) {
        try {
            var md5 = MessageDigest.getInstance("MD5");
            byte[] hash = md5.digest(key.getBytes(StandardCharsets.UTF_8));
            // => MD5 produces deterministic 16-byte hash; same input always same output
            return Math.abs(Arrays.hashCode(hash)) % 100;
            // => Map hash to 0-99 bucket — deterministic, not random
        } catch (Exception e) { return 0; }
    }
 
    void disable(String name) {
        var t = toggles.get(name);
        if (t != null)
            toggles.put(name, new Toggle(t.name(), false, t.rolloutPercentage(), t.allowedUserIds()));
        // => Kill switch: replace with disabled toggle; no redeploy needed
    }
}
 
var store = new FeatureStore();
store.register(new Toggle("new_checkout", true, 20, Set.of("beta-tester-1")));
// => new_checkout: 20% gradual rollout + explicit beta tester allowlist
 
System.out.println(store.isEnabled("new_checkout", "beta-tester-1"));
// => Output: true (beta tester in allowlist — always enabled)
 
long enabledCount = java.util.stream.LongStream.range(0, 10)
    .filter(i -> store.isEnabled("new_checkout", "user-" + i)).count();
System.out.println("Enabled for " + enabledCount + "/10 sample users (target ~20%)");
// => Output: Enabled for ~2/10 sample users (varies by hash distribution)
 
store.disable("new_checkout");
System.out.println(store.isEnabled("new_checkout", "beta-tester-1"));
// => Output: false (kill switch applied — no redeploy needed)

Key Takeaway: Use deterministic hash-based bucketing so the same user always gets the same feature decision, preventing inconsistent UX where a user sees the new feature on one page reload but not another.

Why It Matters: Feature toggles enable trunk-based development at large engineering scales by allowing engineers to commit dark code (toggled off) continuously rather than maintaining long-lived feature branches that create merge conflicts. Toggle infrastructure reduces deployment risk to near zero: a bad feature can be disabled with a config change in seconds, without the full pipeline cycle of a revert-and-redeploy. This decouples deployment frequency from release frequency, which is the key property enabling continuous delivery.


Example 79: Service Mesh Architecture

A service mesh adds a transparent infrastructure layer to handle service-to-service communication concerns — mutual TLS, traffic shaping, retries, circuit breaking, and telemetry — without changing application code. Each service gets a sidecar proxy (typically Envoy) that intercepts all network traffic.

graph TD
    subgraph ServiceA["Service A Pod"]
        A["App A"]
        PA["Envoy Sidecar A"]
    end
    subgraph ServiceB["Service B Pod"]
        B["App B"]
        PB["Envoy Sidecar B"]
    end
    CP["Control Plane<br/>(Istio / Linkerd)"]
 
    PA -- "mTLS encrypted<br/>traffic" --> PB
    CP -- "policy config" --> PA
    CP -- "policy config" --> PB
    A --> PA
    B --> PB
 
    style A fill:#0173B2,stroke:#000,color:#fff
    style PA fill:#029E73,stroke:#000,color:#fff
    style B fill:#0173B2,stroke:#000,color:#fff
    style PB fill:#029E73,stroke:#000,color:#fff
    style CP fill:#DE8F05,stroke:#000,color:#fff
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
 
// => MeshProxy simulates what Envoy does as a sidecar — transparent to the app
class MeshProxy {
    private final String service;
    // => mTLS and retry limit injected by control plane; app code knows nothing
    private final boolean tlsEnabled = true;
    private final int retryLimit = 3;
    // => Telemetry collected per call; exported to Prometheus/Jaeger in production
    private final List<Map<String, Object>> telemetry = new ArrayList<>();
 
    MeshProxy(String service) { this.service = service; }
 
    // => Call a downstream service through the mesh — retries and mTLS are transparent
    String call(String target, Callable<String> func) throws Exception {
        if (!tlsEnabled) throw new SecurityException("mTLS required by mesh policy");
        // => Control plane enforces mutual TLS; app never manages certificates
        Exception lastErr = null;
        for (int attempt = 1; attempt <= retryLimit; attempt++) {
            try {
                String result = func.call(); // => Actual inter-service call via target's sidecar
                record(target, attempt, "success");
                // => Telemetry emitted by proxy; zero instrumentation in application code
                return result;
            } catch (Exception e) {
                lastErr = e;
                record(target, attempt, "error"); // => Failure recorded; retry follows
            }
        }
        throw new RuntimeException("Mesh exhausted retries to " + target, lastErr);
        // => After retryLimit failures the proxy propagates error to caller
    }
 
    private void record(String target, int attempt, String outcome) {
        telemetry.add(Map.of("from", service, "to", target, "attempt", attempt, "outcome", outcome));
        // => In production: metrics go to Prometheus; traces go to Jaeger/Zipkin
    }
 
    List<Map<String, Object>> getTelemetry() { return telemetry; }
}
 
// => Each service gets its own sidecar proxy; apps communicate only with localhost sidecar
MeshProxy proxyA = new MeshProxy("orders-svc");
// => Simulate inventory service: fails on first call, succeeds on second
int[] invCalls = {0};
Callable<String> inventoryCheck = () -> {
    invCalls[0]++;
    if (invCalls[0] == 1) throw new RuntimeException("transient network error");
    // => First call fails; proxy retries transparently — orders-svc has no retry logic
    return "in_stock"; // => Second call succeeds
};
// => Orders service calls Inventory through the mesh — no retry code anywhere in orders-svc
String result = proxyA.call("inventory-svc", inventoryCheck);
System.out.println("Inventory response: " + result); // => Output: Inventory response: in_stock
System.out.println("Telemetry: " + proxyA.getTelemetry());
// => Output: [{from=orders-svc, to=inventory-svc, attempt=1, outcome=error},
//             {from=orders-svc, to=inventory-svc, attempt=2, outcome=success}]

Key Takeaway: Service mesh moves cross-cutting network concerns (retries, mTLS, tracing) to the infrastructure proxy layer so application developers write only business logic.

Why It Matters: Before service meshes, every team independently implemented retry logic, circuit breaking, and TLS in each language and service — many teams meant many different retry implementations with different bugs and different failure modes. A service mesh addresses this by pushing consistent policies to all sidecars simultaneously through a single control plane. Cross-cutting concerns like mTLS adoption that would require touching every service's application code become infrastructure configuration changes that propagate automatically.


Example 80: Interpreter Pattern for Configuration DSL

Paradigm Note: Norvig (1996) classifies Interpreter under "Macros" — absorbed by language features in Lisps. The FP-idiomatic form is free monads or tagless final encoding (Kiselyov; Haskell community). OOP Interpreter requires a class hierarchy per AST node. See the FP framing.

The interpreter pattern defines a grammar for a language and an interpreter that evaluates sentences in that language. In architecture it enables policy engines, query filters, rule evaluators, and configuration DSLs where business logic is expressed in a structured mini-language rather than hardcoded conditionals.

import java.util.Map;
 
// => Abstract expression — every node in the rule tree implements this
interface Expression {
    boolean interpret(Map<String, Object> ctx);
    // => ctx: variable map e.g. {"user.tier":"gold","order.total":150.0}
}
 
// ---- Terminal (leaf) expressions ----
 
// => GreaterThan: numeric comparison against a context variable
record GreaterThan(String variable, double threshold) implements Expression {
    public boolean interpret(Map<String, Object> ctx) {
        double val = ((Number) ctx.getOrDefault(variable, 0)).doubleValue();
        return val > threshold; // => True when context variable exceeds threshold
        // => Enables "order.total > 100" without hardcoded Java conditionals
    }
}
 
// => Equals: equality check — supports String and numeric values
record Equals(String variable, Object value) implements Expression {
    public boolean interpret(Map<String, Object> ctx) {
        return value.equals(ctx.get(variable)); // => Returns true on exact match
    }
}
 
// ---- Non-terminal (composite) expressions ----
 
// => AndExpression: both sub-rules must hold
record AndExpression(Expression left, Expression right) implements Expression {
    public boolean interpret(Map<String, Object> ctx) {
        return left.interpret(ctx) && right.interpret(ctx);
        // => Short-circuits: right not evaluated if left is false
    }
}
 
// => OrExpression: at least one sub-rule must hold
record OrExpression(Expression left, Expression right) implements Expression {
    public boolean interpret(Map<String, Object> ctx) {
        return left.interpret(ctx) || right.interpret(ctx);
        // => Short-circuits: right not evaluated if left is true
    }
}
 
// => Rule: eligible IF (tier==gold) OR (total>100 AND tier==silver)
// => Expression tree built from composable objects — no hardcoded if-chains
Expression discountRule = new OrExpression(
    new Equals("user.tier", "gold"),                 // => Gold users always qualify
    new AndExpression(
        new GreaterThan("order.total", 100.0),       // => Large order threshold
        new Equals("user.tier", "silver")            // => Silver tier required
    )
);
 
var ctxGold         = Map.of("user.tier", "gold",   "order.total", 30.0);
var ctxSilverLarge  = Map.of("user.tier", "silver", "order.total", 150.0);
var ctxBronze       = Map.of("user.tier", "bronze", "order.total", 200.0);
 
System.out.println(discountRule.interpret(ctxGold));        // => Output: true  (gold, any total)
System.out.println(discountRule.interpret(ctxSilverLarge)); // => Output: true  (silver + >100)
System.out.println(discountRule.interpret(ctxBronze));      // => Output: false (bronze excluded)

Key Takeaway: Build expression trees using composable terminal and non-terminal expression objects so business rules can be loaded from config, stored in databases, and evaluated without redeployment.

Why It Matters: Hardcoded business rules require a code change and deployment for every adjustment — unacceptable for pricing, fraud detection, or feature entitlement that business teams change weekly. Retail banks use interpreter-based policy engines to update loan eligibility criteria same-day without a development cycle. AWS IAM policy evaluation, Kubernetes admission webhooks, and Open Policy Agent all implement the interpreter pattern to evaluate externally defined rules against infrastructure context, proving this pattern's applicability at production scale.


Expert-Level Synthesis

Example 81: CQRS (Command Query Responsibility Segregation)

Paradigm Note: Same as Example 49 — read side is naturally a pure projection over immutable data. See the FP framing.

CQRS separates the model used for state-changing commands from the model used for read queries, enabling each side to be optimised independently. The write model enforces business rules and domain invariants; the read model (or multiple read models) is denormalised for fast query performance.

graph LR
    C["Client"]
    CMD["Command Side<br/>(write model)"]
    QRY["Query Side<br/>(read model / projections)"]
    ES["Event Store / DB<br/>(source of truth)"]
    RM["Read Model DB<br/>(denormalised)"]
 
    C -->|"commands"| CMD
    C -->|"queries"| QRY
    CMD --> ES
    ES -->|"events / replication"| RM
    QRY --> RM
 
    style C fill:#CA9161,stroke:#000,color:#fff
    style CMD fill:#0173B2,stroke:#000,color:#fff
    style QRY fill:#029E73,stroke:#000,color:#fff
    style ES fill:#DE8F05,stroke:#000,color:#fff
    style RM fill:#CC78BC,stroke:#000,color:#fff
import java.util.*;
 
// ---- Write side: command + domain model with invariants ----
 
// => Command is a plain data class — carries intent, not behaviour
record CreateProductCommand(String productId, String name, double price, int stock) {}
 
// => Product: write-side entity; holds events produced during command handling
class Product {
    final String productId;
    final String name;
    final double price;
    final int stock;
    // => Domain events accumulated here; consumed by projection after handler returns
    final List<Map<String, Object>> events = new ArrayList<>();
 
    Product(String productId, String name, double price, int stock) {
        this.productId = productId; this.name = name; this.price = price; this.stock = stock;
    }
}
 
class ProductCommandHandler {
    // => Write store: normalised, enforces invariants; never used for queries
    private final Map<String, Product> writeStore = new HashMap<>();
 
    Product handleCreate(CreateProductCommand cmd) {
        if (cmd.price() <= 0) throw new IllegalArgumentException("Price must be positive");
        // => Invariant enforced on write side before any persistence
        if (cmd.stock() < 0) throw new IllegalArgumentException("Stock cannot be negative");
        var product = new Product(cmd.productId(), cmd.name(), cmd.price(), cmd.stock());
        product.events.add(Map.of("type", "ProductCreated",
            "payload", Map.of("id", cmd.productId(), "name", cmd.name(), "price", cmd.price())));
        // => Event appended; read-side projection will consume it after this call
        writeStore.put(cmd.productId(), product);
        return product;
    }
}
 
// ---- Read side: denormalised projection optimised for queries ----
 
// => ProductSummary: pre-formatted for UI; no joins required at query time
record ProductSummary(String productId, String displayName, boolean inStock) {}
 
class ProductProjection {
    // => Read store: denormalised, fast to query; rebuilds from events if stale
    private final Map<String, ProductSummary> readStore = new HashMap<>();
 
    void onProductCreated(Map<String, Object> event) {
        @SuppressWarnings("unchecked")
        var p = (Map<String, Object>) event.get("payload");
        var summary = new ProductSummary(
            (String) p.get("id"),
            p.get("name") + " ($" + String.format("%.2f", p.get("price")) + ")",
            // => Display name pre-formatted — read model owns this transformation
            true // => Derived from stock > 0; avoids query-time computation
        );
        readStore.put(summary.productId(), summary); // => Projection materialised
    }
 
    List<ProductSummary> queryAll() {
        return new ArrayList<>(readStore.values()); // => Fast read: no joins, no business logic
    }
}
 
// => Wire command and query sides via event propagation
var cmdHandler = new ProductCommandHandler();
var projection = new ProductProjection();
 
var product = cmdHandler.handleCreate(new CreateProductCommand("P1", "Widget", 9.99, 100));
for (var event : product.events)
    projection.onProductCreated(event); // => Read side consumes write-side events
 
for (var s : projection.queryAll())
    System.out.printf("%s: %s, inStock=%b%n", s.productId(), s.displayName(), s.inStock());
// => Output: P1: Widget ($9.99), inStock=true

Key Takeaway: Separate command handlers (enforce invariants, emit events) from query handlers (consume projections, optimised for reads) so each side can scale and evolve independently.

Why It Matters: Relational databases optimised for write consistency (with locks, transactions, and normalisation) perform poorly for complex read queries that aggregate data across many tables. CQRS solves this by maintaining separate read models — projected, denormalised, perhaps stored in Elasticsearch or Redis — that answer queries in microseconds without table scans. Systems requiring sub-millisecond query latency alongside strong write consistency benefit most from CQRS because the read model can be shaped precisely for each query, without the normalisation constraints that the write model's correctness guarantees require.


Example 82: Outbox Pattern for Reliable Event Publishing

The outbox pattern solves the dual-write problem: how to atomically persist a database record and publish an event to a message broker. By writing the event to an outbox table in the same database transaction as the business record, then polling and publishing the outbox asynchronously, exactly- once semantics are achievable within a single service.

graph LR
    App["Application"] -->|"single transaction"| DB["Database<br/>(record + outbox)"]
    Relay["Outbox Relay<br/>(poller)"] -->|"reads unpublished"| DB
    Relay -->|"publishes"| Broker["Message Broker<br/>(Kafka / RabbitMQ)"]
    Broker --> Consumers["Downstream<br/>Services"]
 
    style App fill:#0173B2,stroke:#000,color:#fff
    style DB fill:#DE8F05,stroke:#000,color:#fff
    style Relay fill:#029E73,stroke:#000,color:#fff
    style Broker fill:#CC78BC,stroke:#000,color:#fff
    style Consumers fill:#CA9161,stroke:#000,color:#fff
import java.util.*;
import java.util.function.BiConsumer;
 
// => OutboxEntry: a row in the outbox table — same DB as business records
class OutboxEntry {
    final String entryId;
    final String eventType;
    final Map<String, Object> payload;
    boolean published = false; // => False until relay confirms broker receipt
    OutboxEntry(String entryId, String eventType, Map<String, Object> payload) {
        this.entryId = entryId; this.eventType = eventType; this.payload = payload;
    }
}
 
class Database {
    private final Map<String, Map<String, Object>> orders = new HashMap<>();
    private final List<OutboxEntry> outbox = new ArrayList<>();
    // => Outbox table lives in the same database as business records
 
    void saveOrderWithEvent(String orderId, double total, String eventType, Map<String, Object> payload) {
        // => ATOMIC: both writes succeed or both fail — no partial state possible
        orders.put(orderId, Map.of("order_id", orderId, "total", total));
        // => Business record persisted first
        var entry = new OutboxEntry(orderId + "-" + UUID.randomUUID().toString().substring(0, 4),
            eventType, payload);
        outbox.add(entry);
        // => Outbox entry written in same transaction; broker NOT called here
        System.out.println("DB: saved order " + orderId + " + outbox entry " + entry.entryId + " (atomic)");
    }
 
    List<OutboxEntry> getUnpublished() {
        return outbox.stream().filter(e -> !e.published).toList();
        // => Relay queries these; safe to call repeatedly after partial failures
    }
 
    void markPublished(String entryId) {
        outbox.stream().filter(e -> e.entryId.equals(entryId)).findFirst()
              .ifPresent(e -> e.published = true);
        // => Idempotent: marking twice has no additional effect
    }
}
 
class OutboxRelay {
    private final Database db;
    OutboxRelay(Database db) { this.db = db; }
 
    void publishPending(BiConsumer<String, Map<String, Object>> brokerPublish) {
        for (var entry : db.getUnpublished()) {
            brokerPublish.accept(entry.eventType, entry.payload);
            // => Publish to broker (Kafka/SQS); may be retried if broker is unavailable
            db.markPublished(entry.entryId);
            // => Mark published only after broker confirms receipt — at-least-once delivery
            System.out.println("Relay: published " + entry.eventType + " (entry " + entry.entryId + ")");
        }
    }
}
 
// => Simulated broker (stdout in demo)
BiConsumer<String, Map<String, Object>> mockBroker =
    (type, payload) -> System.out.println("Broker: received " + type + " — " + payload);
 
var db = new Database();
var relay = new OutboxRelay(db);
 
db.saveOrderWithEvent("ORD-99", 199.99, "OrderPlaced", Map.of("order_id", "ORD-99", "total", 199.99));
// => Output: DB: saved order ORD-99 + outbox entry ORD-99-xxxx (atomic)
 
relay.publishPending(mockBroker);
// => Output: Broker: received OrderPlaced — {order_id=ORD-99, total=199.99}
// => Output: Relay: published OrderPlaced (entry ORD-99-xxxx)
 
System.out.println("Unpublished after relay: " + db.getUnpublished().size());
// => Output: Unpublished after relay: 0

Key Takeaway: Write business data and the outbox event in a single database transaction, then relay the outbox asynchronously to the broker so the event is never lost even if the broker is temporarily unavailable.

Why It Matters: The naive dual-write — save to DB then publish to Kafka — has a gap: if the service crashes between the two writes, the event is lost and downstream services never see the order placed. The outbox pattern is the standard solution, used by Debezium's CDC (Change Data Capture) connector which reads the outbox table directly from the database's binary log and publishes to Kafka — eliminating the relay polling latency entirely. Financial systems, logistics platforms, and any domain requiring event reliability adopt the outbox pattern as foundational infrastructure.


Example 83: Anti-Corruption Layer

The anti-corruption layer (ACL) is a translation boundary that prevents a downstream model's concepts from leaking into an upstream domain. The ACL translates between two bounded contexts' vocabularies, protecting the upstream domain from being corrupted by legacy or third-party models.

// ---- Legacy CRM system model — uses its own vocabulary ----
 
// => CRM calls customers "accounts" with numeric status codes and string money values
record LegacyCrmAccount(
    String acctNum,     // => CRM's identifier — maps to our customer_id
    String fullName,    // => CRM field name differs from our "name"
    String emailAddr,   // => CRM's emailAddr — our model uses "email"
    int statusCode,     // => 1=active, 2=suspended, 3=closed — integers, not booleans
    String creditLimit  // => Stored as "1500.00" string — type mismatch with our float
) {}
 
// ---- Our domain model — clean vocabulary and correct types ----
 
// => Customer: domain object; never touched by CRM field names or integer status codes
record Customer(
    String customerId,  // => Maps from CRM's "acctNum"
    String name,        // => Clean field — not "fullName"
    String email,       // => Clean field — not "emailAddr"
    boolean active,     // => Boolean — not integer status code
    double creditLimit  // => Correct numeric type — not string
) {}
 
// ---- Anti-Corruption Layer ----
 
class CrmAntiCorruptionLayer {
    private static final int STATUS_ACTIVE = 1;
    // => ACL owns CRM status code knowledge; domain never sees integer codes
 
    // => Translate inbound CRM account to domain Customer
    Customer translateAccount(LegacyCrmAccount acct) {
        return new Customer(
            acct.acctNum(),              // => "acctNum" -> "customerId"
            acct.fullName(),             // => "fullName" -> "name"
            acct.emailAddr(),            // => "emailAddr" -> "email"
            acct.statusCode() == STATUS_ACTIVE,
            // => Integer status code -> boolean; CRM vocabulary stops at the ACL
            Double.parseDouble(acct.creditLimit())
            // => String "2500.00" -> double 2500.0; type mismatch resolved here
        );
    }
 
    // => Translate outbound domain Customer back to CRM account for API calls
    LegacyCrmAccount translateToCrm(Customer c) {
        return new LegacyCrmAccount(
            c.customerId(),             // => "customerId" -> "acctNum"
            c.name(),                   // => "name" -> "fullName"
            c.email(),                  // => "email" -> "emailAddr"
            c.active() ? STATUS_ACTIVE : 2,
            // => Boolean -> integer code; our domain model never stores this
            String.format("%.2f", c.creditLimit()) // => double -> CRM string format
        );
    }
}
 
// => Domain code works only with Customer; ACL handles all CRM translation
var acl = new CrmAntiCorruptionLayer();
var crmData = new LegacyCrmAccount("ACC-001", "Alice Smith", "alice@crm.com", 1, "2500.00");
 
var customer = acl.translateAccount(crmData);
System.out.printf("Customer: %s, active=%b, credit=%.1f%n",
    customer.name(), customer.active(), customer.creditLimit());
// => Output: Customer: Alice Smith, active=true, credit=2500.0
 
var crmOut = acl.translateToCrm(customer); // => Round-trip for CRM update call
System.out.printf("CRM: acct=%s, status=%d, credit=%s%n",
    crmOut.acctNum(), crmOut.statusCode(), crmOut.creditLimit());
// => Output: CRM: acct=ACC-001, status=1, credit=2500.00

Key Takeaway: The ACL is the boundary where foreign vocabulary and types stop; everything inside the boundary uses clean domain language and correct types without compromise.

Why It Matters: Without an ACL, integrating a legacy CRM causes its integer status codes, misspelled field names, and wrong types to propagate into the domain model — creating unmaintainable translation logic scattered across the codebase. Eric Evans identified the ACL as critical to maintaining domain purity during integration; teams that skip it spend disproportionate effort on "translation debt" — conditional mapping code duplicated wherever the legacy system's data is used. Rebuilding a messy integration with a proper ACL is one of the highest-ROI refactorings in enterprise systems.


Example 84: Ports and Adapters (Hexagonal Architecture)

The hexagonal architecture (Ports and Adapters) places the domain model at the centre, surrounded by ports (interfaces) through which the domain communicates with the outside world. Adapters implement those ports for specific technologies (HTTP, databases, message queues), keeping the domain completely free of infrastructure dependencies.

graph TD
    HTTP["HTTP Adapter<br/>(FastAPI controller)"]
    CLI["CLI Adapter<br/>(command-line runner)"]
    PG["Postgres Adapter<br/>(SQLAlchemy repository)"]
    MQ["Message Queue Adapter<br/>(Kafka consumer)"]
    Domain["Domain Core<br/>(business logic + ports)"]
 
    HTTP -- "port: OrderService" --> Domain
    CLI -- "port: OrderService" --> Domain
    Domain -- "port: OrderRepository" --> PG
    Domain -- "port: EventPublisher" --> MQ
 
    style HTTP fill:#0173B2,stroke:#000,color:#fff
    style CLI fill:#0173B2,stroke:#000,color:#fff
    style PG fill:#CA9161,stroke:#000,color:#fff
    style MQ fill:#CA9161,stroke:#000,color:#fff
    style Domain fill:#029E73,stroke:#000,color:#fff
import java.util.*;
 
// ---- Ports — domain-owned interfaces; depend on nothing outside the domain ----
 
// => Driven port: domain drives this to persist and retrieve orders
interface OrderRepository {
    void save(Order order);
    Optional<Order> find(String orderId);
}
 
// => Driven port: domain drives this to publish domain events
interface EventPublisher {
    void publish(String eventType, Map<String, Object> payload);
}
 
// ---- Domain core — depends only on ports ----
 
// => Order: plain domain entity; no JPA annotations, no Kafka types
record Order(String orderId, String customerId, double total, String status) {
    Order(String orderId, String customerId, double total) {
        this(orderId, customerId, total, "pending"); // => Default status on creation
    }
}
 
// => OrderService: primary port — driving adapters (HTTP, CLI) call this
class OrderService {
    private final OrderRepository repo;        // => Injected; domain never knows which adapter
    private final EventPublisher publisher;    // => Injected; domain never imports Kafka
 
    OrderService(OrderRepository repo, EventPublisher publisher) {
        this.repo = repo; this.publisher = publisher;
    }
 
    Order placeOrder(String customerId, double total) {
        var order = new Order(UUID.randomUUID().toString().substring(0, 8), customerId, total);
        repo.save(order);               // => Domain drives repository port — not JDBC directly
        publisher.publish("OrderPlaced", Map.of("order_id", order.orderId(), "total", total));
        // => Domain drives event port — not Kafka directly; adapter handles transport
        return order;
    }
}
 
// ---- Adapters — implement ports for specific technologies ----
 
// => In-memory adapter: used in tests; swap for JPA adapter in production
class InMemoryOrderRepository implements OrderRepository {
    private final Map<String, Order> store = new HashMap<>();
    // => No SQL, no connection pool — domain tests run without any infrastructure
 
    public void save(Order order) { store.put(order.orderId(), order); }
    // => Simple map put — identical semantics to a real repository from domain's perspective
 
    public Optional<Order> find(String orderId) { return Optional.ofNullable(store.get(orderId)); }
    // => Returns Optional.empty() if not found — same contract as JPA adapter
}
 
// => Logging adapter: captures events for test assertions; swap for Kafka in production
class LoggingEventPublisher implements EventPublisher {
    final List<Map<String, Object>> published = new ArrayList<>();
    // => Records events so tests can assert without running Kafka
 
    public void publish(String eventType, Map<String, Object> payload) {
        published.add(Map.of("event_type", eventType, "payload", payload));
        // => Swap for KafkaProducer.send() in production adapter — zero domain changes
    }
}
 
// => Compose domain with adapters — done once at application startup (composition root)
var repo = new InMemoryOrderRepository();
var publisher = new LoggingEventPublisher();
var service = new OrderService(repo, publisher); // => Domain receives adapters via constructor
 
var order = service.placeOrder("CUST-1", 75.50);
System.out.println("Order placed: " + order.orderId() + ", total=" + order.total());
// => Output: Order placed: <id>, total=75.5
 
System.out.println("Events published: " + publisher.published);
// => Output: Events published: [{event_type=OrderPlaced, payload={order_id=<id>, total=75.5}}]

Key Takeaway: Domain logic is testable in complete isolation using in-memory adapters; swapping to real infrastructure adapters requires zero domain code changes — only composition root changes.

Why It Matters: Traditional layered architectures let infrastructure details leak into the domain (JPA annotations in entity classes, Kafka types in service methods), making domains impossible to test without spinning up databases. Hexagonal architecture, described in Alistair Cockburn's original 2005 formulation, enables fast unit tests of all business logic using in-memory adapters, reducing test suite time from 10+ minutes (integration tests with real DB) to seconds — critical for a high-frequency CI/CD pipeline. The domain remains portable across infrastructure changes precisely because it has no compile-time dependency on any specific technology adapter.


Example 85: Reactive Architecture with Backpressure

Paradigm Note: FRP (Conal Elliott & Paul Hudak, Functional Reactive Animation, 1997) is FP-native. OOP reactive frameworks (RxJava, Rx.NET) explicitly emulate the FP model — observables, operators, and backpressure are stream algebra. See the FP framing.

Reactive architecture processes streams of data asynchronously using non-blocking I/O, with backpressure mechanisms that signal upstream producers to slow down when downstream consumers cannot keep up, preventing out-of-memory crashes from unbounded queues.

graph LR
    P["Producer<br/>(fast: 1000 items/s)"]
    B["Backpressure Signal<br/>(slow down!)"]
    Buffer["Bounded Buffer<br/>(max capacity)"]
    C["Consumer<br/>(slow: 100 items/s)"]
 
    P -->|"emit"| Buffer
    Buffer -->|"process"| C
    Buffer -->|"full — drop or<br/>signal backpressure"| B
    B -->|"feedback"| P
 
    style P fill:#0173B2,stroke:#000,color:#fff
    style Buffer fill:#DE8F05,stroke:#000,color:#fff
    style C fill:#029E73,stroke:#000,color:#fff
    style B fill:#CC78BC,stroke:#000,color:#fff
// => Java: Project Reactor — industry-standard reactive library (Spring WebFlux uses this)
// => Dependency: io.projectreactor:reactor-core
import reactor.core.publisher.Flux;
import reactor.core.scheduler.Schedulers;
import java.time.Duration;
import java.util.concurrent.atomic.AtomicInteger;
 
// => AtomicInteger tracks backpressure events across threads safely
AtomicInteger dropped = new AtomicInteger(0);
AtomicInteger processed = new AtomicInteger(0);
 
// => Flux.range: creates a fast producer of 50 integers
Flux.range(1, 50)
    // => onBackpressureDrop: when downstream cannot keep up, drop items and record them
    .onBackpressureDrop(item -> {
        dropped.incrementAndGet(); // => Item dropped — backpressure applied to producer
        // => In production: log dropped item id for monitoring/alerting
    })
    // => limitRate(10): request at most 10 items at a time — bounded demand signal
    .limitRate(10)
    // => subscribeOn: move subscription (production) to a background thread
    .subscribeOn(Schedulers.parallel())
    .flatMap(item ->
        // => flatMap with delayElements simulates slow async consumer (5ms per item)
        Flux.just(item).delayElements(Duration.ofMillis(5)),
        // => concurrency=1: only 1 in-flight item — consumer is the bottleneck
        1
    )
    .doOnNext(item -> processed.incrementAndGet()) // => Count successfully processed items
    .blockLast(Duration.ofSeconds(5)); // => Block until stream completes (demo only)
    // => In production: subscribe() instead of blockLast() — never block reactive threads
 
System.out.println("Dropped (backpressure): " + dropped.get());
// => Output: Dropped (backpressure): ~30-40 (fast producer outpaces slow consumer)
System.out.println("Processed: " + processed.get());
// => Output: Processed: ~10-20 (bounded by limitRate and consumer speed)
// => Key: memory was never at risk — bounded demand signal prevented queue growth

Key Takeaway: Use bounded queues with explicit drop-or-block semantics as the backpressure mechanism; never allow unbounded queuing, which defers the OOM crash rather than preventing it.

Why It Matters: Reactive Streams — standardised in the Reactive Streams specification (RxJava, Project Reactor, Akka Streams, Python's asyncio) — emerged because event-driven systems with unbounded queues inevitably crash under load: the queue fills memory until the process is killed. Stream processing systems implement backpressure to handle traffic spikes well above normal load without OOM crashes. Systems that lack backpressure require over-provisioning by the spike ratio — expensive and wasteful compared to a properly backpressured reactive pipeline that signals producers to slow down rather than accumulating an unbounded backlog.


FP-Native Stubs (Examples 86–90)

The following stubs preserve numbering parity with the FP track. Each pattern is fundamentally FP-shaped; the FP track carries the full treatment.

Example 86: Railway-Oriented Programming (FP-Native)

Paradigm Note: Railway-Oriented Programming (Wlaschin) chains effectful steps via Result/Either monad bind — failure is a value on the error track, not an exception. The OOP equivalent is "Example 24: Service Layer with Error Handling" using sentinel results or exceptions, which approximates but does not capture the value-track property. See the FP framing.


Example 87: Free Monads / Tagless Final (FP-Native)

Paradigm Note: Free monads (Swierstra, 2008) and tagless final (Carette/Kiselyov/Shan, 2009) represent programs as data structures separate from interpreters that run them — multiple interpreters consume the same program. The OOP equivalent is the Interpreter pattern (Example 80), which Norvig (1996) classified as absorbed by language features in Lisps. See the FP framing.


Example 88: Reader Monad for Dependency Injection (FP-Native)

Paradigm Note: Reader monad threads a read-only environment through a computation as a single boundary parameter — the FP answer to OOP DI containers (Spring, Autofac, Dagger). The OOP equivalent is constructor injection (Example 8), which carries the dependency on every instance instead of in the type signature. See the FP framing.


Example 89: Kleisli Composition for Effectful Pipelines (FP-Native)

Paradigm Note: Kleisli composition (>=>) extends function composition to effectful functions — a -> m b and b -> m c combine into a -> m c without explicit bind ceremony. The OOP equivalent is Chain of Responsibility (Example 75) using mutable handler-class chains, which Norvig (1996) classified as absorbed by first-class types. See the FP framing.


Example 90: State Monad for Pure Stateful Computation (FP-Native)

Paradigm Note: State monad threads s -> (a, s) through a chain — imperative-feeling stateful code without mutation, referentially transparent. The OOP equivalent is mutable field updates inside encapsulated objects (Examples 18, 43, 53). The state is part of the type signature in FP, hidden inside an object in OOP. See the FP framing.


OOP-Native Extras (Examples 91–93)

The following examples extend the canonical 85 with patterns that exist primarily in OOP because they assume mutable state, identity, and class hierarchies as first-class building blocks.

Example 91: Active Record

Paradigm Note: Active Record (Fowler, PEAA) is a domain object that owns its persistence — user.save() writes to the database. The pattern requires mutable state + identity + behavior on one object. The FP equivalent is the Repository pattern (Examples 21, 22, 52) — data and persistence functions are split.

Active Record combines the in-memory representation of a row with the queries and operations that read or write it. A single class holds data, validation, persistence, and lifecycle callbacks.

// => Active Record fuses persistence and domain object — `user.save()` writes to DB
public class User {
    private Long id;                  // => primary key managed by AR
    private String email;
    private String name;
    private boolean dirty = false;    // => tracking flag for save decisions
 
    private static final Map<Long, User> store = new HashMap<>(); // => simulated DB
    private static long nextId = 1;
 
    public User(String email, String name) {
        this.email = email;
        this.name = name;
        this.dirty = true;            // => unsaved record
    }
 
    // => persistence concern lives ON the entity — defining feature of Active Record
    public void save() {
        if (!dirty) return;
        if (id == null) id = nextId++; // => insert path
        store.put(id, this);
        dirty = false;
        System.out.println("[DB] saved user " + id);
    }
 
    // => static "finder" methods are also part of the AR contract
    public static Optional<User> findById(Long id) {
        return Optional.ofNullable(store.get(id));
    }
 
    public void setEmail(String email) {
        this.email = email;
        this.dirty = true;             // => mutator marks dirty for next save
    }
 
    public Long getId() { return id; }
    public String getEmail() { return email; }
}
 
// usage
User u = new User("alice@example.com", "Alice");
u.save();
// => [DB] saved user 1
u.setEmail("alice@new.com");
u.save();
// => [DB] saved user 1

Key Takeaway: Active Record collapses data + persistence + behavior into one class. The price is loss of separation: validation, persistence, and domain logic are all on the entity, making the class hard to test in isolation.

Why It Matters: Rails, Django, Laravel, and Sequelize ship with Active Record as the default ORM pattern — it is the highest-velocity persistence pattern for small CRUD apps. The trade-off is rigorous: as the domain grows, lifecycle callbacks and entity-bound queries become hard to refactor. Mature codebases migrate to Repository + Domain Model (Fowler) once domain complexity exceeds CRUD.


Example 92: GRASP Responsibility Assignment

Paradigm Note: GRASP (Larman, Applying UML and Patterns) is a set of nine heuristics for deciding which class should hold which responsibility. The principles are OOP-shaped because they assume responsibilities live on objects. In FP, responsibilities live on functions and modules — the Low Coupling and High Cohesion principles still apply (Examples 16, 17), but Information Expert, Creator, and Controller dissolve into "the function that has the data it needs is the function that does the work".

GRASP names nine patterns: Information Expert, Creator, Controller, Low Coupling, High Cohesion, Polymorphism, Pure Fabrication, Indirection, Protected Variations. Below is a sketch applying the first three.

// => GRASP "Information Expert": assign responsibility to the class with the data
public class Order {
    private List<OrderLine> lines = new ArrayList<>();
 
    // => Order owns the lines, so Order computes the total — Information Expert applied
    public BigDecimal total() {
        return lines.stream()
            .map(OrderLine::subtotal)
            .reduce(BigDecimal.ZERO, BigDecimal::add);
    }
 
    public void addLine(OrderLine line) { lines.add(line); }
}
 
public class OrderLine {
    private BigDecimal price;
    private int qty;
 
    public OrderLine(BigDecimal price, int qty) { this.price = price; this.qty = qty; }
 
    // => OrderLine owns price and qty; OrderLine computes its subtotal
    public BigDecimal subtotal() { return price.multiply(BigDecimal.valueOf(qty)); }
}
 
// => GRASP "Creator": OrderFactory creates Order (and OrderLine via Order)
public class OrderFactory {
    public Order create(List<LineDto> dtos) {
        Order order = new Order();              // => creator instantiates aggregate
        for (LineDto d : dtos) order.addLine(new OrderLine(d.price, d.qty));
        return order;
    }
}
 
// => GRASP "Controller": HTTP controller routes the input to the use case
public class OrderController {
    private final OrderFactory factory;
    public OrderController(OrderFactory f) { this.factory = f; }
 
    public BigDecimal handleCreateOrder(List<LineDto> dtos) {
        Order o = factory.create(dtos);          // => delegate creation to factory
        return o.total();                        // => delegate calculation to expert
    }
}

Key Takeaway: GRASP gives nine reusable answers to "which class should own this responsibility?". Information Expert + Creator + Controller cover the most common decisions; the rest fine-tune coupling and variation points.

Why It Matters: Teams that follow GRASP produce classes with high cohesion and stable boundaries — refactors stay local because each responsibility lives where its data lives. Without an assignment heuristic, responsibilities drift onto whichever class is most convenient at the moment, and the codebase devolves into "manager" and "helper" classes with no clear ownership.


Example 93: Singleton with FP Counterexample

Paradigm Note: Singleton (GoF) ensures a class has one instance with global access. In FP the pattern is unnecessary — module-level definitions are already singletons, immutability eliminates the "shared mutable global" motivation. The Haskell anti-patterns corpus explicitly names Singleton as a Haskell anti-pattern. The FP-native answer for configuration is the Reader monad (Example 88).

// => GoF Singleton: private constructor + static getInstance + volatile holder
public class ConfigRegistry {
    private static volatile ConfigRegistry instance;
    private final Map<String, String> values = new ConcurrentHashMap<>();
 
    private ConfigRegistry() {}  // => private blocks external instantiation
 
    // => double-checked locking guards against duplicate construction under concurrency
    public static ConfigRegistry getInstance() {
        if (instance == null) {
            synchronized (ConfigRegistry.class) {
                if (instance == null) instance = new ConfigRegistry();
            }
        }
        return instance;
    }
 
    public String get(String key) { return values.get(key); }
    public void set(String key, String value) { values.put(key, value); }
}
 
// usage
ConfigRegistry.getInstance().set("env", "prod");
System.out.println(ConfigRegistry.getInstance().get("env"));
// => "prod"

FP counterexample (Haskell module-level value):

-- => In Haskell, module-level definitions are already singletons.
-- => No private constructor, no double-checked locking, no anti-pattern.
module ConfigRegistry (get, set) where
 
import Data.IORef
import qualified Data.Map.Strict as M
import System.IO.Unsafe (unsafePerformIO)
 
-- => Single shared IORef — module-private; only `get` and `set` exposed
{-# NOINLINE store #-}
store :: IORef (M.Map String String)
store = unsafePerformIO (newIORef M.empty)
 
get :: String -> IO (Maybe String)
get k = M.lookup k <$> readIORef store
 
set :: String -> String -> IO ()
set k v = modifyIORef store (M.insert k v)
-- => For pure configuration the Reader monad (see Example 88 in FP track) is preferred;
-- => unsafePerformIO + IORef is shown here only to demonstrate that the GoF Singleton
-- => pattern collapses to a module-level value.

Key Takeaway: Singleton is an OOP pattern compensating for the lack of module-level statefulness. Languages with first-class modules (Haskell, Clojure, Elixir, Kotlin object) make the pattern invisible.

Why It Matters: Singletons accumulate hidden dependencies — every caller of Foo.getInstance() is coupled to that class. Modern OOP codebases prefer dependency injection over Singletons for exactly this reason; FP code reaches the same conclusion via Reader monad or module-scoped immutable values. Recognize Singleton as a legacy pattern: appropriate for some logging or configuration but increasingly replaced by DI.

Last updated March 19, 2026

Command Palette

Search for a command to run...