Claude Skill

sota-architecture

State-of-the-art software and system architecture rules (2026) for both building and auditing. Use when designing, building, refactoring, or extending system architecture — boundaries, DDD, hexagonal/clean architecture, event-driven design, CQRS, sagas, messaging, caching, shardi

LLM Mart · 0 points · 10 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download martinholovsky-sota-skills-skills_sota-architecture-582d6f9.zip · 63 KB
Part of martinholovsky/sota-skills — 39 skills

Install

skills CLI npx skills add https://github.com/martinholovsky/SOTA-skills/tree/main/skills/sota-architecture
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install martinholovsky-sota-skills@llmmart
Git git clone https://github.com/martinholovsky/SOTA-skills.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole martinholovsky/sota-skills collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

SOTA Architecture (2026)

Purpose

Dense, enforceable rules for software and system architecture: choosing styles, drawing boundaries, surviving distributed systems, scaling state, and shipping cloud-natively. One rule set, two modes — apply the rules while building, or check code against them while auditing. All substance lives in rules/; this file routes you to the right one.

BUILD mode — designing or writing code

  1. Identify the decision surface. Before writing code, list the architectural decisions in play (style, boundaries, sync/async, datastore, tenancy, caching). Use the index below to read the relevant rules files before committing to a design — minimum: 01 for any new system/service, 03 for anything crossing a network, 07 always (know what failure looks like).
  2. Default to boring. Modular monolith, one database, sync calls within the deadline budget, queues only where semantics are async. Escalate complexity only when a rule's stated forces apply, and record it.
  3. Write the ADR first for any Type 1 (hard to reverse) decision: context, decision, consequences (must include downsides), rejected alternatives. Put it in docs/adr/.
  4. Apply rules as you code, not after. Timeouts/retries/idempotency at every integration point as you create it; ports before adapters; tenant_id and trace context plumbed from the first commit. Retrofitting these is 10x the cost.
  5. Encode the rules you adopted as fitness functions (import-rule tests, contract tests in CI, SLO alerts) so they survive you.
  6. Before finishing, run the relevant "Audit checklist" sections from each rules file you used against your own output. Fix what fails or document why it's accepted (in the ADR).

AUDIT mode — reviewing existing code or designs

  1. Scope first. Identify what you're auditing (whole system, one service, one PR) and read the matching rules files from the index. For a full architecture audit, work through all seven; for a PR, pick by topic.

  2. Drive from the checklists. Every rules file ends with an "Audit checklist" of yes/no questions — answer each with evidence (file:line, config, trace), never from the README's claims. Use rules/07 detection signals as concrete grep/inspection targets.

  3. Verify mechanically where possible: grep for client construction without timeouts, imports crossing layers, queries missing tenant scoping, latest image tags, dual-writes (DB write + publish in the same function without an outbox), shared DB credentials.

  4. Report every violation as a finding in this exact format:

    [SEVERITY] file:line — Rule violated: <rules-file §section, short rule name>
    Evidence: <what you observed>
    Impact: <what fails and when>
    Fix: <specific, smallest correct remediation>
    Effort: trivial | small | medium | large
    
  5. Severity conventions:

    • Critical — data loss/corruption, cross-tenant leak, full-outage mechanism, money/security path failing open: missing idempotency on payments, business data only in cache, shared-DB writes, liveness probe checking the DB, secrets in git, no owning team.
    • High — outage-magnifier or rollback-blocker: missing timeouts on hot paths, retry multiplication, lockstep deploys, breaking schema changes, sync chains > 2 on revenue paths, DLQ without alerting, missing read-your-writes on user-visible saves.
    • Medium — erodes evolvability/operability: leaky abstractions, anemic core domain, stale feature flags, missing ADRs, event spaghetti without cycles, unscoped shared libs.
    • Low — hygiene: naming drifting from ubiquitous language, missing runbook sections, unjittered crons that haven't yet caused incidents.
    • When in doubt between two severities on a revenue-, security-, or data-touching path, pick the higher.
  6. Summarize findings by severity with counts, then list the top 3 structural themes (not symptoms) and the order to fix them (Critical correctness → rollback/deploy safety → evolvability).

Rules index

File Topics Read this when...
rules/01-architecture-styles-and-decisions.md Modular monolith vs microservices vs serverless, extraction forces, ADRs, evolutionary architecture, fitness functions, Type 1/2 decisions, Conway's law, strangler fig, buy vs build Starting a system, proposing/justifying a service split or merge, reviewing whether the architecture style fits, setting up ADRs or CI architecture gates
rules/02-domain-modeling-and-boundaries.md Bounded contexts, context maps, ubiquitous language, aggregates & invariants, hexagonal ports/adapters, clean-architecture dependency rule, ACLs, domain events, value objects, how absence is encoded — the in-band-sentinel class and its three audit probes, repositories, optimistic concurrency Modeling a domain, defining module/service internals, reviewing layering and imports, fixing anemic models or god aggregates, wrapping vendors
rules/03-distributed-systems-and-events.md CAP/PACELC per-operation, idempotency mechanics, exactly-once myth, outbox/inbox, sagas (orchestration vs choreography, conservative leg ordering), reconciliation against an external system of record, event vs command, CQRS/event-sourcing adoption bar, DLQs, ordering, backpressure, schema evolution, contract tests, IDs & time Anything crosses a network or a queue: designing/reviewing messaging, workflows spanning services, consistency questions, retry/duplicate bugs, integrations with a third party that holds authoritative state, API/event versioning
rules/04-resilience-and-failure-design.md Timeouts & deadline budgets, retries with jitter & budgets, circuit breakers, bulkheads, graceful degradation, fail open/closed, load shedding, liveness vs readiness, chaos engineering, SLOs, safe rollback/restart, stampedes Designing or reviewing any integration point, incident follow-ups, "is this service production-ready", overload or cascading-failure concerns
rules/05-scalability-state-and-data.md Stateless services, autoscaling signals, DB scaling order, replica staleness, caching tiers & invalidation, partitioning/sharding keys, data lifecycle/retention, multi-tenancy models & isolation, workload separation, async heavy work Scaling questions, cache design/review, choosing shard or partition keys, building/auditing multi-tenant systems, read-after-write bugs
rules/06-cloud-native-config-and-delivery.md 12-factor updated 2026: immutable artifacts, config & secrets, GitOps, disposability, observability (logs/metrics/traces/SLOs), backing services & dev/prod parity, feature-flag discipline, progressive delivery, expand/contract migrations, runbooks, cost signals Setting up a new service's operational skeleton, reviewing deployability/config/secrets/flags, CI/CD and migration-safety review
rules/07-anti-patterns-catalog.md Distributed monolith, shared database, god services, premature microservices, leaky abstractions, sync chain of doom, event spaghetti, cache-as-truth, shared common libs, resume-driven design — each with detection signals, fix, default severity Every audit (use as the detection playbook); in BUILD mode as the "never do this" list; naming and severity-rating a smell you've spotted
rules/08-nats-jetstream.md NATS JetStream as primary bus (Go): core NATS vs JetStream, subject/stream design, stream limits & retention/discard, R3/mirrors/sources, publish dedup (Nats-Msg-Id + duplicate window), pull consumers (jetstream pkg), ack/nak/term, MaxAckPending/MaxDeliver, DLQ-via-advisory, KV & Object stores, accounts/domains/leafnodes Designing, building, or auditing anything on NATS JetStream — streams, consumers, KV/Object stores, multi-tenant accounts; apply on top of rules/03 (general eventing) for JetStream-specific config and Go client mechanics

Top-10 non-negotiables

Check these on every build and every audit, regardless of scope:

  1. Every remote call has an explicit timeout, sized from measured latency, fitting the edge deadline budget. (04 §1)
  2. Every message handler and retried operation is idempotent, with dedupe state committed atomically with the state change. (03 §2)
  3. No dual-writes: DB state change + event publish happen via outbox/CDC, never as two independent writes. (03 §4)
  4. One writer-owner per table; services never integrate through a shared database. (07 §2)
  5. Dependencies point inward: domain imports no framework/ORM/vendor/transport types — enforced by an automated architecture test, not convention. (02 §5)
  6. Retries are bounded, jittered, idempotent-only, and owned by exactly one layer per edge. (04 §2)
  7. Every queue has bounded size, a DLQ with alerting, and a tested redrive procedure. (03 §7)
  8. Tenant/user scoping is enforced below application code (RLS or mandatory scoped layer) and present in every query, cache key, message, and log line. (05 §7)
  9. Cross-service contracts (APIs and events) are versioned with CI compatibility checks; changes are expand/contract; previous code version must still run (rollback-safe). (03 §8, 06 §7)
  10. Every Type 1 decision has an ADR with consequences and rejected alternatives, and every prod component has exactly one owning team. (01 §4, §7)

A violation of any of these is at minimum High severity; most are Critical on data-, money-, or security-touching paths.

