Files
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

179 lines
6.9 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# How the seams work
Diagrams only. The prose arguments live in the [README](../README.md) and the
[ADRs](adr/); each diagram below names the file it was traced from, so a
reader can check it against the code rather than trust it.
## The four seams
Who holds authority at each boundary. Seam C is the odd one out: it is not an
HTTP redirect, it is an `ActionLink` with `mode: "redirect"` in the JSON
`actions` block — the **browser** navigates when the user clicks it.
```mermaid
flowchart LR
portal["portal-frontend<br/>Angular"]
api["new-backend<br/>New.Api"]
legapi["legacy-backend<br/>Legacy.Api"]
legweb["legacy-frontend<br/>Beoordeling.cshtml"]
cf["case-framework<br/>vendor"]
portal --> api
api -- "A · read ACL<br/>GET /api/aanvragen<br/><b>legacy owns the data</b>" --> legapi
api -- "B · write-through<br/>PUT .../gegevens<br/><b>legacy owns the rules</b>" --> legapi
api -- "D · conformist<br/>POST /cases<br/><b>vendor owns the rules</b>" --> cf
portal -. "C · redirect — browser navigates<br/><b>legacy owns the workflow</b>" .-> legweb
```
*Traced from `New.Infrastructure.Legacy/LegacyCaseSource.cs`,
`LegacyDetailsWriteThroughTranslator.cs`,
`New.Api/Contracts/CaseDetailResponseFactory.cs`,
`New.Infrastructure.CaseFramework/CaseFrameworkGateway.cs`.*
## Reading a case: one id, two sources
`ApplicationSourceResolver` is the only type in the solution that references
both sources (Architecture.Tests rule 7). A legacy id keeps working after
adoption because this resolver — and only this resolver — checks the ownership
registry first.
The **list** endpoint deliberately does *not* go through it: it takes two
separate reader ports and merges in memory, dropping any legacy row whose
`Migrated` flag is set so adopted cases don't appear twice.
```mermaid
flowchart TB
subgraph byid["GET /api/worklist/legacy/{id} — via the resolver"]
r{"legacy_ownership<br/>has a row?"}
r -->|no| ra["LegacyCaseSource<br/>HTTP → legacy-backend"]
r -->|yes| rb["OwnedApplicationSource<br/>in-process → new-db"]
end
subgraph list["GET /api/worklist — bypasses the resolver"]
l1["ILegacyWorklistReader<br/>HTTP → legacy-backend"]
l2["IOwnedWorklistReader<br/>in-process → new-db"]
m["owned ++ legacy.Where(!Migrated)<br/>filter · sort · page in memory"]
l1 --> m
l2 --> m
end
```
*Traced from `New.Api/Resolution/ApplicationSourceResolver.cs:26` and
`New.Api/Endpoints/WorklistEndpoints.cs:18`.*
## The life of a case
This is the strategy in one picture. Every edge is a real endpoint with a real
guard.
```mermaid
stateDiagram-v2
[*] --> Legacy
Legacy --> Owned: POST take-ownership → 201
Owned --> Legacy: DELETE ownership → 204
Owned --> OwnedDirty: owned edit or assessment
Legacy --> Legacy: preflight — read-only
Legacy --> Legacy: write-through edit — legacy validates
Legacy --> Legacy: take-ownership 422 — nothing written
OwnedDirty --> OwnedDirty: further owned writes
note right of Legacy
no legacy_ownership row
legacy.Migrated = false
legacy is the authority
end note
note right of Owned
legacy_ownership row exists
legacy.Migrated = true
domain_writes_since = 0
still reversible
end note
note right of OwnedDirty
domain_writes_since greater than 0
Release refused with 409: no sync
exists to push these edits back
to legacy first.
end note
```
Not drawn as a state, because it is a failure condition rather than a
lifecycle stage: **split-brain** — a row in `legacy_ownership` while legacy's
`Migrated` is still `false`, left behind when step 6 below fails. Detected by
reconciling the two, not prevented.
*Traced from `New.Application/Ownership/TakeOwnershipHandler.cs`,
`ReleaseOwnershipHandler.cs`, and
`New.Infrastructure.Persistence/Entities/LegacyOwnershipRow.cs`.*
## Take ownership — the strangler step
The step order is load-bearing. Steps 13 touch nothing, which is what makes a
failed adoption free; the preflight endpoint is literally this prefix, stopped
early. Steps 46 are ordered so the least recoverable action happens last, and
each remaining failure window is *detectable* rather than pretended away.
```mermaid
sequenceDiagram
participant P as Portal
participant A as new-backend<br/>TakeOwnershipHandler
participant N as new-db
participant L as legacy-backend
participant C as case-framework
Note over P,C: Steps 13 · CheckAsync() · nothing is written<br/>PreflightAsync() runs exactly this much, then stops
P->>A: POST .../take-ownership
A->>N: 1 · LookupOwnedIdAsync
N-->>A: row exists → 409 AlreadyOwned
A->>L: 2 · GET /api/aanvragen/{id}
L-->>A: legacy row (absent → 404)
A->>A: 3 · map to RegistrationApplication
Note over A: domain invariant fails → 422 naming it,<br/>and nothing has been written anywhere
Note over P,C: Steps 46 · writes begin
A->>C: 4 · POST /cases
C-->>A: caseId
Note over C: failure after this point leaves an orphaned<br/>framework case — no compensating delete exists,<br/>so find it by externalReference
A->>N: 5 · aggregate + legacy_ownership<br/>in ONE transaction
A->>L: 6 · PUT .../migratie-vlag true
Note over L: failure here is swallowed and logged → split-brain:<br/>owned locally, still writable in legacy.<br/>Reconcile legacy_ownership vs legacy.migrated
A-->>P: 201 { registrationApplicationId }
```
*Traced from `New.Application/Ownership/TakeOwnershipHandler.cs` — the numbered
comments there are the source of truth for this diagram.*
## Write-through: whose rules run
The effort argument in one exchange. Three bad fields go in; three field
errors come back, produced entirely by legacy's own validator. No validation
logic crossed the seam.
```mermaid
sequenceDiagram
participant P as Portal
participant A as new-backend
participant T as LegacyDetailsWrite<br/>ThroughTranslator
participant L as legacy-backend<br/>GegevensValidator
P->>A: PUT .../legacy/1001/details<br/>blank surname · no house number · bad postcode
A->>T: ToLegacyRequest — reshape only
T->>L: PUT /api/aanvragen/1001/gegevens
L->>L: every rule runs HERE
L-->>T: 400 · NAAM_VERPLICHT<br/>HUISNR_VERPLICHT · POSTCODE_ONGELDIG
T->>T: ToPortalErrors — veld → field path
T-->>A: 3 field errors, messages verbatim
A-->>P: 400 · surname<br/>address.number · address.postalCode
Note over T: An unrecognized veld is logged and passed<br/>through, never dropped or guessed at
```
The translator carries no business rules at all — see
[ADR-002](adr/ADR-002-write-through-has-no-business-rules.md), which is also
honest that this is enforced by code review, not by a test.
*Traced from `New.Infrastructure.Legacy/LegacyDetailsWriteThroughTranslator.cs`
and `legacy/src/Legacy.Api/Endpoints/GegevensValidator.cs`.*