Rust
/
01 Sep 2026
/
3 min read
Many APIs have lifecycle rules: a connection must be opened before sending, a transaction must begin before committing, or a builder must receive required values before producing output. Runtime flags can enforce these rules, but Rust can sometimes encode them in types instead.
The typestate pattern represents each valid state with a distinct type and makes transitions consume one state to produce another.
Encode states as marker types use std::marker::PhantomData; struct Disconnected; struct Connected; struct Connection<State> { endpoint: String, _state: PhantomData<State>, } impl Connection<Disconnected> { fn new(endpoint: String) -> Self { Self { endpoint, _state: PhantomData } } fn connect(self) -> Connection<Connected> { Connection { endpoint: self.endpoint, _state: PhantomData, } } } impl Connection<Connected> { fn send(&self, payload: &[u8]) { println!("sending {} bytes", payload.len()); } } send does not exist for Connection<Disconnected>. Incorrect call ordering becomes a compile-time error instead of a branch in production.