
May 5, 2026
Probably not.
But sooner or later, you’ll debug one.
Orders that won’t transition. Jobs stuck in weird states. Workflows that “shouldn’t happen”… but do.
That’s when it clicks:
You’ve been using state machines all along just without calling them that.
The hidden complexity

At first, it looks simple:
order.status = :paid
But then reality kicks in:
- Can you go from :pending → :shipped directly?
- What if payment failed midway?
- Can a cancelled order be shipped?
- What happens under concurrency?
Suddenly, your “simple status field” becomes a set of rules.
That’s a state machine.
The usual mistakes
Most ad-hoc implementations fail in subtle ways:
1. Invalid transitions
order.status = :shipped # from :pending 😬
No constraints → inconsistent state.
2. Ignoring conditions (guards)
ship_order(order)
…but:
- payment not confirmed
- warehouse not ready
Now you have invalid reality modeled as valid state.
3. Hidden transitions

Business logic scattered across:
- controllers
- jobs
- services
No single source of truth.
4. Race conditions
Two processes:
- one cancels the order
- another ships it
👉 Welcome to non-deterministic chaos.
A better mental model
A state machine forces you to define:
- States → :pending, :paid, :shipped
- Events → :pay, :ship, :cancel
- Transitions → what is allowed
- Guards → when it’s allowed
Example:
transition :ship, from: :paid, to: :shipped, guard: ->(ctx) { ctx[:warehouse_confirmed] }
Now your system is:
- explicit
- constrained
- predictable
Why this matters (more than you think)

State machines show up everywhere:
- payment systems
- background job pipelines
- onboarding flows
- API integrations
- IoT devices (especially fun here)
You don’t always build one.
But you’re almost always living inside one.
The real takeaway
You don’t need to ship your own state machine library.
But you should understand:
- why invalid transitions happen
- how guards protect invariants
- where race conditions break workflows
Because once systems grow…
State is no longer a variable. It’s a contract.
A small challenge
Try this sometime:
Build a minimal state machine in Ruby.
Not for production.
Just to feel the constraints.
You’ll start noticing patterns you’ve been missing for years.
# =============================================================================# State Machine Implementation in Ruby# =============================================================================class StateMachine class InvalidTransitionError < StandardError; end class StateNotFoundError < StandardError; end attr_reader :current_state def initialize(initial_state) @current_state = initial_state @states = {} @transitions = {} @callbacks = { on_enter: {}, on_exit: {}, on_transition: [] } end # ── DSL ────────────────────────────────────────────────────────────────── def add_state(name, &block) @states[name] = block self end # transition :event_name, from: :state_a, to: :state_b # transition :event_name, from: [:state_a, :state_b], to: :state_c def add_transition(event, from:, to:, guard: nil) Array(from).each do |source| @transitions ||= {} @transitions[event] = { to: to, guard: guard } end self end def on_enter(state, &block) @callbacks[:on_enter][state] = block self end def on_exit(state, &block) @callbacks[:on_exit][state] = block self end def on_transition(&block) @callbacks[:on_transition] << block self end # ── Trigger ─────────────────────────────────────────────────────────────── def trigger(event, **context) transition = available_transition(event) raise InvalidTransitionError, "No transition '#{event}' from state '#{@current_state}'" unless transition if transition[:guard] && !transition[:guard].call(context) raise InvalidTransitionError, "Guard prevented transition '#{event}' from '#{@current_state}'" end previous = @current_state target = transition[:to] fire_callback(:on_exit, previous, previous, target, context) @current_state = target fire_callback(:on_enter, target, previous, target, context) @callbacks[:on_transition].each { |cb| cb.call(previous, target, event, context) } self end # ── Queries ─────────────────────────────────────────────────────────────── def can?(event) available_transition(event) != nil end def in_state?(state) @current_state == state end def available_events (@transitions[@current_state] || {}).keys end def states @states.keys end def to_s "#<StateMachine current=#{@current_state} available_events=#{available_events}>" end private def available_transition(event) (@transitions[@current_state] || {})[event] end def fire_callback(type, state, *args) cb = @callbacks[type][state] cb&.call(*args) endend# =============================================================================# Builder DSL — lets you define a machine declaratively# =============================================================================class StateMachineBuilder def self.build(initial_state, &block) machine = StateMachine.new(initial_state) builder = new(machine) builder.instance_eval(&block) machine end def initialize(machine) @machine = machine end def state(name, &block) = @machine.add_state(name, &block) def transition(event, **opts) = @machine.add_transition(event, **opts) def on_enter(state, &block) = @machine.on_enter(state, &block) def on_exit(state, &block) = @machine.on_exit(state, &block) def on_transition(&block) = @machine.on_transition(&block)end# =============================================================================# Example — Traffic Light# =============================================================================puts "=" * 60puts "Traffic Light State Machine"puts "=" * 60traffic_light = StateMachineBuilder.build(:red) do state :red state :green state :yellow transition :go, from: :red, to: :green transition :slow, from: :green, to: :yellow transition :stop, from: :yellow, to: :red on_enter(:green) { puts " → Light is GREEN — go!" } on_enter(:yellow) { puts " → Light is YELLOW — slow down!" } on_enter(:red) { puts " → Light is RED — stop!" } on_transition do |from, to, event, _ctx| puts " [transition] #{from} --#{event}--> #{to}" endendputs "\nInitial state: #{traffic_light.current_state}"puts "Available events: #{traffic_light.available_events}\n\n"traffic_light.trigger(:go)traffic_light.trigger(:slow)traffic_light.trigger(:stop)puts "\nFinal state: #{traffic_light.current_state}"# Invalid transitionbegin traffic_light.trigger(:slow) # can't slow from redrescue StateMachine::InvalidTransitionError => e puts "\nCaught error: #{e.message}"end# =============================================================================# Example — Order Workflow (with guards)# =============================================================================puts "\n" + "=" * 60puts "Order Workflow State Machine"puts "=" * 60order = StateMachineBuilder.build(:pending) do state :pending state :paid state :shipped state :delivered state :cancelled transition :pay, from: :pending, to: :paid transition :ship, from: :paid, to: :shipped, guard: ->(ctx) { ctx[:warehouse_confirmed] } transition :deliver, from: :shipped, to: :delivered transition :cancel, from: [:pending, :paid], to: :cancelled on_enter(:paid) { puts " → Payment received. Thank you!" } on_enter(:shipped) { puts " → Order is on its way!" } on_enter(:delivered) { puts " → Order delivered. Enjoy!" } on_enter(:cancelled) { puts " → Order cancelled." }endputs "\nInitial: #{order.current_state}"puts "Can pay? #{order.can?(:pay)}"puts "Can ship? #{order.can?(:ship)}"order.trigger(:pay)puts "\nTrying to ship without warehouse confirmation..."begin order.trigger(:ship, warehouse_confirmed: false)rescue StateMachine::InvalidTransitionError => e puts " Blocked: #{e.message}"endputs "\nShipping with warehouse confirmation..."order.trigger(:ship, warehouse_confirmed: true)order.trigger(:deliver)puts "\nFinal state: #{order.current_state}"puts "Available events: #{order.available_events}"
