Repository navigation
Expand file tree
/
Copy pathdiagrams.html
More file actions
187 lines (184 loc) · 81.5 KB
/
Copy pathdiagrams.html
File metadata and controls
187 lines (184 loc) · 81.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
---
layout: default
title: Diagrams
description: "Every figure from the book on one page, with its editable Excalidraw source."
permalink: /diagrams/
---
<style>
.diagram-gallery {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(28rem, 1fr));
gap: 2rem;
margin-top: 2rem;
}
.diagram-gallery .excalidraw-embed { margin: 0; }
@media (max-width: 40rem) {
.diagram-gallery { grid-template-columns: 1fr; }
}
</style>
<section class="section">
<div class="container">
<span class="hero__eyebrow">Diagrams</span>
<h1>Every figure, on one page.</h1>
<p class="hero__lead">
The 149 diagrams from Cloud-Native Design Patterns, in reading order. Each one renders
inline here and ships a paired <code>.excalidraw</code> source you can
open on excalidraw.com and edit.
</p>
</div>
</section>
<section class="section">
<div class="container">
<div class="diagram-gallery">
{%- include excalidraw.html file="00-patterns-literacy" alt="Three stacked layers: design patterns (Gang of Four) at the bottom, architecture patterns in the middle, and system design patterns, the focus of this book, on top, with arrows showing each lower layer informing the next" caption="Figure 0.1: Each lower layer informs the one above it" -%}
{%- include excalidraw.html file="00-running-example" alt="A client calls order-service over REST; order-service calls inventory, payment, and shipping over gRPC and emits an order.placed event to Kafka; a notification service consumes that event; a graphql-gateway composes reads across the internal services" caption="Figure 0.2: Five services and a gateway, each exposing only the protocols that fit its role" -%}
{%- include excalidraw.html file="01-six-pillars" alt="A central hub labelled Cloud-native API on Kubernetes radiates spokes to six surrounding pillars: Disposable (fast start, clean stop, no local state), Resilient (timeouts, retries, circuit breakers, graceful degradation), Automatable (declarative config, GitOps and CRDs, self-healing), Observable (traces, metrics, logs, health, RED/USE signals), Elastic (scale to load, scale to zero with KEDA), and Loosely coupled (contracts not calls, async where it fits, independent deploy). A note reads: cloud-native is a set of runtime properties an API must exhibit to thrive on Kubernetes, not a checklist of tools." caption="Figure 1.1: Cloud-native in six pillars: the runtime properties an API must exhibit on Kubernetes" -%}
{%- include excalidraw.html file="01-twelve-factors" alt="A grid of the twelve factors, each with a number, name, and one-line summary: 01 codebase (one repo, many deploys), 02 dependencies (declare and isolate), 03 config (read from the environment), 04 backing services (treat as attached resources), 05 build/release/run (keep the stages separate), 06 processes (stateless, share-nothing), 07 port binding (export a service via a port), 08 concurrency (scale out with processes), 09 disposability (fast start, graceful stop), 10 dev/prod parity (keep environments alike), 11 logs (treat as event streams), 12 admin processes (run as one-off tasks)." caption="Figure 1.2: The twelve factors at a glance" -%}
{%- include excalidraw.html file="01-twelve-factor-ownership" alt="Two columns: the factors you still write per service (config from the environment, stateless processes, fast clean start and stop, logs as event streams, health and readiness probes) versus the factors the platform provides (build/release/run, concurrency and scaling, disposability, port binding, backing services, retries and timeouts)" caption="Figure 1.3: The twelve factors, split by who owns them" -%}
{%- include excalidraw.html file="01-factors-mapped" alt="Five rows mapping groups of factors to the system: build and ship becomes built once, run anywhere (one repo, pinned requirements, one image per release); configure becomes config from the environment (pydantic-settings, per-profile, secrets injected); run on the platform becomes disposable, stateless, scaled (KEDA scale-out and to-zero, fast start/stop, behind Istio); attach resources becomes backing services as swappable URLs (Postgres, Kafka via Strimzi, Apicurio, never in-process); operate becomes signals and admin off the hot path (OpenTelemetry to LGTM, admin as Jobs and CronJobs). Each row carries a section reference." caption="Figure 1.4: The twelve factors grouped by ask, mapped to where each lives in the system" -%}
{%- include excalidraw.html file="02-pick-protocol" alt="Four columns of interaction styles, each tagged sync or async: REST/HTTP (public and edge surface, wide reach, cacheable, resource-shaped, OpenAPI-described), sync; gRPC (internal service-to-service, low latency, streaming, schema-first proto, grpc + protobuf), sync; GraphQL (client-driven reads, compose many sources, one round-trip, schema (SDL) + resolvers), sync; and Async/events (fire-and-forget facts, decoupled in time, fan-out to many, Kafka/Pulsar), async." caption="Figure 2.1: Four interaction styles, four defaults: three synchronous, one asynchronous" -%}
{%- include excalidraw.html file="02-sync-vs-async" alt="Top row: order makes a synchronous call to payment and blocks on it, so if payment is down the order fails. Bottom row: order emits an order.placed fact to Kafka, and consumers catch up from the log independently" caption="Figure 2.2: A blocking call couples availability; a published fact decouples it" -%}
{%- include excalidraw.html file="02-mesh-handles-wire" alt="Two pods, each containing an app (a REST or gRPC server speaking plain localhost) beside an istio-proxy Envoy sidecar that handles mTLS, retries, timeouts, and traces. The two sidecars communicate over mutual TLS, encrypted and authenticated. The istiod control plane distributes policy, SPIFFE identity, and telemetry config to both pods." caption="Figure 2.3: The mesh moves cross-cutting concerns off your code: sidecars handle mTLS, retries, timeouts, and telemetry, configured by istiod" -%}
{%- include excalidraw.html file="03-composition" alt="A client sends one GraphQL query to the graphql-gateway; the gateway calls order-service over REST for the order fields and inventory over gRPC for the stock field, then joins and returns one result" caption="Figure 3.1: One query in; the gateway fans out to REST and gRPC and joins" -%}
{%- include excalidraw.html file="03-three-ways" alt="Three columns with tags: API Gateway (routing, auth, rate-limit; no business logic; Istio ingress) tagged thin edge; Backend-for-Frontend (one BFF per client type; client-specific shaping; owns aggregation) tagged client-shaped; and GraphQL federation (schema stitched across domains that own subgraphs; gateway plans query) tagged read-optimised. Aggregation moves toward the client as coupling needs loosen." caption="Figure 3.2: Three places to compose: a thin API gateway, a client-shaped BFF, or a read-optimised GraphQL federation" -%}
{%- include excalidraw.html file="04-db-per-service" alt="Four services (order, inventory, payment, shipping), each owning its data, each backed by its own PostgreSQL with a private schema run by CloudNativePG. A banner reads: polyglot persistence, pick the store per workload (relational, key-value, search, object)." caption="Figure 4.1: A database per service: private schemas, reached only through the owning API" -%}
{%- include excalidraw.html file="04-cqrs" alt="A client sends commands (state changes) to a command handler that validates, loads the aggregate, applies invariants, and writes to a normalized transactional write store. The handler emits to an Events/WAL stream (or the write store is CDC-tailed); a projection consumes those events, denormalizes, and updates a read store (wide tables, Elasticsearch, or Mongo) shaped for the query, which serves read-optimised queries back to the client." caption="Figure 4.2: CQRS: commands flow into the write model; events project into a read model shaped for queries" -%}
{%- include excalidraw.html file="04-log-tailing" alt="An application issues ordinary INSERT/UPDATE SQL to a database (Postgres, MySQL, or SQL Server). Tables and rows commit into the write-ahead log (WAL), which is commit-ordered and durable. A Debezium connector tails the WAL through a logical replication slot, converts row deltas into events, and publishes them to Kafka topics (orders.public, orders.outbox, and others)." caption="Figure 4.3: Change Data Capture: Debezium tails the WAL and turns every committed row into an ordered event" -%}
{%- include excalidraw.html file="04-data-outbox" alt="The REST handler writes the order and an outbox row in one local transaction to Postgres; Debezium tails the write-ahead log and publishes the order.placed event to Kafka, which fans out to consumers" caption="Figure 4.4: One local transaction; CDC turns the committed outbox row into an ordered event" -%}
{%- include excalidraw.html file="04-saga" alt="A happy-path chain: order-service (order created) triggers payment-service (payment taken), which triggers inventory-service (stock reserved), which triggers shipping-service (shipment booked). Each local commit emits the event that triggers the next. On failure, payment fails and emits order.cancelled, which drives order-service to compensate by cancelling. No two-phase commit; failures trigger compensating actions, not rollbacks." caption="Figure 4.5: A saga: local transactions chained by events, with compensations instead of rollbacks" -%}
{%- include excalidraw.html file="05-event-backbone" alt="order-service emits order.placed to a Kafka topic; payment, shipping, and notification each consume it independently, and a later fraud-check service is added as a new consumer without changing the producer" caption="Figure 5.1: One published fact; consumers added without touching the producer" -%}
{%- include excalidraw.html file="05-schemas-registry" alt="An Apicurio Registry at top holds versioned schemas with FORWARD/BACKWARD compatibility. The order-service producer serialises with a schema-id and sends id+bytes through Kafka to a consumer that resolves the id and deserialises; both resolve the schema by id from the registry. A CI publish gate rejects incompatible schemas before merge." caption="Figure 5.2: Apicurio holds versioned schemas; producer and consumer resolve by schema-id on the hot path, and a CI gate rejects incompatible changes before merge" -%}
{%- include excalidraw.html file="05-sourcing-vs-streaming" alt="Two columns. Event sourcing: events are the state: aggregate state is a fold of events from t0, the event log is the database, current state is rebuilt by replaying; fit is audit-heavy, financial, temporal; tools Marten, EventStoreDB, Axon. Event streaming: events are the transport: current state lives in a database, events are facts about changes, consumers react and propagate; fit is integration, fan-out, decoupling; tools Kafka, Pulsar, NATS JetStream." caption="Figure 5.3: Event sourcing makes events the state; event streaming uses events as transport, and full CQRS often combines both" -%}
{%- include excalidraw.html file="05-tools-landscape" alt="Three categories. Substrate (the durable log): Kafka and Pulsar. Compute (transform and aggregate): Flink, with alternatives Kafka Streams and Spark Structured Streaming. Movement (data in and out): NiFi and Debezium. Arrows show substrate feeding compute feeding movement; a typical pipeline runs source DB to Debezium/NiFi to Kafka/Pulsar to Flink to sink." caption="Figure 5.4: Three jobs: substrate (the durable log), compute (transform & aggregate), and movement (data in & out)" -%}
{%- include excalidraw.html file="05-kafka-architecture" alt="Producers, keyed by entity, append to a Kafka cluster drawn as a partitioned, replicated log with Partition 0, 1, and 2, each a leader plus in-sync replicas. Two consumer groups read from the cluster: a payment group that tracks its own offsets and an analytics group with independent offsets that can replay. A note: records are retained by time or size, not deleted on read; partitions give parallelism, replicas give durability, and groups read independently." caption="Figure 5.5: Kafka architecture: a partitioned, replicated log read by independent consumer groups" -%}
{%- include excalidraw.html file="05-log-truth" alt="A partitioned append-only log with offsets 0 through 9, head at the latest. Three consumers track independent offsets: a payment consumer at offset 9 (live), an analytics replay reset to offset 0, and a new consumer reading history from the middle with no producer impact." caption="Figure 5.6: A partitioned, append-only log: each consumer tracks its own offset, so replay is just rewinding" -%}
{%- include excalidraw.html file="05-pulsar-architecture" alt="Producers publish to a layer of stateless Pulsar brokers, which write segments to an Apache BookKeeper cluster of bookies; BookKeeper offloads cold segments to tiered object storage. Consumers read from the brokers using subscription modes: exclusive, shared, failover, or key_shared. A note: serving and storage scale independently, stateless brokers fail over fast, and a tenant-to-namespace hierarchy gives native multi-tenancy and geo-replication." caption="Figure 5.7: Pulsar architecture: stateless brokers over BookKeeper storage, with tiered offload" -%}
{%- include excalidraw.html file="05-amqp-architecture" alt="A producer publishes with a routing key to an exchange (direct, topic, or fanout). Bindings route copies to a queue: orders bound by key and a queue: audit bound by pattern; each queue delivers to competing consumers that acknowledge, after which the message is removed. A dead-letter queue receives nacked, expired, or overflowed messages. A note: routing lives in the exchange and an acked message is removed with no replay, which is why AMQP excels at work distribution, RPC, and rich routing." caption="Figure 5.8: AMQP architecture: an exchange routes through bindings to queues that competing consumers drain" -%}
{%- include excalidraw.html file="05-edm-anatomy" alt="An input stream, order.placed, feeds an event-driven microservice drawn as three steps in a row, consume (poll and deserialize), process (map or aggregate), produce (serialize and send), which emits to an output stream, order.validated. A dashed state store hangs off the process step, marked stateful only, with a read/write arrow. A note: consume to process to produce; the broker is the data plane and the service owns its state." caption="Figure 5.9: The event-driven microservice: consume, process, produce, with state only when the work needs it" -%}
{%- include excalidraw.html file="05-sidecar" alt="Two pods, service A in Go and service B in Python. In each, a business-logic component with no broker client talks over local gRPC to a sidecar that handles broker I/O. Both sidecars connect to a single Kafka cluster (topics, offsets, schema) to consume and produce. A note: the sidecar owns serde, schema, offsets, retries, and tracing, so business logic stays pure and polyglot." caption="Figure 5.10: The sidecar pattern: a companion process owns broker I/O so the service stays pure and language-agnostic" -%}
{%- include excalidraw.html file="05-eventification" alt="A source database with an orders table feeds a CDC connector (Debezium) that tails the commit log and turns each row change into an event on the topic order.changed, which consumers read to build their own views. Notes: table-to-stream (eventify) and stream-to-table (materialize) are two views of the same data with the log as the bridge; the outbox, writing the event in the same transaction, is the alternative when you own the writer." caption="Figure 5.11: Eventification: change data capture liberates a table into an event stream" -%}
{%- include excalidraw.html file="05-ecst-vs-notification" alt="Two rows. Event notification: a source service sends a thin OrderPlaced with just an id to a consumer, which must make a synchronous GET /orders/{id} call back to the source API to get the details. Event-carried state transfer: the source service sends a fat OrderPlaced carrying the full state, and the consumer folds it into a local view via upsert, with no call-back. A note: carry enough state and the consumer never calls back, reads go local and the source decouples." caption="Figure 5.12: Event notification forces a call-back; event-carried state transfer lets the consumer keep a local view" -%}
{%- include excalidraw.html file="05-state-store-changelog" alt="An order.placed topic feeds a stateful instance whose process step reads and writes a local state store called customer-totals. Every write is mirrored to a compacted, durable changelog topic. When a rebalance creates a new instance, it restores the store by replaying the changelog. A note: every write also goes to the changelog, so on failure or rebalance you replay it to rebuild the store and state is never lost." caption="Figure 5.13: A stateful instance keeps a local state store and mirrors every write to a compacted changelog it can replay to recover" -%}
{%- include excalidraw.html file="05-order-independent-state" alt="Three status events arrive out of order: v2 PACKED first, v1 PAID second, v3 SHIPPED third. A handler that keeps the highest version (last-writer-wins) feeds a state that converges to v3 SHIPPED, correct in any order. A note: version-keyed or commutative updates plus idempotency on duplicates make state correct regardless of arrival order." caption="Figure 5.14: Last-writer-wins by version makes state converge correctly no matter the arrival order" -%}
{%- include excalidraw.html file="05-event-time-watermark" alt="Events arrive in processing-time order with event-times 12, then 10 (out of order), then 9 (late). A handler orders them by event time with a watermark at T equals 11. Windows up to 11 close into a deterministic result; the event with event-time 9, which is older than T, is late and goes to a side output. A note: key computation off event time, not the wall clock, so replaying the log yields the same answer." caption="Figure 5.15: Event time plus a watermark: close windows by event time and handle late events explicitly" -%}
{%- include excalidraw.html file="05-checkpoint-recovery" alt="A processor consumes from an input log, holding local state and an offset, and periodically snapshots state and offset together to a checkpoint store. On a crash, recovery restores from the checkpoint, seeks the input to the saved offset, and resumes the processor by replaying forward. A note: snapshot state and input offset together so on failure you restore and replay from there, and rewind to reprocess and rebuild." caption="Figure 5.16: A checkpoint snapshots state and offset as a unit, so recovery restores both and replays the gap" -%}
{%- include excalidraw.html file="05-choreography-orchestration" alt="Top: choreography, order-service emits OrderPlaced to payment-service, which emits PaymentTaken to inventory-service, which emits StockReserved to shipping-service, a reacting chain with no coordinator. Bottom: orchestration, a central orchestrator that drives the workflow exchanges command and reply messages with payment, inventory, and shipping services. A note: choreography's flow is emergent and decoupled; orchestration's flow is explicit but the coordinator is a coupling point." caption="Figure 5.17: Choreography reacts to events with no coordinator; orchestration centralizes the flow in one" -%}
{%- include excalidraw.html file="05-saga-compensation" alt="A saga runs T1 charge payment, then T2 reserve stock, then T3 book shipping, which fails. The failure triggers compensation in reverse: C2 release stock, then C1 refund payment. A note: each step is a local transaction; on a failure, run compensating actions in reverse: atomicity without a distributed lock, with the full treatment in Appendix D." caption="Figure 5.18: A saga: local transactions forward, compensating actions in reverse when a step fails" -%}
{%- include excalidraw.html file="05-strangler-integration" alt="An edge router splits traffic: legacy paths go to the legacy monolith plus its database, and migrated paths go to new EDA services that grow over time. The legacy monolith's state is liberated to the event backbone (Kafka) via CDC plus the outbox, and the new services consume from the backbone. A note: the strangler migrates capability by capability behind the router, while CDC and the outbox feed the new side with no dual-write or big-bang cutover." caption="Figure 5.19: Strangling a monolith: route by capability, liberate state with CDC and the outbox, grow the new side" -%}
{%- include excalidraw.html file="05-consistency-window" alt="A client writes to the source of truth, which commits and emits an event; the event is applied to a read model or view after a delay of delta-t. A read of the view during that delta-t window may be stale. A note: read-your-writes means routing a writer's own reads to the source, or waiting until the read model reaches the write's version." caption="Figure 5.20: The consistency window: a read model lags the source by Δt, which read-your-writes hides for the writer" -%}
{%- include excalidraw.html file="06-derive-streams" alt="A streaming topology: the orders and payments topics feed a join-and-window operator (keyed by orderId, 5-minute tumbling, with a local state store); its output feeds an aggregate operator (count, sum, rate per merchant); that emits a derived revenue topic, which a Grafana dashboard reads." caption="Figure 6.1: A streaming app is a topology of sources, processors, and sinks; each output is just another stream" -%}
{%- include excalidraw.html file="06-window-types" alt="Three rows of window types. Tumbling: fixed 5-minute buckets with no overlap, so an event lands in exactly one window. Hopping (sliding): 5-minute windows that advance every 1 minute, overlapping, so an event lands in multiple windows, giving a smoothed view. Session: variable-length windows defined by an inactivity gap; no events for 10 minutes closes the window, suited to user-activity sessions." caption="Figure 6.3: Three window types: tumbling (fixed, no overlap), hopping (fixed, overlapping), and session (variable, gap-based)" -%}
{%- include excalidraw.html file="06-scale-on-lag" alt="KEDA reads the consumer lag on the order.placed topic and scales the payment-consumer pods up when lag grows and down to zero when the topic is idle" caption="Figure 6.2: Scale on lag, not CPU, and to zero when the topic is quiet" -%}
{%- include excalidraw.html file="07-orchestration-vs-choreography" alt="Top: a saga orchestrator commands payment, shipping, and notification in turn. Bottom: payment, shipping, and notification react to each other's events in a chain with no central coordinator" caption="Figure 7.1: A central coordinator that owns the flow, versus services reacting to events" -%}
{%- include excalidraw.html file="07-jobs-cronjobs" alt="Three columns of non-request work on Kubernetes. Job: run-to-completion, data backfill, schema migration, retries with backoffLimit. CronJob: scheduled with a cron expression, nightly reconcile, report generation, concurrencyPolicy. Queue worker: long-lived consumer scaled by KEDA, image resize and email, off the request path." caption="Figure 7.2: Three homes for non-request work: run-once Jobs, scheduled CronJobs, and KEDA-scaled queue workers" -%}
{%- include excalidraw.html file="07-queue-worker" alt="An HTTP request places an order and returns 201 immediately; the order-service publishes an order.placed fact to Kafka. A separate queue-worker pod (scaled by KEDA) drains the topic, processes the work (send email, resize image, generate report), commits the offset, and the heavy work never blocks the request." caption="Figure 7.3: The queue worker drains the topic off the request path; the request returns immediately" -%}
{%- include excalidraw.html file="08-four-planes" alt="Four planes side by side: the runtime plane (gateway and mesh), the contract plane (registry, section 9), the discovery plane (metadata, section 10), and the signal plane (observability, section 11)" caption="Figure 8.1: Four planes, each a section of the platform with its own owner" -%}
{%- include excalidraw.html file="08-contract-lifecycle" alt="A six-stage horizontal lifecycle: design, register, publish, consume, deprecate, retire. Each stage flows into the next; the last two transitions (deprecate, retire) are highlighted as the wind-down." caption="Figure 8.2: One managed contract lifecycle; each arrow is an owned transition, not an accident" -%}
{%- include excalidraw.html file="09-one-registry" alt="REST design (OpenAPI), gRPC (.proto), and events (AsyncAPI) all register into one Apicurio Registry that holds OpenAPI, AsyncAPI, proto, Avro, and JSON Schema artifacts with versions and compatibility rules. The registry fans out to three consumers: a CI gate that blocks breaking changes, client codegen for typed stubs, and runtime resolution that serdes by schema-id." caption="Figure 9.1: One registry for every contract type, feeding the CI gate, code generation, and runtime serde" -%}
{%- include excalidraw.html file="09-registry-gate" alt="A pull request with a schema change posts the new version to the Apicurio registry; the registry checks it against the compatibility rule and either allows merge and publish, or returns 400 and blocks the merge; consumers then fetch the published schema and generate stubs" caption="Figure 9.2: The registry checks compatibility at publish time and blocks the merge on a breaking change" -%}
{%- include excalidraw.html file="10-metadata-catalog" alt="OpenMetadata ingests from OpenAPI specs, the Apicurio registry, Kafka topics, and Postgres schemas through scheduled connectors, building one searchable catalog that delivers discovery, trust, lineage, and ownership to consumers" caption="Figure 10.1: One catalog, fed by connectors, answering the change-safety question" -%}
{%- include excalidraw.html file="10-catalog-payoffs" alt="Four columns of what the catalog gives every consumer. Discovery: search across APIs, topics, and tables, find the owning team and how to get access. Trust: published SLOs, data-quality results, freshness, and a criticality tier. Lineage: column- and topic-level impact analysis answering who breaks if I change this. Ownership: tags and PII classification, an accountable owner, and compliance-readiness." caption="Figure 10.2: Four payoffs the catalog gives every consumer: discovery, trust, lineage, and ownership" -%}
{%- include excalidraw.html file="11-three-signals" alt="One OpenTelemetry SDK and Collector instrumentation layer fans out to three stores: Traces (request across services, Tempo, viewed in Grafana), Metrics (RED/USE histograms, Prometheus to Mimir, alerts and SLOs), and Logs (structured, trace-linked, Loki, filtered by trace_id). A band notes Grafana as the single pane and Kiali as the service-mesh topology view." caption="Figure 11.1: Instrument once with OpenTelemetry; three signals land in the LGTM stack, correlated by trace_id" -%}
{%- include excalidraw.html file="11-signals-in-depth" alt="Five signal columns. Traces: request flow across services, latency per span, where did time go, Tempo/Jaeger. Metrics: aggregate rates, errors, latencies (RED/USE), what's the trend, Prometheus/Mimir. Logs: discrete timestamped events, what exactly happened, Loki/ELK. Baggage: key-value context carried along the trace, tenant_id downstream, W3C Baggage. Profiles: continuous CPU/memory sampling, which function burns CPU, Pyroscope/Parca. All correlated by trace_id, span_id, and resource attributes." caption="Figure 11.2: The signals in depth: each answers a different question, all correlated by trace_id" -%}
{%- include excalidraw.html file="11-trace-propagation" alt="One trace id propagates via the W3C traceparent header from the order-service REST span, through the inventory gRPC span, onto the published order.placed Kafka event, and into the notification consumer span" caption="Figure 11.3: One trace id, carried in band across REST, gRPC, and Kafka" -%}
{%- include excalidraw.html file="11-trace-stitch" alt="A trace waterfall over a time axis: a long REST POST /orders span contains a nested gRPC ReserveStock span, then a publish order.placed span, then a consume and notify span. All share one trace_id, shown as 4af9c, carried onto every span, metric exemplar, and log line." caption="Figure 11.4: One trace_id stitches REST, gRPC, the published event, and the consumer into a single waterfall" -%}
{%- include excalidraw.html file="11-sampling" alt="Two rows. Head sampling: a request enters, the SDK decides now (keep 1 in N probabilistically) before the trace completes, and the Collector forwards only sampled traces to the backend: cheap and low overhead but blind to errors and latency at decision time. Tail sampling: a request enters, the SDK sends all spans with no decision, the Collector buffers the whole trace and then keeps errors, slow, and rare-route traces, forwarding a kept subset to the backend: keeps all errors and slow traces but costs buffering and CPU." caption="Figure 11.5: Head sampling decides early in the SDK; tail sampling decides late in the Collector, after the whole trace is seen" -%}
{%- include excalidraw.html file="12-four-cs" alt="Four concentric layers: Cloud (IAM, VPC, KMS, audit) on the outside, then Cluster (RBAC, NetworkPolicy, Pod Security, admission), then Container (minimal image, non-root, read-only filesystem, signed), with Code (authz, input validation, secrets, dependencies) at the centre" caption="Figure 12.1: Cloud → Cluster → Container → Code, each layer protecting the one inside" -%}
{%- include excalidraw.html file="12-sidecar" alt="Clients reach a Kubernetes pod whose app container holds business logic only (no TLS, auth, rate-limit, or audit code), talking over localhost to an Envoy/OPA sidecar that terminates mTLS, validates JWTs, rate-limits, audits, enforces policy, and emits metrics and traces before egress. The app trusts the sidecar's headers." caption="Figure 12.2: Security concerns move out of the app and into an Envoy/OPA sidecar in the same pod" -%}
{%- include excalidraw.html file="12-valet-key" alt="A client requests a key from the application, which mints a scoped, time-bound, operation-restricted valet key and returns a signed token. The client then PUTs or GETs directly against the resource (object store, file) using the token; the storage/KMS layer validates the token, permits the named operation, and rejects everything else. The app never proxies the bytes." caption="Figure 12.3: The app mints a scoped, time-bound token; the client accesses storage directly and the app never proxies the bytes" -%}
{%- include excalidraw.html file="12-zero-trust" alt="Top: the old perimeter model (VPN to a trusted intranet to any service), where getting in once means trusted forever and lateral movement is trivial. Bottom: the zero-trust model: user/device to an IAP/auth proxy (who, where, device, MFA, posture) to a policy engine making a per-request decision (OPA/Cedar) to a service that accepts only requests carrying a valid claim, repeated for every downstream call, with mTLS and SPIFFE identity inside the cluster." caption="Figure 12.4: Perimeter trust versus zero-trust: identity and policy are checked on every hop, not once at the edge" -%}
{%- include excalidraw.html file="12-policy-as-code" alt="A developer commits to Git/CI (conftest, kyverno, pre-merge gate), then kubectl apply (or ArgoCD/Flux) sends objects to an admission controller (OPA Gatekeeper/Kyverno) that evaluates ConstraintTemplates against incoming objects and the API server accepts or rejects based on policy. Policy in Rego or Kyverno YAML (images must be signed, no privileged containers) is loaded into the admission controller." caption="Figure 12.5: Policy lives in Git and is enforced at admission; no human runs a checklist" -%}
{%- include excalidraw.html file="12-bulkhead" alt="Without bulkheads, a shared pool and shared thread group serve customer A traffic, customer B traffic, background jobs, and scheduled work together, so one client's storm starves everyone. With bulkheads, customer A, customer B, and background each get their own bounded connection pool and thread group, so A's storm fails only A while B and background keep working and the blast radius is bounded." caption="Figure 12.6: Per-tenant pools bound the blast radius: one tenant's storm fails only that tenant" -%}
{%- include excalidraw.html file="12-claim-check" alt="A producer PUTs a large or sensitive payload to an encrypted object store/vault (ACLs per object, retention policy) and publishes only a small, routable claim (a URI plus signed token) to the Kafka message bus, which holds only the token with no PII in topic history. A consumer consumes the claim and then fetches the payload directly from the vault using the token." caption="Figure 12.7: The producer stores the payload and publishes only a claim; the consumer fetches the body out of band" -%}
{%- include excalidraw.html file="13-antipatterns-grid" alt="A grid of nine anti-patterns, each re-coupling what the architecture worked to separate: distributed monolith (services deploy together), shared database (two services one schema), chatty N+1 APIs (a screen makes 40 calls), sync call chains (A to B to C to D, availability multiplies down), no contract or registry (breaking changes go unflagged), smart pipes and dumb endpoints (business logic creeps into the gateway), missing observability (debugging by guesswork), retry storms with no backoff (retries amplify an incident into an outage), and leaky internal models (DB rows exposed as the public API)." caption="Figure 13.1: The recurring anti-patterns; each one re-couples what the platform worked to separate" -%}
{%- include excalidraw.html file="13-antipattern-heuristics" alt="Five anti-patterns on the left (distributed monolith, shared database, chatty call chains, no contract or leaky models, missing observability and retry storms), each paired with the review heuristic on the right that catches it" caption="Figure 13.2: Every anti-pattern is coupling returning; every heuristic is a test you can apply in a PR" -%}
{%- include excalidraw.html file="14-three-paradigms" alt="Three columns for the same place_order intent. REST as resources: nouns plus HTTP verbs, POST to orders, GET an order by id, state transfer, server defines payload (resource-oriented). gRPC as procedures: typed method calls like Order.Place and Inventory.Reserve, behaviour not state, contract is the proto (RPC-oriented). GraphQL as a graph: one typed schema, the client picks fields and traverses relationships, contract is the SDL (query-oriented)." caption="Figure A.1: One intent, three mental models: resources, procedures, and a typed graph" -%}
{%- include excalidraw.html file="14-protocol-roundtrips" alt="REST requires the client to make several round trips to order, items, and stock or shipping, with fixed payloads; GraphQL collapses the same data need into a single query to the gateway where the client names the fields" caption="Figure A.2: Many REST round trips versus one client-shaped GraphQL query" -%}
{%- include excalidraw.html file="14-the-wire" alt="Three columns comparing what travels on the wire. REST: transport HTTP/1.1 or 2, JSON text encoding, request and response interaction, cacheable via HTTP, browser-native reach. gRPC: HTTP/2 only, protobuf binary, unidirectional and bidirectional streaming, no HTTP caching, needs grpc-web in a browser. GraphQL: HTTP POST to one URL, JSON text, request and response plus subscriptions, app-level caching, browser-native reach." caption="Figure A.3: What travels on the wire, per protocol: transport, encoding, streaming, caching, and reach" -%}
{%- include excalidraw.html file="14-rpc-stub-to-stub" alt="A client process and a server process. In the client, application code calls inventory.ReserveStock(req); a generated client stub marshals the request to protobuf. An amber arrow labelled HTTP/2 frame, protobuf crosses to the server, where a generated server skeleton unmarshals the bytes back into a request and invokes the handler reserve(sku, qty). A dashed return arrow carries the ReserveReply protobuf bytes back. One typed contract generates both stubs." caption="Figure A.4: How an RPC travels: a typed call in, protobuf on the wire, a typed call out" -%}
{%- include excalidraw.html file="14-streaming-modes" alt="Four panels comparing RPC modes between a client and a server. Unary: one request, one response. Server-streaming: one request, then N responses (amber). Client-streaming: N requests (amber), then one response. Bidirectional: both directions stream N to N over one connection (amber, double-headed)." caption="Figure A.5: The four RPC modes: unary, server-streaming, client-streaming, and bidirectional" -%}
{%- include excalidraw.html file="15-rest-versioning" alt="Four ways to version a REST API, coarse to fine. URI path: a v2 orders path, visible and trivial to route, couples the version to the URL, most widely used. Query param: an orders path with a version query, easy to default, muddies caching keys, least recommended. Custom header: an X-API-Version header, keeps the URL clean but is invisible and easy to miss, needs discipline. Media type: an Accept header naming the v2 representation, content negotiation, the URL still identifies the resource, the RESTful way." caption="Figure B.1: Four ways to version REST, from coarse URI paths to RESTful media-type negotiation" -%}
{%- include excalidraw.html file="15-grpc-breaking" alt="Two columns for protobuf changes. Safe and wire-compatible: add a field with a new number, add a new rpc method, add a value to an enum, rename a field since the number is unchanged, deprecate with the deprecated option, and widen with reserved for old tags. Breaking and rejected by the gate: change a field's number or type, reuse a removed field number, remove a field without reserving its tag, change an rpc between unary and streaming, rename or move a service or package, or change the request or response message type." caption="Figure B.2: protobuf: safe wire-compatible changes versus breaking ones the registry rejects" -%}
{%- include excalidraw.html file="15-graphql-evolution" alt="A four-step GraphQL evolution flow with no version number. Add: a new field or type, additive and safe, clients opt in. Deprecated: mark the old field with a reason and replacement, still served. Watch usage: field-level metrics show who still queries it and drive migration. Retire: at zero usage, remove the field, the only breaking step." caption="Figure B.3: GraphQL evolves rather than versions: add, deprecate, watch usage, then retire" -%}
{%- include excalidraw.html file="15-kafka-versioning" alt="A producer writes v2 events to one topic, order.placed, which holds an evolving schema. A schema registry over the topic enforces compatibility: v1 to v2 is add-only, BACKWARD. The topic delivers to two consumers: one built against v1 that ignores the added fields, and one built against v2 that uses them. The key is identity and partitioning, never the version; the version is the schema, carried in the registry or a header." caption="Figure B.5: Versioning a Kafka API: one evolving topic, a registry-enforced compatibility rule, old and new consumers side by side" -%}
{%- include excalidraw.html file="15-deprecation-lifecycle" alt="A four-step lifecycle: ADD (additive, always safe), DEPRECATE (with reason, replacement, and sunset), MEASURE (who is still using it), then RETIRE after migration" caption="Figure B.4: The same lifecycle for every protocol: add, deprecate, measure, retire" -%}
{%- include excalidraw.html file="16-ws-scaling-problem" alt="Clients hold long-lived ws connections through a Service or load balancer that is sticky per connection. ws-pod-1 holds 120 connections and ws-pod-2 holds 118, but a newly added ws-pod-3 holds 0: the scale-up problem, since a new pod gets no existing connections, only new ones. Killing a pod is the scale-down or rollout problem: it drops every connection it was holding at once." caption="Figure C.1: A WebSocket pins to a pod, so scale-up leaves new pods idle and scale-down drops every connection at once" -%}
{%- include excalidraw.html file="16-ws-backplane" alt="Three WebSocket pods each own only their own sockets; all subscribe and publish to a shared Redis or Kafka backplane. A message for client X arrives at ws-pod-1, which publishes to the backplane; the backplane delivers it to ws-pod-2, which holds X's socket" caption="Figure C.2: Externalised fan-out: the backplane routes each message to whichever pod holds the client" -%}
{%- include excalidraw.html file="16-ws-resume" alt="A sequence between a client and a ws-pod. A ping/pong heartbeat detects a dead link; the connection is lost; the client reconnects with backoff and jitter; it resumes by sending its last acknowledged sequence number (1042); the pod replays only the missed events (1043 to 1050), which are idempotent; the stream is live again, at-least-once and deduplicated by sequence." caption="Figure C.3: Resume, don't restart: detect the drop, reconnect with backoff, and replay only what was missed from the last acked sequence" -%}
{%- include excalidraw.html file="16-ws-protobuf-envelope" alt="A producer encodes to bytes; the bytes travel inside a WebSocket binary frame (opcode 0x2) made of four fields: version (uint32 = 2), seq (uint64 = 1043), type (the string OrderPlaced), and payload (protobuf bytes, highlighted); a consumer decodes and dispatches. The envelope is versioned by field number like gRPC: add fields, never renumber." caption="Figure C.4: A versioned protobuf Envelope carried in one binary WebSocket frame" -%}
{%- include excalidraw.html file="16-ws-failover" alt="A client that resumes at seq 1043 reconnects with backoff through the LB or Service. The dashed path to ws-pod-2, now terminated, is marked gone; the client lands instead on ws-pod-5, which holds the new socket. ws-pod-5 subscribes to the Redis or Kafka backplane and replays events with seq greater than 1042. The takeaway: backplane plus per-connection seq means a pod dies and the client loses nothing." caption="Figure C.5: Failover without interruption: reconnect lands on a live pod, which replays the gap from the backplane" -%}
{%- include excalidraw.html file="17-saga-state" alt="Four ways a saga can keep its state. In-memory: a dict or object only, lost on pod restart, not for production. DB state machine: one row per saga instance with status and context JSON, and you own the transitions. Event-sourced: append step events and rebuild by replay, with full audit history. Workflow engine: Temporal or Camunda, durable execution with retries and timers built in. Whatever the store, the state transition and the outbound command must commit together." caption="Figure D.1: Where saga state can live; the DB-backed state machine is the workhorse, but the commit-together rule is the same for all" -%}
{%- include excalidraw.html file="17-saga-compensation" alt="Forward path: charge_payment then reserve_stock then book_shipping, which fails. On failure the saga compensates in reverse: release_stock then refund_payment, only the steps that completed, newest first" caption="Figure D.2: On failure, run each completed step's inverse, newest first" -%}
{%- include excalidraw.html file="17-saga-context" alt="The orchestrator owns the saga context. It calls service A, which returns a reservation_id and warehouse region; that output is persisted into the saga context (reservation_id r-77, region eu-1) written after A and read before B; the orchestrator then calls service B with the context, which supplies the reservation_id B needs. If the orchestrator crashes between persisting A's output and calling B, on restart it loads the saga context, sees A is done, and resumes at B without re-calling A." caption="Figure D.3: A's output flows to B through the orchestrator's persisted context; B never calls A, and a crash resumes cleanly" -%}
{%- include excalidraw.html file="18-one-error-model" alt="One error model maps to four wire formats. Every error carries five facts: a stable machine code like STOCK_UNAVAILABLE, a human-readable message safe to show, a correlation or trace id, a retryable flag with a retry-after when known, and field-level details. Those same facts map onto REST as application/problem+json (RFC 9457), gRPC as a status code plus google.rpc.Status details, Kafka as headers plus a dead-letter record, and GraphQL as an errors array with extensions and a code." caption="Figure E.1: One error contract, four wire formats (agree the facts once, then map them per protocol)" -%}
{%- include excalidraw.html file="18-rest-errors" alt="REST error handling in two layers. Choose the status class first: 4xx is a client error, fix the request and do not retry as-is; 429 or 503 means busy, back off and honour the Retry-After header; 5xx is a server error, retry idempotent calls with backoff. The body is an application/problem+json document (RFC 9457) carrying type, title, status 409, detail, instance, the machine code STOCK_UNAVAILABLE, and a traceId for correlation." caption="Figure E.2: REST errors (the HTTP status class is the category; an RFC 9457 problem+json body carries the specifics)" -%}
{%- include excalidraw.html file="18-grpc-errors" alt="gRPC error handling. The canonical status code is the contract: INVALID_ARGUMENT and NOT_FOUND are client bugs, don't retry; FAILED_PRECONDITION and ALREADY_EXISTS are state issues, don't retry; UNAVAILABLE and ABORTED are transient, retry with backoff; RESOURCE_EXHAUSTED means honour the RetryInfo delay; DEADLINE_EXCEEDED means tune the timeout and maybe retry. A google.rpc.Status in the trailers carries the rich details: code UNAVAILABLE 14, a message, an ErrorInfo with reason and domain, and a RetryInfo delay." caption="Figure E.3: gRPC errors (a canonical status code drives retry behaviour, with google.rpc.Status details in the trailers)" -%}
{%- include excalidraw.html file="18-kafka-dlq" alt="A Kafka consumer reads from the orders topic main partition and processes each record. On success it acks and commits, the happy path. On failure it routes the record to a retry.5s delayed-retry topic, then a retry.30s topic, and after N tries to an orders.DLQ dead-letter queue for alerting and tooling. The offset is committed either way, so a poison record never blocks head-of-line processing." caption="Figure E.4: Kafka (route failures to delayed-retry topics then a DLQ, and commit the offset on every branch so the partition never stalls)" -%}
{%- include excalidraw.html file="18-graphql-errors" alt="A GraphQL partial-success response. The HTTP 200 response carries a data object where the order has id 1037 and total 59.90 but stock is null because that field failed. Alongside it an errors array entry names the failure: message inventory unavailable, the path to order then stock, and an extensions object with code UNAVAILABLE, retryable true, and a traceId. Every other field still returns." caption="Figure E.5: GraphQL (one HTTP 200, the failed field nulled, and an errors[] entry carrying the machine code in extensions)" -%}
{%- include excalidraw.html file="18-retry-storm" alt="Top: a feedback loop between clients and a struggling order service: clients retry three times, tripling load, which produces more timeouts, which produces more retries. Bottom: six controls that break the loop: timeout, bounded retries, backoff plus jitter, circuit breaker, retry budget, and shed load." caption="Figure E.6: Unbounded retries amplify a struggling service; each control removes one multiplier from the loop" -%}
{%- include excalidraw.html file="19-strategic-ddd" alt="Strategic DDD classifies the business into three kinds of subdomain by value and rate of change. Core: your competitive edge, complex and volatile, your best people (order and pricing). Supporting: necessary but not a differentiator, kept simple (inventory and shipping). Generic: a solved problem you buy or adopt off the shelf (auth and notification). Each subdomain becomes one or more bounded contexts, a model boundary with its own ubiquitous language, and the API is that boundary: one context's order is not another's, and the contract translates between models rather than sharing internals." caption="Figure F.1: Subdomains classified by value and volatility; each becomes a bounded context whose API is the boundary" -%}
{%- include excalidraw.html file="19-context-mapping" alt="The eight context-mapping patterns for how two bounded contexts integrate. The three API-relevant ones are starred: Open-Host Service, publish a stable API for all comers; Published Language, a shared schema in OpenAPI, proto, or Avro; and Anticorruption Layer, translate a foreign model at your edge. The others are Conformist, accept the upstream model as-is; Customer and Supplier, upstream serves a negotiated downstream; Shared Kernel, share a model with high coupling used sparingly; Partnership, two contexts evolve together; and Separate Ways, don't integrate at all." caption="Figure F.2: The context-mapping patterns; the starred three are the ones an API implements" -%}
{%- include excalidraw.html file="19-hexagonal-ports" alt="A central domain core that depends on nothing, declaring ports. On the left, three driving adapters (REST, gRPC, GraphQL) call inward through an inbound port. On the right, two driven adapters (a Postgres repository and a Kafka publisher) implement outbound ports. All dependency arrows point inward toward the core." caption="Figure F.3: The domain core is isolated; the four protocols are interchangeable adapters around it" -%}
{%- include excalidraw.html file="20-three-dimensions" alt="Khononov's balanced-coupling model has three dimensions, multiplied together. Integration strength: how much knowledge two parts share, from intrusive to contract. Distance: how far apart they sit, from method to class to module to service to system. Volatility: how often the coupled part changes, core versus generic subdomain. The cost of a coupling is that strong plus distant equals global complexity, a distributed monolith, but it only bites when the code is volatile." caption="Figure G.1: Coupling has three dimensions; a coupling only hurts when strength, distance, and volatility are all high at once" -%}
{%- include excalidraw.html file="20-strength-ladder" alt="Integration strength as a four-rung ladder, worst to best, each with its example in our system. Intrusive coupling reaches into another's private internals, such as a shared database or scraping internals. Functional coupling shares business logic and rules, such as duplicated or co-owned domain logic. Model coupling shares a data model or DTOs, such as a shared library of types. Contract coupling integrates via an explicit, stable contract: REST, gRPC, or GraphQL plus a registry schema. Across services, aim for contract coupling." caption="Figure G.2: The integration-strength ladder. Across services, aim for the bottom rung: contract coupling" -%}
{%- include excalidraw.html file="20-coupling-quadrant" alt="A two-by-two of integration strength (vertical) against distance (horizontal). Strong and close is a cohesive module (healthy); strong and distant is the distributed monolith (the trap); weak and close is needless indirection; weak and distant is healthy microservices. A note states that volatility is the trigger: even the trap quadrant is harmless on code that never changes." caption="Figure G.3: Balance means having strength or distance, not both; volatility decides whether an imbalance bites" -%}
{%- include excalidraw.html file="20-connascence" alt="Connascence in two families, each weaker at the top and stronger at the bottom. Static connascence, visible in the code: Name (agree on an identifier), Type (agree on a type or shape), Meaning (a shared convention or magic value), Position (the same order of arguments or fields), Algorithm (the same algorithm on both sides). Dynamic connascence, only at runtime: Execution order (A must run before B), Timing (race-sensitive), Value (values must stay in agreement), Identity (the same instance or entity). The rule: the stronger the connascence between two parts, the more local they must stay." caption="Figure G.4: Connascence (static and dynamic forms, weakest to strongest): keep stronger forms more local" -%}
{%- include excalidraw.html file="20-cynefin-coupling" alt="The four Cynefin domains, each with its coupling posture. Complex (probe, sense, respond): keep coupling loose and reversible: favour events and contracts, and don't split prematurely. Complicated (sense, analyse, respond): design boundaries with DDD context mapping and invest in the right contracts. Chaotic (act, sense, respond): stabilise first with strong, local coupling, even a monolith, then refactor as it clarifies. Clear (sense, categorise, respond): standardise on contract coupling and codify the known-good with registry-checked schemas." caption="Figure G.5: Cynefin and coupling, match the posture to how well you understand the domain" -%}
{%- include excalidraw.html file="21-shutdown-sequence" alt="Two lanes on a left-to-right timeline. Kubernetes: SIGTERM to PID 1, grace clock starts (default 30s), endpoint removal is asynchronous, SIGKILL if you overrun. Your pod, triggered by SIGTERM: flip readiness DOWN, preStop sleep about 5 seconds, drain HTTP plus Kafka and sockets and pools, then exit cleanly before the grace period ends. A note says readiness DOWN plus preStop keep new traffic off the dying pod during the async-removal race, and liveness must stay UP throughout." caption="Figure H.1: Shutdown is a timed two-lane protocol; readiness and preStop cover the deregistration race" -%}
{%- include excalidraw.html file="21-per-workload" alt="Graceful shutdown means something different per workload, in four columns. HTTP REST/gRPC: stop accepting new, finish in-flight requests bounded by a timeout: the one that's automatic. Kafka consumer: stop polling, finish current records, commit offsets, leave the group. WebSocket: send a close or go-away, clients reconnect elsewhere, lean on the per-connection sequence and backplane. Jobs and scheduler: don't start new work, let running jobs finish or checkpoint progress, resume after restart. Underneath: make every handler idempotent, because a hard SIGKILL is always possible." caption="Figure H.2: Only HTTP drains automatically; consumers, sockets, and jobs each need their own shutdown logic, all backed by idempotency" -%}
{%- include excalidraw.html file="22-l4-vs-l7" alt="Two columns. L4 transport sees source and destination IP and port, TCP/UDP protocol, and TLS SNI hostname only; it can pick a backend by IP or port and load-balance by the 5-tuple, but cannot read URLs or headers or route per user or cookie. L7 application sees the host header, URL path, HTTP method, query params, request and response headers, cookies, JWT claims, and the gRPC service.method; it can route by path, header, or cookie, branch by header to a canary, version, or user, and inject or rewrite headers and rate-limit." caption="Figure I.1: L4 sees connections (IPs, ports, the 5-tuple); L7 sees the whole HTTP/gRPC conversation and can route on any of it" -%}
{%- include excalidraw.html file="22-l7-layers" alt="North/south: a client passes through edge/ingress (TLS, host/path), then the API gateway (auth, rate limit), then a mesh sidecar (mTLS, retries), then the service (in-app rules). East/west: service A and service B talk through mesh sidecars that add mTLS, outlier detection, and locality routing. Each layer makes a distinct decision and emits a trace span." caption="Figure I.2: L7 routing is a four-layer stack; the mesh applies the same routing to every internal call, not just at the edge" -%}
{%- include excalidraw.html file="22-l7-journey" alt="A request's L7 path across five components: Client (request), Edge LB (TLS, host and path), API Gateway (auth, rate-limit), Mesh sidecar (mTLS, retry, locality), and the Service (in-app routing). Each box can refuse, transform, or branch the request and emits a trace span for the next layer." caption="Figure I.3: A request's path: Client → Edge LB → API Gateway → Mesh sidecar → Service, each making its own L7 decision" -%}
{%- include excalidraw.html file="22-sticky-sessions" alt="Three affinity modes. No affinity: any pod gets any request, load is even, client state lives elsewhere: the preferred default. Cookie-based: the LB sets a session cookie so the client returns to the same pod, works behind any L7 LB, but ties the session to the pod's life. IP-hash: hash the client IP to a pod, no cookies needed, but NAT and mobile NAT break it and load can skew badly. The cost of stickiness is uneven load, painful pod restarts, broken scale-to-zero, and every restart re-balancing all sessions." caption="Figure I.4: Affinity modes: no-affinity is the default; cookie and IP-hash stickiness both fight elastic scaling" -%}
{%- include excalidraw.html file="22-traffic-steering" alt="Six release shapes built from the same L7 primitive. Canary: 90% v1, 10% v2, watch metrics and ramp. Blue/Green: all v1 to all v2, flip atomically with fast rollback. A/B test: a cohort maps to a variant, stable per user, measure. Header-based: an x-internal header routes to v2 for dark launches and betas. Geo/latency: EU users to EU pods for lower RTT and data residency. Shadow: copy traffic to v2 to test under load with no user impact." caption="Figure I.5: Six familiar release shapes, all the same weighted/header L7 rule with different deploy-and-rollback discipline" -%}
{%- include excalidraw.html file="22-business-rule-routing" alt="Routing by business rules inside the service. Inputs from REST, WebSocket, or Kafka reach an in-service router that enriches the order with context, evaluates an externalised ruleset in a rule engine (Drools, NRules or durable-rules, all forward chaining; Lua in C++ and Go), and dynamically dispatches to a list of destinations. The router fans out to service A on a Kafka topic, service B over HTTP, and a DLQ/audit topic. The rule engine sits beside the router and is consulted per request." caption="Figure I.6: When routing depends on a business rule, the service's router evaluates an externalised ruleset and dispatches to services A, B, or a DLQ" -%}
{%- include excalidraw.html file="22-hop-cost" alt="A latency budget per L7 stage: TLS termination 0.5 to 2 ms, L7 path/header routing under 1 ms, Auth/JWT validation 1 to 5 ms, WAF inspection 2 to 5 ms, mesh sidecar 0.5 to 2 ms per hop, rate-limit Redis lookup 1 to 3 ms, body/payload inspection 5 to 50 ms, and in-app rule evaluation in a rule engine under 1 ms cached. The rule of thumb is to stay under about 10 ms of total L7 on hot paths, so every hop must pay for itself." caption="Figure I.7: Each L7 stage adds latency; stay under ~10 ms of total L7 on hot paths, so every hop must pay for itself" -%}
{%- include excalidraw.html file="22-east-west" alt="East/west L7 between services. Service A talks to service B through a sidecar pair (Envoy or Linkerd) over mTLS, HTTP/2, and gRPC. What the sidecars do per call with no app code: mTLS, retries with backoff and jitter, outlier detection, locality-aware load balancing, header-based routing and canary weights, rich traces and per-call metrics, circuit breakers, and rate limits." caption="Figure I.8: East/west L7: a sidecar pair gives every service-to-service call mTLS, retries, locality, and traces with no app code" -%}
{%- include excalidraw.html file="23-strangler-fig" alt="The strangler-fig migration approach. Clients hit one URL through a proxy routing layer: step zero is adding the proxy first. The proxy sends a widening 1% to 100% slice of traffic to a new service (the extracted slice) while everything else defaults to the shrinking monolith. Identify, move, redirect one bounded asset at a time, with the system live throughout; redirect is a proxy config change, not a client release, so every step is reversible." caption="Figure J.1: Migrate incrementally with a strangler-fig proxy: redirect a widening slice to the new service while the monolith shrinks, system live throughout" -%}
{%- include excalidraw.html file="24-monolith-unit" alt="A monolith as a unit of deployment. Web/mobile and other systems call into a single deployable that contains every module (auth, orders, inventory, catalog, payments, shipping, reporting, notifications, admin, pricing, returns, search), all sharing one database that every module reads and writes, giving schema-level coupling everywhere. A change anywhere means redeploying everything, a bug in reporting can take down payments, and one scaling unit serves different workloads. It is often the simplest, fastest path to value when the system fits in one team's head." caption="Figure K.1: A monolith is a deployment property: one binary, one heartbeat, one shared database; every module ships and scales together" -%}
{%- include excalidraw.html file="24-modular-monolith" alt="A modular monolith, still a single deployable, holding an Orders module (public API PlaceOrder and GetOrder, private internals guarded by the build tool), an Inventory module (public API ReserveStock and CheckAvailability, no shared types), and a Payments module (public API ChargeCard and Refund, its own data schema). They share one database but with separate schemas (orders, inventory, payments) and no cross-schema joins, enforced. Modules talk only through their public API, enforced by linters and build rules rather than a gentlemen's agreement." caption="Figure K.2: The modular monolith: one deployable, strict in-process boundaries enforced by build tooling, separate schemas" -%}
{%- include excalidraw.html file="24-decomposed-databases" alt="A modular monolith that is still one process, but with decomposed databases. The Orders, Inventory, and Payments modules each own a separate database: Orders DB on its own Postgres with its own schema and connection pool, Inventory DB on its own Postgres or Mongo, and Payments DB on its own Postgres, encrypted at rest behind a PII boundary. Cross-module data is now async and event-driven (the same discipline as microservices without the operational cost), so promoting one module to a process is a deployment change, not a data-migration project." caption="Figure K.3: Decompose the data while still one process: each module gets its own store, so promotion later is a deployment change, not a migration" -%}
{%- include excalidraw.html file="24-proxy-step-zero" alt="Step zero of every migration: introduce a routing layer before extracting anything. Clients hit a proxy or reverse proxy (nginx, Envoy, HAProxy, or YARP) through which all traffic now flows, and the proxy forwards everything to the monolith, which still owns everything and doesn't know about the proxy. New services like an extracted Orders.svc and a future Inventory.svc are soon to come behind the proxy's future routes. There is zero behaviour change for clients on day one, but the lever to route traffic to extracted services is now in place, and the proxy is also where TLS, auth, rate-limiting, and observability consolidate." caption="Figure K.5: The proxy is step zero: add the routing layer first, forwarding everything to the monolith, so later cutovers are a config change not a client release" -%}
{%- include excalidraw.html file="24-shared-database" alt="The strangler fig with a shared database as a deliberate intermediate state: process boundary first, data boundary later. Clients hit a proxy that routes to a new Orders service, which reads and writes the same DB and owns the API but not the data, and to the monolith, which still reads and writes the same DB. Both point at a Shared DB whose tables both apps know, with no schema-change isolation. Two apps writing the same tables is a coordination tax (schema changes need both sides to agree), so the next step is to give the new service its own store." caption="Figure K.6: The shared-database strangler: split the process first and the data later; a deliberate intermediate state, never a destination" -%}
{%- include excalidraw.html file="24-decorating-collaborator" alt="The decorating collaborator: the new service wraps the old, adds capability, then becomes the API. Clients talk to a decorating service that sits in front of the legacy API and adds caching, audit, validation, rate-limiting, fan-out to Kafka, and metrics, while the legacy service stays unaware it has been wrapped and remains the source of truth. The decorator also emits events to Kafka as a new capability. Clients now talk to the decorator; the legacy service keeps running and is eventually replaced behind the decorator's API: unlike a plain proxy, the decorator owns behaviour, not just routing." caption="Figure K.7: The decorating collaborator: a real service wraps the legacy API, adds behaviour, and absorbs it over time, for legacy you cannot modify" -%}
{%- include excalidraw.html file="24-cache-aside" alt="A client calls the application, which reads the cache (GET), falls back to the database with a SELECT on a miss, then populates the cache with SETEX before returning. The application coordinates every step." caption="Figure L.1: Cache-aside: the application coordinates; a miss costs two round trips" -%}
{%- include excalidraw.html file="24-read-through" alt="A client calls the application, which calls cache.get on a single interface. On a miss the cache itself runs a loader against the database. The application never sees the database directly." caption="Figure L.2: Read-through: the cache library owns the miss; the app sees one interface" -%}
{%- include excalidraw.html file="24-write-through" alt="A client sends a write to the application, which writes the database first as the source of truth, then writes (SET) the cache second, before responding. Both stores are updated on the write path." caption="Figure L.3: Write-through: DB first, cache second (SET, not invalidate); never stale, pays both latencies" -%}
{%- include excalidraw.html file="24-write-around" alt="A client sends a write to the application, which writes only the database; the cache is untouched. A later read populates the cache lazily." caption="Figure L.4: Write-around: writes go to the DB only; the next read populates the cache" -%}
{%- include excalidraw.html file="24-write-back" alt="A client writes to the application, which writes to the cache and acks immediately. A background flusher drains dirty keys from the cache to the database asynchronously in batches." caption="Figure L.5: Write-back: ack at cache speed; a background flusher persists later; a crash before flush loses unpersisted writes" -%}
{%- include excalidraw.html file="24-refresh-ahead" alt="A client reads through the application from a cache that is always warm. A background refresher tracks hot keys and fetches them from the database before their TTL expires, refreshing the cache so users never hit a cold miss." caption="Figure L.6: Refresh-ahead: a background task re-warms hot keys before TTL, eliminating user-visible cold misses" -%}
{%- include excalidraw.html file="25-failure-taxonomy" alt="Three families feed into a distributed system: NETWORK (partition, split-brain: alive but cannot talk), COMPONENT (partial, gray failure: some fail, some lie), and LOAD (amplification, cascading: a failure feeds on itself). The defensive toolkit answers them: timeouts, retries, circuit breakers, bulkheads, load shedding, fallback, quorum, Istio, graceful shutdown, observability." caption="Figure M.1: Three families of distributed failure, over the defensive toolkit that addresses them" -%}
{%- include excalidraw.html file="25-network-partition" alt="Three nodes on the left form a majority partition A; two nodes on the right form a minority partition B; the link between them is down. Note: every node is healthy and only the network failed, and CAP forces a choice between consistency and availability while partitioned." caption="Figure M.2: A network partition: healthy nodes that cannot reach each other" -%}
{%- include excalidraw.html file="25-split-brain" alt="A partition where each side has self-elected a leader that accepts writes, producing two primaries with conflicting writes. The cure noted is quorum: an action needs a majority, so a minority partition cannot act." caption="Figure M.3: Split-brain: a partition elects two leaders; quorum is the only real cure" -%}
{%- include excalidraw.html file="25-partial-failure" alt="An order service makes one decision per dependency: use the result from a healthy inventory service, fall back or fail for a down payment service, and time out then fall back for a slow shipping service." caption="Figure M.4: Partial failure: each dependency needs its own use / fall-back / time-out decision" -%}
{%- include excalidraw.html file="25-gray-failure" alt="A pod that is slow or dropping 5% of requests has a liveness probe that still returns 200, so the probe sees it as healthy, while real client traffic sees errors and latency: differential observability." caption="Figure M.5: Gray failure: the probe sees healthy, the client sees errors" -%}
{%- include excalidraw.html file="25-amplification" alt="One client request hits a gateway that retries three times, then a service that retries three times, then a service that retries three times, producing 27 requests at the bottom of the stack." caption="Figure M.6: Amplification: retries multiply across layers and synchronise into a thundering herd" -%}
{%- include excalidraw.html file="25-cascading" alt="A slow database blocks Service C's threads, which exhausts its resources so it fails for all callers, which fills Service B's pool, which takes down the edge: the failure propagates up the graph." caption="Figure M.7: Cascading failure: one slow dependency exhausts resources up the whole call graph" -%}
{%- include excalidraw.html file="25-timeouts-deadline" alt="A 1000ms deadline at the edge travels through Service A (uses 200ms, 800 left), Service B (uses 300ms, 500 left), to the database, which decides whether enough budget remains to start." caption="Figure M.8: Deadline propagation: the budget travels and shrinks; too little left means fail now" -%}
{%- include excalidraw.html file="25-retry-backoff" alt="Three attempts with growing, jittered waits between them (about 100ms plus jitter, then about 200ms plus jitter), capped at three attempts." caption="Figure M.9: Retry done right: exponential backoff, jitter, a hard cap, idempotent calls only" -%}
{%- include excalidraw.html file="25-circuit-breaker" alt="A three-state machine: CLOSED (calls pass, count failures) trips to OPEN (fail fast, don't call) when failures exceed the threshold; after a reset timeout OPEN moves to HALF-OPEN (a few trial calls); a successful trial returns to CLOSED, a failed trial returns to OPEN." caption="Figure M.10: The circuit breaker: closed, open, half-open; one breaker per dependency, paired with a fallback" -%}
{%- include excalidraw.html file="25-istio-resilience" alt="An Istio Envoy sidecar wraps service-to-service calls: a VirtualService configures timeout and retries with backoff and jitter; a DestinationRule configures connectionPool (mesh bulkhead) and outlierDetection, which ejects a bad host from the callee pool." caption="Figure M.11: The mesh supplies timeouts, retries, connection-pool bulkheads, and gray-failure ejection" -%}
{%- include excalidraw.html file="26-deploy-release" alt="A pipeline: build and deploy with the flag off, then release to 1% with the flag on, then ramp the percentage, then GA at 100%, then retire the flag. The deploy is decoupled from the release; every release and rollback is a config change, not a new deploy." caption="Figure N.1: Deploy ships the code dark; release is a runtime flag flip, and so is rollback" -%}
{%- include excalidraw.html file="26-flag-types" alt="A two-by-two of flag types by lifetime and owner: release flags (short-lived, dev-owned, ship dark and remove); experiment/A-B flags (short-lived, data-owned, split variants and measure); ops/kill-switch flags (long-lived, SRE-owned, disable a feature or dependency fast); permission/entitlement flags (long-lived, product-owned, gate by plan or tier)." caption="Figure N.2: Four flag types by lifetime and owner, each wanting different defaults and lifecycle" -%}
{%- include excalidraw.html file="26-evaluation-architecture" alt="A request carrying user, plan, and region reaches the application, which calls getBooleanValue(flag, default, context) through the OpenFeature SDK; the flagd provider evaluates locally in-cluster against a flag source (ConfigMap, file, or sync). The default is returned if the provider is unreachable, so a flag-system outage never takes the app down, and the SDK is vendor-neutral." caption="Figure N.3: OpenFeature SDK + flagd: local evaluation, vendor-neutral, fail-safe to the default" -%}
{%- include excalidraw.html file="26-progressive-rollout" alt="A stable userId is hashed into a bucket 0 to 99; the flag returns the new variant if the bucket is below the rollout threshold (5, then 25, then 100) and the old variant otherwise. The same user always lands in the same bucket, so assignment is consistent across repeat visits, and widening or narrowing the threshold ramps or rolls back instantly without a deploy." caption="Figure N.4: Percentage rollout via sticky bucketing: consistent per-user assignment, instant ramp and rollback" -%}
{%- include excalidraw.html file="26-flags-control-plane" alt="A flag control plane (flagd) sits at the centre with four spokes: a kill-switch disables a dependency (pairing with the circuit breaker), a canary combines a flag with L7 routing, a strangler cutover routes a percentage to a new service, and an experiment picks a sticky-bucketed variant." caption="Figure N.5: A flag is a runtime control surface, the same lever behind canaries, cutovers, kill-switches, and experiments" -%}
{%- include excalidraw.html file="26-flag-lifecycle" alt="A lifecycle: create the flag default off, roll it out from 0 to 100 percent with targeting, reach GA fully on, then remove the flag and its dead code. Every release flag is born with a removal ticket because a stale flag is dead code, a hidden branch, and a live risk surface." caption="Figure N.6: The flag lifecycle: a release flag is born with a removal ticket" -%}
{%- include excalidraw.html file="27-newman-run" alt="A collection.json (requests + tests) and an environment.json (base_url, tokens) feed into 'newman run', the headless CLI, which produces reporters (cli for humans, junit.xml for CI, html to share), feeding a CI gate that passes or fails. The same collection the team runs by hand becomes an automated gate with no new test framework." caption="Figure O.1: Newman runs a Postman collection headlessly and emits CI-readable results" -%}
{%- include excalidraw.html file="27-collection-anatomy" alt="A collection contains folders that group requests by resource; each request has a pre-request script, the HTTP call, and tests; the tests are JavaScript using pm.test and pm.expect to assert status, schema, and response time; environment variables like base_url and token are resolved at run time." caption="Figure O.2: A collection is folders of requests, each with assertions; the environment supplies the variables" -%}
{%- include excalidraw.html file="27-black-box" alt="One collection running under Newman tests the same HTTP API (/orders, /payments) against six order-service implementations: Spring Boot, Quarkus, .NET, Python, C++, and Go. Because Newman tests the API as a black box, one collection validates every implementation; the contract is the spec, not the code." caption="Figure O.3: One collection is the conformance suite for all six implementations" -%}
{%- include excalidraw.html file="27-data-driven" alt="A collection of one request set, combined with an iteration-data.csv of rows (id and qty values including a zero and a large number), runs once per row under newman, producing N result sets (one per case)." caption="Figure O.4: Data-driven runs: one request set, many inputs via --iteration-data" -%}
{%- include excalidraw.html file="27-ci-gate" alt="A pipeline: pull request, build image, docker compose up of the order/payment/inventory services, newman run of the collection, then a gate where pass merges and fail blocks. The services run on Docker Engine in CI as they do locally, and a failed assertion fails the build." caption="Figure O.5: Newman as a merge gate: bring the stack up on Docker, run the collection, fail the build on a broken contract" -%}
{%- include excalidraw.html file="27-test-pyramid" alt="A test pyramid: a wide base of unit tests (many, fast, cheap), then service/integration tests, then API/contract tests where Newman runs, then a narrow top of E2E tests (few, slow, brittle). Newman owns the API/contract tier and complements unit tests rather than replacing them." caption="Figure O.6: Newman owns the API/contract tier (above unit tests, below full end-to-end)" -%}
{%- include excalidraw.html file="29-dotnet-compilation" alt="The .NET compilation pipeline. C# source is compiled by Roslyn into CIL bytecode stored in DLL files. At runtime, CoreCLR loads the CIL and RyuJIT compiles it method-at-a-time into native x86-64 code. NativeAOT is an alternative that compiles ahead of time." caption="Figure P.1: From C# source to native code (Roslyn emits CIL bytecode, RyuJIT compiles to native at runtime)" -%}
{%- include excalidraw.html file="29-kestrel-pipeline" alt="Kestrel request processing pipeline. Kestrel handles TLS via OpenSSL, parses HTTP using Span-based parsers, passes requests through the ASP.NET Core middleware pipeline (routing, auth, CORS, diagnostics), and dispatches to application endpoints (minimal APIs, gRPC, GraphQL, WebSockets, health checks)." caption="Figure P.2: Kestrel processes requests end to end (TLS, HTTP parsing, middleware, endpoint dispatch)" -%}
{%- include excalidraw.html file="29-container-layers" alt="Multi-stage container build. The build stage uses the UBI9 .NET 10 SDK image to restore NuGet packages and compile the application. Only the published output is copied to the runtime stage, which uses the smaller UBI9 ASP.NET runtime image. The SDK never ships." caption="Figure P.3: Multi-stage build (the SDK compiles, only the runtime and app DLLs ship)" -%}
{%- include excalidraw.html file="30-blazor-circuit" alt="A browser running the Blazor JS runtime connects to the ASP.NET Core server via a SignalR circuit (WebSocket). The server holds the component tree, handles events (button click, form submit), re-renders the affected components, computes the DOM diff, and sends it over the circuit. The browser applies the diff. All C# logic stays server-side." caption="Figure Q.1: Blazor Server: a persistent SignalR circuit carries DOM diffs, events flow up, diffs flow down" -%}
{%- include excalidraw.html file="30-signalr-backplane" alt="Three Blazor Server pods behind a Kubernetes Service / load balancer, each holding its own set of SignalR circuits. All three subscribe and publish to a shared Redis backplane (StackExchange.Redis). A message sent to a group on pod-1 is published to Redis; Redis delivers it to pod-2 and pod-3, which push it to their local circuits in that group." caption="Figure Q.2: SignalR's Redis backplane: the same pub/sub externalisation as Appendix C, built into the framework" -%}
{%- include excalidraw.html file="31-minikube-topology" alt="The host runs Docker Engine. Inside the node container cndp-k8s there are two namespaces: strimzi holds the Strimzi operator, and cndp-05 holds order-service, the shipping and notification consumers, a Kafka cluster, Postgres, the LGTM stack and the Postgres Secret. The host reaches order-service on 127.0.0.1:30080 and Grafana on 127.0.0.1:30300, and runs kubectl exec against the Kafka and Postgres pods." caption="Figure R.1: The node container holds two namespaces; the host reaches two loopback NodePorts and uses kubectl exec for the Kafka and Postgres checks" -%}
{%- include excalidraw.html file="31-minikube-image-flow" alt="A Containerfile feeds docker build, which uses the host BuildKit cache and produces an image named localhost/cndp-05/language-service:v1 in the Docker image store. minikube image load copies it into containerd in the node container under the same name, and the pod runs it with imagePullPolicy Never." caption="Figure R.2: Images are built on the host and loaded into the node; pods never pull them" -%}
{%- include excalidraw.html file="32-openshift-topology" alt="The host runs curl and verify.sh, a browser and oc with the crc-admin context. Inside OpenShift Local there are two namespaces. cndp-amq-streams holds an OLM Subscription with Manual approval and a pinned CSV, and the AMQ Streams operator, which manages the Kafka cluster. cndp-05 holds order-service behind an edge-TLS Route, the shipping and notification consumers, Kafka 4.2.0, Postgres 18 from the sclorg image, the LGTM stack behind a Grafana Route and the Postgres Secret." caption="Figure S.1: The operator lives in its own namespace and manages Kafka in cndp-05; the host reaches order-service and Grafana through Routes and uses oc exec for the Kafka and Postgres checks" -%}
{%- include excalidraw.html file="32-openshift-image-flow" alt="A Containerfile feeds docker build, which uses the host BuildKit cache and produces an image named localhost/cndp-05/language-service:v1 in the Docker image store. skopeo copy reads it from the docker daemon and pushes it over HTTPS to the internal registry, where it becomes an ImageStream tag v1. The pod pulls it with imagePullPolicy Always. The credential is a ten-minute token for the builder service account, held in a shell variable and passed through a pipe." caption="Figure S.2: skopeo pushes from the Docker image store to the internal registry; the credential never touches a file or an argument list" -%}
</div>
</div>
</section>