Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .github/workflows/content.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,16 @@ jobs:
- name: Validate site content
run: python scripts/validate_site.py

# Professional voice: no ban-tier phrase on any reader-facing page, README or
# diagram label. The scanner is vendored from lgtm-skills (scripts/voice/README.md);
# the scope lives in scripts/voice-scan.sh.
- name: Voice scan (ban tier)
run: bash scripts/voice-scan.sh

# Always passes: logs the all-tier totals, so a review can compare them with a local run.
- name: Voice scan totals (all tiers)
run: bash scripts/voice-scan.sh --all-tiers-summary

# The one check branch protection requires: passes when every job above
# succeeded or was skipped.
content-ok:
Expand Down
16 changes: 13 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,13 @@ approvals**, no direct pushes. The required checks are summary jobs that always
| Check | Workflow | Covers |
|-------|----------|--------|
| `site` | `pages.yml` | Jekyll build (deploys only on a push to main) |
| `content-ok` | `content.yml` | `scripts/validate_site.py` (the "Validate before commit" rules) |
| `content-ok` | `content.yml` | `scripts/validate_site.py` (the "Validate before commit" rules) + voice scan, ban tier (`scripts/voice-scan.sh`) |
| `examples-ok` | `examples.yml` | build matrix per changed example project |

Workflows trigger on `push: branches: [main]` plus `pull_request:` (no `paths:` filters).
`examples.yml` selects projects with `scripts/ci_changed_projects.py`, which you can run
locally (`--base origin/main`). Run `python3 scripts/validate_site.py` before committing doc
changes. Work on a branch, open a PR, wait for the three checks, squash-merge.
locally (`--base origin/main`). Run `python3 scripts/validate_site.py` and `scripts/voice-scan.sh` before
committing doc changes. Work on a branch, open a PR, wait for the three checks, squash-merge.

## Example layout

Expand Down Expand Up @@ -189,6 +189,16 @@ k6, Locust.
- **Validate before commit:** front-matter parses; raw-guard balance; **0 unguarded `{{`**;
each codetab block-count == its `langs` count; referenced figures exist (`.svg` + `.excalidraw`);
**0 positional terms**.
- **Professional voice:** `scripts/voice-scan.sh` must report 0 ban-tier hits (CI gate in
`content-ok`). Watch-tier hits are reviewed in context: rewrite them unless the word is
literal (a machine that fails silently, minikube as the subject, a result table). Scope:
`_docs`, `_parts`, `_example_pages`, `_includes`, `_layouts`, `index.html`, `README.md`,
`assets/diagrams/*.svg`, and `examples/**/*.md`; `lgtm-presentation/` joins in S12. When a
diagram label changes, change the SVG, both `text` and `originalText` in the `.excalidraw`,
and any alt text or caption that repeats it. The scanner is vendored from lgtm-skills
(`scripts/voice/README.md`): fix it upstream and re-vendor, never edit the copy.
Check a few files while writing with `scripts/voice-scan.sh --tier all --hits all <paths>`
(paths relative to the current directory; `examples/` paths are scanned as Markdown).

## Workflow in Claude Code (changed from claude.ai)

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ cd python && docker compose down -v
| `_includes/codetabs.html` + `assets/js/codetabs.js` | the multi-language code tabs (no plugin) |
| `assets/diagrams/` | paired `name.svg` + `name.excalidraw` figures |
| `lgtm-presentation/` | the six source decks + `BUILD.md` manifest |
| `_plans/` | the roadmap and the Phase-3 backlog |
| `_plans/` | internal planning notes |

## Deploy

Expand Down
14 changes: 7 additions & 7 deletions _docs/00-setting-up.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
title: "Setting up · Patterns in the age of AI"
order: 0
part: "Setting up"
description: "Why a shared vocabulary of patterns matters more now that an AI writes the first draft, and the canonical five-service system we design against for the rest of the book."
description: "Why a shared vocabulary of patterns matters more now that an AI writes the first draft, and the canonical five-service system we design against throughout the book."
duration: 10 minutes
---

Expand All @@ -21,13 +21,13 @@ amplifier; the patterns decide which direction it amplifies.

## Four maxims to carry throughout

