Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Order Processing State Machine

Problem Statement

Build a type-safe order processing system that uses enums to model state transitions. You’ll start with basic enum variants, add exhaustive pattern matching for state transitions, then implement compile-time state checking using the typestate patterns

Why It Matters

Real-World Impact: State management bugs are expensive:

The State Confusion Problem:

  • Amazon (2006): Order processing bug caused incorrect shipments, millions in losses
  • Payment processors: Process same payment twice due to state confusion
  • E-commerce: Ship orders that were cancelled, refund orders not yet paid
  • Booking systems: Double-book resources, allow modifications after confirmation

Key Concepts Explained

This project demonstrates how Rust’s type system prevents state management bugs through enums, pattern matching, and zero-cost abstractions.

1. Enums as Discriminated Unions

Enums represent exactly one of several variants:

#![allow(unused)]
fn main() {
enum OrderState {
    Pending { items: Vec<Item>, customer_id: u64 },
    Paid { order_id: u64, payment_id: String, amount: f64 },
    Shipped { order_id: u64, tracking_number: String },
}
}

Why it matters: Each variant has different data - can’t access payment_id on Pending order (compile error).

2. Exhaustive Pattern Matching

Compiler ensures all enum variants handled:

#![allow(unused)]
fn main() {
fn status(&self) -> &str {
    match self {
        OrderState::Pending { .. } => "Pending",
        OrderState::Paid { .. } => "Paid",
        // ❌ Forgot Shipped - compiler error!
    }
}
}

Benefit: Add new state → compiler finds all match sites that need updating.

3. Move Semantics for State Transitions

Methods consume self to prevent using old state:

#![allow(unused)]
fn main() {
fn pay(self, payment_id: String) -> Result<OrderState, String> {
    // Consumes Pending, returns Paid
}

let order = OrderState::new_pending(...);
let order = order.pay("PAY_123")?;
// Can't use old `order` anymore - compiler error!
}

Prevents: Double payment, using stale state, concurrent modifications.

4. Typestate Pattern

Encode states as types for compile-time checking:

#![allow(unused)]
fn main() {
struct Order<State> {
    data: OrderData,
    _state: PhantomData<State>,
}

impl Order<Pending> {
    fn pay(self) -> Order<Paid> { ... }
}

let order: Order<Pending> = Order::new();
order.ship();  // ❌ Compile error - no ship() on Pending!
}

Benefit: Invalid transitions impossible to write.

5. PhantomData for Zero-Cost Abstractions

PhantomData<T> adds type parameter without runtime cost:

#![allow(unused)]
fn main() {
struct Order<State> {
    id: u64,
    _state: PhantomData<State>,  // 0 bytes!
}
// sizeof(Order<Pending>) == sizeof(Order<Paid>) == 8 bytes
}

Zero-cost: Type safety with no memory or performance overhead.

6. Pattern Guards for Business Rules

Add conditions to match arms:

#![allow(unused)]
fn main() {
match self {
    OrderState::Pending { items, .. } if items.is_empty() =>
        Err("Cannot pay for empty order"),
    OrderState::Pending { items, customer_id } =>
        Ok(OrderState::Paid { ... }),
    _ => Err("Can only pay pending orders"),
}
}

Benefit: Combine type checking with validation logic.

7. Result for Recoverable Errors

State transitions can fail gracefully:

#![allow(unused)]
fn main() {
fn cancel(self, reason: String) -> Result<OrderState, String> {
    match self {
        OrderState::Pending { .. } | OrderState::Paid { .. } =>
            Ok(OrderState::Cancelled { reason }),
        _ => Err("Cannot cancel after shipping"),
    }
}
}

vs Panic: Caller can handle errors instead of crashing.

8. Const Generics and Marker Types

Zero-sized types as state markers:

#![allow(unused)]
fn main() {
struct Pending;     // 0 bytes
struct Paid;        // 0 bytes

struct Order<S> {
    data: OrderData,  // 32 bytes
    _state: PhantomData<S>,  // 0 bytes
}
// All Order<S> variants: 32 bytes total
}

Benefit: Type-level computation with zero runtime cost.

9. Sealed Traits for API Control

Prevent external trait implementations:

#![allow(unused)]
fn main() {
mod sealed {
    pub trait Sealed {}
    impl Sealed for Pending {}
    impl Sealed for Paid {}
}

pub trait OrderState: sealed::Sealed {}
}

Benefit: Control which types can be used as state markers.


Connection to This Project

Here’s how each milestone applies these concepts to build increasingly safe state machines.

Milestone 1: Enums as State Machine

Concepts applied:

  • Discriminated unions: Five variants with different data
  • Exhaustive matching: status_string() must handle all states
  • Pattern matching: Extract state-specific data

