Additional Patterns: Enterprise and Resilience

Learning Objectives

Know common non-GoF patterns: Null Object, Specification, Repository, Unit of Work, Service Layer, Data Mapper, Lazy Initialization, Event Aggregator, Publish–Subscribe.

Apply resilience patterns: Retry, Timeout, Circuit Breaker, Bulkhead, Rate Limiter.

Distinguish GoF, enterprise and distributed-system patterns.

Prerequisites: Modules 26–50.

Intuition and Definition

A do-nothing implementation instead of nullptr checks Business rules as combinable predicate objects enterprise (Fowler/DDD)

Collection-like access to aggregates, hiding persistence Track changes and commit them in one transaction

Application operations coordinating domain objects Move data between objects and database, keeping them independent Create on first use Event Aggregator / Pub–Sub

Decouple publishers and subscribers through a channel Repeat transient failures with back-off Bound waiting time

Stop calling a failing dependency; probe for recovery Isolate resources so one failure doesn't sink everything

Cap request rate (token bucket, leaky bucket) The GoF patterns are about object structure inside one program; enterprise patterns (Fowler, *Patterns of Enterprise Application Architecture*) are about applications with databases; resilience patterns (Nygard, *Release It!*) are about remote calls that can be slow or fail.

Why It Matters

Production systems fail at their boundaries. These patterns encode how to keep a system responsive when a dependency misbehaves, and how to keep domain code independent of databases.

Syntax and Language Notes

Mature libraries implement resilience patterns: Polly (.NET), Resilience4j (Java), tenacity (Python), cockatiel (TypeScript). Prefer them to hand-rolled versions in production.

Complete Example

Circuit breaker, Null Object and Specification in one program (time is simulated with an explicit tick counter so the output is deterministic):

#include <functional>
#include <iostream>
#include <memory>
#include <string>
#include <vector>

// ---- Circuit Breaker ----
class CircuitBreaker {
public:
    enum class State { Closed, Open, HalfOpen };
private:
    State state_ = State::Closed;
    int failures_ = 0, openedAt_ = 0;
    const int threshold_, coolDown_;
public:
    CircuitBreaker(int threshold, int coolDown) : threshold_(threshold), coolDown_(coolDown) {}
    std::string call(int now, const std::function<bool()>& op) {
        if (state_ == State::Open) {
            if (now - openedAt_ < coolDown_) return "rejected fast (open)";
            state_ = State::HalfOpen;                            // allow one trial call
        }
        bool ok = op();
        if (ok) { failures_ = 0; state_ = State::Closed; return "ok"; }
        if (state_ == State::HalfOpen || ++failures_ >= threshold_) { state_ = State::Open; openedAt_ = now; return "failed -> OPEN"; }
        return "failed";
    }
};

// ---- Null Object ----
struct Telemetry { virtual ~Telemetry() = default; virtual void send(const std::string&) = 0; };
struct ConsoleTelemetry : Telemetry { void send(const std::string& m) override { std::cout << "  telemetry
…
circuit breaker (threshold 2, cool-down 3 ticks):
  t=1 failed
  t=2 failed -> OPEN
  t=3 rejected fast (open)
  t=4 rejected fast (open)
  t=5 failed -> OPEN
  t=6 rejected fast (open)
  t=7 rejected fast (open)
  t=8 ok
null object:
  telemetry: odometer 120500 km
specification:
  V1: book routine service
  V2: -
  V3: -

Step-by-Step Walkthrough

Ticks 1–2: two failures reach the threshold; the breaker opens.

Ticks 3–4: calls are rejected fast without touching the failing backend (protecting it and the caller's threads).

Tick 5: cool-down elapsed → half-open trial call, still failing → open again.

Tick 6 is still inside the new cool-down → rejected; at tick 8 the trial succeeds and the breaker closes.

Null Object removes if (telemetry) checks; Specifications compose rules (due && !recall) that can be reused in queries and validation.

flowchart LR
  Closed -->|failures >= threshold| Open
  Open -->|cool-down elapsed| HalfOpen[Half-open]
  HalfOpen -->|trial succeeds| Closed
  HalfOpen -->|trial fails| Open

Common Mistakes

Retrying non-idempotent operations (charging a card twice) — make them idempotent or don't retry.

Retry storms: many clients retrying immediately — use exponential back-off with jitter and a retry budget.

No timeouts on remote calls — threads pile up waiting forever.

Repository that leaks query languages (returns IQueryable everywhere) or one repository per table instead of per aggregate.

Null Object hiding real errors — only use it where "do nothing" is a correct behaviour.

Trade-offs and Alternatives

Resilience patterns add state and configuration (thresholds, timeouts) that must be tuned and monitored. Service meshes (Envoy, Istio) can apply timeouts, retries and circuit breaking at the infrastructure level.

Real-World Use

Microservice clients (Polly, Resilience4j), mobile apps talking to unreliable networks, vehicle telematics units buffering data when the cellular connection drops (bulkhead + retry), ORMs (Unit of Work + Data Mapper + Identity Map), Spring Data repositories.

Practice

Q1. (Easy) What does a circuit breaker do when open?

Answer: Rejects calls immediately without calling the dependency.

Q2. (Medium) Why add jitter to exponential back-off?

Answer: To avoid synchronized retries from many clients hitting the recovering service at the same moments (thundering herd).

Q3. (Advanced) How do you make a "charge payment" call safe to retry?

Answer: Send a client-generated idempotency key; the server stores the result per key and returns the same result for repeated requests instead of charging again.

Interview Question

Q1. "How would you protect a service from a slow downstream dependency?"

Answer: Layered defences: a timeout on every call; a bulkhead (separate, bounded thread/connection pool per dependency) so slowness can't exhaust shared resources; retries with exponential back-off and jitter only for idempotent, transient failures, within a retry budget; a circuit breaker that fails fast when the error rate is high and probes for recovery; fallbacks (cached data, degraded response); plus monitoring and alerts on breaker state.

Quick Quiz

Q1. Which pattern tracks changes to commit in one transaction?

Answer: Unit of Work.

Q2. Which pattern replaces null checks with a do-nothing object?

Answer: Null Object.

Q3. Name two rate-limiting algorithms.

Answer: Token bucket and leaky bucket (also fixed/sliding windows).

Summary

Enterprise patterns separate domain from persistence; resilience patterns keep systems responsive under failure.

Distinguish GoF (in-process structure), enterprise (application + DB) and distributed patterns.

Related: Modules 62, 69, 75, 76.