Building Robust State Machines for Transactions
In this lesson, you will learn how to use Rust’s type system to create "state-aware" transaction wrappers that ensure resources can only be committed or rolled back when they are in the correct phase.
The Problem: Accidental Misuse
In complex systems, it is easy to accidentally call commit() twice or try to rollback() a transaction that has already been finalized. Instead of relying on runtime checks (which are prone to human error), we can encode these legal transitions directly into the type system using Generics and PhantomData.
Encoding States with Generics
We define a Transaction<S> where S represents the current phase. By creating empty structs to act as state markers, we can ensure that methods are only available when the transaction is in the appropriate state.
struct Pending;
struct Committed;
struct RolledBack;
struct Transaction<State, R> {
resource: R,
_state: std::marker::PhantomData<State>,
}
impl<R> Transaction<Pending, R> {
pub fn commit(self) -> Transaction<Committed, R> {
// Logic to finalize resource
Transaction { resource: self.resource, _state: std::marker::PhantomData }
}
pub fn rollback(self) -> Transaction<RolledBack, R> {
// Logic to revert resource
Transaction { resource: self.resource, _state: std::marker::PhantomData }
}
}
Why This Works
Notice that commit and rollback are only defined for Transaction<Pending, R>. If you hold a Transaction<Committed, R>, you literally do not have the methods commit or rollback available in your scope. This provides static dispatch—the compiler checks the validity of your transaction flow before your code ever runs.
By parameterizing your resource R alongside the state S, you create a system that is both generic (works with any resource type) and strictly guarded against invalid state transitions. If a developer attempts to call rollback on a Committed transaction, the compiler will stop the build, effectively turning potential runtime bugs into compile-time errors.
Key Takeaway: Use generic state markers to consume the transaction and return a new state, effectively using the type system to enforce strict workflow integrity.