Why this matters: Foundation of type-safe state representation.

Real-world impact:

  • Without enums: struct Order { status: String, ... } allows typos like “Payed”
  • With enums: Only valid states compile

Memory: OrderState enum = size of largest variant + discriminant (~40 bytes)


Milestone 2: State Transitions with Pattern Matching

Concepts applied:

  • Move semantics: self consumed prevents double transitions
  • Pattern guards: Validate business rules in match arms
  • Result propagation: Graceful error handling with ?
  • Exhaustive matching: Compiler ensures all states handled

Why this matters: Runtime validation with compile-time exhaustiveness.

Real-world impact:

#![allow(unused)]
fn main() {
// Prevents this bug:
let order = OrderState::new_pending(...);
process_payment(order.clone());  // Payment succeeds
process_payment(order);  // ❌ Double charge!

// With move semantics:
let order = order.pay(...)?;  // Consumes order
process_payment(order);  // ❌ Already moved - won't compile!
}

Performance: Zero overhead - transitions just move data, no allocation.


Milestone 3: Typestate Pattern

Concepts applied:

  • PhantomData: Encode state in type, not value
  • Type-level state: Order<Pending> vs Order<Paid> are different types
  • Compile-time checking: Invalid transitions don’t compile
  • Zero-cost abstraction: Type safety with no runtime cost

Why this matters: Catch bugs at compile time, not runtime.

Comparison:

ApproachError DetectionRuntime CostType Safety
Enum (M2)Runtime (returns Err)~1ns (discriminant check)Partial
Typestate (M3)Compile time0ns (monomorphization)Complete

Real-world impact:

#![allow(unused)]
fn main() {
// With runtime checking (Milestone 2):
let order = OrderState::new_pending(...);
let result = order.ship("TRACK");  // Compiles, returns Err at runtime
assert!(result.is_err());  // Must handle error

// With compile-time checking (Milestone 3):
let order: Order<Pending> = Order::new();
order.ship("TRACK");  // ❌ Doesn't compile - no ship() method!
}

IDE support: Autocomplete only shows valid methods for current state.

Memory: All Order<S> variants same size (PhantomData is zero-sized).


Project-Wide Benefits

Concrete comparisons - Processing 1M orders:

MetricString StatusEnum (M2)Typestate (M3)Improvement
Invalid transitions caught0 (runtime crash)100% (runtime error)100% (compile error)Compile-time
Double payment preventionManual checksAutomaticImpossible to writeType system
Memory per order48 bytes40 bytes32 bytes20% less
Transition costString compare ~10nsDiscriminant check ~1nsZero ~0ns10x faster
IDE autocompleteAll methodsAll methodsOnly valid methodsBetter DX

Real-world validation:

  • Stripe API: Uses typestate pattern for payment intents
  • diesel ORM: Query builders use typestate for SQL safety
  • tokio: Connection states use typestate pattern
  • embedded-hal: Hardware states encoded in types

This project teaches patterns used in production Rust systems where state correctness is critical.


What You’ll Build: Complete Learning Journey

This project takes you from basic enum understanding to advanced compile-time type safety through three progressive milestones. You’ll build the same order processing system twice—once with runtime checking and once with compile-time checking—to deeply understand the trade-offs.

The Complete State Machine

You’ll model this order lifecycle:

┌─────────┐
│ Pending │ ← Order created, awaiting payment
└────┬────┘
     │ pay()
     ▼
┌─────────┐
│  Paid   │ ← Payment received, awaiting shipment
└────┬────┘
     │ ship()
     ▼
┌─────────┐
│ Shipped │ ← Package in transit
└────┬────┘
     │ deliver()
     ▼
┌───────────┐
│ Delivered │ ← Final state
└───────────┘

(Cancellation allowed from Pending/Paid only)
     Pending ──cancel()──► Cancelled
     Paid ──────cancel()──► Cancelled



Milestone 1: Enums as state machine

This milestone introduces enums as state machines—one of Rust’s most powerful patterns. Unlike structs (which group related data), enums represent alternatives—a value is exactly one variant at any time.

Why Use Enum for State Machines?

State machines have:

  1. Finite states: Known, fixed set of states
  2. Transitions: Rules for moving between states
  3. State-specific data: Each state needs different information
  4. Exclusive states: Can’t be in two states at once

Enums are perfect for this! Each variant = one state, pattern matching = transitions.

What we are building Five order states representing the complete order lifecycle:

  1. Pending: Order created, awaiting payment

    • Contains: items, customer_id
    • Can transition to: Paid, Cancelled
  2. Paid: Payment received, awaiting shipment

    • Contains: order_id, payment_id, amount
    • Can transition to: Shipped, Cancelled
  3. Shipped: Package shipped, in transit

    • Contains: order_id, tracking_number
    • Can transition to: Delivered
  4. Delivered: Package delivered to customer

    • Contains: order_id, delivered_at
    • Terminal state (no further transitions)
  5. Cancelled: Order cancelled

    • Contains: order_id, reason
    • Terminal state

