0022. Order_process_manager owns Account commit and release

#Context

Before this ADR, Account had two parallel paths into its reservation ledger:

  1. The Reserve / Release path. Order_management's saga dispatched Reserve_command at start and (until step 3) EM itself dispatched Release_command directly on each ticket's terminal failed / cancelled event. So EM, with no business stake in Account's ledger, was sending Account a wire command.

  2. The broker-IE path. Account subscribed to three of broker's outbound IEs — Order_filled, Order_rejected, Order_unreachable — and translated each into a Commit_fill / Release on the same reservation. This is the model from when the saga was a single-step intent → submit pipeline (one reservation backs one broker order) and the conflation of placement_id with reservation_id held.

After ADR 0017 introduced the OrderTicket aggregate (one ticket fans out into N broker placements) and ADR 0020 extracted the saga into order_management, both paths broke in spirit:

#Decision

Make Order_management.Order_process_manager the single owner of Account-side reservation orchestration:

#New EM outbound IE: Order_ticket_fill_recorded

To close the per-fill semantic gap, EM publishes a new IE on every Ev_placement_filled domain event:

order_ticket_fill_recorded_integration_event {
  correlation_id   : string;  (saga routing key)
  ticket_id        : int;     (EMS aggregate identity)
  reservation_id   : int;     (Account identity)
  fill_quantity    : string;
  fill_price       : string;
  fee              : string;
  occurred_at      : iso8601;
}

The wire carries both ticket_id (for audit / analytics / operator UI) and reservation_id (the Account-relevant key the saga uses for Commit_fill_command).

#Explicit Reservation_id on the aggregate

The OrderTicket aggregate gains a Reservation_id VO field distinct from its Ticket_id. Today the application layer constructs them with the same numeric value (one reservation backs one ticket) — but the types are separate so a future one-to-many or different-id-space model lands without refactoring the aggregate. Every aggregate event that crosses the BC boundary (Ticket_opened, Ticket_completed, Ticket_cancelled, Ticket_failed, plus the new Placement_filled IE wire shape) carries reservation_id so consumers don't reach across the aggregate's identity-space boundary.

#Per-fill: fire-and-forget vs request/response

The saga emits Commit_fill_command on every Ticket_fill_recorded and does NOT wait for an Account-side acknowledgement before moving to the next fill. Fire-and-forget.

The invariant this assumes: Commit_fill_command is idempotent at Account (re-applying a fill that's already been recorded is a no-op). The in-memory bus delivers at-least-once and the saga state machine itself doesn't deduplicate fills. With durable persistence and explicit deduplication, this assumption holds for free; the in-memory model relies on the fact that each Ticket_fill_recorded IE corresponds to one Ev_placement_filled which the aggregate's terminal-absorption invariant guarantees fires at most once per fill.

Request/response per-fill (saga tracks fills_pending, waits for Reservation_filled IE) is plausible if backpressure or recovery semantics ever require it; the upgrade leaves the wire commands untouched.

#Settled vs Released

The saga's two terminal flavours match the two ways a ticket finishes:

Saga terminal Triggering IE Command emitted
Settled Ticket_completed none — fills did it all
Released Ticket_cancelled / _failed Release_command
Compensated Reservation_rejected none — never reserved

Settled is conceptually "all the cash has been drawn down by per-fill commits; there's nothing left to release"; Released is "the ticket terminated with unfilled remainder, release the unused portion at Account".

This relies on Account's Commit_fill_command accepting fees on top of the price × quantity draw; the Reservation's reserved_cash covers the cash earmark plus a slippage / fee buffer, and the residual buffer is what Release frees when the ticket fails.

#Consequences

Easier:

Harder:

To watch for:

#References