# 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
in the legacy system"]) --> Q1 Q1{"Does the new UI
only need to read it?"} Q1 -->|yes| A["Read ACL
translate at the boundary
legacy stays authoritative"] Q1 -->|no| Q2{"Is it owned by a system
you don't control?"} Q2 -->|yes| D["Conformist
surface its rules as-is
don't fight or hide them"] Q2 -->|no| Q3{"Have you rebuilt the
domain rules yet?"} Q3 -->|yes| E["Take ownership
new system becomes
the authority"] Q3 -->|"no — and it's
a whole workflow"| C["Redirect
send the user back out
to the legacy screen"] Q3 -->|"no — but it's
a simple edit"| B["Write-through
legacy still validates
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
already migrated?"] --> S2["2 · read the source"] --> S3["3 · map it
invariants run here"] end subgraph writes["Each failure leaves a trace"] direction TB S4["4 · external system"] --> S5["5 · your local tx
atomic"] --> S6["6 · flip the old flag"] end free ==>|"a failure up to here
means nothing happened"| writes S4 -.->|"fails after?"| F1["orphaned external record
find by external reference"] S6 -.->|"fails?"| F2["split-brain
reconcile the two flags"] ``` 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
+ dry run"] ~~~ P4["reversal, while
still reversible"] end subgraph yours["Yours to decide"] direction LR Y1["bulk / scheduled
cutover"] ~~~ Y2["new → old sync"] ~~~ Y3["authn / authz"] ~~~ Y4["metrics, alerting,
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.