Starter Code:

#![allow(unused)]
fn main() {
use std::time::Instant;

// Item: Represents a product in an order
// Role: Stores product details for order line items
#[derive(Debug, Clone)]
struct Item {
   // TODO: add the fields: 
   // Unique identifier for the product
   // Product display name
   // Product price in dollars
}

// OrderState: Enum representing all possible order states
// Role: Type-safe state machine where each variant has state-specific data
#[derive(Debug, Clone)]
enum OrderState {
   //TODO: Define variants: - Pending - Paid  - Shipped - Delivered - Cancelled
}

impl OrderState {
    // new_pending: Creates a new order in Pending state
    // Role: Constructor for initial order state with items and customer
    fn new_pending(items: Vec<Item>, customer_id: u64) -> Self {
        // TODO: Create Pending variant
        todo!()
    }

    // status_string: Returns human-readable status
    // Role: Provides string representation of current state for display
    fn status_string(&self) -> &str {
        // TODO: Match on self and return appropriate status string
        // Hint: "Pending", "Paid", "Shipped", "Delivered", "Cancelled"
        todo!()
    }
}
}

Checkpoint Tests:

#![allow(unused)]
fn main() {
#[test]
fn test_create_pending_order() {
    let items = vec![
        Item { product_id: 1, name: "Widget".to_string(), price: 9.99 },
    ];
    let order = OrderState::new_pending(items, 123);

    assert_eq!(order.status_string(), "Pending");
}

#[test]
fn test_all_states() {
    // TODO: Test creating each state variant
    let pending = OrderState::Pending {
        items: vec![],
        customer_id: 1,
    };

    let paid = OrderState::Paid {
        order_id: 1,
        payment_id: "pay_123".to_string(),
        amount: 99.99,
    };

    // Test pattern matching works
    match pending {
        OrderState::Pending { .. } => { /* OK */ }
        _ => panic!("Should be pending"),
    }
}
}

Check Your Understanding:

  • Why does each variant have different associated data?
  • What prevents you from accessing payment_id on a Pending order?
  • How does this compare to having optional fields on a single struct?

Why Milestone 1 Isn’t Enough

Limitations:

  1. No state transitions: Can create any state, but can’t safely transition between them
  2. Validation missing: Nothing prevents creating Paid order with negative amount
  3. No business rules: Could go directly from Pending to Delivered, skipping payment
  4. Manual state checking: Users must pattern match everywhere to get data

What we’re adding: State transition methods that:

  • Consume the current state and return a new state
  • Enforce valid transitions (can’t ship before paying)
  • Validate business rules (can’t cancel after delivery)
  • Use Result for error handling

Improvements:

  • Type-safe transitions: order.pay() consumes Pending, returns Paid
  • Exhaustive matching: Compiler ensures all current states handled
  • Business logic: Validation happens in transition methods
  • Self-documenting: Method names show valid transitions

Milestone 2: State Transitions with Pattern Matching

Goal: Implement methods that transition between states with validation, using pattern matching to consume states and enforce valid transitions.

Issues:

  1. No validation: Can create Paid state without actually processing payment
  2. Skip steps: Can go directly from Pending to Shipped, bypassing payment
  3. No business logic: Amount calculation, inventory checks, fraud detection—all missing
  4. Manual construction: Easy to forget required fields or use wrong data

Solution: Controlled Transition:

#![allow(unused)]
fn main() {
// pay example code
impl OrderState {
    // Only way to go from Pending to Paid
    fn pay(self, payment_id: String) -> Result<Self, String> {
        match self {
            OrderState::Pending { items, customer_id } => {
                // ✅ Validate items
                if items.is_empty() {
                    return Err("Cannot pay for empty order".to_string());
                }

                // ✅ Calculate total
                let amount: f64 = items.iter().map(|i| i.price).sum();

                // ✅ Process payment (in real system)
                // payment_processor.charge(payment_id, amount)?;

                // ✅ Transition to Paid state
                Ok(OrderState::Paid {
                    order_id: customer_id,
                    payment_id,
                    amount,
                })
            }
            // ✅ Reject invalid transitions
            _ => Err("Can only pay for pending orders".to_string()),
        }
    }
}

// Now the only way to get Paid state:
let order = OrderState::new_pending(items, 123);
let order = order.pay("PAY_123".to_string())?;  // Validated!
}

What We’re Building:

