Rust's Type-State and Sealed Traits for Order-Lifecycle State Machines
Encoding submitted, acknowledged, filled, and reconciled in the type system.
§ IFrame
Today's Ops lesson named a per-order state machine: an order enters the executor as a sized intent, leaves the executor as a venue-bound request, returns as an acknowledgment, accumulates fills, and reaches a terminal state. The state machine has the property every state machine in adversarial markets has — every transition is a place where the operator's accounting can go wrong, and every state is a place where the executor must refuse to act on the wrong information.
A state machine described in prose, or implemented with an enum and a match, lets bad transitions through at compile time. The bug catches at runtime in production, three days into the live deployment, when a fill event arrives for an order the executor believes is still in the submitted but unacknowledged state and the fill handler tries to update the venue order ID field — a field the submitted struct does not have because the order ID was never received.
Rust's type-state pattern moves the state machine into the type system. Each state is a distinct type. Transitions are functions that consume one type and return another. Operations that are valid only in a particular state live as methods on that state's type. The compiler refuses to call record_fill() on an order in the Submitted state because Submitted has no such method. The bug that took three days to surface in production becomes a compile error.
§ IILanguage Idiom — Type-State, Sealed Traits, and Ownership
The Rust type-state pattern has three building blocks. First, a phantom-typed state marker — a generic parameter State carries no runtime data and exists only at compile time to distinguish Order<Submitted> from Order<Acknowledged>. Second, a sealed trait closes the trait against downstream implementation; only types defined in the executor's crate can be State markers. Third, ownership transfer — the transition method consumes the old-state value and returns a new-state value, so the caller can never observe the prior representation again after a transition.
The combination gives the executor a state machine that the compiler enforces, that downstream crates cannot subvert, and that races cannot corrupt.
§ IIICode Worked Example — An Executor's Per-Order Lifecycle
The executor's order types live in a module that exposes only the Order constructor and the transition methods. The state markers and the sealed trait live as a private inner module that nothing outside the crate can extend.
The state markers are zero-sized types. The sealed trait gates the set of states the Order type accepts as its State parameter.
mod state {
pub trait Sealed {}
pub struct Submitted;
pub struct Acknowledged;
pub struct PartiallyFilled;
pub struct Terminal;
impl Sealed for Submitted {}
impl Sealed for Acknowledged {}
impl Sealed for PartiallyFilled {}
impl Sealed for Terminal {}
}
pub trait OrderState: state::Sealed {}
impl<T: state::Sealed> OrderState for T {}
The Sealed trait is private to the module. Only the four state types defined inside the module can implement it. The public OrderState trait inherits from Sealed, so any external type that tries to declare impl OrderState for ForeignType {} fails to compile — the foreign type cannot implement the private Sealed and therefore cannot satisfy the OrderState bound.
The Order struct carries the data common to every state and a phantom marker for the state itself.
use std::marker::PhantomData;
use std::time::Instant;
pub struct Order<S: OrderState> {
intent_id: IntentId,
instrument: Instrument,
side: Side,
requested_size: Quantity,
venue_order_id: Option<VenueOrderId>,
fills: Vec<Fill>,
created_at: Instant,
state: PhantomData<S>,
}
The venue_order_id and fills fields are present in every state but populated only in the states that have them. The type system gates which methods can access these fields through state-specific impl blocks.
The constructor produces an order in the Submitted state. No public function produces an order in any other state.
impl Order<state::Submitted> {
pub fn submit(intent_id: IntentId, instrument: Instrument, side: Side, size: Quantity) -> Self {
Order {
intent_id, instrument, side,
requested_size: size,
venue_order_id: None,
fills: Vec::new(),
created_at: Instant::now(),
state: PhantomData,
}
}
pub fn acknowledge(self, venue_order_id: VenueOrderId) -> Order<state::Acknowledged> {
Order {
intent_id: self.intent_id,
instrument: self.instrument,
side: self.side,
requested_size: self.requested_size,
venue_order_id: Some(venue_order_id),
fills: self.fills,
created_at: self.created_at,
state: PhantomData,
}
}
}
The acknowledge() method consumes self (the Submitted order) and returns an Acknowledged order. After acknowledge() returns, the caller no longer holds the Submitted value — Rust's ownership system enforces this at compile time. There is no path for the executor to record a fill on an order that was never acknowledged.
The fill-handling state — PartiallyFilled — accepts more fills or transitions to Terminal.
impl Order<state::Acknowledged> {
pub fn record_fill(self, fill: Fill) -> Order<state::PartiallyFilled> { /* ... */ }
pub fn cancel(self) -> Order<state::Terminal> { /* ... */ }
}
impl Order<state::PartiallyFilled> {
pub fn record_fill(self, fill: Fill) -> Self { /* append fill, return Self */ }
pub fn complete(self) -> Order<state::Terminal> { /* terminate */ }
}
Methods that read order state are available across multiple states through a trait the state markers also implement.
pub trait Inspectable {
fn requested_size(&self) -> Quantity;
fn filled_size(&self) -> Quantity;
fn remaining_size(&self) -> Quantity;
}
impl<S: OrderState> Inspectable for Order<S> {
fn requested_size(&self) -> Quantity { self.requested_size }
fn filled_size(&self) -> Quantity {
self.fills.iter().map(|f| f.size).sum()
}
fn remaining_size(&self) -> Quantity {
self.requested_size - self.filled_size()
}
}
Inspectable is implemented for Order<S> for any S: OrderState. Read-only operations are available in every state. State-mutating operations are available only in the states that grant them.
The order map is the place runtime polymorphism lives. An enum AnyState wraps the four type-state variants:
pub enum AnyState {
Submitted(Order<state::Submitted>),
Acknowledged(Order<state::Acknowledged>),
PartiallyFilled(Order<state::PartiallyFilled>),
Terminal(Order<state::Terminal>),
}
The executor's on_fill_event() matches on the enum, calls the appropriate transition, and reinserts the new state. Crucially, the match arm for Submitted is impossible to reach without a logic bug — a fill event for an order in Submitted means the venue acknowledged the order, sent a fill, and the executor never processed the acknowledgment. The arm is the place to log the inconsistency and trigger the reconciliation overlay from the Ops lesson.
§ IVConnection to Today's Ops Lesson
The Ops lesson described three operator disciplines. The Rust encoding maps each to a compile-time guarantee. Smart routing emits child slices, each becoming its own Order<Submitted>; the idempotency key is the intent_id field, computed once at construction, never mutated. Fill reconciliation maps to record_fill(), available only on Acknowledged and PartiallyFilled; a fill for Submitted is impossible to record through the type system. Position truth maps to the Inspectable trait — cheap, safe-under-concurrent-read inspection of every order's filled size for the per-instrument reconciliation overlay.
§ VPrior-Lesson Reach
This lesson is the third Rust type-state lesson in the curriculum. The 2026-05-20 mTLS lesson encoded a connection lifecycle (Closed → Connecting → Authenticated → Closed). The 2026-05-23 validator-signing-key lesson encoded a key-material lifecycle (Generated → Initialized → Active → Withdrawn). The 2026-05-26 secrets lesson encoded a Secret<T> type whose Display and Debug implementations refuse to leak the inner value.
The pattern recurs because the pattern fits. Whenever a system has an object that moves through a finite set of states, where some operations are valid only in some states, and where the cost of acting in the wrong state is a production failure, Rust's type-state encoding is the natural expression. The curriculum has been working through that pattern across application domains — connections, keys, secrets, now orders — to build a Rust intuition that recognizes the shape and reaches for the same three building blocks.
§ VIClosing
The order-lifecycle state machine the Ops lesson described in prose became, in this lesson, a state machine the Rust compiler enforces. The three building blocks — phantom-typed state markers, sealed traits, ownership transfer — give the executor a guarantee that no bad transition compiles. The cost is a small amount of ceremony at the module boundary; the payoff is that an entire class of execution-layer bugs becomes impossible.
The state machine the compiler enforces is the state machine the operator does not have to read about in a postmortem.
Filed 2026-05-29 Fajr ANCHOR #13 — third Rust type-state lesson (mTLS → validator → secrets → orders)