Pattern Catalogue¶
Ten architecture patterns are demonstrated in this reference implementation. Each is backed by an Architecture Decision Record, a concrete implementation in the codebase, and cross-references to the SLA contracts that motivate it.
Patterns are grouped by concern. Many interact — the pattern interaction map at the bottom shows how.
Migration Patterns¶
Strangler Fig¶
ADR: ADR-0003 · Type: Migration
The Kong API gateway is deployed as a facade in front of the Java EE monolith from day one. Traffic is routed by URL path prefix. As each bounded context is extracted, its paths are re-routed from the monolith to the new service. The monolith shrinks one context at a time; it is never rewritten.
Each phase ends with a validation period (SLA compliance confirmed) before the monolith handlers for that context are disabled. Any phase can be reversed within 5 minutes by updating Kong routing config.
Where it lives in the codebase
| Artifact | Purpose |
|---|---|
docs/migration/strategy.md |
5-phase migration plan with rollback gates |
infrastructure/helm/charts/api-gateway/ |
Kong Helm chart with strangler-fig routing config |
docs/adrs/ADR-0003 |
Why not a big-bang rewrite |
Key constraint: Each extraction is independently reversible. The monolith is not modified — only Kong routing changes.
Leave and Layer¶
ADR: ADR-0011 · Type: Migration / Integration
The Warehouse Management System (WMS) is a vendor on-prem product that cannot be modified. Rather than replacing it in parallel with the Orders extraction, the Fulfillment Gateway is deployed as a new asynchronous layer: it subscribes to OrderConfirmed events on Kafka and translates them to WMS SOAP calls.
This is the critical distinction from Strangler Fig: Strangler Fig replaces the old system. Leave-and-Layer accepts it as a long-lived dependency and adapts to it. The WMS vendor contract, SOAP API, and deployment are completely untouched.
Where it lives in the codebase
| Artifact | Purpose |
|---|---|
services/fulfillment-gateway/src/infrastructure/WarehouseClient.ts |
The layer: translates domain types to legacy SOAP vocabulary |
services/fulfillment-gateway/src/application/OrderConfirmedHandler.ts |
Kafka consumer — the entry point |
contracts/slas/fulfillment-sla.yaml |
Independent SLA for the gateway; WMS outage ≠ Orders SLA breach |
docs/migration/warehouse-leave-and-layer.md |
Deployment phases and rollback |
Key insight: WarehouseClient.ts contains two vocabulary layers. WmsDispatchRequest (private type, matches 2004 SOAP schema) is the legacy surface. FulfillmentRequest (exported) is the domain surface. Nothing outside WarehouseClient.ts knows WmsDispatchRequest exists.
Data Patterns¶
Event Sourcing¶
ADR: ADR-0006 · Type: Data / State Management
The Orders domain uses EventStoreDB as its system of record. Every state transition appends an immutable event to the order's stream. Current state is reconstructed by replaying the stream. The mutable row is gone.
Three properties justify event sourcing specifically for Orders (not for Inventory or Customers):
- Full audit trail — every price change, cancellation reason, and address update is in the stream permanently
- Saga compensation — when the place-order saga must be rolled back, the event stream gives a causal chain to replay in a test context before committing compensation
- Temporal queries — "what was the state of this order at 14:32?" is a stream replay, not a dedicated audit table
Where it lives in the codebase
| Artifact | Purpose |
|---|---|
services/orders/src/domain/Order.ts |
Aggregate root: reconstitute() replays events, apply() dispatches each |
services/orders/src/domain/OrderStatus.ts |
State machine with assertValidTransition() guard |
services/orders/src/infrastructure/OrderRepository.ts |
EventStoreDB-backed repository stub |
contracts/event-schemas/ |
Canonical JSON Schemas for all domain events |
Key constraint: Event sourcing is used only in the Orders domain (ADR-0006). Inventory and Customers use PostgreSQL CRUD with row versioning.
CQRS¶
ADR: ADR-0007 · Type: Data / Performance
Inventory has a 10:1 read-to-write ratio. Stock level queries must be sub-10ms; reservation writes need strong consistency. A single PostgreSQL store cannot satisfy both requirements at the required throughput.
The write model (PostgreSQL) handles all ReserveStock and ReleaseStock commands with row-level locking. A Kafka consumer maintains a Redis hash projection of current stock levels. Reads go to Redis exclusively.
The two models diverge by at most one Kafka consumer lag interval (target: <100ms). This divergence is explicitly documented in the Inventory SLA and enforced by a consumer lag alert.
Where it lives in the codebase
| Artifact | Purpose |
|---|---|
contracts/domain-invariants/inventory-invariants.md |
INV-INV-006: Redis is not authoritative for write decisions |
contracts/slas/inventory-sla.yaml |
Read p99 100ms; write p99 300ms; oversell tolerance 0 |
observability/slos/inventory-slo.yaml |
SLO spec derived from the SLA |
observability/alerts/inventory-burnrate.yaml |
Burn rate alerts for reservation success rate and read latency |
Key constraint: INV-INV-006 states that no write decision may use the Redis projection as input. A reservation that reads from Redis to check stock could race with an uncommitted write. Aggregates read from PostgreSQL; Redis serves only query endpoints.
Outbox Pattern¶
Referenced in: ADR-0006, ADR-0004 · Type: Data / Reliability
Domain events are never published directly from command handlers. A dedicated outbox process reads the EventStoreDB stream and publishes to Kafka topics. This decouples the command commit from the Kafka publish and guarantees at-least-once delivery without distributed transactions.
Where it lives in the codebase
| Artifact | Purpose |
|---|---|
services/orders/src/infrastructure/KafkaEventPublisher.ts |
Outbox consumer stub: reads EventStoreDB, publishes to Kafka |
ai-agents/tasks/agent/architectural-compliance-review.md |
Phase 5 check: events published via outbox, not directly from command handlers |
Integration Patterns¶
Saga Orchestration¶
ADR: ADR-0006 · Type: Integration / Distributed Transactions
The place-order flow is a distributed transaction across three systems. The Orders service acts as the orchestrator: it issues commands, waits for outcomes, and triggers compensation when a step fails.
PlaceOrder command
↓
1. ReserveStock (Inventory) — wait for StockReserved
↓
2. CapturePayment (Payment Gateway) — wait for PaymentCaptured
↓ (if PaymentFailed)
→ ReleaseStock (compensation) → OrderCancelled
↓ (if PaymentCaptured)
3. ConfirmOrder → OrderConfirmed published
Where it lives in the codebase
| Artifact | Purpose |
|---|---|
services/orders/src/application/PlaceOrderCommand.ts |
Saga orchestrator skeleton |
services/orders/src/domain/Order.ts |
Enforces INV-ORD-007: stock reserved before payment captured |
contracts/slas/orders-sla.yaml |
saga_compensation.max_ms: 5000 — compensation must complete within 5s |
observability/alerts/orders-burnrate.yaml |
OrdersSagaCompensationRateHigh alert fires when compensation rate exceeds 5% |
Key constraint: Compensation events must be published via the outbox (not in-memory) so they survive a crash during the compensation phase. The saga_compensation.max_ms SLA target is enforced by a histogram bucket in OtelInstrumentation.ts.
Choreography¶
ADR: ADR-0015 · Type: Integration / Event-Driven
Orchestration is used when outcome matters. Choreography is used when it doesn't. When an order is confirmed, the Orders service publishes OrderConfirmed and is done — it has no knowledge of the Fulfillment Gateway, inventory cache consumers, or any future analytics pipeline.
| Event | Producer | Consumers | Why choreography |
|---|---|---|---|
OrderConfirmed |
Orders | Fulfillment Gateway | WMS failure ≠ Orders SLA breach |
CustomerRegistered |
Customers | Inventory (cache warm) | Cache miss is a performance issue, not a correctness issue |
CustomerSuspended |
Customers | Orders (cache invalidation) | Invalidation is eventually consistent by design |
Where it lives in the codebase
| Artifact | Purpose |
|---|---|
docs/architecture/diagrams/sequence-choreography.md |
Sequence diagram: OrderConfirmed fan-out and independent consumer failure |
services/fulfillment-gateway/src/application/OrderConfirmedHandler.ts |
Primary choreography consumer |
api/asyncapi/orders-events-v1.yaml |
AsyncAPI contract listing all OrderConfirmed consumers |
Resilience Patterns¶
Circuit Breaker¶
ADR: ADR-0012 · Type: Resilience
Two external calls in the system can fail unpredictably: Orders → Payment Gateway, and Fulfillment Gateway → WMS. Without a circuit breaker, a degraded downstream holds connections open until the caller's pool is exhausted.
The pattern is implemented at two levels:
DestinationRule with outlierDetection in each service's Helm chart. No application code changes. Applied to all in-mesh calls.
The WMS endpoint is out-of-mesh (on-prem SOAP). WarehouseClient.ts implements a three-state machine (closed/open/half-open) with thresholds sourced from contracts/slas/fulfillment-sla.yaml.
Where it lives in the codebase
| Artifact | Purpose |
|---|---|
services/fulfillment-gateway/src/infrastructure/WarehouseClient.ts |
Application-level state machine |
infrastructure/helm/charts/orders-service/templates/destination-rule.yaml |
Istio rule: Payment Gateway egress (tight: 3 failures, 60s ejection) |
infrastructure/helm/charts/fulfillment-gateway/templates/destination-rule.yaml |
Istio rule: in-mesh dependencies |
contracts/slas/fulfillment-sla.yaml |
Circuit breaker thresholds (human-authored source of truth) |
Key constraint: The circuit_breaker_state gauge (0=closed, 1=open, 2=half-open) is a required OTEL metric for any service with external calls. The state must be observable in Grafana without log parsing.
Bulkhead Isolation¶
ADR: ADR-0013 · Type: Resilience
Failures in the Fulfillment Gateway (a background service) must not be able to evict Orders pods or exhaust Orders' connection pools. Two isolation layers enforce this.
Each bounded context has a ResourceQuota that caps total CPU and memory. A Fulfillment Gateway memory leak hits the chakra-fulfillment quota before it can affect chakra-orders.
| Namespace | CPU limit | Memory limit |
|---|---|---|
chakra-orders |
6 cores | 6Gi |
chakra-inventory |
12 cores | 12Gi |
chakra-customers |
1.5 cores | 1.5Gi |
chakra-fulfillment |
1.2 cores | 1.2Gi |
Each service's DestinationRule sets explicit connectionPool limits per downstream. A degraded Payment Gateway can hold at most 20 TCP connections from the Orders service — additional requests get a fast 503, which the circuit breaker counts.
Where it lives in the codebase
| Artifact | Purpose |
|---|---|
infrastructure/kubernetes/policies/resource-quotas.yaml |
All five namespace quotas + LimitRanges with formula comments |
infrastructure/helm/charts/orders-service/templates/destination-rule.yaml |
connectionPool.tcp.maxConnections: 20 for Payment Gateway |
tooling/service-manifest.yaml |
Source of truth for resource sizing; quotas derived from limits × max_replicas × 1.2 |
Sidecar¶
ADR: ADR-0014 · Type: Resilience / Observability
Three cross-cutting behaviors — mTLS, traffic policy, and telemetry — are moved out of application code into two injected sidecars. Service authors write business logic and OTEL SDK calls. They implement no TLS code, no retry logic, and no metrics transport.
| Sidecar | Injected by | Responsibilities |
|---|---|---|
| Envoy | Istio control plane | mTLS (SPIFFE/X.509), DestinationRule enforcement, trace context propagation |
| OTEL Collector | OpenTelemetry Operator | Receives OTLP on localhost:4317, batches, forwards to Grafana Tempo/Mimir/Loki |
Where it lives in the codebase
| Artifact | Purpose |
|---|---|
infrastructure/helm/charts/orders-service/templates/deployment.yaml |
Both injection annotations on the pod spec |
observability/otel/instrumentation-cr.yaml |
OTEL Operator Instrumentation CR: which namespaces get the collector |
services/orders/src/infrastructure/OtelInstrumentation.ts |
Application-side: creates meters, registers counters and histograms — no exporter config |
Key constraint: The compliance agent (Phase 4 check) verifies sidecar.istio.io/inject: "true" is present in every Deployment and that PeerAuthentication mode is STRICT in every chakra-* namespace.
Pattern Interaction Map¶
The patterns don't stand alone. Each one enables or constrains another.
graph TD
SF[Strangler Fig\nKong facade] -->|extracts| ES[Event Sourcing\nOrders]
SF -->|extracts| CQRS[CQRS\nInventory]
SF -->|extracts| LL[Leave and Layer\nFulfillment Gateway]
ES -->|requires| OB[Outbox Pattern\nKafkaEventPublisher]
ES -->|enables| SAGA[Saga Orchestration\nplace-order flow]
OB -->|produces events for| CHOR[Choreography\nOrderConfirmed fans out]
SAGA -->|compensation requires| ES
CHOR -->|consumed by| LL
LL -->|protected by| CB[Circuit Breaker\nWarehouseClient]
CB -->|state reported via| SCP[Sidecar\nOTEL Collector]
BH[Bulkhead\nNamespace quotas] -->|isolates| LL
BH -->|isolates| ES
SCP -->|enforces policy for| CB
style SF fill:#3b82f6,color:#fff
style LL fill:#3b82f6,color:#fff
style ES fill:#10b981,color:#fff
style CQRS fill:#10b981,color:#fff
style OB fill:#10b981,color:#fff
style SAGA fill:#f59e0b,color:#fff
style CHOR fill:#f59e0b,color:#fff
style CB fill:#ef4444,color:#fff
style BH fill:#ef4444,color:#fff
style SCP fill:#8b5cf6,color:#fff
Pattern Summary Table¶
| Pattern | ADR | Type | Used in |
|---|---|---|---|
| Strangler Fig | ADR-0003 | Migration | All service extractions via Kong |
| Leave and Layer | ADR-0011 | Migration | Fulfillment Gateway → WMS |
| Event Sourcing | ADR-0006 | Data | Orders domain only |
| CQRS | ADR-0007 | Data | Inventory (PostgreSQL write + Redis read) |
| Outbox | ADR-0006 | Data | EventStoreDB → Kafka |
| Saga Orchestration | ADR-0006 | Integration | Place-order flow |
| Choreography | ADR-0015 | Integration | OrderConfirmed → Fulfillment, cache warm |
| Circuit Breaker | ADR-0012 | Resilience | Istio (in-mesh) + WarehouseClient (out-of-mesh) |
| Bulkhead | ADR-0013 | Resilience | Namespace quotas + connection pool limits |
| Sidecar | ADR-0014 | Resilience / Observability | Envoy + OTEL Collector in every pod |