Four transition methods representing the order lifecycle:

  1. pay(self, payment_id) -> Result<OrderState, String>

    • Consumes: Pending
    • Produces: Paid or error
    • Validates: Items not empty, calculates total amount
    • Business rule: Must have items to pay for
  2. ship(self, tracking_number) -> Result<OrderState, String>

    • Consumes: Paid
    • Produces: Shipped or error
    • Business rule: Can’t ship unpaid orders
  3. deliver(self) -> Result<OrderState, String>

    • Consumes: Shipped
    • Produces: Delivered or error
    • Adds: Delivery timestamp
    • Business rule: Can’t deliver unshipped orders
  4. cancel(self, reason) -> Result<OrderState, String>

    • Consumes: Pending or Paid only
    • Produces: Cancelled or error
    • Business rule: Can’t cancel after shipping

Why Take self (Ownership)? Consuming transitions prevent:

  • Using old states after transition
  • Paying for same order twice
  • Concurrent access to transitioning state
  • Forgetting to use the new state

Memory and Performance:

  • No allocation overhead: Transitions just move data, no extra allocations
  • No copying: self moved by value, not copied
  • Same memory size: OrderState always max(variants) + discriminant
  • Stack-based: Entire state machine lives on stack

State Machine Guarantees:

✅ Compile-time guarantees:

  • All variants handled in match (exhaustiveness)
  • Type-correct data in each variant
  • Can’t create variant without required fields

✅ Runtime guarantees (via transitions):

  • Can’t skip payment and go straight to shipped
  • Can’t pay for empty order
  • Can’t cancel after shipping
  • Can’t pay for same order twice (consuming self)

❌ Still possible (will fix in Milestone 3):

  • Calling order.ship() on Pending order (returns Err at runtime)
  • IDE shows all methods on all states (no compile-time filtering)
  • Can store mixed states in collections but lose type info

Starter Code:

#![allow(unused)]
fn main() {
impl OrderState {
    // pay: Transitions from Pending to Paid state
    // Role: Processes payment, validates items, calculates total
    fn pay(self, payment_id: String) -> Result<Self, String> {
        // Match on current state to enforce valid transitions
        match self {
            OrderState::Pending { items, customer_id } => {
                // TODO: Validate items not empty
                // TODO: Calculate total amount by summing item prices
                // TODO: Return Paid variant with order_id, payment_id, amount
                todo!("Consumes Pending state, returns Paid state or error")
            }
            _ => Err("Can only pay for pending orders".to_string()),
        }
    }

    // ship: Transitions from Paid to Shipped state
    // Role: Records shipment with tracking number
    fn ship(self, tracking_number: String) -> Result<Self, String> {
        // TODO: Match on self
        todo!("Consumes Paid state, returns Shipped state or error")
    }

    // deliver: Transitions from Shipped to Delivered state
    // Role: Marks order as delivered with timestamp
    fn deliver(self) -> Result<Self, String> {
        // TODO: Match on self
        todo!("Consumes Shipped state, returns Delivered state or error")
    }

    // cancel: Transitions to Cancelled state (only from Pending/Paid)
    // Role: Cancels order with reason, enforces business rules
    fn cancel(self, reason: String) -> Result<Self, String> {
        todo!("Consumes current state, returns Cancelled state or error")
    }

    // can_cancel: Checks if order can be cancelled
    // Role: Query method that doesn't consume state
    fn can_cancel(&self) -> bool {
        // TODO: Return true only for Pending or Paid states
        todo!()
    }
}
}

Checkpoint Tests:

#![allow(unused)]
fn main() {
#[test]
fn test_valid_transitions() {
    let items = vec![
        Item { product_id: 1, name: "Widget".to_string(), price: 9.99 },
    ];

    let order = OrderState::new_pending(items, 123);
    let order = order.pay("payment_123".to_string()).unwrap();
    assert_eq!(order.status_string(), "Paid");

    let order = order.ship("TRACK123".to_string()).unwrap();
    assert_eq!(order.status_string(), "Shipped");

    let order = order.deliver().unwrap();
    assert_eq!(order.status_string(), "Delivered");
}

#[test]
fn test_invalid_transitions() {
    let items = vec![
        Item { product_id: 1, name: "Widget".to_string(), price: 9.99 },
    ];

    let order = OrderState::new_pending(items, 123);

    // Can't ship before paying
    assert!(order.clone().ship("TRACK123".to_string()).is_err());

    // Can pay
    let order = order.pay("payment_123".to_string()).unwrap();

    // Can't pay again
    assert!(order.clone().pay("payment_456".to_string()).is_err());
}

#[test]
fn test_cancellation_rules() {
    let items = vec![
        Item { product_id: 1, name: "Widget".to_string(), price: 9.99 },
    ];

    // Can cancel pending order
    let order = OrderState::new_pending(items.clone(), 123);
    assert!(order.can_cancel());
    assert!(order.cancel("Customer request".to_string()).is_ok());

    // Can cancel paid order
    let order = OrderState::new_pending(items.clone(), 123)
        .pay("payment_123".to_string())
        .unwrap();
    assert!(order.can_cancel());

    // Cannot cancel shipped order
    let order = OrderState::new_pending(items, 123)
        .pay("payment_123".to_string())
        .unwrap()
        .ship("TRACK123".to_string())
        .unwrap();
    assert!(!order.can_cancel());
    assert!(order.cancel("Too late".to_string()).is_err());
}
}

