0029. Single terminal commit: per-trade Trade_executed, one fill_recorded at ticket close

Status: Accepted Date: 2026-05-25

#Context

The place-order saga settles a reservation against the actual execution of an OrderTicket. Before this ADR the settlement path had grown three coupled mechanisms that, together, were more than the problem needed:

  1. The broker aggregated fills. Each ACL adapter (Finam, BCS) kept a per-placement Cumulative_sum and shipped a new_total_filled snapshot on every Order_filled event. But the broker is a recognizer of venue facts (ADR 0015, Vernon's "external system as a source of Domain Events"), not a fill bookkeeper. Reconciling legs into a running total is the consuming aggregate's job — the OrderTicket already owns that in Progress (ADR 0017). The broker carried bookkeeping it had no business owning.

  2. EM emitted one Order_ticket_fill_recorded per leg. A TWAP with 50 slices produced 50 fill-recorded IEs for one reservation.

  3. The saga committed per leg, fire-and-forget, and settled on Ticket_completed (ADR 0022). To make many partial commits add up correctly, Account grew progressive drawdown — the commit_fill cover/open Drawn_down | Fully_committed outcome variant (ADR 0028).

The original, simpler intent was: reserve once, commit once, at the end, with the actual executed total. A reservation is a hold on availability; the real debit happens at settlement with the venue's actual numbers. If settlement is a single commit of the cumulative executed quantity, the progressive-drawdown machinery is unnecessary and the per-leg cadence is noise.

#Decision

#1. broker emits Trade_executed per trade leg, no aggregation

The per-leg fill event/IE Order_filled becomes Trade_executed, carrying the trade only: placement_id, trade_id, instrument, side, quantity, price, fee, ts. The new_total_filled field and the fill_* prefixes are dropped (a trade event is intrinsically about the trade — the prefixes were parasitic). The adapters' Cumulative_sum bookkeeping is deleted. Bus topic broker.order-filledbroker.trade-executed. paper_broker, the simulated venue, is wire-equivalent and changes symmetrically.

The per-leg Order.trade record is promoted to a child Entity of the Order aggregate (Order.Trade) with its identity field trade_id: two fills with equal attributes but distinct trade_ids are distinct executions and must not be conflated.

#2. EM records one cumulative fill at ticket close

The OrderTicket aggregate's Progress is the single fill aggregator. It gains cumulative_notionalquantity × price) and a volume_weighted_average_price accessor. EM publishes Order_ticket_fill_recorded exactly once, on Ev_ticket_completed, carrying the ticket-level totals: cumulative filled quantity, the VWAP fill price, and total fees. There is no per-leg fill IE.

#3. The saga commits once and settles on Reservation_filled

Order_process_manager dispatches a single Commit_fill_command for that one Order_ticket_fill_recorded, then waits for Account's Reservation_filled to reach Settled (request/response at ticket granularity). This replaces the per-leg fire-and-forget commit and the Ticket_completed-driven Settled of ADR 0022.

#4. Account is unchanged; progressive drawdown goes dormant

A single commit of the full executed total (quantity == reserved quantity) draws the reservation to zero in one step → Fully_committedReservation_filled. The progressive-drawdown path of ADR 0028 (Drawn_down, commit_fill_outcome) is no longer exercised by this flow. It is left in place as dead code pending a deliberate removal, rather than ripped out speculatively.

#Alternatives considered

#Consequences

Easier:

Harder / to watch for:

#References