Advanced Rust Design Patterns

State Pattern with Enums

While the Typestate pattern encodes compile-time states in generic type parameters (Connection<State>), many real-world state machines—such as network protocol parsers, circuit breakers, order lifecycles, and game actors—transition dynamically based on runtime events and need to be stored in a homogeneous variable or Vec. In Rust, the State pattern with enums represents each state as an enum variant carrying only the fields valid in that state, making invalid field combinations impossible to construct.

Anti-Pattern (Bag-of-Optionals Struct):
  struct CircuitBreaker {
      state: StateTag,
      failure_count: Option<u32>,         // Only valid in Closed!
      opened_at_ms: Option<u64>,          // Only valid in Open!
      trial_successes: Option<u32>,       // Only valid in HalfOpen!
  }

Idiomatic Rust Enum State Machine (Invalid States Unrepresentable!):
  enum CircuitState {
      Closed { failures: u32 },
      Open { opened_at_ms: u64 },
      HalfOpen { successes: u32 },
  }

Comparing Enum State Machines, Typestate, and OOP Box<dyn State>

Dimension Enum State Machine (enum State) Typestate (Struct<S>) OOP Trait Object (Box<dyn State>)
Transition Determined At Runtime (based on network/user events) Compile time (static code sequence) Runtime
Can Live in a Vec<Machine>? Yes (Single concrete type!) No (Struct<A> and Struct<B> are different types) Yes
Heap Allocation on Transition? 0 allocations (In-place stack/inline transition!) 0 allocations Allocates a new Box<dyn State> on every transition
Exhaustive Transition Checking Yes (match (state, event) checks all pairs!) Yes (methods only exist on valid state) No

Taking Ownership of State Fields During &mut self Transitions (std::mem::replace)

When transitioning an enum field self.state inside a &mut self method, you often want to move an owned value (such as a String or Vec) out of the old variant into the new variant without cloning it.

Use std::mem::replace(&mut self.state, State::TemporaryPlaceholder) (or std::mem::take) to take ownership of the old enum value, match on it by value to compute the new state, and assign the new state back to self.state:

#[derive(Debug, PartialEq, Eq)]
pub enum JobState {
    Queued {
        payload: String,
    },
    Running {
        payload: String,
        worker_id: u32,
    },
    Completed {
        summary: String,
    },
    Failed {
        reason: String,
    },
}

#[derive(Debug)]
pub enum JobEvent {
    AssignWorker(u32),
    FinishSuccess,
    Abort(&'static str),
}

pub struct Job {
    state: JobState,
}

impl Job {
    pub fn new(payload: impl Into<String>) -> Self {
        Self {
            state: JobState::Queued {
                payload: payload.into(),
            },
        }
    }

    pub fn state(&self) -> &JobState {
        &self.state
    }

    /// Transitions `self.state` in-place while moving `payload: String` by value with 0 clones!
    pub fn apply(&mut self, event: JobEvent) {
        let placeholder = JobState::Failed {
            reason: String::new(),
        };
        let current = std::mem::replace(&mut self.state, placeholder);

        self.state = match (current, event) {
            (JobState::Queued { payload }, JobEvent::AssignWorker(worker_id)) => {
                // `payload` is MOVED into `Running` without cloning!
                JobState::Running { payload, worker_id }
            }
            (JobState::Running { payload, worker_id }, JobEvent::FinishSuccess) => {
                JobState::Completed {
                    summary: format!("worker #{worker_id} processed {} bytes", payload.len()),
                }
            }
            (_, JobEvent::Abort(msg)) => JobState::Failed {
                reason: msg.to_string(),
            },
            // Ignore invalid transitions cleanly by restoring `unhandled_state`:
            (unhandled_state, _ignored_event) => unhandled_state,
        };
    }
}

fn main() {
    let mut job = Job::new("batch_export_2026.parquet");
    job.apply(JobEvent::AssignWorker(7));
    assert_eq!(
        job.state(),
        &JobState::Running {
            payload: "batch_export_2026.parquet".to_string(),
            worker_id: 7,
        }
    );

    job.apply(JobEvent::FinishSuccess);
    println!("Final job state: {:?}", job.state());
}
Final job state: Completed { summary: "worker #7 processed 25 bytes" }