Guard RemoveSupportingDocumentCommand with the documentId being un-reconciled #45

Closed
opened 2026-07-04 12:11:38 +00:00 by momsse · 0 comments
Owner

Migré depuis viziertronic/octant#102 — ouvert le 2026-06-26 par @momsse.

Follow-up from the #100 review (epic #42).

Problem

SupportingDocumentSaga.detachForReconciliation issues RemoveSupportingDocumentCommand({ bankTransactionId }) with no documentId, so the banking aggregate removes whatever document is currently attached — not necessarily the one that belonged to the reconciliation being undone.

Reachable in a cross-stream eventual-consistency window:

  1. R1 (doc=D1) established → D1 attached to T1.
  2. R2 (doc=D2) established → SupportingDocumentSaga attaches D2 to T1 (replacing D1); SupersedeReconciliationSaga has not yet appended ReconciliationSupersededEvent to R1.
  3. A user undoes R1 — still established in its own stream, so ReconciliationUndoneEvent fires.
  4. SupportingDocumentSaga processes R1's undo → removeSupportingDocument(T1) strips D2 (the replacement). R2 stays established but T1 has no supporting document.

Proposed work

  • Add a documentId to RemoveSupportingDocumentCommand; decideRemoveSupportingDocument rejects (or no-ops) the removal when the currently-attached document is not the one named. The saga already knows the reconciliation's documentId from the read model.
  • Document/confirm the projector runner's ordering guarantees across saga partitions, which bound how wide this window is.

Scope note

This is a banking-domain change (command + aggregate decide + event contract), deliberately kept out of the #100 composition PR. Race-dependent, not blocking at current concurrency.

> _Migré depuis [viziertronic/octant#102](https://github.com/viziertronic/octant/issues/102) — ouvert le 2026-06-26 par @momsse._ Follow-up from the #100 review (epic #42). ## Problem `SupportingDocumentSaga.detachForReconciliation` issues `RemoveSupportingDocumentCommand({ bankTransactionId })` with no `documentId`, so the banking aggregate removes *whatever document is currently attached* — not necessarily the one that belonged to the reconciliation being undone. Reachable in a cross-stream eventual-consistency window: 1. R1 (doc=D1) established → D1 attached to T1. 2. R2 (doc=D2) established → `SupportingDocumentSaga` attaches D2 to T1 (replacing D1); `SupersedeReconciliationSaga` has not yet appended `ReconciliationSupersededEvent` to R1. 3. A user undoes R1 — still `established` in its own stream, so `ReconciliationUndoneEvent` fires. 4. `SupportingDocumentSaga` processes R1's undo → `removeSupportingDocument(T1)` strips D2 (the replacement). R2 stays established but T1 has no supporting document. ## Proposed work - Add a `documentId` to `RemoveSupportingDocumentCommand`; `decideRemoveSupportingDocument` rejects (or no-ops) the removal when the currently-attached document is not the one named. The saga already knows the reconciliation's `documentId` from the read model. - Document/confirm the projector runner's ordering guarantees across saga partitions, which bound how wide this window is. ## Scope note This is a banking-domain change (command + aggregate decide + event contract), deliberately kept out of the #100 composition PR. Race-dependent, not blocking at current concurrency.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
momsse/octant#45
No description provided.