# 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`.*