0020. Order_management as a separate Bounded Context

#Context

ADR 0017 introduced an OMS / EMS split inside the execution_management BC: Order_process_manager (the saga that orchestrated cash reservation + handoff) lived as an application- layer process manager alongside the OrderTicket aggregate and the six execution strategies. The hand-off between them was an in-process function-port — a Dispatch_open_ticket saga command routed by the factory closure into Open_order_ticket_command_workflow.execute, justified at the time by "a model inside a BC cannot send a Command to itself over its own bus."

That justification was correct but the framing was wrong: the OMS and EMS layers don't share anything domain-level. The saga owns reservation-cycle state (correlation_id → reservation_id); the aggregate owns execution state (ticket → placements → fills). They communicate through a single command and produce different outbound IE sets. Keeping them in the same BC was an artifact of how the saga grew historically, not of any shared invariant.

Step 3 in the trajectory (extending the saga to drive Commit_fill_command / Release_command against Account on OrderTicket lifecycle events) further amplifies the mismatch. A saga that orchestrates both Account-side reservation lifecycle and observes EMS-side aggregate events naturally sits between those two BCs, not inside one of them.

#Decision

Extract Order_process_manager into a new BC order_management. After extraction:

PM   → Trade_intents_planned     → PTR
PTR  → Trade_intent_approved     → OM (saga starter)
OM   → Reserve_command           → Account
Account → Amount_reserved        → OM (saga advance to Done)
OM   → Open_order_ticket_command → EM (cross-BC wire command)
EM   → opens OrderTicket aggregate
EM   ↔ Broker (Submit / Cancel ↔ Order_accepted / Filled / Rejected / Unreachable / Cancelled)
EM   → Order_ticket_* IEs        → (telemetry consumers)
EM   → Release_command           → Account  (on terminal failed / cancelled — step 2 transitional; step 3 moves this to OM)

#Option A — wire command on the bus

Open_order_ticket_command becomes a real cross-BC wire command: ATD contract at shared/contracts/execution_management/commands/, atdgen-generated _t / _j on EM's side, hand-coded wire shape on OM's outbound factory. The in-process function-port from ADR 0017 is removed; the command goes over the bus like every other cross-BC command in the system.

The original rationale for the function port (the model inside-a-BC-can't-self-command rule) no longer applies once the saga lives in a separate BC. Cross-BC commands over the bus are the orthodox pattern; this just brings the OMS→EMS handoff in line with Reserve_command, Submit_order_command, and every other cross-BC command in the project.

#Transitional gate placement

Kill_switch and rate_limit stay in execution_management for this step. The factory now enforces them at Open_order_ticket_command receipt: on a tripped gate, EM publishes Trade_submission_blocked and Release_command (to undo the Account-side reservation that already landed). The reservation cycle is therefore wasted on tripped intents — accepted as a short-lived transitional cost.

The longer-term home is pre_trade_risk (per the follow-up step 2.5): PTR already subscribes to Reservation_filled, its Risk_view domain already maintains the equity invariant, and it is the canonical pre-trade gate. The wasteful reservation cycle disappears once the gate moves to PTR — PTR rejects the assessment, no IE reaches OM, no reservation gets allocated.

#Saga scope unchanged in this step

Step 2 keeps the saga at its current scope: Awaiting_reservation → Done | Compensated. Step 3 extends it to subscribe to OrderTicket lifecycle events from EM (per-fill, terminal) and emit Commit_fill_command / Release_command to Account directly from the saga. After step 3, EM no longer publishes Release_command itself — the saga becomes the single Account-facing orchestrator.

#Why not a smaller change

Three alternatives were considered:

Keep the saga in EM. Rejected on the grounds that step 3 requires the saga to subscribe to OrderTicket events. Today's in-process function port routes EM → OrderTicket; for step 3 we'd need a reverse function port OrderTicket → saga, which would mean two opposing in-process couplings between the OMS and EMS layers. Two function ports in opposite directions is the moment a BC split becomes inevitable; better to split now.

Make the saga subscribe to bus IEs from inside EM. Still violates the project's "BC doesn't react to its own IEs" rule. Pushing the saga into a separate BC removes the rule violation by construction.

Compose at the composition root via two function ports. A non-orthodox shape: would let the saga stay in OM (clean) and EM stay separate (clean) but require bin/main.ml to wire two direct function-call closures across BC boundaries. Plausible in a single-process deployment; rejected because every other cross-BC interaction in the system is bus-mediated and asymmetry here would be surprising for future readers.

#Consequences

Easier:

Harder:

To watch for:

#References