Advanced Rust Design Patterns

Typestate Pattern

The Typestate pattern encodes the runtime lifecycle state of an object directly into its compile-time type parameter (Connection<Disconnected>, Connection<Authenticated>, Transaction<Committed>). State transition methods consume self by value in the old state (fn login(self) -> Connection<Authenticated>), making it a compile-time error to call an operation in the wrong state or to reuse a stale state handle!

State Machine Enforced at Compile Time (`0` Bytes of Runtime State Tag!):

  HttpResponse<BuildingHeaders>
       |  Can call `.header(k, v)`
       |  Cannot call `.send()` yet!
       v  `.body(bytes)` (Consumes `BuildingHeaders` by value!)
  HttpResponse<ReadyToSend>
       |  Cannot call `.header(k, v)` anymore!
       v  `.send()` (Consumes `ReadyToSend` by value!)
  SentReceipt

How Zero-Sized State Markers Achieve Zero Runtime Cost

Each state marker (pub struct BuildingHeaders;, pub struct ReadyToSend;) is a Zero-Sized Type (ZST) held inside the generic struct via PhantomData<State> (or directly as a ZST field state: State when state-specific data is carried).

Because size_of::<PhantomData<State>>() == 0:

  1. HttpResponse<BuildingHeaders> and HttpResponse<ReadyToSend> have the exact same memory layout as the underlying fields without State.
  2. There is no runtime match self.state check or InvalidStateError branch—invalid state transitions simply do not exist as methods on that type!
use std::marker::PhantomData;
use std::mem::size_of;

// 1. Define Zero-Sized Type (ZST) state markers:
pub struct BuildingHeaders;
pub struct ReadyToSend;

pub struct HttpResponse<State> {
    status: u16,
    headers: Vec<(String, String)>,
    body: Vec<u8>,
    _state: PhantomData<State>,
}

// 2. Methods available ONLY in the `BuildingHeaders` state:
impl HttpResponse<BuildingHeaders> {
    pub fn new(status: u16) -> Self {
        Self {
            status,
            headers: Vec::new(),
            body: Vec::new(),
            _state: PhantomData,
        }
    }

    pub fn header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
        self.headers.push((key.into(), value.into()));
        self
    }

    /// Transitions from `BuildingHeaders` -> `ReadyToSend` by consuming `self`!
    /// Automatically appends the exact `Content-Length` header so headers can no longer change.
    pub fn body(mut self, payload: impl Into<Vec<u8>>) -> HttpResponse<ReadyToSend> {
        let body = payload.into();
        self.headers
            .push(("Content-Length".to_string(), body.len().to_string()));

        HttpResponse {
            status: self.status,
            headers: self.headers,
            body,
            _state: PhantomData,
        }
    }
}

// 3. Methods available ONLY in the `ReadyToSend` state:
impl HttpResponse<ReadyToSend> {
    pub fn send(self) -> String {
        format!(
            "HTTP/1.1 {} ({} headers, {} body bytes)",
            self.status,
            self.headers.len(),
            self.body.len()
        )
    }
}

// 4. Methods available in ANY state `<S>`:
impl<S> HttpResponse<S> {
    pub fn status(&self) -> u16 {
        self.status
    }
}

fn main() {
    // Verify `PhantomData<State>` adds 0 bytes to the struct size:
    assert_eq!(
        size_of::<HttpResponse<BuildingHeaders>>(),
        size_of::<HttpResponse<ReadyToSend>>()
    );

    let receipt = HttpResponse::new(200)
        .header("Content-Type", "application/json")
        .body(b"{\"ok\":true}".to_vec())
        // `.header(...)` here would be a compile-time error because we are now `ReadyToSend`!
        .send();

    println!("Sent typestate response: {receipt}");
}
Sent typestate response: HTTP/1.1 200 (2 headers, 11 body bytes)

Sealing Typestate Marker Traits

When you write generic methods over impl<S: ResponseState> HttpResponse<S>, seal the ResponseState trait using a private supertrait (mod private { pub trait Sealed {} }) so external callers cannot invent fake state marker structs outside your state machine.