Files
strangler-fig-demo/docs/playbook.md
T
ehoandClaude Haiku 4.5 ddec15ccb2 docs(architecture): add diagrams and implementation playbook
Added three new documents with nine Mermaid diagrams to make the strangler
fig strategy visible:

- README: container topology diagram at the start, with the proxy entry point
  and three seams labelled
- docs/architecture.md: five diagrams tracing the exact implementation:
  - The four seams and who holds authority at each boundary
  - How by-id read goes through the resolver, but list-read bypasses it
  - Case lifecycle state machine (the strategy in one picture)
  - Take-ownership sequence with failure windows annotated
  - Write-through error round-trip showing zero validation logic crossed
- docs/playbook.md: how to apply this to a production system:
  - Write-path decision tree (five read/write patterns)
  - Cutover ordering diagram (side-effects-free first, least recoverable last)
  - Seven transferable rules with pointers to the files that demonstrate them
  - Scope diagram of what's proven vs. left as your decisions

Resolved all 13 dangling § citations (to an absent spec doc) by linking to
the actual files or dropping them. Replaced portal-frontend/README.md
boilerplate with accurate content. All diagrams parse and link-check clean.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-08-01 09:15:39 +02:00

4.8 KiB
Raw Blame History

Applying this to a production system

The demo shows that the seams hold. This page is the transferable part: how to pick a path for each capability, and the order that keeps a cutover cheap to get wrong.

Which write path for which capability

Ask this per capability, not per system. Most systems end up running all five answers at once — that is the point of the pattern, not a sign of a messy migration.

flowchart TD
    S(["Pick one capability<br/>in the legacy system"]) --> Q1

    Q1{"Does the new UI<br/>only need to read it?"}
    Q1 -->|yes| A["<b>Read ACL</b><br/>translate at the boundary<br/>legacy stays authoritative"]

    Q1 -->|no| Q2{"Is it owned by a system<br/>you don't control?"}
    Q2 -->|yes| D["<b>Conformist</b><br/>surface its rules as-is<br/>don't fight or hide them"]

    Q2 -->|no| Q3{"Have you rebuilt the<br/>domain rules yet?"}
    Q3 -->|yes| E["<b>Take ownership</b><br/>new system becomes<br/>the authority"]

    Q3 -->|"no — and it's<br/>a whole workflow"| C["<b>Redirect</b><br/>send the user back out<br/>to the legacy screen"]
    Q3 -->|"no — but it's<br/>a simple edit"| B["<b>Write-through</b><br/>legacy still validates<br/>translator gets zero rules"]

Read ACL and write-through are the cheap ones and where most capabilities should sit for most of the migration. Take-ownership is the only path that moves authority, so it is the only one that needs a rollback story.

Ordering a cutover so failures stay cheap

The generalisable rule from TakeOwnershipHandler: free checks first, external systems before your local transaction, least-recoverable action last — and for whatever remains unrecoverable, write down how you would detect it instead of pretending it rolls back.

flowchart LR
    subgraph free["Costs nothing to fail"]
        direction TB
        S1["1 · cheap guard<br/>already migrated?"] --> S2["2 · read the source"] --> S3["3 · map it<br/>invariants run here"]
    end
    subgraph writes["Each failure leaves a trace"]
        direction TB
        S4["4 · external system"] --> S5["5 · your local tx<br/>atomic"] --> S6["6 · flip the old flag"]
    end
    free ==>|"a failure up to here<br/>means nothing happened"| writes

    S4 -.->|"fails after?"| F1["orphaned external record<br/><i>find by external reference</i>"]
    S6 -.->|"fails?"| F2["split-brain<br/><i>reconcile the two flags</i>"]

Steps 13 doubling as a dry-run endpoint is what makes the rehearsal trustworthy: it is the real cutover's own check code, so it cannot drift away from what the real call will do.

Seven rules worth stealing

# Rule Why Demonstrated by
1 Migrate the smallest unit that already exists in the domain Here it's one case. Per-unit cutover means a failure is one bad row, not a bad weekend ADR-003
2 The translator carries no business rules The urge to "just check the postcode here too" is the signal to migrate that capability instead ADR-002
3 Give yourself a dry run built from the real thing A rehearsal that shares code with the performance cannot go stale GET .../take-ownership/preflight
4 Let the old system keep saying no Every legacy error surfaces verbatim; none is invented or hidden LegacyDetailsWriteThroughTranslator
5 Make the boundary fail the build Exactly one type may know both sources exist; a reviewer will eventually miss that, a test won't tests/Architecture.Tests (11 rules)
6 Write down what you deliberately did not build An omission you named is a decision; an omission you didn't is a bug waiting sync-not-implemented.md
7 Reversibility expires — say so out loud Release works until the new side holds authoritative writes; after that the honest answer is a 409, not a silent discard ReleaseOwnershipHandler

What this demo deliberately leaves for you

None of these are hard to add; all of them are decisions a real migration has to make explicitly, so the demo declines to make them for you.

flowchart TB
    subgraph shown["Proven here"]
        direction LR
        P1["4 seams"] ~~~ P2["3 write paths"] ~~~ P3["per-case cutover<br/>+ dry run"] ~~~ P4["reversal, while<br/>still reversible"]
    end
    subgraph yours["Yours to decide"]
        direction LR
        Y1["bulk / scheduled<br/>cutover"] ~~~ Y2["new → old sync"] ~~~ Y3["authn / authz"] ~~~ Y4["metrics, alerting,<br/>reconciliation jobs"]
    end
    shown --> yours

Two of these are already argued in writing rather than left blank: ADR-003 on why bulk migration is a separate later capability, and sync-not-implemented.md on the two visible consequences of having no sync.