Files (sota-skills)
  • rules
    • 01-architecture-styles-and-decisions.md 17.9 KB
      # 01 — Architecture Styles & Decision-Making
      
      Rules for choosing an architecture style, recording decisions, and keeping the
      architecture evolvable. Apply before writing the first service; re-apply at every
      major boundary change.
      
      ## 1. Default to a modular monolith
      
      **Rule:** Start every new system as a modular monolith: one deployable, internal
      modules with explicit boundaries, separate schemas-per-module inside one database.
      Extract services only when a *measured* force demands it.
      
      **Rationale:** Network boundaries are the most expensive boundaries you can buy.
      They convert function calls into failure modes (latency, partial failure, retries,
      versioning). A modular monolith gives you boundary discipline at refactor cost,
      not distributed-systems cost.
      
      **Forces that justify extraction (need at least one, measured):**
      - Independent scaling: one module needs 50x the replicas of the rest.
      - Independent deployment cadence blocked by org structure (multiple teams stepping on one release train).
      - Divergent runtime needs (GPU inference vs CRUD; different language/runtime).
      - Hard fault isolation requirements (a crash in module A must not take down module B).
      - Regulatory isolation (data residency, PCI scope reduction).
      
      **Never extract because:** "microservices are best practice", résumé pressure,
      "we might need to scale later", or to fix a tangled codebase (you'll get a
      tangled distributed system — see rules/07, distributed monolith).
      
      ```text
      GOOD: one repo, one deploy
        app/
          billing/      (public API: billing/api.*, everything else internal)
          catalog/
          shipping/
        Module imports only other modules' api/ surface. CI fails on deep imports.
      
      BAD: 14 services, one team of 5, shared DB, lockstep deploys.
      ```
      
      ## 2. Enforce module boundaries mechanically
      
      **Rule:** Boundaries that aren't enforced by tooling don't exist. Use import
      linting / architecture tests (dependency-cruiser, ArchUnit, deptrac, Nx tags,
      Go internal/ packages, or equivalent) in CI. A human code-review rule is not
      enforcement.
      
      **Rule:** Each module exposes exactly one public surface (an `api`/facade
      package and/or published events). Cross-module calls go through that surface.
      Cross-module data access goes through that surface — never through the other
      module's tables.
      
      ## 3. Choose style by problem shape, not fashion
      
      | Style | Wins when | Loses when |
      |---|---|---|
      | Modular monolith | Small/medium team, evolving domain, unknown load profile | Genuinely independent scaling/deploy needs across teams |
      | Microservices | Many teams, independent deploy cadence, per-service scaling, mature platform (CI/CD, observability, on-call) | Team < ~3 squads, no platform engineering, chatty domain |
      | Serverless (FaaS) | Spiky/bursty load, event glue, low ops budget, embarrassingly stateless handlers | Long-lived connections, latency-critical p99 (cold starts), heavy local state, cost at sustained high throughput |
      | Event-driven backbone | Many consumers per fact, audit/replay needs, temporal decoupling | Request/response semantics forced through events (see rules/03) |
      
      **Rule:** Serverless is an operational model, not an architecture. You still owe
      module boundaries, idempotency, and observability. A pile of 200 lambdas sharing
      a database is a distributed monolith with cold starts.
      
      **Rule:** Never mix request/response and event-driven semantics blindly. Decide
      per interaction: does the caller need an answer now (sync), or is it publishing
      a fact (async)? Commands that need answers are sync or async-with-correlation;
      facts are events.
      
      ## 4. Record every significant decision as an ADR
      
      **Rule:** Any decision that is expensive to reverse (datastore, message broker,
      service boundary, auth model, multi-tenancy model, sync vs async for a flow)
      gets an Architecture Decision Record before implementation. Store ADRs in the
      repo (`docs/adr/NNNN-title.md`), immutable once accepted; supersede, never edit
      history.
      
      **Minimum ADR format:**
      
      ```text
      # NNNN: Use outbox pattern for order events
      Status: Accepted (2026-03-02)  Supersedes: 0007
      Context: Orders must emit events; dual-write to DB + broker loses events on crash.
      Decision: Write events to outbox table in the same TX; relay publishes async.
      Consequences: + atomicity, + replay; - eventual consistency (~1s lag), - relay to operate.
      Alternatives considered: CDC (Debezium) — rejected: no ops capacity for Kafka Connect.
      ```
      
      **Rationale:** ADRs are the only durable defense against re-litigating decisions
      and against cargo-culting old constraints after they expire. "Why is it like
      this?" must have a greppable answer.
      
      **Rule:** An ADR without a *Consequences* section listing at least one downside
      is marketing, not a decision record. Reject it in review.
      
      **Directory discipline.** Sequential kebab-case filenames
      (`0012-use-outbox-for-order-events.md`) plus an `index.md` holding one row per
      ADR — number, title, status, date. The index is what makes the practice
      auditable at a glance: a status column that is all `proposed` is a stalled
      process, and a superseded chain is legible without opening a file. Commit the
      ADR **in the PR that implements the decision** where one exists, so rationale
      and code land together; a pre-implementation decision lands on its own.
      
      ## 4a. A control's cost profile must be checked against decisions already in force
      
      A new control that consumes a **scarce resource on every use** — a hardware touch, a human
      approval, an interactive prompt, a rate-limited or paid API call — can silently reintroduce
      the exact friction an earlier decision removed. Both artifacts are locally correct; the
      contradiction lives in the gap between them, and no test sees it.
      
      Field-reported: a repository decided, with the reasoning written into `CONTRIBUTING`, to
      stop signing every commit because the key sat on a touch-required token and the tap per
      commit blocked automated work. About an hour later, in the same session, a gate ledger was
      built whose `anchor` command signed its chain head with that same key — and anchoring was
      recommended after every push. Every gate passed, every test passed, and the user found it.
      
      **At the point of adding the control, write one line: *this costs X, Y times per Z*. Then
      grep the repo's own decision records (§4) for X.** If a decision already rejected X at that
      frequency, either the new control is wrong or the old decision needs revisiting — the two
      cannot both stand unexamined. The tell that you missed it is a user asking *"why am I being
      asked for this again?"*, which is late and expensive.
      
      This is a **coherence** failure, not a code defect, so it is invisible per-artifact and to
      per-file review. It is worst in agent-authored work: an agent can hold both decisions in
      one context and still miss the interaction, because attention is on the artifact being
      built, not on the policy it lands inside.
      
      ## 4b. A deferral is a standing question — give it somewhere to accumulate answers
      
      **Rule:** When a decision record defers an idea behind a condition ("revisit if…",
      "reconsider when we see…", "not yet — wait for a second case"), the record must also say
      **where the evidence toward that condition accrues, and who reads it**. A trigger with no
      evidence store is not a decision that will be revisited; it is a decision that will be
      re-derived from scratch by whoever next asks, at full cost, with no memory of the last
      answer.
      
      **Rationale:** The deferral itself is usually right — one instance is a trade-off, not a
      pattern, and generalising from it produces rules that fit one project. What fails is the
      return path. The evidence arrives incrementally, months apart, in places the record does
      not watch; each near-miss is recognised, judged not-quite-enough, and forgotten. The next
      person repeats the whole search to reach the same verdict, so the deferral never converges
      either way. Cost is asymmetric and invisible: writing the trigger takes a sentence, and
      answering it can take a full sweep of everything logged since.
      
      **What the record must carry**, next to the trigger:
      
      - **The trigger in falsifiable terms.** "A second implementation *ships* this design" is
        checkable; "if it becomes a problem" is not, and cannot ever be closed.
      - **A running list of candidates checked, with dates and verdicts** — including the ones
        that *did not* count. A near-miss and a deliberate refusal are both evidence about the
        class, and both are lost by default.
      - **What the trigger would buy.** If part of the idea is already covered elsewhere, say
        which part, so a future instance is judged on the remainder rather than re-argued whole.
      
      **The same rule covers a deliberately-unfixed state kept as evidence** — a known-bad left
      in place to prove a control fires, a pin left stale so an automated bump proves the
      automation runs. That is a good technique and an experiment, so it carries an experiment's
      obligation: write down where the result will show up and who looks. **An experiment with
      no scheduled read-back is indistinguishable from a note**, and it decays the same way —
      the state resolves, nobody returns, and the record still describes the world before the
      answer arrived. Reviewers reading it then act on a question that is already closed.
      
      **Smell:** a decisions log where several entries say "revisit when…" and none of them has
      ever been revisited. Check the dates: if the oldest trigger predates the last two people
      who joined, the log is recording intent, not process.
      
      ## 5. Practice evolutionary architecture with fitness functions
      
      **Rule:** Encode architectural qualities as automated, continuously-run checks
      (fitness functions). If a quality matters, test it; if you can't test it, you
      can't claim it.
      
      **Examples of fitness functions (run in CI or scheduled):**
      - Dependency direction: "no module imports `billing/internal`" — architecture test.
      - Coupling budget: cyclic dependencies between modules = build failure.
      - Latency: p99 of checkout API < 300 ms under k6 load profile — perf gate on main.
      - Resilience: weekly chaos run kills one replica of each service; SLOs must hold (see rules/04).
      - Cost: per-tenant infra cost stays under $X — scheduled report with alert.
      - Schema safety: migration linter forbids destructive DDL without expand/contract.
      
      **Rule:** When two qualities conflict (e.g., consistency vs availability), the
      ADR picks the winner per context; the fitness function enforces the chosen
      trade-off, not both.
      
      ## 6. Plan for reversibility; classify decisions by exit cost
      
      **Rule:** Classify every decision: **Type 1** (hard to reverse: datastore,
      broker, cloud provider, tenancy model, public API contract) vs **Type 2**
      (cheap to reverse: library, internal interface, queue topology detail).
      Spend design effort proportionally. Type 2 decisions get minutes and a code
      comment; Type 1 decisions get an ADR, a spike, and an exit strategy.
      
      **Rule:** For every Type 1 dependency, write down the exit strategy in the ADR
      ("we wrap the broker behind `EventBus` port; migration = reimplement adapter +
      dual-publish for N days"). Don't build a full abstraction layer preemptively —
      a thin port is enough (see rules/02 on ports).
      
      ## 7. Conway's law: design team and system boundaries together
      
      **Rule:** Service boundaries that cross team boundaries will erode. Align one
      service (or module) to one owning team; shared ownership means no ownership.
      If the org chart and the architecture disagree, change one of them deliberately
      (inverse Conway maneuver) — don't let the disagreement fester.
      
      **Rule:** Each module/service has a single on-call/owning team recorded in a
      machine-readable catalog (`catalog-info.yaml`, CODEOWNERS, or equivalent).
      "Orphaned service" is a Critical audit finding.
      
      ## 8. Extraction playbook (monolith → service)
      
      **Rule:** Extract via strangler fig, never big-bang rewrite:
      1. Harden the module boundary in-process (own schema, api-only access, events).
      2. Add an anti-corruption layer at the seam if models differ.
      3. Route a slice of traffic to the new service behind a flag; compare outputs (shadow/dark launch).
      4. Migrate data with expand/contract: dual-write or CDC, backfill, verify, cut over, contract.
      5. Delete the old path. Extraction isn't done until the old code is deleted.
      
      **Rule:** Never extract two things at once (e.g., new service AND new datastore
      AND new language). One variable per migration.
      
      ## 9. Buy/adopt vs build
      
      **Rule:** Build only what differentiates the business. Auth, payments, search,
      feature flags, observability, workflow engines: adopt proven solutions and wrap
      them behind a thin port. Building commodity infrastructure is a Type 1 decision
      disguised as a weekend project.
      
      **Rule:** Adopted dependencies still get an ADR (lock-in is a consequence) and
      a fitness function (e.g., "all calls to vendor X go through adapter Y" —
      architecture test).
      
      ## 10. Edge composition: gateways and BFFs
      
      **Rule:** Clients never call internal services directly. Put an API gateway at
      the edge for cross-cutting concerns (authn, rate limiting, TLS, routing) and —
      when client needs diverge (mobile vs web vs partner API) — a Backend-for-
      Frontend per client class that composes internal calls into client-shaped
      responses.
      
      **Rule:** Keep business logic out of the gateway. A gateway that transforms
      payloads, enforces domain rules, or orchestrates workflows is an unowned god
      service (rules/07 §3) written in YAML. Gateways route and protect; BFFs compose;
      services decide.
      
      **Rule:** Each BFF is owned by the client team it serves. A shared BFF for all
      clients recreates the coupling BFFs exist to remove.
      
      ## 11. Diagrams and design docs as code
      
      **Rule:** Maintain a current C4-style picture: one system-context diagram and
      one container diagram minimum, stored in the repo as text (Mermaid, Structurizr
      DSL, PlantUML) so diffs are reviewable and rot is visible in PRs. A diagram in a
      wiki dies in a quarter; a diagram in the repo dies in review.
      
      **Rule:** Significant designs get a short RFC/design doc *before* implementation
      (problem, constraints, options, recommendation), circulated to affected teams
      with a comment deadline. The ADR (§4) records the outcome; the RFC records the
      debate. Skip the RFC for Type 2 decisions — process must be proportional too.
      
      ## 12. Sacrificial architecture and rewrite discipline
      
      **Rule:** Accept that successful systems outgrow their architecture (~10x scale
      changes the right design). Write code expecting parts to be replaced: boundaries
      and contracts are the durable assets, implementations are sacrificial. Optimize
      boundary quality over implementation polish.
      
      **Rule:** Never green-light a full rewrite while the old system keeps taking
      features ("second-system" trap). A rewrite must be: scoped to one bounded
      context at a time, strangler-style (§8), with a feature freeze on the replaced
      slice and a kill date for the old path. "Big rewrite, both evolve in parallel"
      fails at a rate that rounds to always.
      
      ## 13. Architecture review cadence
      
      **Rule:** Review the architecture on triggers, not just calendars: 10x traffic
      growth, new compliance regime, team doubling, p99 SLO erosion two quarters
      running, or a third incident with the same structural cause. Each review:
      re-validate prior ADRs' assumptions (load numbers, team size, vendor constraints)
      and explicitly supersede the ones whose context expired.
      
      **Rule:** Track architecture debt in the same backlog as features, each item
      tied to a measurable symptom (incident class, lead-time drag, cost line).
      "Refactor someday" items without symptoms get deleted, not hoarded.
      
      ## Audit checklist
      
      - [ ] **Every deferral with a "revisit if…" trigger names where the evidence accrues and
            who reads it** (§4b), and the trigger is stated so that some observation could close
            it. Probe: list every deferred entry, then ask when each was last checked and what
            was found — a trigger nobody has evaluated since it was written, or one no evidence
            could ever satisfy, is a finding. Same probe for a known-bad deliberately left in
            place as proof a control fires: if no one can say where the result appears, it is a
            note, not an experiment.
      
      - [ ] **Every control that spends a scarce resource per use states its cost profile**
            (*what, how often*), and that frequency was checked against the ADRs and contributor
            docs (§4). A control whose per-use cost contradicts a decision already in force is a
            finding even though it works.
      
      - Is there a written rationale (ADR) for the current architecture style, with consequences and alternatives?
      - Could this system be a modular monolith? If it's microservices, can the team name the measured force that justified each extraction?
      - Are module/service boundaries enforced by CI tooling (import rules, architecture tests), not just convention?
      - Does any module reach into another module's internals or tables directly?
      - Do services deploy independently in practice (check release history), or do they ship in lockstep?
      - Is every Type 1 decision (datastore, broker, tenancy, public contracts) covered by an ADR with an exit strategy?
      - Are ADRs immutable and superseded rather than edited? Is the most recent ADR less than ~3 months old (i.e., is the practice alive)?
      - Are architectural qualities (latency, coupling, resilience, cost) encoded as automated fitness functions that run in CI or on a schedule?
      - Does every service/module have exactly one owning team, recorded machine-readably?
      - Were recent extractions done strangler-style with a deleted old path, or do zombie code paths remain?
      - Is any commodity capability (auth, flags, queues, search) hand-built without an ADR justifying it?
      - Do sync vs async interaction choices match the semantics (answers vs facts), or are request/response flows tunneled through events?
      - Do clients reach internal services directly, or through a gateway/BFF? Does the gateway contain business logic?
      - Are context/container diagrams stored as text in the repo and current (spot-check three services against the diagram)?
      - Is any rewrite running big-bang style with parallel feature development on old and new?
      - Do superseded ADRs exist (evidence assumptions get re-validated), or has nothing been revisited since launch?
      
    • 02-domain-modeling-and-boundaries.md 18.8 KB
      # 02 — Domain Modeling & Boundaries (DDD, Hexagonal, Clean Layering)
      
      Rules for carving the domain into bounded contexts, modeling aggregates, and
      keeping dependencies pointed the right way. These rules apply identically inside
      a monolith module and across services.
      
      ## 1. Bounded contexts first, services second
      
      **Rule:** Identify bounded contexts (areas where a model and its language are
      internally consistent) before drawing any service or module boundary. A service
      boundary that splits a bounded context, or fuses two, will generate chatty calls
      and translation bugs forever.
      
      **Rule:** One term, one meaning per context. If "Customer" means payer in
      Billing and recipient in Shipping, those are two models in two contexts —
      never one shared `Customer` class with 40 nullable fields.
      
      ```text
      BAD:  shared `Customer` entity used by Billing, Shipping, Support
            → every change fans out to all three; fields nobody owns.
      GOOD: Billing.Payer, Shipping.Recipient, Support.Contact
            Each context maps from `CustomerRegistered` events into its own model.
      ```
      
      **Rule:** Maintain a context map: which contexts exist, who owns each, and the
      relationship type at every seam (customer/supplier, conformist, anti-corruption
      layer, published language, separate ways). Unlabeled seams default to accidental
      conformist coupling.
      
      ## 2. Use ubiquitous language end to end
      
      **Rule:** Code, schema, APIs, and events use the domain experts' vocabulary
      exactly. If the business says "quote expires," the code says `quote.expire()`,
      not `setStatus(3)`. Translation between business language and code language is
      where requirements bugs breed.
      
      **Rule:** When the business distinguishes two things, the model distinguishes
      them (no boolean flags standing in for missing concepts: `isReturn && isExchange`
      means you're missing a `ResolutionType`).
      
      ## 3. Aggregates: small, consistent, the only write path
      
      **Rule:** An aggregate is a consistency boundary: the set of objects that must
      be transactionally consistent with each other. Keep aggregates small — usually
      one entity plus value objects. Big aggregates serialize writes (lock contention)
      and bloat loads.
      
      **Rule:** One transaction modifies one aggregate. Cross-aggregate consistency is
      eventual, coordinated by domain events or sagas (rules/03). If you "need" to
      update two aggregates atomically, your boundary is wrong or the invariant is
      weaker than you think — interrogate the invariant first.
      
      **Rule:** Reference other aggregates by ID, never by object pointer. Loading an
      order must not drag the customer, the product catalog, and the warehouse into
      memory.
      
      **Rule:** All writes go through aggregate methods that enforce invariants.
      No service-layer code mutating aggregate fields directly; no "anemic" model
      where entities are bags of getters and the rules live in 12 service classes.
      
      ```text
      GOOD: order.addLine(product_id, qty)   # enforces max-lines, status checks inside
      BAD:  order.lines.append(line); order.total += price   # invariants enforced nowhere
      ```
      
      **Rule:** Enforce uniqueness and cross-aggregate invariants at the right layer:
      DB unique constraints for identity invariants; reservation patterns or eventual
      checks for the rest. Don't pretend an aggregate can guard an invariant spanning
      millions of rows.
      
      ## 4. Hexagonal architecture: ports and adapters
      
      **Rule:** The domain core depends on nothing but itself. Define **ports**
      (interfaces the core needs: `OrderRepository`, `PaymentGateway`, `Clock`,
      `EventPublisher`) inside the core; implement **adapters** (Postgres, Stripe,
      Kafka, system clock) outside it. Frameworks, ORMs, and SDK types never appear
      in core signatures.
      
      **Rationale:** This is what makes the core testable without infrastructure and
      makes Type 1 dependencies (rules/01 §6) swappable. It is also the cheapest
      insurance against vendor lock-in you can buy.
      
      **Rule:** Ports are defined by the consumer's need, not the provider's API.
      `PaymentGateway.charge(order_id, amount) -> ChargeResult`, not a passthrough of
      Stripe's 40-field request object. A port that mirrors the vendor SDK is a leaky
      abstraction (rules/07).
      
      ```text
              inbound adapters              core                outbound adapters
        HTTP handler ──► CommandHandler ──► Domain ──► OrderRepository (port)
        Kafka consumer ─► (use case)        model      └─► PostgresOrderRepo (adapter)
                                                └─► PaymentGateway (port)
                                                     └─► StripeAdapter
      ```
      
      ## 5. Layering and the dependency rule
      
      **Rule:** Dependencies point inward only: `adapters → application (use cases) →
      domain`. Nothing in domain imports application; nothing in application imports
      adapters. Enforce with architecture tests (rules/01 §2), not convention.
      
      **Rule:** Use cases (application services) orchestrate: load aggregate, call
      domain method, persist, publish events. They contain no business rules and no
      I/O details. If a use case has an `if` encoding a business policy, push it into
      the domain; if it builds SQL, push it into an adapter.
      
      **Rule:** Don't over-layer. A CRUD-only context doesn't need aggregates, ports,
      and four layers — a transaction script over a table is honest and cheaper.
      Match modeling rigor to domain complexity: core domains get full DDD; supporting
      and generic subdomains get the simplest thing that works. Applying ceremony
      uniformly is itself an anti-pattern.
      
      ## 6. Anti-corruption layers at external seams
      
      **Rule:** Every integration with an external system or legacy model goes through
      an anti-corruption layer (ACL): a translator that maps their model to yours at
      the boundary. External DTOs, vendor enums, and legacy field names stop at the
      ACL; they never propagate into the domain.
      
      **Rule:** When consuming another context's events, translate to your own types
      in the consumer adapter. Storing someone else's event payload as your domain
      state couples your model to their release schedule.
      
      ## 7. Domain events as first-class outputs
      
      **Rule:** Aggregates record domain events (`OrderPlaced`, `QuoteExpired`) as
      part of their state changes; the application layer publishes them after commit
      (via outbox — rules/03 §4). Events are named in past tense, in ubiquitous
      language, and carry the data consumers need without forcing a callback query
      for every field ("event-carried state transfer" for hot fields, IDs for the rest).
      
      **Rule:** Events published across context boundaries are a public contract:
      versioned, schema-checked, additive-only changes by default (see rules/03 §8).
      Internal events can change freely; published ones cannot.
      
      ## 8. Value objects and invariant-encoding types
      
      **Rule:** Model domain values as types that cannot exist in invalid states:
      `Money(amount, currency)`, `EmailAddress`, `DateRange(start <= end)`. Parse,
      don't validate — convert raw input into rich types once at the boundary, then
      trust the types everywhere inside.
      
      **Rule:** Never represent money as a float, identity as a bare string/int passed
      through five layers, or a state machine as a string column with stringly-typed
      transitions. Encode the state machine: explicit states, explicit allowed
      transitions, transition methods on the aggregate.
      
      ## 8a. Absence is not a value — the in-band sentinel
      
      **Rule:** Encode "absent", "unknown" and "failed" **outside** the value's domain —
      `Option`/`Maybe`, a nullable type, a second return value, `NULL`, an omitted
      property, or an exception. Never a member of the domain itself: `-1`, `0`, `""`,
      `9999-12-31`, `0.0.0.0`, `MAX_INT`.
      
      It is the same defect as a stringly-typed state machine (§8), and it is harder to
      see because it **type-checks**. Three consequences, none of which any compiler
      reports:
      
      - **Distinct causes collapse into one value.** *Absent* and *malformed* returned as
        the same constant can never be told apart, counted, or alerted on downstream — so
        a parser regression upstream is indistinguishable from ordinary sparse data.
      - **Presence checks silently pass.** `if x:` / `if (x)` reads as "do I have a
        value?" and admits `-1`, which is truthy in every language that has a truthiness
        rule. `0` fails the same check on a legitimate value. Neither spelling can work,
        because the value carries no absence information to test.
      - **It has an ordering, so it changes answers rather than crashing.** A sentinel
        **loses** every `<` against a real value and **wins** every `>`. Wherever a
        number is compared as a proxy for sequence — line/offset numbers for "happens
        before", version numbers, timestamps-as-ints, retry counts — one missing operand
        flips the predicate, and *which way* depends on which side went missing:
      
        ```text
        use(-1) > alloc(20)  -> False   # fails closed: the real relation is not reported
        use(20) > alloc(-1)  -> True    # fails OPEN: reported, on an unknown operand
        ```
      
        Both from one input, in one comparison. (Measured, Python 3.14; the arithmetic is
        the same in every language whose sentinel is an ordinary number.)
      
      **Rule:** When a wire format, a fixed-width store, or a stdlib you don't own forces
      a sentinel, it is **per field with its domain written down** — not one constant
      applied across a set of fields whose domains differ. That last shape is the common
      one and the unrecoverable one: on a field where the producer *also* emits the
      sentinel legitimately, a stored value is ambiguous forever and no downstream care
      recovers it.
      
      **Auditing it.** The constant is a poor signal — it is everywhere and usually fine.
      Three probes, decreasing precision:
      
      1. **Producer:** one function returning the *same* constant from a not-found branch
         and from an error/`except` branch. Near-zero false positives; fix it once and
         every caller is fixed.
      2. **Asymmetric guard** — the highest-yield tell. A comparison where **one operand
         is filtered against the sentinel and the other is not**
         (`min(x for x in xs if x > 0) < line_num`, where `line_num` is unfiltered).
         Sentinel-filtering is applied per site, so it lands wherever the author was
         thinking about it and is omitted everywhere else. Visible in a diff.
      3. **Truthiness-as-presence:** `if n:` guarding a number whose domain includes the
         sentinel. High recall, high false-positive rate — use it to build a reading
         list, not a gate.
      
      **A value-based lint is only sound on a field with a stated non-negative (or
      otherwise sentinel-free) domain.** Measure the per-field distribution before
      linting; a field where the sentinel is 99% of rows and one where it is 0% are
      different problems, and a field whose producer emits it legitimately makes such a
      lint 100% false-positive. Language-specific stdlib traps and the idiomatic
      alternative: each `sota-<language>` skill. Persistence: `sota-databases` rules/01.
      
      ## 9. Repositories and persistence
      
      **Rule:** One repository per aggregate (not per table). Repositories return
      whole aggregates, accept whole aggregates, speak domain language
      (`findOverdueInvoices()`, not `query(sql)`).
      
      **Rule:** The database schema serves the aggregate, not the other way around.
      ORM convenience (lazy loading across aggregates, shared base entities, bi-
      directional mappings spanning contexts) must not redraw your consistency
      boundaries. If the ORM fights the aggregate shape, map manually at the adapter.
      
      **Rule:** Use optimistic concurrency (version column) on aggregates by default.
      Lost updates are a modeling failure, not a tuning detail.
      
      ## 10. Explicit state machines
      
      **Rule:** Any entity with a lifecycle (order, subscription, claim, deployment)
      gets an explicit state machine: enumerated states, enumerated transitions,
      transition methods that reject invalid moves, and a domain event per transition.
      
      ```text
      GOOD:
        states: Draft -> Submitted -> Approved -> Fulfilled
                            └-> Rejected (terminal)
        order.approve(approver)   # raises if state != Submitted; emits OrderApproved
      
      BAD:
        order.status = "approved"   # any code can set any string at any time
        if order.status in ("approved", "aproved", "APPROVED"): ...
      ```
      
      **Rule:** Persist the state machine honestly: a `state` column with a CHECK/enum
      constraint, plus a transition history table (who/when/why) for anything audited
      or money-touching. Reconstructing "how did it get into this state" from logs is
      not a design.
      
      ## 11. Domain services, factories, and where logic goes
      
      **Rule:** Logic placement, in priority order: (1) value object, (2) aggregate
      method, (3) domain service — only for rules genuinely spanning aggregates
      (`PricingPolicy`, `TransferService`), (4) application use case — orchestration
      only. Each step down loses cohesion; justify the descent.
      
      **Rule:** Complex aggregate creation goes through a factory (function or class)
      that returns only valid instances; "create empty then set fields" construction
      makes invalid intermediate states representable and they will escape.
      
      **Rule:** Resist `Helper`, `Manager`, `Util`, `Processor` classes in the domain.
      Each is usually an aggregate method or value object that hasn't found its home;
      the name is the smell.
      
      ## 12. Queries and read models inside the architecture
      
      **Rule:** Don't force reads through aggregates. Queries that cross aggregates
      (dashboards, lists, search) bypass the domain model via dedicated read models /
      query services that return DTOs — read side may join freely against tables or
      projections (lightweight CQRS, see rules/03 §6). Loading 500 aggregates to
      render a list is the standard self-inflicted performance bug of strict-DDD
      codebases.
      
      **Rule:** Read models never feed writes. A write decision is made by loading the
      aggregate (current, invariant-protected state), not by trusting a possibly-stale
      projection that a list view rendered.
      
      ## 13. Sizing modules and avoiding entity-service decomposition
      
      **Rule:** Cut boundaries around *capabilities and change-reasons*
      ("Pricing", "Fulfillment", "Risk"), not around nouns ("UserService",
      "OrderService", "ProductService"). Noun-services force every real use case to
      choreograph three services and own no invariant (rules/07 §13).
      
      **Rule:** A bounded context should be explainable in one sentence naming its
      responsibility and its key invariants. If the sentence needs "and", consider
      splitting; if two contexts' sentences keep mentioning each other, consider
      merging or formalizing the seam (customer/supplier with a contract).
      
      ## 14. The testability boundary (humble object)
      
      **Rule:** Every system has a shell that automated tests cannot meaningfully
      drive — GUI and rendering, external devices, real clocks and networks, process
      spawning, anything that opens a window, needs a display, or hangs without one.
      Draw that boundary **explicitly** and keep the shell **thin**: all decisions
      move inward to the testable core, and the shell degenerates into a translator
      with no branches worth asserting (the "humble object"). This is §4's adapter
      rule stated as a testability constraint rather than a dependency one.
      
      **Rule:** No business `if` in the shell. A conditional, loop, or calculation in
      a UI callback, device driver wrapper, or CLI entrypoint is logic that has
      escaped the core — move it in and leave a call behind.
      
      **Rule:** Make the boundary **machine-readable** — a package/module split the
      build understands, not a convention in someone's head. Coverage, mutation, and
      complexity tooling then measure the core only. Measuring the untestable shell
      distorts every number in both directions and buries the signal (see
      `sota-testing` rules/07 §7.2 and rules/06 §6.3).
      
      **Rationale:** this is the precondition for any test metric meaning anything.
      "62% coverage" over a codebase where a third is an untestable UI shell is not
      a number, it's noise; the same 62% over an explicitly bounded core is a signal
      you can act on. Teams that skip this step end up either gaming the metric or
      disbelieving it — and disbelieving it is the more expensive outcome.
      
      ```text
      BAD:  render_invoice_row(inv):          # in the view layer
              if inv.total > credit_limit and not inv.customer.is_exempt:
                badge = "OVER LIMIT"          # a business rule, untestable without a UI
      
      GOOD: core:  invoice.limit_status(customer) -> OverLimit | Ok   # unit-tested
            shell: render_invoice_row(inv) -> badge_for(inv.limit_status(cust))
                   # no branch of its own; one smoke test proves it is wired
      ```
      
      **Rule:** The shell still needs *some* verification — a small number of
      end-to-end or smoke tests that prove it is wired up (`sota-testing` rules/05),
      not unit metrics. And treat shell growth as an **architecture** finding: if the
      untestable region keeps expanding, logic is leaking outward, and no amount of
      test discipline will reach it.
      
      ## Audit checklist
      
      - [ ] Absence/unknown/failure encoded **outside** the value domain (option type,
            nullable, second return value, `NULL`, omitted property, exception) — no
            `-1`/`0`/`""`/`9999-12-31` sentinels. Where one is forced: per-field domain
            documented, and the **comparison** sites audited first (§8a).
      - Can the team produce a context map? Are seam relationship types (ACL, conformist, published language) explicit?
      - Does any domain term ("customer", "order", "account") have different meanings sharing one class/table across contexts?
      - Are aggregates small, referenced by ID, and modified one-per-transaction? Search for transactions touching multiple aggregates.
      - Are business invariants enforced inside aggregate methods, or scattered across service classes (anemic model)?
      - Does domain code import frameworks, ORM types, vendor SDKs, or transport types (HTTP requests, Kafka records)?
      - Are ports defined by consumer need, or do they mirror vendor APIs one-to-one?
      - Is the dependency rule (adapters → application → domain) enforced by an automated architecture test?
      - Do external/legacy models cross into the domain without an anti-corruption layer?
      - Are cross-context events versioned, past-tense, schema-validated public contracts?
      - Is money a float anywhere? Are state machines stringly-typed with unguarded transitions?
      - Is modeling rigor proportional: full DDD on core domains, simple transaction scripts on CRUD subdomains — or uniform ceremony/uniform mud?
      - Do repositories use optimistic concurrency, or can concurrent writers silently lose updates?
      - Are entity lifecycles explicit state machines with guarded transitions and history, or free-form status strings?
      - Do `Helper`/`Manager`/`Util` classes in the domain hold logic that belongs on aggregates or value objects?
      - Do list/dashboard reads go through dedicated read models, or do they load aggregates in bulk?
      - Do any write decisions consume read-model/projection data instead of loading the aggregate?
      - Are services/modules named after capabilities or after entities (noun-services)?
      - Is the boundary between the testable core and the environment-bound shell (GUI, device, IO, process spawn) explicit in the build, or only in convention? Does business branching live in that shell?
      - Does the shell keep growing release over release (logic leaking outward), and do coverage/mutation/complexity tools measure the core or the whole tree?
      
    • 03-distributed-systems-and-events.md 22 KB
      # 03 — Distributed Systems, Messaging & Eventing
      
      Rules for any system where two components are separated by a network. The
      network will partition, messages will duplicate and reorder, clocks will lie.
      Design for that on day one; it cannot be patched in later.
      
      ## 1. CAP/PACELC: pick trade-offs per operation, not per system
      
      **Rule:** During a partition you choose consistency or availability; even
      without a partition you choose latency or consistency (PACELC). Make the choice
      *per operation*, write it down (ADR), and verify the datastore's defaults match.
      "We use a CP database" is not a design; "balance reads are linearizable,
      product-view reads are eventually consistent with ≤5 s staleness" is.
      
      **Rule:** Never assume read-your-writes across replicas or caches. If a flow
      needs it (user saves, next page shows the save), engineer it explicitly: sticky
      session to primary, version-pinned reads, or write-through cache.
      
      **Rule:** Distrust distributed transactions (2PC/XA) across heterogeneous
      systems. Coordinators block on failure and availability collapses to the worst
      participant. Prefer sagas (§5) and the outbox (§4).
      
      ## 2. Idempotency is mandatory, everywhere
      
      **Rule:** Every message handler, every retried API call, and every job must be
      safe to execute at least twice. Delivery guarantees are at-least-once in
      practice (§3), so non-idempotent consumers corrupt data — it's a when, not an if.
      
      **Mechanisms, in order of preference:**
      1. Naturally idempotent operations (`set status = 'shipped'`, upsert by key).
      2. Idempotency key: caller supplies a key; handler records `(key → result)` and
         replays the stored result on duplicates. Store key + result *in the same
         transaction* as the state change.
      3. Version/sequence checks: reject events older than current aggregate version.
      
      ```text
      GOOD (dedupe inside the TX):
        BEGIN
          INSERT INTO processed(msg_id) VALUES ($id)   -- unique constraint
            ON CONFLICT DO NOTHING; IF no row inserted: ROLLBACK, ACK, return
          ...apply state change...
        COMMIT; ACK
      
      BAD: if redis.exists(msg_id): skip      -- check and write not atomic;
           apply(); redis.set(msg_id)         -- crash between = duplicate apply
      ```
      
      **Rule:** Public write APIs accept an `Idempotency-Key` header (or equivalent)
      and document retention. Payment-ish operations without idempotency keys are a
      Critical finding.
      
      ## 3. Exactly-once delivery is a myth; exactly-once *processing* is yours to build
      
      **Rule:** No broker gives you end-to-end exactly-once across your database and
      side effects. "Exactly-once" broker features (Kafka transactions) cover
      broker-internal read-process-write only. Your contract is: at-least-once
      delivery + idempotent processing = effectively-once outcomes. Any design doc
      that depends on exactly-once delivery is wrong; fix the design, not the broker.
      
      **Rule:** Acknowledge messages only after the state change is durably committed.
      Ack-then-process loses messages on crash; process-then-ack duplicates them —
      which is fine, because §2.
      
      ## 4. Outbox pattern: never dual-write
      
      **Rule:** Never write to your database and publish to a broker as two separate
      operations — a crash between them either loses the event or emits a phantom.
      Write the event into an `outbox` table in the same transaction as the state
      change; a relay (poller or CDC e.g. Debezium) publishes from the outbox.
      
      ```text
      BEGIN
        UPDATE orders SET status='placed' WHERE id=$1;
        INSERT INTO outbox(event_id, type, payload, created_at) VALUES (...);
      COMMIT
      -- relay: SELECT unpublished → publish → mark published (at-least-once; consumers dedupe)
      ```
      
      **Rule:** The same applies inbound: to atomically consume and act, use an inbox
      table (record msg_id + effects in one TX), then ack.
      
      ## 5. Sagas for cross-service workflows
      
      **Rule:** A business transaction spanning services is a saga: a sequence of
      local transactions, each with a **compensating action** for rollback. Define
      compensations at design time; a saga step without a compensation (or an explicit
      "pivot — no return past this point" marker) is undesigned failure handling.
      
      **Orchestration vs choreography:**
      - **Orchestrate** (explicit coordinator / workflow engine: Temporal, Step
        Functions, or a state-machine table) when steps ≥ 3, ordering matters, or you
        need to answer "where is order 123 stuck?". Default choice.
      - **Choreograph** (each service reacts to events) only for short, stable, 2–3
        step flows. Beyond that, the workflow exists only in engineers' heads —
        unauditable and undebuggable.
      
      **Rule:** Compensations are not undo. `cancelReservation` after `reserveStock`
      must handle "stock already shipped". Compensations are themselves idempotent,
      retried, and may need their own compensations escalated to humans (incident
      queue), which must be designed, not improvised.
      
      **Rule:** Every saga has a timeout and a terminal failure state visible in
      monitoring. Sagas stuck "in progress" for hours are a High finding.
      
      **Rule:** Order the steps so a partial completion leaves the *conservative*
      state — take value out of the source first, put it into the destination last.
      Debit then credit; consume the coupon then grant the discount; decrement stock
      then confirm the order; revoke the old grant then issue the new one. A crash
      between legs then leaves value **missing**, which compensation or retry can
      recover, instead of value **duplicated**, which nothing recovers once the
      destination has spent it. Credit-first is defensible only when the credited
      resource provably cannot be consumed before the saga reaches its terminal state
      — and that must be *enforced* (a pending/held balance excluded from the
      spendable one), never assumed. The cost is real: debit-first strands the payer's
      value while the saga is in flight, which is why the timeout-to-terminal-state
      above and a user-visible pending status are part of this rule, not extras.
      
      ## 5b. Reconciliation: the completeness check on an external integration
      
      **Rule:** Where an external system holds authoritative state that you also
      record — payment processor, exchange, carrier, billing vendor, another team's
      service — the two records *will* diverge: a webhook is dropped, a call times out
      after the far side committed, a retry posts twice. Idempotency and retries bound
      the damage; neither tells you a divergence happened. A periodic reconciliation
      does. On a money-, inventory-, or entitlement-bearing integration, its absence
      is a High finding. Note what this is: §2–§4 give you *integrity* — each record
      you wrote is correct. Reconciliation is the only thing that gives you
      **completeness** — that no record is missing.
      
      **Rule:** Reconcile against the counterparty's own extract, not against your own
      records. Their settlement file, statement, or list endpoint is an independent
      observation; recomputing your database from your own event log proves internal
      consistency and says nothing about the seam. Where they publish no extract, the
      paginated read API over the period is the substitute — the same call the webhook
      was meant to save you (webhook senders owe consumers exactly this, see
      `sota-api-design` rules/06).
      
      **Rule:** Classify breaks; a count alone is not a result. Three buckets with
      three different actions: **(a)** known shape, safe to adjust by a written rule —
      automate it, and log every adjustment; **(b)** known shape, needs a human —
      queue it; **(c)** unclassified — page someone, because an unclassified break is
      indistinguishable from a bug still producing them. An automated (a) adjustment
      on a money path posts as a reversing ledger entry, never an in-place correction
      (`sota-databases` rules/01). Breaks also **age**: alert on the oldest unresolved
      break, not only on the count, or one permanently stuck break becomes background
      noise that hides the next twenty.
      
      **Rule:** A reconciliation that has never reported a break is not evidence of
      correctness — it is the inert-control signature. Print the denominator (rows
      compared on *each* side, and the period covered), fail closed when either side
      reads zero, and prove it can fire by seeding a known break. Diagnostics:
      `sota-code-security` rules/11 §2.2 and rules/15.
      
      ## 6. Event-driven architecture: events are facts
      
      **Rule:** An event states a fact that happened (`PaymentCaptured`), in past
      tense, owned by the producer. A command requests work (`CapturePayment`), owned
      by the consumer. Don't disguise commands as events ("EmailShouldBeSent") — that
      inverts ownership and creates hidden coupling.
      
      **Rule:** Producers don't know consumers. If producer code must change when a
      consumer is added, you have RPC over a message bus, not eventing.
      
      **Rule:** Consumers must tolerate: duplicates (§2), reordering (use aggregate
      version numbers or partition-key ordering, never wall-clock timestamps), and
      unknown fields (forward-compatible deserialization).
      
      **Rule:** Use CQRS only where read and write shapes genuinely diverge (complex
      domains, heavy read fan-out, projections per consumer). CQRS does not require
      event sourcing; start with "write model + projected read tables". Event sourcing
      is a Type 1 decision — adopt only with replay/audit requirements and ops
      capacity for snapshotting, upcasting, and GDPR-compliant deletion (crypto-
      shredding), each of which must be designed before adoption.
      
      **Rule (the precondition replay quietly assumes):** rebuilding state means re-running the
      **apply** step over old events, so that step must be a **pure function of (state, event)**.
      Every non-deterministic input it touches — the wall clock, a random or UUID generator, a
      config lookup, a call to another service — makes the rebuilt state differ from the
      original, silently and without an error. The classic case is an external query: *"if I ask
      for an exchange rate on December 5th and replay that event on December 20th, I will need
      the exchange rate on Dec 5"* (Fowler, *Event Sourcing*). Two remedies, and you need both:
      **capture the answer in the event** (the rate, the generated id, the timestamp — resolved
      once, at command time), and **gate outbound effects during replay** so a rebuild does not
      re-send emails or re-charge cards. A gateway that cannot be switched into replay mode is a
      replay you can only run in a scratch environment, which is the same as not having one.
      
      **Rule:** the **event log is the durable record; commands are not.** A command is a
      *request* that may be rejected, deduplicated, or rewritten; the event is what actually
      happened. So the durability, retention and backup budget belongs to the event store, and a
      lost command is a retry while a lost event is unrecoverable state. If your design gives
      commands the stronger guarantee, the roles have been swapped.
      
      ## 7. Queues: DLQs, ordering, backpressure
      
      **Rule:** Every queue/subscription has: a max-retry policy with backoff, a
      dead-letter queue, an alert on DLQ depth > 0, and a documented + tested
      redrive procedure. A DLQ nobody drains is a data-loss buffer with extra steps.
      
      **Rule:** Distinguish poison messages (malformed/bug — will never succeed; DLQ
      immediately after schema validation fails) from transient failures (dependency
      down; retry with backoff). Retrying poison messages 10 times just delays the DLQ
      and burns ordering.
      
      **Rule:** Ordering is per-partition-key only. Pick the partition key = the
      entity whose events must be ordered (order_id, account_id). Global ordering
      doesn't scale; don't design flows that require it.
      
      **Rule:** Backpressure is designed, not discovered:
      - Bounded queues everywhere (unbounded queues turn overload into OOM + huge latency).
      - Consumers pull at their own rate; producers get pushback (block, shed, or buffer-with-bound).
      - Define the overload policy per queue: shed lowest-priority work first (see rules/04 §6).
      - Monitor consumer lag with alerts; lag growing monotonically = under-provisioned consumer or poison loop.
      
      ## 7b. Broker queues (AMQP/RabbitMQ): semantics the log model misses
      
      AMQP-style brokers delete on ack and *push* to consumers — different failure
      modes than Kafka-style logs:
      
      **Rule:** Manual ack only after the state change is durably committed (§3
      applied to AMQP) — never auto-ack: messages count as delivered once written to
      the TCP socket, so a consumer crash under auto-ack loses them silently. With
      manual acks, unacked deliveries requeue on channel/connection close — which is
      why §2 idempotency is mandatory here too.
      
      **Rule:** Nack/requeue is a decision, not a reflex. Requeue only transient
      failures; every consumer requeueing the same delivery is a redelivery storm
      (CPU + bandwidth burn). Poison messages are rejected with `requeue=false` to a
      dead-letter exchange after validation fails (§7), with redelivery count bounded.
      
      **Rule:** Consumer prefetch (`basic.qos`) is the backpressure knob. AMQP pushes,
      so the cap on unacked deliveries is your only bound; unset/0 = unlimited
      prefetch = an unbounded buffer in the consumer (and memory growth on the node).
      Set it explicitly: ~100–300 for fast handlers, low single digits for heavy
      ones (sota-async-concurrency rules/06).
      
      **Rule:** Claim-check pattern: messages carry references (entity IDs,
      object-store URLs), never large blobs and never secrets — the broker is a
      shared failure domain, not storage. Payloads stay small, schema'd (§8), with
      correlation IDs (§9).
      
      **Rule:** Quorum queues are the default for anything that must survive node
      loss. Classic queue mirroring was removed in RabbitMQ 4.0; classic queues are
      single-replica now. Quorum queues default to delivery-limit 20 — messages
      exceeding it are dead-lettered or *dropped*, so configure the DLX target; and
      default dead-lettering is at-most-once — where DLQ loss is unacceptable, set
      `dead-letter-strategy: at-least-once` + `overflow: reject-publish`.
      
      **Rule:** Broker hardening in one line: per-service users, vhost isolation,
      least-privilege configure/write/read permissions, TLS, and no default `guest`
      account beyond localhost dev — details in sota-cloud-infrastructure and
      sota-secrets-management.
      
      ## 8. Contracts and schema evolution
      
      **Rule:** Every cross-service message and API has an explicit, versioned schema
      (OpenAPI, protobuf, Avro/JSON Schema in a registry) with compatibility checks in
      CI. Changes are additive by default (expand/contract): add optional field →
      migrate consumers → remove old field. Breaking changes require a new version
      published alongside the old, with a deprecation window.
      
      **Rule:** Use consumer-driven contract tests (Pact or equivalent) or a schema
      registry with compatibility mode — one of the two, enforced in CI. "We'll
      coordinate releases in Slack" is the distributed monolith (rules/07).
      
      **Rule:** Tolerant reader: consumers ignore unknown fields and validate only
      what they use. Strict full-payload validation on consume turns every producer
      addition into your outage.
      
      ## 9. Time, IDs, and causality
      
      **Rule:** Never use wall-clock timestamps for ordering or uniqueness across
      machines. Use per-aggregate version numbers for ordering, UUIDv7/ULID/KSUID
      for IDs (sortable, collision-free), and the database for authoritative time
      where one exists.
      
      **Rule:** Propagate correlation/trace context (W3C traceparent) through every
      hop — HTTP, queue message headers, scheduled jobs. A distributed flow you can't
      trace end-to-end is undebuggable; this is an audit-blocking gap, not polish.
      
      ## 10. Async request/response done right
      
      **Rule:** When a caller needs an answer but the work is async, use correlation
      explicitly: caller sends command with `correlation_id` + reply channel (reply
      queue, callback URL, or polling endpoint with job status), worker replies with
      the same `correlation_id`. Define the no-reply timeout and its user-facing
      behavior up front.
      
      **Rule:** Job-status resources beat held connections: `POST /exports → 202 +
      /jobs/123`, then poll or push notification. States exposed: `queued → running →
      succeeded(result_url) | failed(error, retryable?)`. Every async API the system
      offers should converge on one such job pattern, not five bespoke ones.
      
      ## 11. Competing consumers and partition assignment
      
      **Rule:** Scale consumers via the competing-consumers pattern, but know your
      broker's unit of parallelism (Kafka: partitions; SQS: messages; AMQP: prefetch).
      Max useful consumers = partition count in partitioned brokers; provision
      partitions for target parallelism *at topic creation* — repartitioning later
      breaks key→partition ordering during the transition.
      
      **Rule:** Keep per-message work small and uniform. One 10-minute message behind
      500 fast ones on the same partition is head-of-line blocking; split heavy work
      into a separate queue/topic with its own consumers (bulkheads, rules/04 §4).
      
      **Rule:** Handle rebalances: consumers must expect partition reassignment
      mid-stream (commit offsets only after processing; idempotency §2 covers the
      overlap window).
      
      ## 12. Distributed coordination: avoid it, then do it right
      
      **Rule:** The best lock is no lock: partition the work (each worker owns a key
      range), or let the database arbitrate via constraints and conditional updates
      (`UPDATE ... WHERE version = $expected`). Reach for distributed locks only when
      neither applies.
      
      **Rule:** If you must lock distributed-ly: use a lease (lock with TTL +
      fencing token), never an unbounded lock — holders crash. Verify the fencing
      token at the resource, not just at acquisition; a paused process can wake up
      believing it still holds an expired lock.
      
      ```text
      GOOD: token = lock.acquire(ttl=30s)        # token = monotonically increasing
            storage.write(data, fence=token)      # storage rejects stale tokens
      BAD:  if redis.setnx(key): do_work()        # GC pause > TTL → two holders, no fence
      ```
      
      **Rule:** Singleton jobs (schedulers, relays) use leader election (lease in
      etcd/DB/k8s Lease) with the same fencing discipline, and must be idempotent
      anyway because elections overlap.
      
      ## 13. The fallacies, applied
      
      Design reviews must not contain these assumptions; each appears in real designs
      weekly:
      - "The call will succeed" — every remote call needs a failure branch with a decision (retry? degrade? surface?), not just a log line.
      - "Latency is negligible" — N sequential cross-service calls = N × RTT floor; budget it (rules/04 §1).
      - "Bandwidth is infinite" — fat events/payloads (full entity snapshots on every change) saturate brokers; send deltas + version, or IDs + hot fields.
      - "Topology doesn't change" — pin nothing to instance identity; discovery via the platform (DNS/service registry), connections re-resolve on failure.
      - "The clock is right" — clock skew between machines is unbounded for ordering purposes (§9); never compare timestamps from two machines to decide order or expiry of anything critical.
      
      ## Audit checklist
      
      - [ ] **Event sourcing: is replay actually runnable, or only theoretically?** Does the
            `apply` step read a wall clock, a random/UUID generator, config, or another service?
            Each one makes a rebuild differ from the original **silently** (§6). The values must
            be resolved at command time and **captured in the event**.
      - [ ] Can outbound effects be **gated during replay** — a gateway with a replay mode? If
            not, replay only runs in a scratch environment, which is not a rebuild capability
            (§6).
      - [ ] Do **events** get the durability/retention/backup budget rather than commands? A
            lost command is a retry; a lost event is unrecoverable state (§6).
      
      - Is the consistency model (linearizable vs eventual, staleness bound) written down per critical operation?
      - Is every message handler and retried endpoint provably idempotent? Is dedupe state committed atomically with the state change?
      - Do public write APIs accept idempotency keys?
      - Does anything depend on exactly-once *delivery*? Any naked dual-writes (DB write + publish without outbox/CDC)?
      - Does every multi-service workflow have defined, idempotent compensations and a timeout-to-terminal-failure path? Can you answer "where is workflow X stuck" from a dashboard?
      - Do multi-leg money/inventory/entitlement workflows take value from the source *before* granting it at the destination? Where a credit lands first, is that balance provably held out of the spendable one — enforced, not assumed?
      - Does every integration where an external party holds authoritative state have a periodic reconciliation against **their** extract (not your own event log), with breaks classified into auto-adjustable / manual / unclassified, aged, and alerted? Has that reconciliation ever been observed reporting a break, and does it print how many rows it compared on each side?
      - Are events past-tense facts with producer ownership, or commands in disguise?
      - Does every queue have bounded size, retry-with-backoff, DLQ, DLQ alerting, and a tested redrive runbook?
      - Are poison messages separated from transient failures, or retried identically?
      - AMQP consumers: manual ack after durable commit (no auto-ack)? Requeue limited to transient failures, with poison messages rejected (`requeue=false`) to a DLX?
      - Is prefetch (`basic.qos`) set explicitly on every AMQP consumer, or is any consumer running with unlimited prefetch?
      - Durable RabbitMQ queues: quorum (not classic), delivery-limit dead-lettering configured (not silently dropping)? Messages carry references rather than blobs/secrets? Broker locked down (per-service users, vhosts, least privilege, TLS)?
      - Is ordering guaranteed only where a partition key provides it? Any logic assuming global order or wall-clock ordering?
      - Are message/API schemas versioned with CI compatibility checks or consumer-driven contract tests?
      - Are consumers tolerant readers (ignore unknown fields)?
      - Is trace/correlation context propagated across every async hop?
      - If event sourcing is used: are snapshotting, upcasting, and deletion (crypto-shredding) designed and tested?
      - Do async request/response flows use correlation IDs and a standard job-status pattern with defined no-reply timeouts?
      - Is consumer parallelism aligned with broker partitioning? Any head-of-line blocking from mixed-weight messages on one queue?
      - Are distributed locks lease-based with fencing tokens verified at the resource? Any bare `setnx`-style locks guarding critical sections?
      - Are singleton/scheduled jobs leader-elected and idempotent under overlapping elections?
      - Do any designs assume reliable network, ordered clocks, or stable topology (check failure branches on every remote call)?
      
    • 04-resilience-and-failure-design.md 12.9 KB
      # 04 — Resilience & Failure Design
      
      Rules for surviving the failures that will happen: slow dependencies, dead
      dependencies, overload, and your own retries. Resilience is configuration +
      code + tests; any leg missing means it doesn't exist.
      
      ## 1. Timeouts: every remote call, no exceptions
      
      **Rule:** Every network call (HTTP, DB, cache, DNS, queue publish) has an
      explicit timeout. Library defaults are usually infinite or absurd (e.g., many
      HTTP clients default to no timeout) — a missing timeout converts a slow
      dependency into thread-pool exhaustion and a full outage. Missing timeout on a
      critical path is a Critical finding.
      
      **Rule:** Set timeouts from the callee's measured p99 plus margin (e.g., p99 ×
      1.5), not folklore. A 30 s timeout on a 50 ms-p99 service doesn't protect
      anything; it queues doomed work for 30 s.
      
      **Rule:** Enforce a deadline budget across hops: the caller's total deadline is
      distributed down the chain (propagate remaining-deadline in context/headers).
      Inner calls whose timeouts sum to more than the outer deadline do useless work
      after the client has already given up.
      
      ```text
      Client deadline 1000ms
        → gateway (deadline left: 950) → svc A (timeout 400) → svc B (timeout 300)
      BAD: A and B each configured 5000ms while the edge gives up at 1000ms.
      ```
      
      ## 2. Retries: bounded, jittered, idempotent-only
      
      **Rule:** Retry only idempotent operations (rules/03 §2), only on retryable
      errors (timeouts, 503, connection reset — never 400/401/422), with **bounded
      attempts** (2–3), **exponential backoff + full jitter**, and a **retry budget**
      (e.g., retries ≤ 10% of requests) so retries can't melt a struggling dependency.
      
      ```text
      delay = random(0, min(cap, base * 2^attempt))   # full jitter
      ```
      
      **Rationale:** Synchronized un-jittered retries create thundering herds; the
      dependency recovers, gets hit by the synchronized wave, dies again.
      
      **Rule:** Retry at ONE layer. Client retries × mesh retries × queue redelivery
      multiply: 3 layers of 3 attempts = up to 27 calls per request — a self-inflicted
      DDoS. Decide which layer owns retries per edge and disable the rest.
      
      **Rule:** Never retry on the user's synchronous critical path more than once;
      prefer failing fast and letting the user/job-queue retry. Latency added by
      retries must fit the deadline budget (§1).
      
      ## 3. Circuit breakers: stop calling the dead
      
      **Rule:** Wrap dependencies that can fail-slow with a circuit breaker: after a
      failure-rate threshold, open the circuit (fail instantly), then half-open with
      probe requests before closing. Failing fast preserves your threads and gives the
      dependency air to recover.
      
      **Rule:** Breakers are per-dependency (and ideally per-endpoint), never global.
      One breaker shared across all dependencies means a dead recommendation service
      opens the circuit to your payment provider.
      
      **Rule:** Every breaker open/close event is logged and alerted; breaker state is
      a dashboard metric. A breaker that silently opens turns "degraded" into
      "mysteriously missing data".
      
      **Rule:** Define what happens when the breaker is open *as part of the design*:
      fallback value, cached last-known-good, degraded UX, or explicit error. An open
      breaker with no defined fallback is just a faster outage.
      
      ## 4. Bulkheads: isolate the blast radius
      
      **Rule:** Partition resources per dependency and per workload class: separate
      connection pools, thread pools/semaphores, and queue consumers so one slow
      dependency or one greedy tenant can't starve everything else.
      
      ```text
      BAD:  one 100-conn pool shared by /checkout and /export-report
            → slow report queries consume all 100; checkout dies.
      GOOD: checkout pool: 60, reports pool: 10, admin: 5 — reports saturate alone.
      ```
      
      **Rule:** Apply bulkheads at infra level too: critical and batch workloads on
      separate node pools / autoscaling groups; noisy-neighbor isolation for
      multi-tenant systems (rules/05 §7).
      
      ## 5. Graceful degradation: rank your features
      
      **Rule:** Classify every dependency of each user flow as *required* or
      *optional*. Optional-dependency failure degrades (skip recommendations, show
      cached prices with a staleness badge, queue the email) — it never 500s the flow.
      This classification is a design artifact, reviewed like code.
      
      **Rule:** Fallbacks must be tested under real failure (chaos, §8) and must be
      cheap. A fallback that calls another remote service can fail too; a fallback
      that recomputes expensively turns partial failure into overload.
      
      **Rule:** Fail closed for security and money (authz unavailable → deny;
      fraud-check down → hold the order or apply strict limits). Fail open only for
      genuinely optional features — and record the decision per dependency.
      
      ## 6. Load shedding and admission control
      
      **Rule:** Decide what you drop *before* you're overloaded. Under saturation,
      reject early (HTTP 429/503 + `Retry-After`) at the edge, cheapest-first:
      rejecting a request at admission costs microseconds; timing it out after queuing
      costs seconds of capacity.
      
      **Rule:** Shed by priority: health checks and payments last; analytics,
      prefetch, and crawlers first. Requires a request-priority signal (header,
      route class) plumbed to the shedder.
      
      **Rule:** Bound every in-process queue and use deadline-aware queue draining:
      if a request has already exceeded its deadline while queued, drop it without
      processing. Serving dead requests is how overload becomes collapse (congestion
      collapse / metastable failure).
      
      **Rule:** Protect against retry storms after recovery: combine load shedding
      with client retry budgets (§2) and slow-start (gradually re-admit traffic).
      
      ## 7. Health checks: liveness ≠ readiness ≠ dependency health
      
      **Rule:** Implement both: **liveness** ("process is not wedged" — never checks
      dependencies) and **readiness** ("can serve traffic now" — may check critical
      local state, warm caches, config loaded). A liveness probe that pings the
      database restarts every replica when the DB blips — converting a dependency
      outage into a fleet restart. That's a Critical finding.
      
      **Rule:** Readiness should reflect *this instance's* ability to serve, not
      shared dependencies' health: if all replicas mark unready when the DB is down,
      you remove all capacity and serve connection errors instead of useful 503s with
      degradation. Prefer degrading (§5) over going unready for shared-dependency
      failure.
      
      **Rule:** Health endpoints are cheap (<10 ms, no fan-out), unauthenticated only
      on internal interfaces, and excluded from load shedding last.
      
      ## 8. Test failure on purpose (chaos engineering)
      
      **Rule:** Resilience claims require evidence. Regularly and deliberately inject:
      dependency latency (+500 ms), dependency errors (10% 503s), instance kills, AZ
      loss, and full dependency outage — in staging always, in production once SLOs
      and rollback are in place. Verify SLOs hold and fallbacks fire. Untested
      fallbacks fail when needed; this is the most reliable finding in chaos history.
      
      **Rule:** Start with game days (hypothesis → inject → observe → fix), automate
      the validated experiments into a recurring suite (fitness functions, rules/01
      §5). Every incident's failure mode becomes a permanent chaos experiment.
      
      **Rule:** Define SLOs (availability, latency) per critical user journey with
      error budgets. Chaos results, degradation policies, and shedding priorities all
      derive from SLOs; without SLOs, "resilient" is an opinion.
      
      ## 9. Recovery and operability
      
      **Rule:** Design for fast rollback over fast fix: every deploy is reversible in
      minutes (previous artifact kept warm), schema changes are expand/contract so
      code rollback never requires schema rollback.
      
      **Rule:** Make restarts safe and boring: graceful shutdown (stop accepting,
      drain in-flight within deadline, then exit), startup that tolerates dependency
      unavailability (retry with backoff, don't crash-loop the fleet), and idempotent
      startup migrations guarded by locks.
      
      **Rule:** Avoid synchronized fleet behavior: jitter cron jobs, cache TTLs, and
      token refreshes. Thousands of instances doing anything at the same second is a
      self-inflicted spike (cache stampede: use TTL jitter + request coalescing /
      single-flight).
      
      ## 10. Rate limiting: protect yourself and your dependencies
      
      **Rule:** Rate-limit at every trust boundary: per-client at the public edge
      (token bucket, keyed by API key/user, with `429 + Retry-After` and documented
      limits), per-tenant inside pooled systems (rules/05 §7), and *outbound* toward
      third parties whose limits you must respect — a client-side limiter beats
      discovering their limit via a ban.
      
      **Rule:** Prefer adaptive concurrency limits (AIMD/gradient on observed latency)
      over fixed RPS numbers for internal hops; fixed numbers go stale the week after
      the next deploy changes the cost per request.
      
      **Rule:** Burst handling is a policy decision: token bucket (allows bursts) for
      user-facing APIs, leaky bucket / shaping for downstream protection. Pick per
      edge and write it down.
      
      ## 11. Hedging and tail-latency control
      
      **Rule:** For idempotent reads with bad tail latency, consider hedged requests:
      send a second attempt after the p95 mark, take the first response, cancel the
      loser. Cap hedging (≤5% extra load) and never hedge writes or non-idempotent
      calls.
      
      **Rule:** Attack tail latency at the source before hedging: eliminate
      synchronized pauses (GC tuning, connection re-establishment storms), avoid
      cross-AZ hops on hot paths, and precompute/coalesce instead of fanning out.
      Hedging is a patch over variance, not a substitute for removing it.
      
      ## 12. Failure-mode analysis as a design step
      
      **Rule:** For every critical flow, run a lightweight FMEA before launch: list
      each dependency hop; for each, ask "what happens if it's slow / erroring /
      returning garbage?"; record the designed response (timeout value, retry policy,
      breaker, fallback, shed, alert). The output is a table in the design doc;
      gaps in the table are gaps in the system.
      
      ```text
      Checkout flow — failure table (excerpt)
      dep          slow                 down                    garbage
      payments     timeout 2s, 1 retry  breaker→ hold order,    schema-validate,
                   then hold-order      notify user, alert      reject + alert
      inventory    timeout 300ms        fallback: optimistic    treat as down
                   no retry             reserve, reconcile async
      recs         timeout 150ms        skip section            skip section
      ```
      
      **Rule:** Classify dependencies into tiers (T0: flow fails without it; T1:
      degrade; T2: invisible loss) and let the tier dictate the minimum machinery:
      T0 = timeout + breaker + tested fallback-or-fail-closed + page; T2 = timeout +
      silent skip + ticket-level alert.
      
      ## 13. Disaster recovery is resilience at the largest blast radius
      
      **Rule:** Define RTO/RPO per datastore *with the business*, then verify the
      architecture delivers it: backup restore is **tested by actually restoring**
      on a schedule (an unrestored backup is a hope, not a backup), and cross-region
      posture (backup-restore / pilot-light / active-passive / active-active) is an
      ADR with cost attached.
      
      **Rule:** Active-active across regions is a Type 1 decision that drags
      consistency design with it (conflict resolution, data residency, ID generation).
      Don't back into it via "we just added a second region for latency".
      
      ## Audit checklist
      
      - Does every remote call (HTTP, DB, cache, queue) have an explicit timeout? Grep for client construction sites and verify.
      - Are timeouts derived from measured callee latency, and do nested timeouts fit within the edge deadline?
      - Are retries bounded, jittered, restricted to idempotent operations and retryable errors, and owned by exactly one layer per edge?
      - Is there a retry budget or equivalent guard against retry storms?
      - Are circuit breakers per-dependency, with alerting on state change and a defined fallback per open circuit?
      - Are connection/thread pools bulkheaded per dependency and per workload class, or is there one shared pool?
      - Is each dependency of each critical flow classified required/optional, with degradation behavior implemented and tested?
      - Do security- and money-touching paths fail closed?
      - Is there admission control / load shedding with priority ordering, bounded queues, and deadline-aware dropping?
      - Do liveness probes avoid dependency checks? Does readiness avoid mass-unready on shared-dependency failure?
      - Are SLOs defined per user journey, and are chaos experiments (latency, errors, instance/AZ kill) run on a schedule with results tracked?
      - Can every service roll back in minutes, shut down gracefully, and start while its dependencies are down?
      - Are cache TTLs/crons jittered, and is stampede protection (single-flight) in place for hot keys?
      - Is rate limiting present at the public edge (per client), per tenant internally, and outbound toward third-party limits?
      - Is hedging, if used, restricted to idempotent reads with a load cap?
      - Does each critical flow have a failure-mode table (slow/down/garbage per dependency) with designed responses, and are dependencies tiered T0/T1/T2?
      - Are RTO/RPO defined per datastore, backups restore-tested on a schedule, and the cross-region posture an explicit ADR?
      
    • 05-scalability-state-and-data.md 12.9 KB
      # 05 — Scalability, State & Data Architecture
      
      Rules for scaling horizontally, placing state deliberately, caching without
      lying, partitioning data, and isolating tenants. Scaling problems are state
      problems; everything stateless is trivially scalable.
      
      ## 1. Stateless services by default
      
      **Rule:** Service instances hold no request-scoped state between requests: no
      sticky sessions, no local user files, no in-memory state another request depends
      on. Session state goes to a shared store (Redis/DB) or signed tokens; files go
      to object storage. Any instance can serve any request; any instance can die
      mid-flight without data loss.
      
      **Test:** Can you kill any instance at any moment and scale 1→N→1 with zero
      correctness impact? If not, find the hidden state and evict it.
      
      **Permissible local state:** caches (rebuildable, with bounded staleness) and
      buffers already made durable elsewhere. Local state that is the *only* copy of
      anything is a Critical finding.
      
      **Rule:** If state must live in the service (stateful stream processing,
      websocket hubs, game servers), be explicit: consistent-hash routing, replication,
      and a rebalancing story. Accidental statefulness is the problem; deliberate
      statefulness is a design.
      
      ## 2. Scale out, not up; know your bottleneck first
      
      **Rule:** Design for horizontal scaling (more replicas behind a load balancer),
      but **measure before scaling**: profile and load-test to find the actual
      bottleneck. Scaling app replicas when the database is the bottleneck adds
      connections and makes it worse.
      
      **Rule:** Autoscale on the constraining signal (queue depth, p95 latency,
      concurrent requests) — CPU only when CPU is genuinely the constraint. Set
      sane min/max, and verify scale-*down* behaves (connection draining, no flapping).
      
      **Rule:** Every shared resource downstream of a scalable tier needs a guard:
      connection poolers (e.g., pgbouncer) in front of databases, concurrency limits
      per instance, rate limits per client. N autoscaled replicas × M connections
      each is the classic way to murder a database.
      
      ## 3. The database is the hard part
      
      **Rule:** Scale reads first via replicas + caching; scale writes via partitioning
      (§5) only when measured write throughput or data volume demands it. In between,
      exhaust the boring options: indexes, query tuning, fewer round-trips, bigger box.
      Vertical scaling is honest and cheap up to a surprisingly high ceiling.
      
      **Rule:** Read replicas are eventually consistent. Audit every read-after-write
      flow: route them to the primary, pin by session, or use version tokens
      (read-your-writes, rules/03 §1). Random replica reads after writes produce
      "my save disappeared" bugs.
      
      **Rule:** Long-running queries, analytics, and reporting never run against the
      OLTP primary. Use a replica, CDC into an analytical store, or scheduled
      extracts. One BI query table-scanning production is a recurring outage pattern.
      
      ## 4. Caching: every cache is a consistency decision
      
      **Rule:** For each cache, write down: key shape, TTL, max staleness tolerated,
      invalidation trigger, stampede protection, and fallback when cold/down. A cache
      without an invalidation story is a bug factory with good latency.
      
      **Tiering (apply outside-in; each tier only if measured need):**
      1. CDN/edge — static assets, anonymous pages.
      2. Gateway/HTTP cache — cacheable GETs with correct `Cache-Control`/ETags.
      3. Distributed cache (Redis/Memcached) — hot entities, computed views, sessions.
      4. In-process cache — tiny, hottest keys, short TTL (it multiplies staleness by replica count).
      
      **Patterns:**
      - Default: **cache-aside** with TTL + explicit invalidation on write (delete, don't update, the key — update races produce stale-forever entries).
      - Stampede protection on hot keys: single-flight/request coalescing + TTL jitter + optionally serve-stale-while-revalidate.
      - Negative caching with short TTL for "not found" to stop miss-storms.
      
      **Rule:** The system must be *correct* (if slow) with the cache completely cold
      or down. If cache loss = outage or wrong answers, you've built an unmanaged
      datastore; either make it a real datastore (durable, replicated) or fix the
      dependency.
      
      **Rule:** Never cache authorization decisions or feature-flag evaluations beyond
      their tolerated staleness (seconds, not hours), and never share cached responses
      across users/tenants without the user/tenant in the key. Missing tenant in cache
      key = cross-tenant data leak = Critical.
      
      ## 5. Partitioning (sharding): defer, then commit properly
      
      **Rule:** Partition when a single primary measurably can't hold the write load,
      working set, or data volume — not before. Sharding multiplies every operational
      task (migrations, backups, queries, rebalancing).
      
      **Rule:** Choose the partition key by access pattern: the key that appears in
      ~all hot queries (tenant_id for B2B SaaS, user_id for consumer). Getting it
      wrong means scatter-gather on every request; changing it later is a full data
      migration. This is a Type 1 decision — ADR required.
      
      **Rule:** Design for resharding from day one: use many logical partitions
      mapped to few physical nodes (consistent hashing or a directory/lookup service),
      so adding nodes moves logical partitions instead of rehashing the world.
      
      **Rule:** Accept and design around the losses: no cross-shard transactions
      (use sagas, rules/03 §5), no cross-shard joins (denormalize or query a
      projection), hot-partition monitoring (one celebrity tenant can melt a shard —
      have an isolation/relocation plan).
      
      ## 6. Data lifecycle and growth
      
      **Rule:** Every table/collection/topic has a growth model and a lifecycle:
      retention policy, archival path (object storage), and deletion (legal +
      GDPR/erasure). "Keep everything forever in the OLTP store" is a slow-motion
      incident: backups, migrations, and queries all degrade.
      
      **Rule:** Time-series and append-mostly data (events, logs, audit) goes to
      stores built for it (or native table partitioning by time with partition
      dropping), not into the same tables as hot transactional rows.
      
      ## 7. Multi-tenancy: pick the model consciously
      
      **Models (per tier of customer, not necessarily one global choice):**
      
      | Model | Isolation | Cost/tenant | Use when |
      |---|---|---|---|
      | Pooled: shared schema, `tenant_id` column | Lowest (logical only) | Lowest | Long tail of small tenants |
      | Schema-per-tenant / DB-per-tenant | Medium–high | Medium | Hundreds of tenants, compliance asks |
      | Silo: dedicated stack per tenant | Highest | Highest | Regulated/enterprise tier, residency requirements |
      
      **Rule:** In pooled models, tenant isolation must be enforced *below* the
      application's good intentions: row-level security in the DB, or a mandatory
      tenant-scoped repository layer that makes it impossible to build a query without
      `tenant_id` — verified by tests that attempt cross-tenant access. One forgotten
      `WHERE tenant_id = ?` is a breach.
      
      **Rule:** Tenant context flows from authenticated identity at the edge through
      every hop (headers/context), is never accepted from request bodies or query
      params, and appears in: every query, every cache key (§4), every queue message,
      every log line, and every metric label (for noisy-neighbor attribution).
      
      **Rule:** Apply fairness controls in pooled tiers: per-tenant rate limits,
      per-tenant concurrency caps, per-tenant queue quotas (bulkheads, rules/04 §4).
      Without them, your biggest tenant defines everyone's worst day.
      
      **Rule:** Plan tenant mobility: onboarding, offboarding (export + verified
      deletion), and *migration between models* (a pooled tenant grows into a silo).
      If moving one tenant's data out is impossible, you've built a roach motel.
      
      ## 8. Workload separation
      
      **Rule:** Separate latency-sensitive request serving from throughput-oriented
      batch/async work: different deployments, pools, and scaling policies (bulkheads,
      rules/04 §4). Background jobs run on workers consuming queues — never as fire-
      and-forget threads inside request-serving instances, where deploys and
      autoscaling silently kill them.
      
      **Rule:** All heavy work triggered by users (exports, imports, report
      generation) is async: enqueue, return a job handle, notify on completion.
      Holding an HTTP connection open for a 5-minute job fails at every proxy,
      timeout, and deploy between you and the user.
      
      ## 9. Capacity: utilization math you can't ignore
      
      **Rule:** Queueing theory is not optional: at high utilization, wait time
      explodes nonlinearly (M/M/1: wait ∝ ρ/(1−ρ); 80%→90% utilization roughly
      doubles queueing delay). Plan steady-state utilization of latency-sensitive
      tiers at ~50–70%, not 90% — the "wasted" headroom is what absorbs bursts,
      deploys, and AZ loss.
      
      **Rule:** Capacity-plan against peak + failure: the fleet must serve peak
      traffic with one AZ down and one deploy in flight. N+1 at the *peak*, not the
      average.
      
      **Rule:** Load-test with production-shaped traffic (real key skew, real
      read/write mix, real payload sizes) before major launches. Uniform-random
      synthetic load hides hot keys and lock contention — the two things that
      actually fall over.
      
      ## 10. Hot keys and skew
      
      **Rule:** Assume skew: some tenant, product, or celebrity will be 1000x median.
      Detect it (top-K key metrics on caches, partitions, and rate limiters) and have
      a playbook: replicate hot read keys (key suffixing: `item:42#1..N`), isolate
      hot tenants to dedicated capacity, collapse duplicate in-flight work
      (single-flight), precompute hot aggregations.
      
      **Rule:** Counters and append-heavy rows (likes, view counts, "current balance"
      rows updated by everything) serialize on row locks. Shard the counter
      (N sub-rows summed on read) or buffer increments through a queue and apply in
      batches.
      
      ## 11. Derived data and projections
      
      **Rule:** Treat search indexes, materialized views, denormalized read tables,
      and analytics copies as *derived data*: rebuildable from the source of truth by
      a documented, tested backfill job. If you can't rebuild a projection, it's
      secretly a primary store with no backup discipline.
      
      **Rule:** Keep derivation pipelines idempotent and ordered-per-key (rules/03
      §2, §7); track and alert on projection lag the same way as replica lag.
      Consumers of a projection must know its staleness contract.
      
      **Rule:** Push denormalization to the read side, never the write side: the
      write model stays normalized around invariants (rules/02 §3); projections
      denormalize per consumer. Denormalizing the write model "for performance"
      trades correctness machinery for a cache you can't invalidate.
      
      ## 12. Large objects and blob handling
      
      **Rule:** Binary/large content (images, exports, documents) lives in object
      storage with the database holding metadata + key. Uploads and downloads go
      direct-to-storage via presigned URLs — streaming gigabytes through your API
      tier burns its memory and connection slots for zero value.
      
      **Rule:** Lifecycle-manage blobs like rows (§6): retention classes, orphan
      sweeps (DB row deleted but blob remains, or vice versa — reconcile on a
      schedule), and per-tenant prefixes so tenant offboarding (§7) can actually
      delete their data.
      
      ## Audit checklist
      
      - Can every service instance be killed at any moment without data loss? Any sticky sessions, local files, or solo in-memory state?
      - Is session/user state in a shared store or token, not instance memory?
      - Is autoscaling driven by the constraining metric, with verified scale-down/drain behavior?
      - Are database connections guarded (pooler, per-instance caps) against replica multiplication?
      - Are read-after-write flows protected against replica/cache staleness?
      - Do analytics or long-running queries run against the OLTP primary?
      - Does every cache have documented TTL, invalidation trigger, stampede protection, and a correct cold-cache fallback? Is anything cache-as-only-copy?
      - Do cache keys include user/tenant scope everywhere responses differ by user/tenant?
      - If sharded: was the partition key chosen from access patterns (ADR)? Are logical→physical partitions resharding-friendly? Any cross-shard transactions or joins?
      - Does every large table/topic have retention, archival, and erasure paths?
      - Is the multi-tenancy model explicit per tenant tier? Is tenant isolation enforced below application code (RLS or mandatory scoped layer) with cross-tenant access tests?
      - Is tenant context derived from identity (not request params) and present in queries, cache keys, messages, logs, metrics?
      - Are per-tenant rate/concurrency/queue quotas in place in pooled tiers?
      - Is all user-triggered heavy work async with job handles, and are background jobs on dedicated workers?
      - Is steady-state utilization of latency-sensitive tiers planned with headroom (~50–70%), and does capacity cover peak with an AZ down?
      - Are load tests production-shaped (key skew, payload sizes), and is hot-key detection (top-K metrics) with a mitigation playbook in place?
      - Are high-contention counters sharded or queue-buffered rather than single-row hot spots?
      - Is every projection/search index/denormalized table rebuildable via a tested backfill, with lag monitored and staleness contracts known to consumers?
      - Are blobs in object storage with presigned direct transfer, orphan reconciliation, and tenant-scoped prefixes?
      
    • 06-cloud-native-config-and-delivery.md 13.1 KB
      # 06 — Cloud-Native Operations, Config & Delivery (12-Factor, 2026 Edition)
      
      The 12-factor principles, updated for containers, orchestrators, and modern
      delivery. These are the operational contract every service must satisfy
      regardless of architecture style.
      
      ## 1. Build/release/run: one artifact, promoted everywhere
      
      **Rule:** Build one immutable artifact (container image, digest-pinned) per
      commit; promote that exact artifact through dev → staging → prod. Environment
      differences live entirely in config (§2). Rebuilding per environment means you
      test one binary and ship another.
      
      **Rule:** Releases are versioned and rollback-able: previous release = previous
      (artifact, config) pair, redeployable in minutes (rules/04 §9). Tag images with
      commit SHA; `latest` in any deployment manifest is a High finding.
      
      **Rule:** Declare all dependencies explicitly with lockfiles; build in clean
      environments. Anything "installed on the host" that the app needs is an
      undeclared dependency and will bite during scale-out or migration.
      
      ## 2. Config: environment-injected, schema-validated, secrets separate
      
      **Rule:** Config that varies by environment (URLs, pool sizes, flags defaults,
      credentials) is injected at runtime (env vars, mounted files, config service) —
      never compiled in, never `if env == "prod"` branches in code. Code with
      environment-name conditionals can't be tested for prod behavior outside prod.
      
      **Rule:** Validate all config at startup against a typed schema and **fail fast**
      with a message naming the bad key. A service that boots with a missing config
      and fails on first use turns a deploy-time error into a 3 a.m. incident.
      
      **Rule:** Secrets are not config: they come from a secret manager (Vault, cloud
      secret stores) or orchestrator secrets with rotation support — never in env-var
      dumps in CI logs, never in the image, never in git (enforce with secret-scanning
      in CI). Support rotation without redeploy where feasible (file-mounted, reloaded).
      
      **Rule:** Keep declarative deployment config (manifests, IaC) in git, reviewed
      like code (GitOps). Live-mutated infrastructure ("someone changed it in the
      console") is configuration drift; drift detection should alarm.
      
      ## 3. Processes, disposability, and concurrency
      
      **Rule:** Processes are stateless and share-nothing (rules/05 §1); scale via
      process/replica count, not threads-per-giant-instance alone. Start fast
      (seconds), shut down gracefully on SIGTERM (drain, then exit; rules/04 §9),
      and tolerate sudden death (orchestrators kill freely).
      
      **Rule:** Run one concern per process: web serving, queue workers, and schedulers
      are separate deployments with separate scaling (rules/05 §8). Cron-in-a-web-pod
      runs N times on N replicas — use the platform scheduler or a leader-elected/
      distributed-locked runner, designed idempotent (rules/03 §2).
      
      **Rule:** Logs go to stdout/stderr as structured JSON events; the platform
      ships them. Apps never manage log files, rotation, or destinations.
      
      ## 4. Observability is a launch requirement (the 13th factor)
      
      **Rule:** Every service ships with, before first prod deploy:
      - **Structured logs** with correlation/trace IDs and tenant/user context (no PII/secrets — enforce with log scrubbing).
      - **Metrics**: RED (rate, errors, duration) per endpoint/consumer + key business metrics + saturation (pools, queues).
      - **Distributed traces** (OpenTelemetry), context propagated across HTTP *and* queues (rules/03 §9).
      - **SLOs with alerts on burn rate** — alert on user-impacting symptoms (SLO burn), page on those; cause-based alerts are tickets, not pages.
      
      **Rule:** If you can't answer "what did request X do across services, and why was
      it slow?" from telemetry alone, observability is insufficient — that's a High
      finding regardless of how the dashboards look.
      
      ## 5. Backing services are attached, swappable resources
      
      **Rule:** Treat every backing service (DB, cache, broker, email, third-party
      API) as an attached resource addressed by config and accessed through a port/
      adapter (rules/02 §4). Swapping prod Postgres for a different instance — or a
      local container in dev — must require only a config change.
      
      **Rule:** Dev/prod parity: develop and CI-test against the same *kind* of
      backing services as production (Postgres in a container, real broker via
      testcontainers), not in-memory fakes of databases. SQLite-in-dev/Postgres-in-
      prod ships query bugs straight to production.
      
      ## 6. Feature flags: decouple deploy from release
      
      **Rule:** Deploy continuously; release via flags. Every risky or user-visible
      change ships dark behind a flag, then rolls out progressively (1% → 10% → 50% →
      100%) with metrics watched per cohort and instant kill-switch rollback. This —
      not deploy rollback — is your primary mitigation lever.
      
      **Rule:** Flag discipline, enforced:
      - Every flag has an owner, a description, and an **expiry/removal ticket** at creation. Permanent "flags" (ops kill switches, entitlements) are explicitly marked as such; everything else is temporary.
      - Remove flags within weeks of 100% rollout. A codebase with hundreds of stale flags has 2^N untested configuration states — that's the anti-pattern, not a capability.
      - Flag evaluation is local/SDK-cached with a hard default if the flag service is down (flag service must never be a request-path single point of failure).
      - Flag checks live at the smallest number of choke points (one branch at the use-case boundary), not sprinkled through every layer.
      - Flag changes are audited (who flipped what when) — they are production changes.
      
      **Rule:** Use flags for operational levers too: load-shedding tiers (rules/04
      §6), degradation switches (rules/04 §5), and migration cutovers (rules/01 §8).
      Test both states of every active flag in CI for critical paths.
      
      **Rule:** A progressive rollout means **two versions of the application are running at
      once**, so both flag states must be compatible with the schema *as it is right now* —
      not with the schema either version was written against. The flag rule and the migration
      rule interact and neither is sufficient alone: expand/contract makes the *schema* safe
      for two readers (`sota-databases` rules/02), and the flag makes the *behaviour*
      switchable, but nothing checks that the off-state still works after the contract step
      has run. Order it: expand → deploy both-compatible code → roll the flag → **remove the
      flag** → contract. Contracting while a flag can still route traffic to the old path is
      the version-skew outage, and it presents as "the rollback made it worse" because the
      rollback target no longer matches the data.
      
      ## 7. Delivery pipeline and environments
      
      **Rule:** Trunk-based development with small PRs; every merge produces a
      deployable artifact passing: unit + contract tests (rules/03 §8), architecture
      fitness functions (rules/01 §5), security and secret scans, and migration lint.
      Long-lived feature branches are inventory and merge risk; flags (§6) replace them.
      
      **Rule:** Deploys are progressive (canary or rolling with automated rollback on
      SLO regression) and boring: no scheduled "deployment windows," no manual steps.
      Deploy frequency is a health metric; if deploys are scary, fix the pipeline, not
      the calendar.
      
      **Rule:** Database migrations are decoupled from code deploys and always
      expand/contract (additive change → deploy code reading both → backfill →
      contract). Any migration that would break the *previous* code version blocks
      rollback and is a High finding.
      
      ## 8. Admin processes and operational surface
      
      **Rule:** One-off admin tasks (backfills, fixes, replays) run as versioned,
      reviewed code (jobs/scripts in the repo) in the same environment/config as the
      app — never as ad-hoc SQL in a prod console. Make the dangerous path inconvenient
      and the safe path easy.
      
      **Rule:** Every service exposes a minimal operational surface: health endpoints
      (rules/04 §7), build/version info endpoint, and runtime-adjustable log level.
      Every service has a runbook covering: dashboards, common failure modes, DLQ
      redrive, scaling levers, and rollback.
      
      ## 9. Cost and sustainability as architectural signals
      
      **Rule:** Tag every resource with service + team + (where applicable) tenant;
      review cost per service monthly. Cost anomalies are architecture smells: an
      unexpectedly expensive service usually has a chatty integration, a missing
      cache, unbounded retention (rules/05 §6), or over-provisioned idle capacity.
      
      **Rule:** Set cost fitness functions for the big levers (egress, storage growth,
      per-request compute) with alerts — treat a 2x cost regression like a 2x latency
      regression.
      
      ## 10. Supply-chain integrity
      
      **Rule:** Pin and verify everything that enters the artifact: lockfiles for app
      dependencies, digest-pinned base images, dependency update automation
      (Renovate/Dependabot) with CI gates rather than hand-rolled upgrades twice a
      year. Generate an SBOM per build and scan images for known vulnerabilities as a
      pipeline gate (severity threshold, with an expiring-exception process — not a
      permanent ignore file).
      
      **Rule:** Sign artifacts (e.g., Sigstore/cosign) and verify signatures at
      deploy/admission time, so "what's running" provably equals "what CI built".
      Build provenance (SLSA-style) matters most for anything handling money or PII.
      
      **Rule:** Third-party base images and Helm charts are dependencies too: mirror
      them into your registry; never deploy straight from a public registry that can
      change or vanish under you.
      
      ## 11. Environments: ephemeral over snowflake
      
      **Rule:** Environments are created and destroyed from code (IaC + seeded data),
      not curated by hand. Prefer ephemeral per-PR preview environments for
      integration verification over one perpetually-broken shared "staging" that
      serializes every team.
      
      **Rule:** Staging-like environments earn their cost only if they're
      production-shaped where it matters: same topology (real broker, real DB engine,
      same proxy chain), scaled down. A staging that differs in kind, not just size,
      validates nothing — pair it with progressive prod delivery (§7) and testing in
      production behind flags (§6) instead of pretending.
      
      **Rule:** Never let environments share backing state (queues, buckets, third-
      party sandboxes) without namespacing — cross-environment bleed produces the
      least reproducible bug class there is.
      
      ## 12. Telemetry cost and cardinality discipline
      
      **Rule:** Metrics labels are a contract with your bill and your query latency:
      never label by unbounded dimensions (user ID, request ID, full URL). Tenant ID
      as a label is acceptable only below a known tenant-count ceiling; beyond it,
      use exemplars or logs.
      
      **Rule:** Trace-sample deliberately: head-sample a base rate, tail-sample
      errors and slow requests at 100%. Log levels are runtime-adjustable (§8);
      DEBUG-in-prod-by-default is a cost incident and occasionally a PII incident.
      
      **Rule:** Telemetry pipelines are backpressure-aware and lossy-by-design for
      the app: dropping a span must never block or crash request serving (bounded
      buffers, drop-and-count).
      
      ## Audit checklist
      
      - [ ] For any flag rolled out across a schema change: is the **contract step ordered after
            flag removal**, and does the off-state still work against the current schema? Two app
            versions run during a rollout, and contracting while the old path is still reachable is
            the version-skew outage that presents as "the rollback made it worse" (§6).
      
      - Is exactly one immutable, digest-pinned artifact built per commit and promoted across environments? Any `latest` tags or per-env rebuilds?
      - Is all environment-varying config injected at runtime, schema-validated at startup with fail-fast? Any `if env == "prod"` branches?
      - Are secrets sourced from a secret manager with rotation, absent from git/images/CI logs, with scanning enforced in CI?
      - Are deployment manifests/IaC in git with drift detection?
      - Do processes start in seconds, drain on SIGTERM, and survive sudden kill? Are web/workers/schedulers separate deployments?
      - Are scheduled jobs single-run-safe (leader election/locks) and idempotent?
      - Do logs go to stdout as structured events with trace and tenant context, scrubbed of secrets/PII?
      - Are RED metrics, OTel traces (propagated through queues), and burn-rate SLO alerts in place? Do pages fire on symptoms, not causes?
      - Does dev/CI use production-kind backing services (real DB/broker in containers)?
      - Does every flag have an owner and expiry? Are stale flags (>100% rollout for months) present? Does flag evaluation have local defaults if the flag service dies?
      - Are releases progressive (canary + auto-rollback) and decoupled from deploys via flags?
      - Are all migrations expand/contract such that the previous code version still runs (rollback-safe)?
      - Do admin/backfill tasks run as reviewed, versioned jobs rather than console surgery?
      - Are resources cost-tagged per service/team with anomaly alerting?
      - Are base images digest-pinned and mirrored, SBOMs generated, image scans gating CI with expiring exceptions, and artifacts signed + verified at admission?
      - Can a full environment be recreated from code? Are preview/ephemeral environments available, and is shared backing state namespaced per environment?
      - Do metric labels avoid unbounded cardinality, is trace sampling tail-biased toward errors/slow requests, and can telemetry loss never block request serving?
      
    • 07-anti-patterns-catalog.md 11.8 KB
      # 07 — Anti-Patterns Catalog
      
      Named failure modes with detection signals and remediation. In AUDIT mode, use
      the detection signals as grep/inspection targets; in BUILD mode, treat each as a
      "never do this" with the listed alternative.
      
      ## 1. Distributed monolith
      
      **What:** Microservices' costs (network, ops, versioning) with a monolith's
      coupling (lockstep deploys, shared data, synchronous chains).
      
      **Detection signals:**
      - Release notes/pipelines deploy multiple services together "because they have to".
      - Service A's PR requires a matching PR in service B to not break (no contract versioning, rules/03 §8).
      - Synchronous call chains ≥ 3 services deep on the request path; availability = product of every link.
      - Shared libraries containing *domain* logic that force coordinated upgrades.
      
      **Fix:** Either merge them back (modular monolith — usually right) or make them
      actually independent: versioned contracts + consumer tests, async where the
      semantics allow, kill deep sync chains via composition at the edge (BFF/gateway)
      or data replication into the caller.
      
      **Severity default:** Critical (it's all of the cost, none of the benefit).
      
      ## 2. Shared database integration
      
      **What:** Two or more services/modules read or write the same tables.
      
      **Why it's fatal:** The schema becomes an unversionable public API; no one can
      migrate without breaking unknown others; invariants are enforced nowhere;
      ownership is fiction.
      
      **Detection signals:** multiple services with credentials to the same schema;
      cross-service foreign keys; "read-only access to their DB for reporting";
      views shared as integration interfaces.
      
      **Fix:** One writer-owner per table/schema. Other consumers integrate via the
      owner's API, published events + their own projection, or CDC into an analytics
      store (for reporting). Strangle existing shared access: inventory consumers,
      give each a sanctioned path, then revoke credentials.
      
      **Severity default:** Critical for shared writes; High for shared reads.
      
      ## 3. God service / god module / god aggregate
      
      **What:** One component that accretes every new feature (often named `core`,
      `common`, `platform`, `UserService`, or the original monolith in a "micro-
      services" system).
      
      **Detection signals:** disproportionate size/churn (one service receives 60% of
      all commits); every cross-team change touches it; its test suite dominates CI
      time; every other service depends on it synchronously; aggregates loading
      hundreds of rows to change one field.
      
      **Fix:** Split along bounded contexts (rules/02 §1) using strangler extraction
      (rules/01 §8). For god aggregates: re-interrogate true invariants and split into
      smaller aggregates linked by events (rules/02 §3).
      
      **Severity default:** High.
      
      ## 4. Premature microservices
      
      **What:** Decomposing into services before the domain is understood or the team
      can operate them. Wrong boundaries in a distributed system are 10x costlier to
      move than module boundaries.
      
      **Detection signals:** more services than engineers; "we'll need it at scale"
      with no load data; nano-services (an entity-per-service, services that only
      CRUD one table); local dev requires running 12 containers; no platform
      engineering yet plenty of services.
      
      **Fix:** Consolidate into a modular monolith with enforced boundaries
      (rules/01 §1–2); keep events/contracts at module seams so future extraction
      stays cheap.
      
      **Severity default:** High.
      
      ## 5. Leaky abstraction / vendor bleed-through
      
      **What:** Infrastructure or vendor details escaping their boundary: ORM
      entities as API DTOs, vendor SDK types in domain signatures, HTTP status logic
      in domain code, broker-specific headers in business logic.
      
      **Detection signals:** importing the database/ORM/cloud SDK package in domain
      or use-case layers (architecture test catches this, rules/02 §5); API responses
      that change when an internal column is renamed; "port" interfaces that mirror a
      vendor API one-to-one (rules/02 §4).
      
      **Fix:** DTOs at every boundary, consumer-shaped ports, mapping in adapters.
      Cheapest enforced as an import-rule fitness function.
      
      **Severity default:** Medium (High when public API = DB schema, since clients
      now pin your internals).
      
      ## 6. Sync chain of doom / temporal coupling
      
      **What:** Request-path workflows requiring N services up simultaneously;
      availability multiplies (0.99^5 ≈ 0.95) and tail latency compounds.
      
      **Detection signals:** traces showing deep sequential fan-out per user request;
      a "simple" page making 30 internal calls; timeout budgets that can't fit
      (rules/04 §1); incident reports where an unrelated service's outage broke checkout.
      
      **Fix:** Replicate the data you need (event-carried state transfer), accept
      staleness; collapse hops (merge services or compose at the edge); make non-
      essential steps async (queue, saga). Set a fitness function: max sync depth ≤ 2
      on critical paths.
      
      **Severity default:** High on revenue-critical paths.
      
      ## 7. Event spaghetti / hidden workflow
      
      **What:** Choreographed events forming a workflow nobody can see: cycles
      (A's event triggers B, whose event re-triggers A), side-effect cascades, and
      "why did this run?" archaeology.
      
      **Detection signals:** no diagram or registry of who consumes what; event
      cycles; an innocuous event causing a 9-hop cascade; debugging requires grepping
      all consumers for a topic name; commands disguised as events (rules/03 §6).
      
      **Fix:** Event catalog (producer, consumers, schema, purpose) generated from
      code/registry; orchestrate multi-step workflows explicitly (rules/03 §5);
      detect and break cycles.
      
      **Severity default:** Medium; High once a cycle exists.
      
      ## 8. Cache as source of truth (accidental database)
      
      **What:** Data that exists only in Redis/Memcached, or correctness depending on
      cache content (rules/05 §4).
      
      **Detection signals:** writes that go to cache without a durable backing write;
      "don't restart Redis, we'll lose X"; missing or infinite TTLs on entries nobody
      can rebuild; cache-down = wrong answers rather than slow answers.
      
      **Fix:** Make the durable store authoritative and the cache rebuildable, or
      promote the data to a real durable store (Redis with AOF/replication *declared*
      as a database, with backup/DR treated accordingly).
      
      **Severity default:** Critical when business data is cache-only.
      
      ## 9. Distributed big ball of mud via shared "common" libraries
      
      **What:** A `common`/`shared-utils` library hoarding domain types, clients, and
      helpers, version-pinned by every service — coupling all services through the
      dependency graph instead of the network.
      
      **Detection signals:** bumping `common` requires releasing everything; domain
      entities defined in the shared lib; teams blocked on another team's lib release.
      
      **Fix:** Share only truly generic, stable code (logging, telemetry, auth
      middleware) with strict semver. Domain types are duplicated per context
      (rules/02 §1) or shared via *schemas* (protobuf/OpenAPI), not via code libraries.
      
      **Severity default:** Medium; High when domain logic lives in the shared lib.
      
      ## 10. Resume-driven and dogma-driven architecture
      
      **What:** Technology chosen for novelty or ideology, not problem fit: Kafka for
      10 msg/s, Kubernetes for one container, event sourcing for a CRUD app, "no
      foreign keys because microservices" inside a single schema.
      
      **Detection signals:** infrastructure whose capacity exceeds need by 100x; no
      ADR with rejected alternatives (rules/01 §4); ops burden dominated by tools
      serving no measured requirement; the only justification on record is an analogy
      to a FAANG blog post.
      
      **Fix:** Demand the ADR with load numbers and consequences; downgrade to boring
      technology (managed Postgres, a simple queue, one deployable) where the numbers
      say so.
      
      **Severity default:** Medium (High when ops burden causes incidents).
      
      ## 11. Lava layer / strangler that never strangles
      
      **What:** Successive half-finished migrations sedimented in the codebase: three
      HTTP clients, two ORMs, "old auth" and "new auth" both live, a strangler fig
      whose old path was never deleted. Every new engineer adds a fourth pattern
      because no one can tell which is canonical.
      
      **Detection signals:** multiple frameworks/libraries serving the same purpose
      with no deprecation markers; migration ADRs/tickets open > 2 quarters with both
      paths in prod; `_v2`/`_new`/`_legacy` suffixes older than a year; nobody can
      answer "which one do I use?" without asking in chat.
      
      **Fix:** Every migration gets a kill date and a completion definition ("old path
      deleted") tracked like a feature; freeze new usages of the deprecated pattern
      mechanically (lint rule failing on new imports of the old module); finish or
      explicitly abandon — a documented "we keep both because X" beats sediment.
      
      **Severity default:** Medium; High when the duplicated layer is security-
      relevant (two auth paths = the attacker picks the weaker one).
      
      ## 12. Snowflake environments and config sprawl
      
      **What:** Production works because of hand-applied settings nobody recorded;
      hundreds of config keys with unknown consumers; per-environment behavior that
      exists nowhere in git.
      
      **Detection signals:** "don't touch that box"; IaC plan/apply shows permanent
      diff (drift); config keys grep to zero readers; staging incident playbooks
      differ from prod's; restoring an environment from code has never been done.
      
      **Fix:** Import live state into IaC, enable drift detection alarms (rules/06
      §2, §11); delete config keys with no readers (after a deprecation log-on-read
      period); rebuild one environment from code per quarter as a fitness function.
      
      **Severity default:** High (it's unrecoverability in disguise — DR depends on
      rebuildability).
      
      ## 13. Smaller but deadly (quick list)
      
      - **Fan-out N+1 over the network:** per-item remote calls in a loop → batch APIs / data replication. High on hot paths.
      - **Chatty two-way coupling:** A calls B and B calls A → merge them or invert one direction with events. High.
      - **Timeout-free integration / retry-everywhere:** see rules/04 §1–2. Critical/High.
      - **Config-in-code, env conditionals, secrets in git:** see rules/06 §2. Critical for secrets.
      - **Anemic domain + transaction-script-everywhere in a complex core domain:** rules/02 §3. Medium.
      - **Entity services (`UserService`, `OrderService` as bags of CRUD)** instead of capability-oriented boundaries → re-cut along use cases/contexts. Medium.
      - **Queue as database / years of retention in the broker** for operational reads → project into a store; brokers are transport. Medium.
      - **Unowned components:** no team on-call for a prod service (rules/01 §7). Critical.
      
      ## Audit checklist
      
      - Do any two services deploy in lockstep or require synchronized PRs (distributed monolith)?
      - Does more than one service write — or read without a sanctioned contract — the same tables (shared database)?
      - Is there a god component (top of churn + dependency in-degree + size by a wide margin)?
      - Are there more services than the team can independently deploy, operate, and debug (premature microservices)? Could the system collapse to fewer deployables?
      - Do domain/use-case layers import ORM, transport, or vendor SDK types (leaky abstraction)? Are API DTOs the same classes as DB entities?
      - What is the maximum synchronous call depth on revenue-critical paths? Is it > 2?
      - Is there an event catalog? Are there event cycles or multi-hop cascades nobody documented?
      - Is any business data stored only in a cache? Would a cache flush cause wrong answers?
      - Does a shared `common` library contain domain types or force coordinated releases?
      - Is every heavyweight technology (broker, orchestrator, event sourcing) justified by an ADR with measured load, or by fashion?
      - Are there per-item remote calls in loops on hot paths (network N+1)?
      - Does every production component have an owning, on-call team?
      - Are there sedimented half-migrations (duplicate clients/ORMs/auth paths, `_v2`/`_legacy` older than a year) without kill dates?
      - Can every environment be rebuilt from git, or does production depend on hand-applied, unrecorded state (drift in IaC plans)?
      
    • 08-nats-jetstream.md 22.1 KB
      # 08 — NATS JetStream
      
      Scope: building on or auditing NATS JetStream as a primary message bus (Go
      services). The general distributed-messaging doctrine of **rules/03** —
      exactly-once is a myth, ack after durable commit, outbox/never-dual-write, DLQs,
      ordering, schema evolution, claim-check, idempotent consumers — applies in full
      and is **not repeated here**. This file maps that doctrine onto JetStream's
      concrete mechanisms and flags JetStream-specific failure modes. Consumer-loop
      and backpressure mechanics live in **sota-async-concurrency**; TLS/mesh/leafnode
      transport security in **sota-network-security**; NKEY/JWT/account auth in
      **sota-identity-access** and **sota-secrets-management**.
      
      Version: written against **NATS Server 2.11/2.12**; 2.14 has since superseded
      2.12 (2.13 was skipped; 2.12.x continues as a maintenance line) — run the
      latest stable release and verify the current line with a quick web search at
      time of use. KV and Object stores are GA and JetStream-backed. The `jetstream`
      Go package is the current API; the older `nc.JetStream()` JetStreamContext is
      legacy. Pin and track your server line — per-message TTL, KV limit markers, and
      the 2.12+ additions (atomic batch publish, distributed counters, message
      scheduling — recurring/cron schedules from 2.14) are gated on server API level.
      Upgrade note: the v2 ack-subject format becomes the default in 2.15 — accounts
      with granular `$JS.ACK.<stream>.>`/`$JS.FC.<stream>.>` permissions or
      imports/exports must update their ACLs before upgrading (a catch-all
      `$JS.ACK.>` needs no change).
      
      ## Core NATS vs JetStream: choose per subject, not per cluster
      
      ### Rule: Use core NATS for fire-and-forget; reach for JetStream only when you need persistence, replay, or dedup.
      - **Core NATS** (plain `nc.Publish`/`Subscribe`, request-reply, queue groups) is
        at-most-once: no subscriber online means the message is gone. Correct for RPC,
        health pings, cache invalidations, ephemeral fan-out — anything where loss is
        acceptable and latency is king.
      - **JetStream** adds a persistent log with acks, replay, and publish-dedup. Use
        it when a missed message is a bug: orders, state changes, work queues, audit.
      - Request-reply (`nc.Request`) and **queue groups** (load-balanced delivery to a
        named group of subscribers) are core-NATS primitives — you do not need
        JetStream for competing consumers on transient work. Putting RPC traffic
        through a stream is a common over-reach: it pays the storage/ack cost for
        no benefit.
      - "Exactly-once" on JetStream is **dedup-on-publish + idempotent consumption**,
        not a delivery guarantee (rules/03 §3). Do not design as if it were one.
      
      ## Subject design: the schema of your whole bus
      
      ### Rule: Design the subject hierarchy deliberately; tokens are your routing and authz surface.
      - Dot-separated tokens, hierarchy from general to specific:
        `orders.eu.created`, `orders.eu.shipped`. Tokens are the unit of wildcard
        matching and of account permissions — design them so a single subject pattern
        grants exactly the access a service needs.
      - Wildcards: `*` matches one token, `>` matches one-or-more trailing tokens.
        `orders.*.created` = any region's created events; `orders.>` = everything
        under orders. A consumer filtering on `>` at the top of a busy stream is a
        firehose — filter as specifically as the work requires.
      - One stream binds a set of subjects; a subject belongs to **one stream** (two
        streams capturing the same subject double-store and confuse consumers). Map
        subjects→streams on paper before creating either.
      - **Subject transforms** (`SubjectTransform`, and per-source transforms) rewrite
        subjects on ingest or when sourcing — use for namespacing aggregated streams
        (`orders.>` → `agg.orders.>`), not as a substitute for getting the producer's
        subjects right.
      
      ## Streams: an unbounded stream is a latent outage
      
      ### Rule: Every stream has explicit limits. No max-bytes and no max-age = a disk-fill incident waiting to happen.
      - Set `MaxBytes` and `MaxAge` on every stream, sized to retention need and disk.
        `MaxMsgs`, `MaxMsgsPerSubject`, and `MaxMsgSize` as the workload requires.
        A stream with all limits unset grows until the file store fills and the node —
        and its RAFT peers — degrade. This is the JetStream form of rules/03's
        "bounded queues everywhere"; treat unbounded as **High**.
      - **Storage**: `FileStorage` (default, durable, survives restart) for anything
        that matters; `MemoryStorage` only for fast, loss-tolerant, regenerable data.
      - **Retention policy** — pick by consumption shape:
        - `LimitsPolicy` (default): messages kept until a limit evicts them;
          many independent consumers replay the same log. Use for event streams.
        - `WorkQueuePolicy`: each message consumable exactly once, removed on ack;
          a true job queue. Requires non-overlapping consumer filter subjects.
        - `InterestPolicy`: message retained only while a bound consumer hasn't acked
          it; storage tracks consumer interest. Use when you want event semantics but
          automatic cleanup once all known consumers have processed.
      - **Discard policy**: `DiscardOld` (default — evict oldest to make room) for
        logs where newest matters; `DiscardNew` (reject the publish) when losing old
        data is unacceptable and you want the producer to feel backpressure. Pair
        `DiscardNew` with `DiscardNewPerSubject` + `MaxMsgsPerSubject` to bound each
        subject independently.
      
      ```text
      GOOD (bounded, durable, work queue):
        nats stream add ORDERS \
          --subjects 'orders.>' --storage file --retention work \
          --discard new --max-bytes 10GB --max-age 720h \
          --max-msgs-per-subject 1000 --replicas 3 --dupe-window 2m
      
      BAD: nats stream add ORDERS --subjects 'orders.>' --storage file
           # no max-bytes, no max-age, no replicas, R1 → unbounded + no HA
      ```
      
      ### Rule: R3 for anything you can't afford to lose; R1 is dev-only. Mirrors/sources for aggregation, not HA.
      - `Replicas: 3` (R3) gives a per-stream RAFT group that survives one node loss.
        R1 has no redundancy — a single disk failure loses the stream. Production
        durable streams are R3 (or R5 across failure zones); `--replicas 1` in prod is
        **High**. Use `Placement` (cluster + tags) to pin streams to the right
        hardware/zone.
      - **Mirrors** (`Mirror`) are a read-only 1:1 copy of one origin stream — for
        fan-out reads, backup, or cross-region read replicas. **Sources** (`Sources`)
        aggregate many streams into one (fan-in, cross-region roll-up), with optional
        per-source subject transforms and filters. Neither is a substitute for R3:
        they copy asynchronously and lag; HA is replicas, aggregation is mirrors/sources.
      - **Mirror promotion** (2.12+) converts a mirror into a writable stream for DR
        failover — a recovery lever, not HA: the mirror is async and may lag behind
        the origin at promotion time, so reconcile for the tail you may have lost.
      
      ## Publish & idempotent dedup: the duplicate window
      
      ### Rule: Publishers set `Nats-Msg-Id`; the stream's duplicate window dedups. This is the publish half of effectively-once.
      - JetStream publish is at-least-once: a publish ack can be lost and the client
        retries, producing a duplicate. Set the **`Nats-Msg-Id`** header to a stable
        business id; the stream rejects a second message with the same id seen within
        its **duplicate window** (`Duplicates`/`--dupe-window`, default 2 minutes).
      - Size the window to your **maximum publish-retry horizon**, not larger — the
        window is held in memory; large windows cost RAM. If a retry can arrive 10
        minutes later, a 2-minute window won't dedup it; if retries resolve in seconds,
        don't set hours.
      - **Always check the publish ack.** Use `js.Publish` (sync — blocks for the ack)
        or `js.PublishAsync` with a **bounded** in-flight window (`PublishAsyncMaxPending`)
        and drain/await before considering messages durable. Ignoring the ack is silent
        message loss — the rules/03 "no dual-write / confirm the write" rule applied to
        the publish path; treat fire-and-forget JetStream publishing as **High**.
      - This is only half of effectively-once. The other half is **idempotent /
        de-duplicated consumption** (rules/03 §2) — the duplicate window does nothing
        for duplicates a consumer sees from redelivery.
      - **Atomic batch publish** (2.12+) writes a multi-message batch to one stream
        all-or-nothing — use it where multi-message atomicity previously forced an
        outbox-style workaround; 2.14 adds a fast flow-controlled variant
        (`AllowBatchPublish`). It does **not** replace the outbox for DB-write +
        publish dual-writes (rules/03) — that atomicity spans two systems.
      
      ```go
      js, _ := jetstream.New(nc)
      ack, err := js.Publish(ctx, "orders.eu.created", payload,
          jetstream.WithMsgID(order.ID)) // dedup key; checked vs the stream's window
      if err != nil { return err }       // NOT durable until ack returns without error
      _ = ack.Sequence
      ```
      
      ## Consumers: pull is the modern default; explicit ack after processing
      
      ### Rule: Use durable pull consumers via the `jetstream` package. Push and JetStreamContext are legacy.
      - The `github.com/nats-io/nats.go/jetstream` package is the current API. Create a
        consumer, then pull with `Consume` (callback, continuous), `Messages`
        (iterator), or `Fetch` (explicit batch). The older `nc.JetStream()`
        JetStreamContext + `Subscribe` is legacy — don't write new code against it.
      - **Durable** (named, `Durable`/explicit name) for work that must resume where it
        left off after restart; **ephemeral** for throwaway tail-follows. An ephemeral
        consumer for durable work loses its position on disconnect — **High** for any
        at-least-once job (rules/03 anti-pattern).
      - **`AckExplicit`** (default and correct): ack **after** the state change is
        durably committed (rules/03 §3). `AckNone` and `AckAll` discard the
        per-message safety net; ack-on-receipt before processing loses messages on
        crash — never do it.
      - For stronger guarantees use **double-ack** (`msg.DoubleAck(ctx)`/`AckSync`):
        the client waits for the server to confirm the ack landed, closing the window
        where a lost ack causes redelivery of an already-processed message.
      
      ### Rule: `MaxAckPending` is your primary backpressure knob; `MaxDeliver` bounds poison redelivery.
      - **`MaxAckPending`**: the cap on un-acked in-flight messages per consumer — the
        JetStream analogue of AMQP prefetch (rules/03 §7b). Unbounded (`-1`) lets a
        slow consumer pull more than it can process; set it explicitly, sized to
        handler throughput (sota-async-concurrency rules/06). Unbounded `MaxAckPending`
        on a real consumer is **High**.
      - **`AckWait`**: how long the server waits for an ack before redelivering. Size
        it above your worst-case processing time, or in-flight work gets redelivered
        while still running. Send `msg.InProgress()` to extend it for long handlers.
      - **`MaxDeliver`**: cap redelivery attempts. **Unset (infinite) means a poison
        message redelivers forever**, burning a consumer slot and CPU — rules/03's
        poison-vs-transient rule, and **High** here. Set `MaxDeliver` and a `BackOff`
        array (per-attempt delays) so transient failures retry with growing spacing.
      - **`FilterSubjects`** (multi-filter supported) narrows a consumer to the
        subjects it handles — essential for `WorkQueuePolicy` streams, where consumer
        filters must not overlap.
      - **`DeliverPolicy`**: `DeliverAll` (replay from start), `DeliverNew` (only new),
        `DeliverByStartSequence`/`DeliverByStartTime` (replay from a point),
        `DeliverLastPerSubject` (latest per subject — current-state bootstrap).
        **`ReplayPolicy`**: `ReplayInstant` (default) or `ReplayOriginal` (reproduce
        original inter-message timing).
      - **Ordered consumers** (`OrderedConsumer`) give a single client an in-order,
        gap-detecting replay that auto-recreates on error — use for single-consumer
        projections/replay, not for scaled competing-consumer work.
      
      ```go
      cons, _ := stream.CreateOrUpdateConsumer(ctx, jetstream.ConsumerConfig{
          Durable:       "orders-projector",
          AckPolicy:     jetstream.AckExplicitPolicy,
          AckWait:       30 * time.Second,
          MaxDeliver:    5,
          MaxAckPending: 256,                       // backpressure
          FilterSubjects: []string{"orders.eu.>"},
          BackOff:       []time.Duration{1*time.Second, 5*time.Second, 30*time.Second},
      })
      cc, _ := cons.Consume(func(msg jetstream.Msg) {
          if err := process(ctx, msg); err != nil { // do the work first
              _ = msg.NakWithDelay(5 * time.Second) // transient: retry later
              return
          }
          _ = msg.DoubleAck(ctx)                     // commit only after success
      })
      defer cc.Stop()
      // BAD: msg.Ack() before process()  → crash = silent loss (rules/03 §3)
      ```
      
      ## Error handling: ack/nak/term/inprogress, and the DLQ you must build
      
      ### Rule: JetStream has no built-in DLQ. Route poison messages out yourself after `MaxDeliver`.
      - The four dispositions:
        - **Ack** — processed successfully and committed; remove from redelivery.
        - **Nak** (`Nak`/`NakWithDelay`) — transient failure; redeliver (with delay).
        - **Term** (`Term`) — permanently unprocessable (poison); stop redelivery
          immediately, do **not** wait out `MaxDeliver`. Use it the moment you know a
          message will never succeed (schema-invalid, references a deleted entity).
        - **InProgress** — still working; reset the `AckWait` timer.
      - Distinguish transient from poison (rules/03 §7): `Nak` transient, `Term`
        poison. Don't `Nak` a poison message `MaxDeliver` times — that's the redelivery
        storm rule.
      - Because there's no native dead-letter, build one: subscribe to the advisory
        **`$JS.EVENT.ADVISORY.CONSUMER.MAX_DELIVERIES.>`** and republish the exhausted
        message to a dedicated **DLQ stream**, or have the handler republish on its
        final attempt. Then apply rules/03 §7 in full: alert on DLQ depth > 0, and have
        a tested redrive runbook. A DLQ nobody drains is a data-loss buffer.
      - Consumers must be **idempotent** regardless (rules/03 §2): redelivery after a
        lost ack, double-ack timeout, or `AckWait` expiry will hand you the same
        message again.
      
      ## KV & Object stores: config, coordination, and the claim-check
      
      ### Rule: Use JetStream KV for config/locks/dedup-state; Object store for large blobs (claim-check).
      - **KV** is a JetStream-backed bucket (GA). Each key is versioned with a
        monotonic **revision**; do optimistic concurrency with **compare-and-set**
        (`Update` against an expected revision — rejects if the key moved under you),
        the rules/03 §12 "let the store arbitrate" pattern without a separate lock
        service. **Watchers** stream changes (config hot-reload, coordination);
        **history** keeps N prior revisions (bucket `History`, default 1).
      - NATS KV is a good fit for **token revocation** lists and similar hot lookups:
        fast reads, watch-driven propagation, CAS for safe updates.
      - KV TTL: bucket-level TTL is long-standing; **per-key TTL** is newer (NATS 2.11+,
        via per-message TTL / `Nats-TTL` and KV limit markers) — verify your server
        supports it before relying on it, and note the stream's `MaxAge` still takes
        precedence over a per-key TTL. Don't assume per-key expiry on an older server.
      - **Object store** holds large payloads. Apply rules/03's **claim-check**: keep
        big blobs and secrets out of messages; publish a reference (object id/bucket),
        let the consumer fetch from the object store. The bus is a shared failure
        domain, not a file server — fat messages saturate streams and replication.
      
      ## Clustering, HA & multi-tenancy: accounts are the isolation boundary
      
      ### Rule: Accounts isolate tenants; domains/leafnodes bridge edge↔hub; per-account quotas bound blast radius.
      - JetStream runs a cluster-wide **meta RAFT** group plus a per-stream/per-consumer
        RAFT group; leaders are elected per group. R3 streams tolerate one node loss.
      - **Accounts** are the hard multi-tenant boundary: separate subject space, separate
        JetStream assets, no cross-account visibility except via explicit subject
        exports/imports. Multi-tenant systems isolate tenants by **account**, not by
        subject-prefix convention within one account (rules/05 tenancy discipline) —
        subject-prefix-only "isolation" is a cross-tenant leak risk: **High**.
      - Set **per-account JetStream limits/quotas** (max memory, max file store, max
        streams/consumers) so one tenant can't exhaust the cluster — the rules/04
        bulkhead applied to storage.
      - **Leafnodes** extend a cluster to the edge; **gateways/superclusters** connect
        clusters across regions; **JetStream domains** name an isolated JetStream
        instance (e.g. an edge leafnode hub) so edge and hub streams don't collide and
        can be addressed/mirrored explicitly. Use domains for edge↔hub topologies.
      - Auth and transport are out of scope here: NKEY/JWT and account design live in
        **sota-identity-access**/**sota-secrets-management**; TLS, leafnode/gateway
        transport security, and mesh in **sota-network-security**. JetStream over a
        cluster with no TLS and a shared account is not production-ready.
      
      ## Operations: monitor lag, snapshot, drain
      
      ### Rule: Watch consumer lag and ack-pending; back up streams; drain on shutdown.
      - Monitor per consumer: **`num_pending`** (unprocessed backlog),
        **`num_ack_pending`** (in-flight un-acked), **`redelivered`** count, and stream
        bytes vs `MaxBytes`. `num_pending` climbing monotonically = under-provisioned
        consumer or a poison loop (rules/03 §7). Alert on it.
      - `nats stream report` and `nats consumer report` are the first-line operability
        tools; advisories on `$JS.EVENT.ADVISORY.>` surface max-deliveries, terminated
        messages, and leader elections. Export metrics vendor-neutrally (the server's
        monitoring endpoints / `nats-surveyor`) into whatever scrapes them — keep the
        collection stack out of the design.
      - **Snapshot/restore**: streams support snapshot/backup and restore — schedule
        them, store off-cluster, and rehearse restore (rules/03 / sota-databases
        backup discipline). Replicas are HA, not backup.
      - **Graceful drain**: on shutdown call `Stop`/`Drain` on consume contexts and
        `nc.Drain()` so in-flight messages finish and acks flush rather than being cut
        mid-process and redelivered (sota-async-concurrency graceful-shutdown).
      - Don't block the consume callback: offload slow work and let `MaxAckPending`
        bound concurrency (sota-async-concurrency rules/06). A blocking callback stalls
        delivery and inflates ack-pending.
      
      ## Anti-patterns (JetStream-specific)
      
      - **Unbounded stream** — no `MaxBytes`/`MaxAge`: disk fills, RAFT peers degrade.
      - **No `MaxDeliver`** — a poison message redelivers forever (rules/03 §7).
      - **Ack before processing** / `AckNone` on durable work — silent loss on crash.
      - **One giant catch-all stream** (`>` over everything) — couples unrelated
        workloads, breaks per-subject limits and per-consumer reasoning; the rules/07
        "god service" smell in stream form.
      - **At-least-once publisher with no dedup** — no `Nats-Msg-Id`/duplicate window,
        so publish retries duplicate silently.
      - **Ephemeral consumer for durable work** — loses position on disconnect.
      - **Unbounded `MaxAckPending`** — no consumer backpressure; slow consumer OOMs.
      - **Ignored publish acks** — fire-and-forget JetStream publish = silent loss.
      - **Subject-prefix "tenancy"** in one account instead of per-account isolation.
      - **Blocking the consume callback** — stalls delivery (sota-async-concurrency).
      
      ## Audit checklist
      
      - [ ] Is core NATS used for fire-and-forget/RPC and JetStream reserved for
            persistence/replay/dedup, rather than RPC pushed through streams?
            (`nats stream ls`; look for streams capturing request-reply subjects.)
      - [ ] Does **every** stream set `MaxBytes` and `MaxAge` (and per-subject limits
            where relevant)? (`nats stream info <s>` → Limits; grep IaC/config for
            stream definitions missing `max_bytes`/`max_age`.)
      - [ ] Are durable production streams **R3+**? Any `--replicas 1` / `Replicas: 1`
            outside dev? (`nats stream report` shows replica counts.)
      - [ ] Is retention policy correct for the access pattern (work queue vs limits vs
            interest), and `DiscardNew` used where old-data loss is unacceptable?
      - [ ] Do publishers set **`Nats-Msg-Id`** and is the **duplicate window** sized to
            the retry horizon? (grep for `WithMsgID`/`Nats-Msg-Id`; `nats stream info`
            → Duplicate Window.)
      - [ ] Are publish acks checked — `js.Publish` sync, or `PublishAsync` with bounded
            pending and a drain/await? Any fire-and-forget publishes? (grep `PublishAsync`
            without `PublishAsyncMaxPending`/await.)
      - [ ] Are consumers **durable pull** via the `jetstream` package, not ephemeral
            and not legacy `JetStreamContext`/`Subscribe` for durable work? (grep
            `nc.JetStream(`, `.Subscribe(` in JS paths.)
      - [ ] Is `AckPolicy` **explicit**, with ack/DoubleAck **after** the commit, never
            before processing and never `AckNone` on durable work? (read the consume
            callback; ack should be the last successful step.)
      - [ ] Is `MaxAckPending` set (not `-1`) and sized to handler throughput?
            (`nats consumer info` → Max Ack Pending.)
      - [ ] Is `MaxDeliver` bounded with a `BackOff`, and poison `Term`ed (not `Nak`ed
            to exhaustion)? (grep `MaxDeliver`; consumer config; advisory consumers.)
      - [ ] Is there a **DLQ stream** fed by `MAX_DELIVERIES` advisories or a final-attempt
            republish, with depth alerting and a redrive runbook? (`nats stream ls` for a
            DLQ/dead-letter stream; subscriber on `$JS.EVENT.ADVISORY.CONSUMER.MAX_DELIVERIES.>`.)
      - [ ] Are consumers idempotent under redelivery (rules/03 §2)?
      - [ ] KV: optimistic concurrency via revision CAS (`Update`), not blind `Put`,
            for contended keys (token revocation, locks)? Per-key TTL only relied on if
            the server supports it? (`nats kv ls`; grep `kv.Update` vs `kv.Put`.)
      - [ ] Large payloads via Object store claim-check, not inline in messages?
            (`nats object ls`; check message sizes / `MaxMsgSize`.)
      - [ ] Multi-tenant isolation by **account** (with per-account JetStream quotas),
            not subject-prefix convention in a shared account? (server/account config.)
      - [ ] Edge↔hub topologies use leafnodes + **JetStream domains**; TLS and
            NKEY/JWT auth in place (defer to network-security / identity-access)?
      - [ ] Monitoring on `num_pending`/`num_ack_pending`/`redelivered` with lag
            alerts; stream snapshots scheduled and restore-rehearsed; graceful
            `Drain` on shutdown? (`nats consumer report`; check shutdown path.)
      
  • SKILL.md 10.4 KB
    ---
    name: sota-architecture
    description: >-
      State-of-the-art software and system architecture rules (2026) for both
      building and auditing. Use when designing, building, refactoring, or
      extending system architecture — boundaries, DDD, hexagonal/clean
      architecture, event-driven design, CQRS, sagas, messaging, caching,
      sharding, multi-tenancy, resilience, scalability, 12-factor/cloud-native —
      AND when auditing existing architecture for quality (design review,
      anti-patterns like distributed monolith or shared database,
      reliability/scalability assessment). Trigger keywords: architecture, system
      design, microservices, monolith, serverless, bounded context, DDD,
      aggregate, hexagonal, clean architecture, event-driven, Kafka, NATS,
      JetStream, messaging, pub/sub, stream, consumer, queue, saga, outbox,
      idempotency, CQRS, resilience, circuit breaker, retry, timeout,
      backpressure, caching, sharding, partitioning, multi-tenant, 12-factor,
      cloud-native, feature flag, ADR, scalability, anti-pattern, design review,
      architecture audit.
    ---
    
    # SOTA Architecture (2026)
    
    ## Purpose
    
    Dense, enforceable rules for software and system architecture: choosing styles,
    drawing boundaries, surviving distributed systems, scaling state, and shipping
    cloud-natively. One rule set, two modes — apply the rules while **building**,
    or check code against them while **auditing**. All substance lives in `rules/`;
    this file routes you to the right one.
    
    ## BUILD mode — designing or writing code
    
    1. **Identify the decision surface.** Before writing code, list the architectural
       decisions in play (style, boundaries, sync/async, datastore, tenancy, caching).
       Use the index below to read the relevant rules files *before* committing to a
       design — minimum: 01 for any new system/service, 03 for anything crossing a
       network, 07 always (know what failure looks like).
    2. **Default to boring.** Modular monolith, one database, sync calls within the
       deadline budget, queues only where semantics are async. Escalate complexity
       only when a rule's stated forces apply, and record it.
    3. **Write the ADR first** for any Type 1 (hard to reverse) decision: context,
       decision, consequences (must include downsides), rejected alternatives. Put it
       in `docs/adr/`.
    4. **Apply rules as you code, not after.** Timeouts/retries/idempotency at every
       integration point as you create it; ports before adapters; tenant_id and trace
       context plumbed from the first commit. Retrofitting these is 10x the cost.
    5. **Encode the rules you adopted as fitness functions** (import-rule tests,
       contract tests in CI, SLO alerts) so they survive you.
    6. **Before finishing**, run the relevant "Audit checklist" sections from each
       rules file you used against your own output. Fix what fails or document why
       it's accepted (in the ADR).
    
    ## AUDIT mode — reviewing existing code or designs
    
    1. **Scope first.** Identify what you're auditing (whole system, one service, one
       PR) and read the matching rules files from the index. For a full architecture
       audit, work through all seven; for a PR, pick by topic.
    2. **Drive from the checklists.** Every rules file ends with an "Audit checklist"
       of yes/no questions — answer each with evidence (file:line, config, trace),
       never from the README's claims. Use rules/07 detection signals as concrete
       grep/inspection targets.
    3. **Verify mechanically where possible:** grep for client construction without
       timeouts, imports crossing layers, queries missing tenant scoping, `latest`
       image tags, dual-writes (DB write + publish in the same function without an
       outbox), shared DB credentials.
    4. **Report every violation as a finding** in this exact format:
    
       ```
       [SEVERITY] file:line — Rule violated: <rules-file §section, short rule name>
       Evidence: <what you observed>
       Impact: <what fails and when>
       Fix: <specific, smallest correct remediation>
       Effort: trivial | small | medium | large
       ```
    
    5. **Severity conventions:**
       - **Critical** — data loss/corruption, cross-tenant leak, full-outage mechanism, money/security path failing open: missing idempotency on payments, business data only in cache, shared-DB writes, liveness probe checking the DB, secrets in git, no owning team.
       - **High** — outage-magnifier or rollback-blocker: missing timeouts on hot paths, retry multiplication, lockstep deploys, breaking schema changes, sync chains > 2 on revenue paths, DLQ without alerting, missing read-your-writes on user-visible saves.
       - **Medium** — erodes evolvability/operability: leaky abstractions, anemic core domain, stale feature flags, missing ADRs, event spaghetti without cycles, unscoped shared libs.
       - **Low** — hygiene: naming drifting from ubiquitous language, missing runbook sections, unjittered crons that haven't yet caused incidents.
       - When in doubt between two severities on a revenue-, security-, or data-touching path, pick the higher.
    6. **Summarize** findings by severity with counts, then list the top 3 structural
       themes (not symptoms) and the order to fix them (Critical correctness →
       rollback/deploy safety → evolvability).
    
    ## Rules index
    
    | File | Topics | Read this when... |
    |---|---|---|
    | `rules/01-architecture-styles-and-decisions.md` | Modular monolith vs microservices vs serverless, extraction forces, ADRs, evolutionary architecture, fitness functions, Type 1/2 decisions, Conway's law, strangler fig, buy vs build | Starting a system, proposing/justifying a service split or merge, reviewing whether the architecture style fits, setting up ADRs or CI architecture gates |
    | `rules/02-domain-modeling-and-boundaries.md` | Bounded contexts, context maps, ubiquitous language, aggregates & invariants, hexagonal ports/adapters, clean-architecture dependency rule, ACLs, domain events, value objects, **how absence is encoded — the in-band-sentinel class and its three audit probes**, repositories, optimistic concurrency | Modeling a domain, defining module/service internals, reviewing layering and imports, fixing anemic models or god aggregates, wrapping vendors |
    | `rules/03-distributed-systems-and-events.md` | CAP/PACELC per-operation, idempotency mechanics, exactly-once myth, outbox/inbox, sagas (orchestration vs choreography, conservative leg ordering), reconciliation against an external system of record, event vs command, CQRS/event-sourcing adoption bar, DLQs, ordering, backpressure, schema evolution, contract tests, IDs & time | Anything crosses a network or a queue: designing/reviewing messaging, workflows spanning services, consistency questions, retry/duplicate bugs, integrations with a third party that holds authoritative state, API/event versioning |
    | `rules/04-resilience-and-failure-design.md` | Timeouts & deadline budgets, retries with jitter & budgets, circuit breakers, bulkheads, graceful degradation, fail open/closed, load shedding, liveness vs readiness, chaos engineering, SLOs, safe rollback/restart, stampedes | Designing or reviewing any integration point, incident follow-ups, "is this service production-ready", overload or cascading-failure concerns |
    | `rules/05-scalability-state-and-data.md` | Stateless services, autoscaling signals, DB scaling order, replica staleness, caching tiers & invalidation, partitioning/sharding keys, data lifecycle/retention, multi-tenancy models & isolation, workload separation, async heavy work | Scaling questions, cache design/review, choosing shard or partition keys, building/auditing multi-tenant systems, read-after-write bugs |
    | `rules/06-cloud-native-config-and-delivery.md` | 12-factor updated 2026: immutable artifacts, config & secrets, GitOps, disposability, observability (logs/metrics/traces/SLOs), backing services & dev/prod parity, feature-flag discipline, progressive delivery, expand/contract migrations, runbooks, cost signals | Setting up a new service's operational skeleton, reviewing deployability/config/secrets/flags, CI/CD and migration-safety review |
    | `rules/07-anti-patterns-catalog.md` | Distributed monolith, shared database, god services, premature microservices, leaky abstractions, sync chain of doom, event spaghetti, cache-as-truth, shared `common` libs, resume-driven design — each with detection signals, fix, default severity | Every audit (use as the detection playbook); in BUILD mode as the "never do this" list; naming and severity-rating a smell you've spotted |
    | `rules/08-nats-jetstream.md` | NATS JetStream as primary bus (Go): core NATS vs JetStream, subject/stream design, stream limits & retention/discard, R3/mirrors/sources, publish dedup (`Nats-Msg-Id` + duplicate window), pull consumers (`jetstream` pkg), ack/nak/term, `MaxAckPending`/`MaxDeliver`, DLQ-via-advisory, KV & Object stores, accounts/domains/leafnodes | Designing, building, or auditing anything on NATS JetStream — streams, consumers, KV/Object stores, multi-tenant accounts; apply on top of rules/03 (general eventing) for JetStream-specific config and Go client mechanics |
    
    ## Top-10 non-negotiables
    
    Check these on every build and every audit, regardless of scope:
    
    1. **Every remote call has an explicit timeout**, sized from measured latency, fitting the edge deadline budget. (04 §1)
    2. **Every message handler and retried operation is idempotent**, with dedupe state committed atomically with the state change. (03 §2)
    3. **No dual-writes**: DB state change + event publish happen via outbox/CDC, never as two independent writes. (03 §4)
    4. **One writer-owner per table**; services never integrate through a shared database. (07 §2)
    5. **Dependencies point inward**: domain imports no framework/ORM/vendor/transport types — enforced by an automated architecture test, not convention. (02 §5)
    6. **Retries are bounded, jittered, idempotent-only, and owned by exactly one layer** per edge. (04 §2)
    7. **Every queue has bounded size, a DLQ with alerting, and a tested redrive procedure.** (03 §7)
    8. **Tenant/user scoping is enforced below application code** (RLS or mandatory scoped layer) and present in every query, cache key, message, and log line. (05 §7)
    9. **Cross-service contracts (APIs and events) are versioned with CI compatibility checks**; changes are expand/contract; previous code version must still run (rollback-safe). (03 §8, 06 §7)
    10. **Every Type 1 decision has an ADR with consequences and rejected alternatives**, and every prod component has exactly one owning team. (01 §4, §7)
    
    A violation of any of these is at minimum High severity; most are Critical on
    data-, money-, or security-touching paths.
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related