Design · 007-reconciliation

Nightly reconciliation

How we will build it — the happy path, which acknowledges ADR-0007 and corrects through the use case.

Status Accepted Spec 007-reconciliation Owner Ops Engineering Branch feat/nightly-reconciliation Created Read time

The job reads the custodian file and the current store (through the persistence adapter's read side), computes deltas, and applies each correction through the position use case — so every correction emits PositionChanged. This plan touches the persistence package, so retrieval surfaces ADR-0007; the plan acknowledges it and resolves to route corrections through the use case rather than writing the store.

1 · Principles check

The reconciliation stays on the sanctioned path: reads via the adapter, writes via the use case. No new writer of the positions store, so ADR-0007 holds by construction.

Governing decisions acknowledged

This plan's surface touches the persistence package, governed by 002-downstream-consumers/D-007 (ADR-0007 — only the persistence adapter touches the data store; every position change must emit PositionChanged). The plan honours it: corrections are applied through the position use case (which emits the event), and the only persistence-package addition is a read-side snapshot reader that uses the existing adapter — it adds no new writer. Retrieval: spectastic adrs --for src/main/java/xyz/briancorbin/pk/hex/persistence/CustodianSnapshotReader.java returns D-007; this section is its acknowledgment.

2 · Technical context

Language / runtime
Java 21, hexagonal.
Architecture
A scheduled job in the application layer; reads via the adapter, writes via the use case.

3 · Data model & contracts

The job consumes the custodian file (instrument, quantity) and emits corrections as ordinary use-case calls carrying a reconciliation tradeId. Since change 2026-09-18-http-corrections the same correction path is also reachable over HTTP, for a downstream service that computes its own deltas — the contract below.

POST /corrections with {instrument, quantity, tradeId} brings the instrument to quantity through PositionService.correct — the same path the nightly job uses — and answers the resulting position. A caller may rely on: a repeated tradeId is a 200 that changes nothing; every applied correction emitted PositionChanged before the response; the caller never touches the store. Authored at the proposed location and promoted by /spectastic.apply.

openapi: 3.1.0 info: title: Position keeper — corrections version: 0.1.0 description: >- Corrections accepted from a downstream service (007-reconciliation, FR-005). Every correction is applied through the position use case, so it emits PositionChanged like any other mutation; the caller never touches the store. Idempotent on tradeId. paths: /corrections: post: operationId: postCorrection summary: Bring an instrument to a target quantity requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/Correction" } example: { instrument: AAPL, quantity: 100, tradeId: "recon:b7078fc6:AAPL" } responses: "200": description: The position after the correction — unchanged when the tradeId was already applied content: application/json: schema: { $ref: "#/components/schemas/Position" } example: { instrument: AAPL, quantity: 100 } "400": description: The body is not a correction — missing or blank field, non-integer quantity content: application/json: schema: { $ref: "#/components/schemas/Error" } "405": description: Any method but POST components: schemas: Correction: type: object required: [instrument, quantity, tradeId] properties: instrument: { type: string, minLength: 1, maxLength: 32 } quantity: { type: integer, format: int64, description: The target quantity, signed whole units } tradeId: { type: string, minLength: 1, maxLength: 64, description: Attribution and idempotency key } Position: type: object required: [instrument, quantity] properties: instrument: { type: string } quantity: { type: integer, format: int64 } Error: type: object required: [error] properties: error: { type: string }

5 · Grounding & evidence

ClaimSourceStatusFinding
ADR-0007 governs the positions store and this plan touches it002-downstream-consumers/design.html#D-007verifiedScope src/**/persistence/**; the snapshot reader lives there, so the decision is in-scope and acknowledged.
Reconciliation is against the custodian's settled snapshot; drift lives in the settlement windowSettlement domain — KB-0001@2026-09-13verifiedThe book is trade-date, the custodian settlement-date; the delta the job corrects is exactly that window's drift.

6 · Approach

A scheduled job parses the custodian file, reads current quantities through a persistence-package snapshot reader (using the existing adapter, not a new writer), computes per-instrument deltas, and applies each as a use-case correction carrying the run's tradeId. Every correction therefore emits PositionChanged, and ADR-0007's boundary is untouched.

7 · Alternatives considered

OptionEmits eventsHonours ADR-0007SimpleScore
Correct through the use case (chosen)✓✓✓9
Write the positions table directly (the bypass)✗ silent✗ violates ADR-0007✓ fast1

The direct write is faster and is exactly what the guardrail exists to stop — it is the bypass the demonstrations exercise.

8 · Decisions

D-001 · Corrections flow through the position use case

Context. Reconciliation touches positions, governed by 002-downstream-consumers/D-007; the domain rationale is settlement-window drift (KB-0001@2026-09-13). Decision. Apply every correction through the use case (emitting the event); read the current store through the adapter; add no new writer. Consequences. + ADR-0007 holds; Risk/P&L/audit see every correction. − One event per corrected instrument (fine at end-of-day volumes).

9 · Project structure

src/main/java/xyz/briancorbin/pk/hex/recon/ReconciliationJob.java             # the job (application layer)
src/main/java/xyz/briancorbin/pk/hex/recon/CustodianFileParser.java           # parse the EOD file
src/main/java/xyz/briancorbin/pk/hex/persistence/CustodianSnapshotReader.java  # read current positions via the adapter (read-only)
src/main/java/xyz/briancorbin/pk/hex/http/CorrectionsHandler.java              # POST /corrections → PositionService.correct (change 2026-09-18-http-corrections)
specs/007-reconciliation/contracts/corrections.openapi.yaml                    # the contract, proposed (→ api/corrections.openapi.yaml at apply)

10 · Risks & mitigations

A future maintainer optimises by writing the store directly

Likelihood medium, impact high. Mitigation: ADR-0007's merge verdict catches a direct UPDATE positions outside the adapter — the demonstrations prove it.

11 · Complexity tracking

None.

12 · Open questions

None outstanding.

13 · Change log

  1. Declares the corrections contract (api/corrections.openapi.yaml) and the CorrectionsHandler path for change 2026-09-18-http-corrections: a downstream service reaches the same use-case correction path over POST /corrections. No decision changes; ADR-0007 is honoured the same way.
  2. Status flipped Draft → Accepted on 18 Sep 2026 — zero remaining unchecked tasks (11/11); ./gradlew build green with ReconciliationJobTest (SC-001, SC-002) verified passing and the reconcile subcommand exercised twice against one store (2 corrections, then 0) per author pre-confirmation for 003–007. Sibling bundle (REQ-LIFECYCLE-005).
  3. Design drafted (happy path) — corrections through the use case; acknowledges ADR-0007 (002-downstream-consumers/D-007); grounds settlement-window drift in KB-0001. The demonstrations also exercise a bypass variant (caught at plan) and a direct-write commit (caught at merge).