Every concrete choice in this book is a deliberate exercise in four ideas. Keep
Every concrete choice in this book applies four ideas. Keep
them in view:

- **The least-worst decision.** Every architecture choice is a set of trades —
latency for throughput, consistency for availability, simplicity for
flexibility. The job is not to find the option with no downside; it is to make
the trades visible and deliberate.
the trades visible and explicit.
- **Risk management.** You manage the *distribution* of failures, not their
existence. Failure doesn't go away; you choose where it lands and how it's
contained.
Expand All @@ -36,8 +36,8 @@ them in view:
operational complexity. The question is whether you are buying the right thing
for what you are paying.
- **No problem cannot be made worse.** A confidently-wrong intervention amplifies
the original pain. AI tooling makes confident interventions cheap and easy,
which is exactly why this maxim earns its place.
the original pain. AI tooling makes confident interventions cheap,
which is exactly why this maxim is needed.

## Three layers of patterns literacy

Expand Down Expand Up @@ -88,9 +88,9 @@ When a later chapter shows the transactional outbox, or content-based routing, o
a saga, it is wiring up *these* services. Keeping one concrete system in mind is
what stops the patterns from staying abstract.

## How the rest of the book is ordered
## How the book is ordered

The order is deliberate: principles first, because they justify every later
The order follows the dependencies: principles first, because they justify every later
choice; then the synchronous surface (communications, composition); then data
ownership; then the asynchronous backbone (events, streams); then the operational
concerns (workflows and the platform planes); and finally anti-patterns as a
Expand Down
8 changes: 4 additions & 4 deletions _docs/01-cloud-native-principles.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ Read each as a behaviour you design toward, not a product you install:
- **Automatable** — declarative config, GitOps, self-healing.
- **Loosely coupled** — contracts not calls, async where it fits.

These six are the rubric for the rest of the book. Hold each design decision up
These six are the rubric for every pattern in this book. Hold each design decision up
to them.

## Lean on the platform
Expand All @@ -49,7 +49,7 @@ Design-first is the cultural prerequisite for everything that follows. The
contract — OpenAPI, `.proto`, AsyncAPI — is agreed and registered *before* code
is written, owned by the producing domain, and gated in CI. Consumers code
against the contract and its generated stubs, never against your database. That
is what actually delivers independent deployability: the contract *is* the
is what delivers independent deployability: the contract *is* the
service boundary. Skip it and you get distributed coupling — the worst of both
worlds.

Expand All @@ -66,7 +66,7 @@ Here are all twelve, one line each.

The useful way to read it today is not to recite all twelve, but to split them by
**who owns each one** — the few you still write, versus the many the platform now
provides for free.
provides built in.

{% include excalidraw.html
file="01-twelve-factor-ownership"
Expand Down Expand Up @@ -100,7 +100,7 @@ group is treated in depth — most of the twelve are now adopted, not authored.

## A twelve-factor service, ready for the platform

Two factors dominate the code you actually write: **config from the environment**
Two factors dominate the code you write: **config from the environment**
(factor III) and **health and readiness probes** the platform calls. Here it is
in each stack — pick your tab.

Expand Down
10 changes: 5 additions & 5 deletions _docs/02-communications.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,19 @@ one.

{% 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, FastAPI) — sync; gRPC (internal service-to-service, low latency, streaming, schema-first proto) — sync; GraphQL (client-driven reads, compose many sources, one round-trip, Strawberry/Graphene) — sync; and Async/events (fire-and-forget facts, decoupled in time, fan-out to many, Kafka/Pulsar) — async."
alt="Four columns of interaction styles, each tagged sync or async: REST/HTTP (public and edge surface, wide reach, cacheable, resource-shaped, FastAPI), sync; gRPC (internal service-to-service, low latency, streaming, schema-first proto), sync; GraphQL (client-driven reads, compose many sources, one round-trip, Strawberry/Graphene), 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" %}