Check Your Understanding:

  • Why do transition methods take self (ownership) instead of &self?
  • How does the compiler help you handle all possible current states?
  • What happens if you forget to handle a variant in a match?
  • Why is returning Result better than panicking on invalid transitions?

Why Milestone 2 Isn’t Enough

Remaining Issues:

  1. Runtime checks: Still possible to call wrong method at runtime, just returns Err
  2. API not self-documenting: IDE doesn’t show which methods available for current state
  3. Enum still mutable: Someone could manually construct invalid transitions
  4. No compile-time guarantees: order.ship() compiles even on Pending order

What we’re adding: Typestate pattern - use the type system to encode states:

  • Each state is a separate type: struct Pending, struct Paid, etc.
  • Generic wrapper: Order<State> where State is the current state type
  • Methods only available on appropriate state: Order<Pending> can’t call ship()
  • Transitions return new type: pay() consumes Order<Pending>, returns Order<Paid>

Improvements:

  • Compile-time checking: pending_order.ship() doesn’t compile!
  • Zero runtime cost: State stored in type, not value
  • IDE support: Autocomplete only shows valid methods for current state
  • Impossible states impossible: Can’t have Order<Shipped> without going through payment

Trade-offs:

  • More complex: More types and trait bounds
  • Less dynamic: Can’t store mixed states in Vec without trait objects
  • Worth it when: State transitions known at compile-time

Milestone 3: Typestate Pattern for Compile-Time Safety

Goal: Use phantom types to encode states in the type system, making invalid state transitions impossible to compile rather than just returning errors at runtime.

Problems with runtime checking:

  1. Late error detection: Find bugs when testing, not when coding
  2. Poor IDE support: Autocomplete shows all methods on all states
  3. Defensive programming: Must handle all Result::Err cases
  4. Lost type information: Vec<OrderState> loses which specific state each order is in
  5. Runtime cost: Every transition checks state discriminant

What We’re Building:

Five marker types and one generic struct:

State Markers (zero-sized types):

#![allow(unused)]
fn main() {
struct Pending;    // 0 bytes
struct Paid;       // 0 bytes
struct Shipped;    // 0 bytes
struct Delivered;  // 0 bytes
struct Cancelled;  // 0 bytes
}

Generic Order:

#![allow(unused)]
fn main() {
struct Order<State> {
    id: u64,
    customer_id: u64,
    items: Vec<Item>,
    _state: PhantomData<State>,  // Zero-sized!
}
}

Memory layout: All Order<State> variants are exactly the same size!

What is PhantomData<State>? PhantomData is a zero-sized type that tells the compiler “this struct owns a State type, even though we don’t actually store it”:

#![allow(unused)]
fn main() {
use std::marker::PhantomData;

struct Order<State> {
    id: u64,
    items: Vec<Item>,
    _state: PhantomData<State>,  // "Pretend" we have a State
}

// Why PhantomData is needed:
struct OrderBroken<State> {  // ❌ Error: parameter `State` is never used
    id: u64,
    items: Vec<Item>,
}

struct OrderFixed<State> {   // ✅ OK: State appears in PhantomData
    id: u64,
    items: Vec<Item>,
    _state: PhantomData<State>,
}
}

PhantomData properties:

  • Size: 0 bytes (optimized away at compile-time)
  • Purpose: Make generic parameter State “used” so compiler accepts it
  • Ownership: Tells compiler about ownership/lifetime relationships
  • Convention: Field name starts with _ to indicate “unused at runtime”

Advantages of Typestate Pattern:

✅ Compile-time safety: Invalid transitions caught before runtime ✅ Better IDE support: Autocomplete shows only valid methods for current state ✅ Self-documenting: Type signatures show state flow ✅ Zero runtime cost: State stored in type, not value ✅ Impossible states impossible: Can’t have Order<Shipped> without paying first ✅ Clearer error messages: Compiler explains what went wrong and suggests fixes

Disadvantages of Typestate Pattern:

❌ More complex: More types and impl blocks than enum approach ❌ Less dynamic: Can’t store Vec<Order<?>> with mixed states easily ❌ Verbose generics: Type signatures get longer: Order<Pending> vs OrderState ❌ Trait objects difficult: Need trait bounds for dynamic dispatch ❌ Learning curve: PhantomData and type-level programming are advanced concepts

