Specification · 007-reconciliation

Nightly reconciliation

"Add a nightly job that corrects positions from the custodian file." The request that, two years and five features after ADR-0007 was written, tests whether the guardrail still holds.

Status Accepted Spec ID 007-reconciliation Owner Ops Engineering Reviewers Position Keeping team Created Principlesv1.0.0 Smallest demoableThe nightly job reads the custodian file, computes deltas, and corrects each position through the position use case, so every correction emits PositionChanged. Read time

A nightly job reconciles the position store against the custodian's end-of-day file, correcting drift. It touches positions — the surface ADR-0007 governs — so the guardrail requires it to correct through the position use case (which emits PositionChanged), never by writing the store directly. This spec is the setup for the seven demonstrations: the same request, driven the right way and several wrong ways, against a decision written long before anyone thought about reconciliation.

I Independent
Builds on the position core (001) and the adapter (003).
N Negotiable
Outcomes contract; the file format and schedule are the design's.
V Valuable
Unlocks SC-001 — positions stay reconciled to the custodian of record.
E Estimable
Plan derivable; the use case and store already exist.
S Small
One job, one file, deltas through the use case. Six requirements.
T Testable
Every requirement observable via a corrected position and its emitted event.
Exec / PM
§Context, §Success criteria.
Reviewer
§Requirements, §Open questions.
Implementer
§Requirements, §Data model.

1 · Context

Positions drift from the custodian's records — a corporate action booked late, a settlement fail. The custodian sends an end-of-day file; a nightly job must bring the store into line. The naive implementation writes the corrected quantities straight into the positions table, which is fast, clean, and exactly what ADR-0007 forbids: a direct write bypasses the PositionChanged emission, so Risk, P&L, and the audit trail never learn the position moved. The reconciliation must correct through the use case, emitting an event per correction.

2 · User scenarios & testing

A drifted position is corrected — audibly. The custodian file shows a quantity that differs from the store; the job computes the delta and applies it through the position use case, which emits PositionChanged. Risk and P&L see the correction.

No direct write. The job never touches the positions table directly; the correction path is the same adapter every other mutation uses.

3 · Requirements

The job MUST read the custodian end-of-day file and compute, per instrument, the delta between the custodian quantity and the stored quantity. Each correction MUST be applied through the position use case, so every corrected position emits a PositionChanged event — the job MUST NOT write the position store directly. (This is what ADR-0007 governs.) A correction MUST carry a synthetic tradeId identifying the reconciliation run, so consumers stay idempotent and the audit trail attributes the change. The job SHOULD be re-runnable for the same custodian file without double-correcting (idempotent on the run's tradeId).

The service MUST accept a correction from a downstream reconciliation service over POST /corrections — instrument, the custodian quantity that service reconciles against, and a synthetic tradeId identifying that service's reconciliation run — and apply it through the position use case, so it emits PositionChanged like every other mutation; a repeated tradeId MUST change nothing and answer the current position.

Rationale

ADR-0007's verdict on a non-owner service tells it to route the change through the owner. This is the route. It reuses the job's correction path so that there is still exactly one way a position changes.

A reconciliation run MUST leave the store and the emitted events consistent: no position is corrected without a matching event.
  • How the custodian file arrives (SFTP, drop) — an ops concern.
  • Alerting on large drift — a later observability feature.

4 · Data model

Custodian record
An instrument and the custodian's settled end-of-day quantity — the source of truth to reconcile against.
Correction
A delta applied through the use case, carrying a reconciliation tradeId; it emits an event.

5 · Success criteria

After a reconciliation run over a file with drift, every corrected position has emitted a PositionChanged event that Risk observed, and the store matches the custodian quantities.
Observed at

An integration test running the job over a fixture custodian file and asserting one event per correction.

Re-running the job over the same custodian file produces no second correction (idempotent on the run's tradeId).
Observed at

A test running the job twice over one file and asserting a single correction per instrument.

6 · Assumptions

7 · Open questions

None outstanding — the correction path (through the use case) is dictated by ADR-0007.

8 · Conformance index

9 · Change log

  1. 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).
  2. Drafted — the reconciliation request. Its design (and the seven demonstrations) test whether ADR-0007, written in feature 002, still holds after five intervening features.
  3. Applied 2026-09-18-http-corrections: Added FR-005 — a downstream reconciliation service can bring an instrument to its custodian quantity over POST /corrections, through the same use-case path the nightly job uses; the corrections contract promoted to api/corrections.openapi.yaml.