rust 130 lines · 3 tabs

Type-State Order Lifecycle Machine With Compile-Time Transition Safety in Rust

Shared by codesnips Jul 2026
3 tabs
use std::fmt;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OrderState {
    Pending,
    Paid,
    Shipped,
    Delivered,
    Cancelled,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OrderEvent {
    Pay,
    Ship,
    Deliver,
    Cancel,
}

#[derive(Debug, PartialEq, Eq)]
pub enum TransitionError {
    Illegal { from: OrderState, event: OrderEvent },
}

impl fmt::Display for TransitionError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            TransitionError::Illegal { from, event } => {
                write!(f, "cannot apply {:?} while {:?}", event, from)
            }
        }
    }
}

impl OrderState {
    pub fn apply(self, event: OrderEvent) -> Result<OrderState, TransitionError> {
        use OrderEvent::*;
        use OrderState::*;
        match (self, event) {
            (Pending, Pay) => Ok(Paid),
            (Pending, Cancel) => Ok(Cancelled),
            (Paid, Ship) => Ok(Shipped),
            (Paid, Cancel) => Ok(Cancelled),
            (Shipped, Deliver) => Ok(Delivered),
            (from, event) => Err(TransitionError::Illegal { from, event }),
        }
    }

    pub fn is_terminal(self) -> bool {
        matches!(self, OrderState::Delivered | OrderState::Cancelled)
    }
}
3 files · rust Explain with highlit

This snippet models an order lifecycle as an explicit state machine using Rust enums, with a runtime-checked API and a compile-time type-state layer on top. The core idea is that an order can only be in one of a fixed set of states, and only certain transitions are legal — the code enforces that in two complementary ways so illegal transitions become impossible rather than merely discouraged.

In order_state.rs, the OrderState enum enumerates every stage — Pending, Paid, Shipped, Delivered, Cancelled — and the OrderEvent enum enumerates the things that can happen. The apply method is the heart of the runtime machine: it pattern-matches on the (state, event) pair and returns either the next state or a TransitionError. This total-match style is why enums shine here — the compiler forces every combination to be considered, so an unhandled pairing falls into the catch-all arm that produces TransitionError::Illegal rather than silently doing nothing. Terminal states like Delivered and Cancelled simply have no outgoing arms, which is how the model expresses finality.

Because a runtime error is still a bug that reaches production, typed_order.rs lifts the same rules into the type system. Zero-sized marker structs (Pending, Paid, Shipped) implement the sealed State trait, and Order<S> carries the state as a phantom-like type parameter. Methods such as pay and ship are defined only on the specific impl Order<Pending> or impl Order<Paid> block, and they consume self and return a new Order<Paid>. Calling ship on an unpaid order is not a runtime failure — it does not compile, because that method does not exist for Order<Pending>.

The trade-off is real: the type-state approach gives the strongest guarantees but is rigid, so it fits linear happy-path flows, while the enum machine in apply handles dynamic, data-driven transitions (like events arriving from a queue). main.rs exercises both, showing the runtime machine folding events and the typed builder chaining pay().ship(). A common pitfall is trying to store heterogeneous Order<S> values in one collection; when that is needed, the enum representation is the pragmatic choice. Using both together lets a system validate untrusted input at the boundary and then move into type-safe territory.


Related snips

javascript
// Creating a Promise
const myPromise = new Promise((resolve, reject) => {
  const success = true;

  setTimeout(() => {
    if (success) {

Promises and async/await patterns for asynchronous JavaScript

javascript promises async-await
by Alex Chang 1 tab
ruby
class CreateDeadJobs < ActiveRecord::Migration[7.1]
  def change
    create_table :dead_jobs do |t|
      t.string  :jid, null: false
      t.string  :queue, null: false
      t.string  :klass, null: false

Background Job Dead Letter Queue (DLQ) Table

rails reliability background-jobs
by codesnips 4 tabs
typescript
import axios from 'axios';

export type NormalizedErrors = {
  fields: Record<string, string>;
  formLevel: string | null;
};

Frontend: normalize and display server validation errors

ux typescript react
by codesnips 3 tabs
rust
use anyhow::{Context, Result};
use std::fs;

fn load_config(path: &str) -> Result<String> {
    fs::read_to_string(path)
        .with_context(|| format!("failed to read config from {}", path))

anyhow::Context for adding error context without custom types

rust error-handling cli
by Marcus Chen 1 tab
ruby
module ApiErrorHandler
  extend ActiveSupport::Concern

  included do
    rescue_from StandardError, with: :handle_standard_error
    rescue_from ActiveRecord::RecordNotFound, with: :handle_not_found

Structured JSON error responses

rails api error-handling
by Alex Kumar 1 tab
rust
use std::sync::{Arc, Mutex};
use std::thread;

fn main() {
    let data = Arc::new(Mutex::new(vec![1, 2, 3]));

Send and Sync traits for safe concurrency guarantees

rust concurrency traits
by Marcus Chen 1 tab

Share this code

Here's the card — post it anywhere.

Type-State Order Lifecycle Machine With Compile-Time Transition Safety in Rust — share card
Link copied