When to Use Typestate vs Enum States:

Use Typestate When…Use Enum When…
State flow is known at compile-timeState changes based on runtime data
Want maximum compile-time safetyNeed to store mixed states (Vec<OrderState>)
Building APIs where mistakes are costlyBuilding flexible workflow engines
IDE support is criticalDynamic state transitions (e.g., config-driven)
Zero runtime cost is importantSimplicity is more important than type safety

Starter Code:

#![allow(unused)]
fn main() {
use std::marker::PhantomData;

// State marker types (zero-sized types)
// Role: Compile-time type markers that carry no runtime data
struct Pending;    // Order created, awaiting payment
struct Paid;       // Payment received, awaiting shipment
struct Shipped;    // Order shipped, in transit
struct Delivered;  // Order delivered to customer
struct Cancelled;  // Order cancelled (terminal state)

// Order<State>: Generic order struct parameterized by state
// Role: Holds order data, state encoded in type parameter
struct Order<State> {
    // TODO: Unique order identifier
    // TODO: Customer who placed order
    // TODO: Items in the order
    // TODO: Zero-sized type marker for compile-time state
}

// Pending state implementation
// Role: Methods available only when Order is in Pending state
impl Order<Pending> {
    // new: Creates a new order in Pending state
    // Role: Constructor validating items and initializing order
    fn new(customer_id: u64, items: Vec<Item>) -> Result<Self, String> {
        // TODO: Validate items not empty
        // TODO: Create Order<Pending> with generated id (e.g., customer_id)
        todo!()
    }

    // pay: Transitions from Pending to Paid
    // Role: Processes payment, consumes Order<Pending>, returns Order<Paid>
    fn pay(self, payment_id: String) -> Result<Order<Paid>, String> {
        // TODO: Validate items, calculate total amount
        // TODO: Simulate payment processing
        // TODO: Return Order<Paid> with same id, customer_id, items
        todo!()
    }

    // cancel: Transitions from Pending to Cancelled
    // Role: Cancels order before payment
    fn cancel(self, reason: String) -> Order<Cancelled> {
        todo!()
    }
}

// Paid state implementation
// Role: Methods available only when Order is in Paid state
impl Order<Paid> {
    // ship: Transitions from Paid to Shipped
    // Role: Marks order as shipped with tracking number
    fn ship(self, tracking_number: String) -> Order<Shipped> {
        // Note: In real system, would store tracking_number in Order struct
        // For this exercise, just transition the state
        todo!()
    }

    // cancel: Transitions from Paid to Cancelled
    // Role: Cancels order after payment but before shipping
    fn cancel(self, reason: String) -> Order<Cancelled> {
        todo!()
    }
}

// Shipped state implementation
// Role: Methods available only when Order is in Shipped state
impl Order<Shipped> {
    // deliver: Transitions from Shipped to Delivered
    // Role: Marks order as delivered (terminal state)
    fn deliver(self) -> Order<Delivered> {
        todo!()
    }

    // Note: No cancel method! Can't cancel after shipping - enforced at compile-time
}

// Delivered state implementation (terminal state)
// Role: No state transitions available from Delivered
impl Order<Delivered> {
    // No state transition methods - terminal state
}

// Common methods available in all states
// Role: Generic implementation over any state type
impl<State> Order<State> {
    // id: Returns order ID
    // Role: Accessor available in all states
    fn id(&self) -> u64 {
       todo!()
    }

    // customer_id: Returns customer ID
    // Role: Accessor available in all states
    fn customer_id(&self) -> u64 {
        todo!()
    }

    // items: Returns reference to order items
    // Role: Accessor available in all states
    fn items(&self) -> &[Item] {
       todo!()
    }
}
}

Checkpoint Tests:

