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>
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
# 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.
|
||||
|
||||
```mermaid
|
||||
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.
|
||||
|
||||
```mermaid
|
||||
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 1–3 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](adr/ADR-003-ownership-is-taken-per-case.md) |
|
||||
| 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](adr/ADR-002-write-through-has-no-business-rules.md) |
|
||||
| 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](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.
|
||||
|
||||
```mermaid
|
||||
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](adr/ADR-003-ownership-is-taken-per-case.md) on why bulk migration is
|
||||
a separate later capability, and
|
||||
[sync-not-implemented.md](sync-not-implemented.md) on the two visible
|
||||
consequences of having no sync.
|
||||
Reference in New Issue
Block a user