# 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
Angular"]
api["new-backend
New.Api"]
legapi["legacy-backend
Legacy.Api"]
legweb["legacy-frontend
Beoordeling.cshtml"]
cf["case-framework
vendor"]
portal --> api
api -- "A · read ACL
GET /api/aanvragen
legacy owns the data" --> legapi
api -- "B · write-through
PUT .../gegevens
legacy owns the rules" --> legapi
api -- "D · conformist
POST /cases
vendor owns the rules" --> cf
portal -. "C · redirect — browser navigates
legacy owns the workflow" .-> 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
has a row?"}
r -->|no| ra["LegacyCaseSource
HTTP → legacy-backend"]
r -->|yes| rb["OwnedApplicationSource
in-process → new-db"]
end
subgraph list["GET /api/worklist — bypasses the resolver"]
l1["ILegacyWorklistReader
HTTP → legacy-backend"]
l2["IOwnedWorklistReader
in-process → new-db"]
m["owned ++ legacy.Where(!Migrated)
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 1–3 touch nothing, which is what makes a
failed adoption free; the preflight endpoint is literally this prefix, stopped
early. Steps 4–6 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
TakeOwnershipHandler
participant N as new-db
participant L as legacy-backend
participant C as case-framework
Note over P,C: Steps 1–3 · CheckAsync() · nothing is written
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,
and nothing has been written anywhere
Note over P,C: Steps 4–6 · writes begin
A->>C: 4 · POST /cases
C-->>A: caseId
Note over C: failure after this point leaves an orphaned
framework case — no compensating delete exists,
so find it by externalReference
A->>N: 5 · aggregate + legacy_ownership
in ONE transaction
A->>L: 6 · PUT .../migratie-vlag true
Note over L: failure here is swallowed and logged → split-brain:
owned locally, still writable in legacy.
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
ThroughTranslator
participant L as legacy-backend
GegevensValidator
P->>A: PUT .../legacy/1001/details
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
HUISNR_VERPLICHT · POSTCODE_ONGELDIG
T->>T: ToPortalErrors — veld → field path
T-->>A: 3 field errors, messages verbatim
A-->>P: 400 · surname
address.number · address.postalCode
Note over T: An unrecognized veld is logged and passed
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`.*