#![allow(unused)]
fn main() {
#[test]
fn test_typestate_valid_flow() {
    let items = vec![
        Item { product_id: 1, name: "Widget".to_string(), price: 9.99 },
    ];

    let order = Order::<Pending>::new(1, items).unwrap();
    let order = order.pay("payment_123".to_string()).unwrap();
    let order = order.ship("TRACK123".to_string());
    let order = order.deliver();

    // order is now Order<Delivered>
    assert_eq!(order.customer_id(), 1);
}

#[test]
fn test_compile_time_enforcement() {
    let items = vec![
        Item { product_id: 1, name: "Widget".to_string(), price: 9.99 },
    ];

    let pending_order = Order::<Pending>::new(1, items).unwrap();

    // These won't compile! Uncomment to see errors:
    // pending_order.ship("TRACK123".to_string()); // ❌ No ship method on Pending
    // pending_order.deliver(); // ❌ No deliver method on Pending

    let paid_order = pending_order.pay("payment_123".to_string()).unwrap();
    // paid_order.pay("payment_456".to_string()); // ❌ No pay method on Paid (consumed)

    let shipped_order = paid_order.ship("TRACK123".to_string());
    // shipped_order.cancel("Oops".to_string()); // ❌ No cancel method on Shipped!
}

#[test]
fn test_cancellation_only_early_states() {
    let items = vec![
        Item { product_id: 1, name: "Widget".to_string(), price: 9.99 },
    ];

    // Can cancel pending
    let order = Order::<Pending>::new(1, items.clone()).unwrap();
    let _cancelled = order.cancel("Customer request".to_string());

    // Can cancel paid
    let order = Order::<Pending>::new(1, items).unwrap();
    let order = order.pay("payment_123".to_string()).unwrap();
    let _cancelled = order.cancel("Changed mind".to_string());

    // Shipped orders don't have cancel method - compile-time enforcement!
}

#[test]
fn test_common_methods_all_states() {
    let items = vec![
        Item { product_id: 1, name: "Widget".to_string(), price: 9.99 },
    ];

    let pending = Order::<Pending>::new(1, items.clone()).unwrap();
    assert_eq!(pending.customer_id(), 1);

    let paid = Order::<Pending>::new(1, items).unwrap()
        .pay("payment_123".to_string())
        .unwrap();
    assert_eq!(paid.customer_id(), 1);

    // Common methods available in all states
}
}

Check Your Understanding:

  • Why is _state: PhantomData<State> needed?
  • What’s the memory size of Order<Pending> vs Order<Paid>? (Hint: same!)
  • Why can’t you store Vec<Order<??>> with mixed states?
  • How does IDE autocomplete know which methods are available?
  • When would you prefer runtime enum states vs compile-time typestates?

Complete Working Example

use std::marker::PhantomData;
use std::time::{SystemTime, UNIX_EPOCH};

/* ============================================================
 * Shared domain types
 * ============================================================
 */

#[derive(Debug, Clone, PartialEq)]
struct Item {
    product_id: u64,
    name: String,
    price: f64,
}

/* ============================================================
 * Milestone 1 + 2: Enum-based runtime state machine
 * ============================================================
 */

#[derive(Debug, Clone, PartialEq)]
enum OrderState {
    Pending {
        items: Vec<Item>,
        customer_id: u64,
    },
    Paid {
        order_id: u64,
        payment_id: String,
        amount: f64,
    },
    Shipped {
        order_id: u64,
        tracking_number: String,
    },
    Delivered {
        order_id: u64,
        delivered_at: u64,
    },
    Cancelled {
        order_id: u64,
        reason: String,
    },
}

impl OrderState {
    fn new_pending(items: Vec<Item>, customer_id: u64) -> Self {
        OrderState::Pending { items, customer_id }
    }

    fn status_string(&self) -> &str {
        match self {
            OrderState::Pending { .. } => "Pending",
            OrderState::Paid { .. } => "Paid",
            OrderState::Shipped { .. } => "Shipped",
            OrderState::Delivered { .. } => "Delivered",
            OrderState::Cancelled { .. } => "Cancelled",
        }
    }

    fn pay(self, payment_id: String) -> Result<Self, String> {
        match self {
            OrderState::Pending { items, customer_id } => {
                if items.is_empty() {
                    return Err("Cannot pay for empty order".into());
                }

                let amount: f64 = items.iter().map(|i| i.price).sum();

                Ok(OrderState::Paid {
                    order_id: customer_id,
                    payment_id,
                    amount,
                })
            }
            _ => Err("Can only pay for pending orders".into()),
        }
    }

    fn ship(self, tracking_number: String) -> Result<Self, String> {
        match self {
            OrderState::Paid { order_id, .. } => Ok(OrderState::Shipped {
                order_id,
                tracking_number,
            }),
            _ => Err("Can only ship paid orders".into()),
        }
    }

    fn deliver(self) -> Result<Self, String> {
        match self {
            OrderState::Shipped { order_id, .. } => Ok(OrderState::Delivered {
                order_id,
                delivered_at: SystemTime::now()
                    .duration_since(UNIX_EPOCH)
                    .unwrap()
                    .as_secs(),
            }),
            _ => Err("Can only deliver shipped orders".into()),
        }
    }

    fn cancel(self, reason: String) -> Result<Self, String> {
        match self {
            OrderState::Pending { customer_id, .. }
            | OrderState::Paid { order_id: customer_id, .. } => Ok(OrderState::Cancelled {
                order_id: customer_id,
                reason,
            }),
            _ => Err("Cannot cancel after shipping".into()),
        }
    }