Read it by the sync/async tags along the bottom: REST, gRPC, and GraphQL are all
request/response — the caller waits — while events are fire-and-forget. Three of the
four couple the caller to the callee's availability for the duration of the call; only
the fourth breaks that coupling. Choosing a style is therefore choosing how much
availability you are willing to inherit — the thread the rest of the chapter pulls on.
availability you are willing to inherit — the thread that runs through this chapter.
REST wins where reach and caching matter; gRPC where an internal hop must be fast and
strongly typed; GraphQL where one client read would otherwise be a handful of round
trips; and events wherever immediacy is not strictly required.

Two interaction styles are deliberately deferred from this chapter. **GraphQL** gets
Two interaction styles are deferred from this chapter. **GraphQL** gets
its own treatment in **03 · Composition**, because GraphQL is fundamentally a
composition pattern — a gateway that fans out to REST and gRPC back-ends — and that
context matters more than the wire format. **WebSockets** are covered in
Expand Down Expand Up @@ -214,10 +214,10 @@ func (s *Server) routes() {
}
```

The `events.publish("order.placed", …)` line is shown simply here so the focus
The `events.publish("order.placed", …)` line is shown in simplified form here so the focus
stays on the edge concerns; **04 · Data** makes that emission transactionally safe
with the outbox. FastAPI, Spring, and the others also emit an OpenAPI document
for free, which becomes the registered contract.
automatically, which becomes the registered contract.

## gRPC for internal calls

Expand Down
4 changes: 2 additions & 2 deletions _docs/03-composition.md
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ domains must present as one graph and reads dominate.
The anti-pattern to name explicitly is the opposite of all three:
**service-to-service call chains**, where `order` calls `inventory` calls
`pricing` calls… — synchronous, deeply coupled, and impossible to reason about. It
quietly rebuilds a distributed monolith. Composition belongs at the edge, not
rebuilds a distributed monolith. Composition belongs at the edge, not
buried in a chain of internal calls.

## Composition through the coupling lens
Expand Down Expand Up @@ -265,7 +265,7 @@ exactly why it becomes a distributed monolith.

The gateway is just HTTP. Send the same query with `curl` or Postman and request
`stock` in one call, then omit it in another — confirm the gateway only calls
`inventory` when `stock` is actually selected. Then request a *list* of orders with
`inventory` when `stock` is selected. Then request a *list* of orders with
`stock` and watch the back-end call count: without batching it climbs with the
list size; with a DataLoader it stays flat. That flat line is the N+1 fix working.

Expand Down
10 changes: 5 additions & 5 deletions _docs/04-data.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,15 @@ one atomic act.
The first rule of data in a microservice system is the one most often broken:
**each service owns its data, and no other service touches its tables.** Peers reach
an owned dataset only through the owning service's API, never by connecting to its
database. Sharing a database may look like reuse, but it silently couples every
database. Sharing a database may look like reuse, but it couples every
consumer to the owner's schema — a column rename becomes a cross-team migration, and
two services' deploys can no longer move independently. A shared database is the
single most common way a nominally microservice system collapses back into a
distributed monolith.

{% 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 band below reads: polyglot persistence, pick the store per workload (relational, key-value, search, object)."
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" %}

Ownership also frees each service to pick the store that fits its workload —
Expand All @@ -53,7 +53,7 @@ are linked by a stream of changes flowing from the write side to the read side.

The cost is eventual consistency — a small lag between the write committing and
the read model catching up. The benefit is that each side can scale and evolve
independently. Reach for it when reads and writes genuinely have different
independently. Reach for it when reads and writes have different
shapes and access patterns; skip it when a single store handles both happily,
because the separation is not free.

Expand Down Expand Up @@ -104,7 +104,7 @@ Kubernetes the rest of the stack runs on.

> **WAL — Write-Ahead Log.** The database's commit-ordered, durable log. It
> already exists so the database can replicate and recover from a crash;
> Debezium simply subscribes to it.
> Debezium subscribes to it.

## The dual-write bug, and the outbox that fixes it

Expand All @@ -128,7 +128,7 @@ guaranteed, ordered consequence of it.

### The code, in your language

Pick your stack — the tab selection follows you through the rest of the book. The
Pick your stack — the tab selection follows you across all chapters. The
shape is identical everywhere: open one local transaction, persist the state
change, persist the outbox row in the *same* transaction, commit. No broker call
sits on the request path.
Expand Down
Loading
Loading