State Machine Pattern Real-World Examples
Production-ready examples demonstrating the State Machine pattern in real-world scenarios.
Example 1: Payment Processing Gateway
The Problem
A payment gateway needs to manage complex transaction states including authorization, capture, refunds, and chargebacks with strict rules about what operations are allowed at each stage.
The Solution
Use State Machine to enforce valid transaction state transitions.
The Code
public enum PaymentState
{
Created, Authorizing, Authorized, Capturing, Captured,
Refunding, Refunded, Voided, Declined, ChargebackPending, ChargebackLost
}
public enum PaymentEvent
{
Authorize, AuthorizeSuccess, AuthorizeFailed,
Capture, CaptureSuccess, CaptureFailed,
Void, Refund, RefundSuccess, RefundFailed,
ChargebackReceived, ChargebackWon, ChargebackLost
}
public class PaymentTransaction
{
private readonly StateMachine<PaymentState, PaymentEvent> _machine;
private readonly IPaymentGateway _gateway;
private readonly IAuditLog _audit;
public Guid TransactionId { get; }
public decimal Amount { get; }
public PaymentState State => _machine.CurrentState;
public PaymentTransaction(Guid id, decimal amount, IPaymentGateway gateway, IAuditLog audit)
{
TransactionId = id;
Amount = amount;
_gateway = gateway;
_audit = audit;
_machine = StateMachine<PaymentState, PaymentEvent>.Create(PaymentState.Created)
.State(PaymentState.Created)
.OnEntry(() => _audit.Log(id, "Payment created"))
.On(PaymentEvent.Authorize)
.Execute(() => StartAuthorization())
.TransitionTo(PaymentState.Authorizing)
.State(PaymentState.Authorizing)
.On(PaymentEvent.AuthorizeSuccess).TransitionTo(PaymentState.Authorized)
.On(PaymentEvent.AuthorizeFailed).TransitionTo(PaymentState.Declined)
.State(PaymentState.Authorized)
.OnEntry(() => _audit.Log(id, "Payment authorized"))
.On(PaymentEvent.Capture)
.Execute(() => StartCapture())
.TransitionTo(PaymentState.Capturing)
.On(PaymentEvent.Void)
.Execute(() => VoidAuthorization())
.TransitionTo(PaymentState.Voided)
.State(PaymentState.Capturing)
.On(PaymentEvent.CaptureSuccess).TransitionTo(PaymentState.Captured)
.On(PaymentEvent.CaptureFailed).TransitionTo(PaymentState.Authorized)
.State(PaymentState.Captured)
.OnEntry(() =>
{
_audit.Log(id, "Payment captured");
SendReceipt();
})
.On(PaymentEvent.Refund)
.When(() => CanRefund())
.Execute(() => StartRefund())
.TransitionTo(PaymentState.Refunding)
.On(PaymentEvent.ChargebackReceived)
.Execute(() => NotifyMerchant())
.TransitionTo(PaymentState.ChargebackPending)
.State(PaymentState.Refunding)
.On(PaymentEvent.RefundSuccess).TransitionTo(PaymentState.Refunded)
.On(PaymentEvent.RefundFailed).TransitionTo(PaymentState.Captured)
.State(PaymentState.ChargebackPending)
.On(PaymentEvent.ChargebackWon).TransitionTo(PaymentState.Captured)
.On(PaymentEvent.ChargebackLost).TransitionTo(PaymentState.ChargebackLost)
// Terminal states
.State(PaymentState.Declined)
.OnEntry(() => _audit.Log(id, "Payment declined"))
.State(PaymentState.Voided)
.OnEntry(() => _audit.Log(id, "Payment voided"))
.State(PaymentState.Refunded)
.OnEntry(() => _audit.Log(id, "Payment refunded"))
.State(PaymentState.ChargebackLost)
.OnEntry(() => _audit.Log(id, "Chargeback lost"))
.Build();
}
public async Task AuthorizeAsync(CancellationToken ct)
{
_machine.Fire(PaymentEvent.Authorize);
var result = await _gateway.AuthorizeAsync(TransactionId, Amount, ct);
_machine.Fire(result.Success ? PaymentEvent.AuthorizeSuccess : PaymentEvent.AuthorizeFailed);
}
public async Task CaptureAsync(CancellationToken ct)
{
_machine.Fire(PaymentEvent.Capture);
var result = await _gateway.CaptureAsync(TransactionId, ct);
_machine.Fire(result.Success ? PaymentEvent.CaptureSuccess : PaymentEvent.CaptureFailed);
}
private bool CanRefund() => (DateTime.UtcNow - CapturedAt) <= TimeSpan.FromDays(90);
}
Why This Pattern
- Enforced transitions: Invalid operations rejected at state level
- Clear audit trail: Entry actions log all state changes
- Business rules: Guards enforce refund windows, etc.
Example 2: CI/CD Pipeline Orchestrator
The Problem
A CI/CD system needs to manage build pipelines through stages (checkout, build, test, deploy) with proper handling of failures, retries, and manual approvals.
The Solution
Use State Machine to orchestrate pipeline stages with guards for approvals.
The Code
public enum PipelineState
{
Queued, CheckingOut, Building, Testing, AwaitingApproval,
Deploying, Deployed, Failed, Cancelled
}
public enum PipelineEvent
{
Start, CheckoutComplete, CheckoutFailed,
BuildComplete, BuildFailed,
TestsPass, TestsFail,
Approve, Reject,
DeployComplete, DeployFailed,
Retry, Cancel
}
public class Pipeline
{
private readonly StateMachine<PipelineState, PipelineEvent> _machine;
private int _retryCount;
public string Id { get; }
public PipelineState State => _machine.CurrentState;
public List<string> Logs { get; } = new();
public Pipeline(string id, PipelineConfig config, IApprovalService approvals)
{
Id = id;
_machine = StateMachine<PipelineState, PipelineEvent>.Create(PipelineState.Queued)
.State(PipelineState.Queued)
.OnEntry(() => Log("Pipeline queued"))
.On(PipelineEvent.Start)
.Execute(() => StartCheckout())
.TransitionTo(PipelineState.CheckingOut)
.On(PipelineEvent.Cancel).TransitionTo(PipelineState.Cancelled)
.State(PipelineState.CheckingOut)
.OnEntry(() => Log("Checking out source"))
.On(PipelineEvent.CheckoutComplete).TransitionTo(PipelineState.Building)
.On(PipelineEvent.CheckoutFailed).TransitionTo(PipelineState.Failed)
.State(PipelineState.Building)
.OnEntry(() =>
{
Log("Building");
StartBuild();
})
.On(PipelineEvent.BuildComplete).TransitionTo(PipelineState.Testing)
.On(PipelineEvent.BuildFailed).TransitionTo(PipelineState.Failed)
.State(PipelineState.Testing)
.OnEntry(() =>
{
Log("Running tests");
StartTests();
})
.On(PipelineEvent.TestsPass)
.When(() => config.RequiresApproval)
.TransitionTo(PipelineState.AwaitingApproval)
.On(PipelineEvent.TestsPass)
.When(() => !config.RequiresApproval)
.TransitionTo(PipelineState.Deploying)
.On(PipelineEvent.TestsFail).TransitionTo(PipelineState.Failed)
.State(PipelineState.AwaitingApproval)
.OnEntry(() =>
{
Log("Awaiting approval");
approvals.RequestApproval(Id, config.Approvers);
})
.On(PipelineEvent.Approve)
.When(() => approvals.IsApproved(Id))
.TransitionTo(PipelineState.Deploying)
.On(PipelineEvent.Reject).TransitionTo(PipelineState.Cancelled)
.State(PipelineState.Deploying)
.OnEntry(() =>
{
Log($"Deploying to {config.Environment}");
StartDeploy();
})
.On(PipelineEvent.DeployComplete).TransitionTo(PipelineState.Deployed)
.On(PipelineEvent.DeployFailed).TransitionTo(PipelineState.Failed)
.State(PipelineState.Deployed)
.OnEntry(() =>
{
Log("Deployment successful");
NotifySuccess();
})
.State(PipelineState.Failed)
.OnEntry(() =>
{
Log("Pipeline failed");
NotifyFailure();
})
.On(PipelineEvent.Retry)
.When(() => _retryCount < config.MaxRetries)
.Execute(() => _retryCount++)
.TransitionTo(PipelineState.Queued)
.State(PipelineState.Cancelled)
.OnEntry(() => Log("Pipeline cancelled"))
.Build();
}
private void Log(string message)
{
Logs.Add($"[{DateTime.UtcNow:HH:mm:ss}] {message}");
}
}
Why This Pattern
- Stage progression: Clear flow through pipeline stages
- Conditional paths: Approval required only when configured
- Retry logic: Failed pipelines can retry with limits
Example 3: Game Character Controller
The Problem
A game character needs to manage states like idle, walking, running, jumping, and attacking with proper animations and collision handling.
The Solution
Use State Machine to manage character states with physics-based guards.
The Code
public enum CharacterState
{
Idle, Walking, Running, Jumping, Falling, Attacking, Stunned, Dead
}
public enum CharacterInput
{
Move, Run, Jump, Attack, Land, TakeDamage, Die, Recover, Stop
}
public class CharacterController
{
private readonly StateMachine<CharacterState, CharacterInput> _machine;
private readonly IAnimator _animator;
private readonly IPhysics _physics;
public CharacterState State => _machine.CurrentState;
public float Health { get; private set; }
public bool IsGrounded => _physics.IsGrounded;
public bool IsAlive => Health > 0;
public CharacterController(IAnimator animator, IPhysics physics)
{
_animator = animator;
_physics = physics;
Health = 100;
_machine = StateMachine<CharacterState, CharacterInput>.Create(CharacterState.Idle)
.State(CharacterState.Idle)
.OnEntry(() => _animator.Play("Idle"))
.On(CharacterInput.Move).TransitionTo(CharacterState.Walking)
.On(CharacterInput.Run).TransitionTo(CharacterState.Running)
.On(CharacterInput.Jump)
.When(() => IsGrounded)
.Execute(() => _physics.ApplyJumpForce())
.TransitionTo(CharacterState.Jumping)
.On(CharacterInput.Attack).TransitionTo(CharacterState.Attacking)
.On(CharacterInput.TakeDamage)
.Execute(() => ApplyDamage())
.TransitionTo(CharacterState.Stunned)
.On(CharacterInput.Die).TransitionTo(CharacterState.Dead)
.State(CharacterState.Walking)
.OnEntry(() => _animator.Play("Walk"))
.On(CharacterInput.Stop).TransitionTo(CharacterState.Idle)
.On(CharacterInput.Run).TransitionTo(CharacterState.Running)
.On(CharacterInput.Jump)
.When(() => IsGrounded)
.Execute(() => _physics.ApplyJumpForce())
.TransitionTo(CharacterState.Jumping)
.On(CharacterInput.Attack).TransitionTo(CharacterState.Attacking)
.State(CharacterState.Running)
.OnEntry(() => _animator.Play("Run"))
.On(CharacterInput.Stop).TransitionTo(CharacterState.Idle)
.On(CharacterInput.Move).TransitionTo(CharacterState.Walking)
.On(CharacterInput.Jump)
.When(() => IsGrounded)
.Execute(() => _physics.ApplyJumpForce(1.2f)) // Higher jump when running
.TransitionTo(CharacterState.Jumping)
.State(CharacterState.Jumping)
.OnEntry(() => _animator.Play("Jump"))
.On(CharacterInput.Land)
.When(() => IsGrounded)
.TransitionTo(CharacterState.Idle)
.On(CharacterInput.Attack)
.Execute(() => PerformAirAttack())
.TransitionTo(CharacterState.Falling)
.State(CharacterState.Falling)
.OnEntry(() => _animator.Play("Fall"))
.On(CharacterInput.Land)
.When(() => IsGrounded)
.TransitionTo(CharacterState.Idle)
.State(CharacterState.Attacking)
.OnEntry(() =>
{
_animator.Play("Attack");
PerformAttack();
})
.On(CharacterInput.Stop)
.When(() => _animator.IsComplete("Attack"))
.TransitionTo(CharacterState.Idle)
.State(CharacterState.Stunned)
.OnEntry(() => _animator.Play("Stunned"))
.On(CharacterInput.Recover)
.When(() => IsAlive)
.TransitionTo(CharacterState.Idle)
.On(CharacterInput.Die)
.When(() => !IsAlive)
.TransitionTo(CharacterState.Dead)
.State(CharacterState.Dead)
.OnEntry(() =>
{
_animator.Play("Death");
DisableCollision();
})
.Build();
}
public void Update(float deltaTime)
{
// Physics-based state updates
if (!IsGrounded && State == CharacterState.Jumping)
{
if (_physics.Velocity.Y < 0)
_machine.TryFire(CharacterInput.Land);
}
}
}
Why This Pattern
- Animation sync: Entry actions trigger correct animations
- Physics guards: Jump only when grounded
- Interruptible states: Damage can interrupt any state
Example 4: Hotel Room Booking System
The Problem
A hotel reservation system needs to manage room states from available through booking, check-in, stay, and checkout with proper handling of cancellations and no-shows.
The Solution
Use State Machine to enforce booking lifecycle rules.
The Code
public enum RoomState
{
Available, Reserved, CheckedIn, Occupied, CheckedOut, Cleaning, Maintenance
}
public enum RoomEvent
{
Reserve, ConfirmReservation, CancelReservation, NoShow,
CheckIn, CheckOut, StartCleaning, FinishCleaning,
ReportIssue, CompleteMaintenance
}
public class HotelRoom
{
private readonly StateMachine<RoomState, RoomEvent> _machine;
private readonly INotificationService _notifications;
public string RoomNumber { get; }
public RoomState State => _machine.CurrentState;
public Reservation? CurrentReservation { get; private set; }
public HotelRoom(string roomNumber, INotificationService notifications)
{
RoomNumber = roomNumber;
_notifications = notifications;
_machine = StateMachine<RoomState, RoomEvent>.Create(RoomState.Available)
.State(RoomState.Available)
.On(RoomEvent.Reserve)
.Execute(() => SendConfirmation())
.TransitionTo(RoomState.Reserved)
.On(RoomEvent.ReportIssue).TransitionTo(RoomState.Maintenance)
.State(RoomState.Reserved)
.OnEntry(() => BlockCalendar())
.On(RoomEvent.CheckIn)
.When(() => IsCheckInTime())
.Execute(() => ActivateKeyCard())
.TransitionTo(RoomState.CheckedIn)
.On(RoomEvent.CancelReservation)
.Execute(() => ProcessRefund())
.TransitionTo(RoomState.Available)
.On(RoomEvent.NoShow)
.When(() => IsNoShowTime())
.Execute(() => ChargeNoShowFee())
.TransitionTo(RoomState.Available)
.State(RoomState.CheckedIn)
.OnEntry(() =>
{
_notifications.Send($"Welcome to room {RoomNumber}");
RecordCheckIn();
})
.On(RoomEvent.CheckOut)
.When(() => IsMinimumStayComplete())
.TransitionTo(RoomState.Occupied)
.State(RoomState.Occupied)
.On(RoomEvent.CheckOut)
.Execute(() =>
{
ProcessPayment();
DeactivateKeyCard();
})
.TransitionTo(RoomState.CheckedOut)
.On(RoomEvent.ReportIssue)
.Execute(() => CreateMaintenanceTicket())
.TransitionTo(RoomState.Maintenance)
.State(RoomState.CheckedOut)
.OnEntry(() =>
{
RecordCheckOut();
_notifications.Send("Thank you for staying!");
})
.On(RoomEvent.StartCleaning).TransitionTo(RoomState.Cleaning)
.State(RoomState.Cleaning)
.OnEntry(() => AssignHousekeeping())
.On(RoomEvent.FinishCleaning)
.Execute(() => InspectRoom())
.TransitionTo(RoomState.Available)
.On(RoomEvent.ReportIssue).TransitionTo(RoomState.Maintenance)
.State(RoomState.Maintenance)
.OnEntry(() =>
{
BlockCalendar();
NotifyMaintenance();
})
.On(RoomEvent.CompleteMaintenance)
.Execute(() => ClearMaintenanceTickets())
.TransitionTo(RoomState.Cleaning)
.Build();
}
private bool IsCheckInTime() =>
CurrentReservation != null &&
DateTime.Now >= CurrentReservation.CheckInTime.AddHours(-1);
private bool IsNoShowTime() =>
CurrentReservation != null &&
DateTime.Now > CurrentReservation.CheckInTime.AddHours(6);
private bool IsMinimumStayComplete() =>
CurrentReservation != null &&
DateTime.Now >= CurrentReservation.CheckInTime.AddHours(4);
}
Why This Pattern
- Lifecycle enforcement: Room must go through proper states
- Time-based guards: Check-in only at appropriate times
- Automatic transitions: No-show handling, cleaning queues
Key Takeaways
- Model real-world states: Use business terminology for state names
- Guard complex transitions: Use predicates for conditional logic
- Entry/Exit for side effects: Notifications, logging, cleanup
- Query permitted triggers: Drive UI based on available actions
- Test edge cases: Verify guards and unexpected events