    fn can_cancel(&self) -> bool {
        matches!(
            self,
            OrderState::Pending { .. } | OrderState::Paid { .. }
        )
    }
}

/* ============================================================
 * Milestone 3: Typestate pattern (compile-time safety)
 * ============================================================
 */

struct Pending;
struct Paid;
struct Shipped;
struct Delivered;
struct Cancelled;

struct Order<State> {
    id: u64,
    customer_id: u64,
    items: Vec<Item>,
    _state: PhantomData<State>,
}

impl Order<Pending> {
    fn new(customer_id: u64, items: Vec<Item>) -> Result<Self, String> {
        if items.is_empty() {
            return Err("Order must contain at least one item".into());
        }

        Ok(Self {
            id: customer_id,
            customer_id,
            items,
            _state: PhantomData,
        })
    }

    fn pay(self, _payment_id: String) -> Result<Order<Paid>, String> {
        Ok(Order {
            id: self.id,
            customer_id: self.customer_id,
            items: self.items,
            _state: PhantomData,
        })
    }

    fn cancel(self, _reason: String) -> Order<Cancelled> {
        Order {
            id: self.id,
            customer_id: self.customer_id,
            items: self.items,
            _state: PhantomData,
        }
    }
}

impl Order<Paid> {
    fn ship(self, _tracking_number: String) -> Order<Shipped> {
        Order {
            id: self.id,
            customer_id: self.customer_id,
            items: self.items,
            _state: PhantomData,
        }
    }

    fn cancel(self, _reason: String) -> Order<Cancelled> {
        Order {
            id: self.id,
            customer_id: self.customer_id,
            items: self.items,
            _state: PhantomData,
        }
    }
}

impl Order<Shipped> {
    fn deliver(self) -> Order<Delivered> {
        Order {
            id: self.id,
            customer_id: self.customer_id,
            items: self.items,
            _state: PhantomData,
        }
    }
}

impl<State> Order<State> {
    fn id(&self) -> u64 {
        self.id
    }

    fn customer_id(&self) -> u64 {
        self.customer_id
    }

    fn items(&self) -> &[Item] {
        &self.items
    }
}

/* ============================================================
 * Demo (cargo run)
 * ============================================================
 */

fn main() {
    let items = vec![Item {
        product_id: 1,
        name: "Widget".into(),
        price: 9.99,
    }];

    println!("== Runtime enum state machine ==");
    let order = OrderState::new_pending(items.clone(), 42);
    let order = order.pay("PAY123".into()).unwrap();
    let order = order.ship("TRACK123".into()).unwrap();
    let order = order.deliver().unwrap();
    println!("Final state: {}", order.status_string());

    println!("\n== Typestate machine ==");
    let order = Order::<Pending>::new(42, items).unwrap();
    let order = order.pay("PAY123".into()).unwrap();
    let order = order.ship("TRACK123".into());
    let order = order.deliver();
    println!("Delivered order for customer {}", order.customer_id());
}

/* ============================================================
 * Tests (cargo test)
 * ============================================================
 */

#[cfg(test)]
mod tests {
    use super::*;

    fn sample_items() -> Vec<Item> {
        vec![Item {
            product_id: 1,
            name: "Widget".into(),
            price: 9.99,
        }]
    }

    #[test]
    fn test_enum_valid_transitions() {
        let order = OrderState::new_pending(sample_items(), 1);
        let order = order.pay("pay".into()).unwrap();
        let order = order.ship("track".into()).unwrap();
        let order = order.deliver().unwrap();

        assert_eq!(order.status_string(), "Delivered");
    }

    #[test]
    fn test_enum_invalid_transitions() {
        let order = OrderState::new_pending(sample_items(), 1);
        assert!(order.clone().ship("track".into()).is_err());

        let order = order.pay("pay".into()).unwrap();
        assert!(order.clone().pay("pay2".into()).is_err());
    }

    #[test]
    fn test_enum_cancellation_rules() {
        let order = OrderState::new_pending(sample_items(), 1);
        assert!(order.can_cancel());

        let order = order.pay("pay".into()).unwrap();
        assert!(order.can_cancel());

        let order = order.ship("track".into()).unwrap();
        assert!(!order.can_cancel());
    }

    #[test]
    fn test_typestate_valid_flow() {
        let order = Order::<Pending>::new(1, sample_items()).unwrap();
        let order = order.pay("pay".into()).unwrap();
        let order = order.ship("track".into());
        let order = order.deliver();

        assert_eq!(order.customer_id(), 1);
    }

    #[test]
    fn test_typestate_common_methods() {
        let order = Order::<Pending>::new(1, sample_items()).unwrap();
        assert_eq!(order.items().len(), 1);
        assert_eq!(order.customer_id(), 1);
    }
}