Development

What Is a State Machine, and When Should You Reach for One?

Learn what state machines are, how states and transitions work, and when explicit state modeling makes application logic simpler, safer, and easier to test.

What Is a State Machine, and When Should You Reach for One?

Imagine a checkout flow that starts simple.

A customer adds an item to the cart, enters shipping details, pays, receives a confirmation email, and waits for delivery. Then reality arrives. Payment can fail. Inventory can disappear. Fraud checks can hold the order. The customer can cancel. The warehouse can ship the package. A refund can be requested. Support can manually override something.

At first, this logic often appears as a handful of boolean flags:

isPaid
isShipped
isCancelled
isRefunded
isPendingReview

That works until the flags start contradicting each other. Can an order be both isShipped and isCancelled? Can it be isRefunded before it was paid? What should happen if payment succeeds after the order has expired?

This is where a state machine becomes useful. Instead of scattering the rules across conditionals, a state machine makes the possible states explicit and defines exactly how the system is allowed to move between them.

What Is a State Machine?

A state machine is a model of behavior built around a finite set of states and the transitions between them.

In plain terms, it answers three questions:

  • What state is the system in right now?
  • What events can happen from this state?
  • What state should the system move to next?
Current State
     |
     v
Event Occurs
     |
     v
Next State

The important part is that the current state matters. The same event can be valid in one state and invalid in another. Pressing “submit payment” makes sense while an order is awaiting payment. It should not do anything after the order has already shipped.

Martin Fowler describes the pattern as modeling a system through explicit states and transitions between them in his State Machine catalog entry. That short definition captures the heart of it: make the hidden behavior visible.

A Simple Example

Consider a basic traffic light.

It has three states:

  • red
  • green
  • yellow

And one event:

  • timer

The transitions are predictable:

red --timer--> green
green --timer--> yellow
yellow --timer--> red

The light cannot jump from red directly to yellow unless the machine says it can. It cannot be red and green at the same time. It always has one current state, and every allowed transition is defined.

That is a state machine in its simplest form.

States, Events, and Transitions

Most state machines are built from three basic pieces.

States describe the mode the system is currently in. Examples include idle, loading, success, error, draft, published, locked, and archived.

Events describe something that happened. Examples include SUBMIT, RESOLVE, REJECT, CANCEL, TIMEOUT, RETRY, and APPROVE.

Transitions define how the system moves from one state to another when a specific event occurs.

idle --SUBMIT--> loading
loading --RESOLVE--> success
loading --REJECT--> error
error --RETRY--> loading

Once those rules exist in one place, the behavior becomes easier to reason about. You can ask, “What can happen from loading?” and get a clear answer.

Why State Machines Exist

Software spends a surprising amount of time answering state questions.

Is this button enabled? Is this request still active? Can this user edit the document? Has the email been verified? Is the wizard complete? Should this retry happen? Is this job terminal, paused, queued, or running?

Without an explicit model, the answers often spread across components, API handlers, database fields, and background jobs. Eventually, the system contains the same state rules in many places, slightly differently each time.

A state machine pulls those rules into a single model. It does not remove complexity. It gives complexity a shape.

The Boolean Flag Problem

Boolean flags are fine when the state space is genuinely tiny. They become dangerous when they represent mutually exclusive conditions.

Consider a network request:

isLoading
isSuccess
isError

Those three flags allow impossible combinations:

isLoading = true
isSuccess = true
isError = true

That state should never exist, but the data structure allows it.

A state machine expresses the same idea more safely:

idle
loading
success
error

Only one state is active at a time. The impossible combinations disappear because they cannot be represented.

This is the same basic benefit developers get from choosing a structured model instead of loose pattern matching. When the structure matters, the system should understand the structure. That boundary comes up often in regex vs parsing.

A Login Form Example

A login form might start with four states:

editing
submitting
authenticated
failed

The events might be:

SUBMIT
SUCCESS
FAILURE
EDIT
LOG_OUT

The transitions might look like this:

editing --SUBMIT--> submitting
submitting --SUCCESS--> authenticated
submitting --FAILURE--> failed
failed --EDIT--> editing
authenticated --LOG_OUT--> editing

This model immediately answers practical UI questions.

Should the submit button be disabled? Yes, when the state is submitting.

Should the error message be visible? Yes, when the state is failed.

Should the form fields be editable? Yes in editing and failed, no in submitting.

Instead of deriving all of that from a pile of booleans, the UI can derive it from the current state.

State Machines Make Invalid Transitions Obvious

One of the biggest benefits of state machines is that they define what cannot happen.

In a checkout flow, you might allow:

awaiting_payment --PAYMENT_SUCCEEDED--> paid
paid --SHIP--> shipped
paid --CANCEL--> cancelled
shipped --DELIVER--> delivered

But you might not allow:

delivered --CANCEL--> cancelled
cancelled --SHIP--> shipped
awaiting_payment --REFUND--> refunded

Those restrictions are business rules. If they live only in scattered if statements, they are easy to miss. If they live in a state machine, they are part of the system’s explicit contract.

That makes state machines useful for workflows where the order of operations matters: approvals, payments, publishing, onboarding, document review, fulfilment, and support escalation.

State Machines vs Flowcharts

State machines and flowcharts look similar because both use boxes and arrows, but they focus on different things.

A flowchart usually describes a sequence of steps. It often reads like a procedure: do this, then this, then this.

A state machine describes how a system behaves over time. It focuses on current state, incoming events, and allowed transitions.

Flowchart:
  What steps do we perform?

State machine:
  What state are we in, and what events are valid now?

For simple linear processes, a flowchart may be enough. For reactive systems where events can arrive in different orders, a state machine is usually a better fit.

State Machines vs Statecharts

A finite state machine is the core idea: finite states, events, and transitions.

Statecharts extend that idea with features such as nested states, parallel states, and richer event behavior. David Harel’s 1987 paper, Statecharts: A Visual Formalism for Complex Systems, introduced statecharts as a way to model more complex reactive systems without exploding into unreadable diagrams.

For example, a media player might be playing, but inside that state it might also be buffering, normal, or casting. A checkout process might have payment and inventory checks happening in parallel. Statecharts give you ways to model those relationships without flattening everything into hundreds of separate state names.

Libraries like XState bring state machines and statecharts into JavaScript and TypeScript applications, letting teams model behavior in a way that can be both visualized and executed.

When Should You Reach for a State Machine?

Reach for a state machine when the behavior has clear modes and meaningful transitions.

Good candidates include:

  • Multi-step forms and wizards
  • Checkout and payment flows
  • Login, signup, and account recovery
  • Document approval workflows
  • Upload, processing, and retry flows
  • Background jobs and queues
  • Media players
  • Feature onboarding
  • Subscription lifecycle states
  • Incident and support ticket workflows

These systems usually have rules like “you can only do this after that” or “this action is invalid once the process is complete.” Those are state machine-shaped problems.

When a State Machine Is Overkill

Not every conditional deserves a state machine.

If a component has one toggle, a state machine may add ceremony without much benefit. A dropdown is open or closed. A checkbox is checked or unchecked. A simple modal is visible or hidden.

In those cases, ordinary state is often clearer:

isOpen = true

The moment the behavior grows beyond a simple toggle, the trade-off changes. If the modal can be closed, opening, open, saving, error, and closing, a state machine may suddenly be the simpler model.

The question is not “can I model this as a state machine?” You almost always can. The better question is “does the explicit model reduce confusion?”

Signs You Might Need One

State machines are especially useful when you notice these symptoms:

  • You have several boolean flags that can contradict each other
  • You keep adding guards like “unless already submitted”
  • Different parts of the app disagree about what should happen next
  • Bugs appear when events arrive in unexpected orders
  • A workflow has terminal states like cancelled, failed, or completed
  • You need to explain the lifecycle to developers, testers, or product people
  • Tests require long setup just to reach a specific scenario
  • The same transition rules are duplicated across frontend and backend

These are all signs that the behavior already has a state machine. It just has not been named yet.

UI State Machines

Frontend applications are full of state machine problems.

Search boxes move between idle, typing, loading, results, and error. Upload widgets move between empty, selected, uploading, processing, complete, and failed. A button might behave differently depending on whether a request is pending, already submitted, or blocked by validation.

Some UI problems are mostly about controlling event frequency. For those, debouncing vs throttling is the right mental model. But when the issue is “which actions are valid in this mode?”, a state machine is usually more precise.

The two ideas often work together. A search interface might debounce user input while using a state machine to represent idle, loading, success, and error.

Backend Workflow State Machines

State machines are just as useful on the backend.

An invoice might move through:

draft -> sent -> paid
draft -> voided
sent -> overdue
sent -> cancelled
paid -> refunded

A background job might move through:

queued -> running -> succeeded
queued -> running -> failed
failed -> retrying -> running
running -> cancelled

These transitions often matter for data integrity. If a job is already succeeded, should a retry event be accepted? If an invoice is voided, should payment be allowed? If a support ticket is closed, can a customer reply reopen it?

State machines make those lifecycle rules explicit, which also makes them easier to test.

State Machines and APIs

APIs frequently expose resources with lifecycle states.

For example:

{
  "id": "ord_123",
  "status": "awaiting_payment"
}

That status field is not just a label. It controls which actions are valid. A client may be allowed to submit payment while the order is awaiting_payment, but not after it is cancelled.

When API clients and servers depend on lifecycle fields, the state model becomes part of the contract. That is where contract testing vs integration testing becomes relevant: clients need confidence that the states, transitions, and response shapes they rely on will not drift unexpectedly.

For structured API payloads, the available fields may be described with schema tools. But a schema usually says what shape the data has, not which transitions are valid over time. That distinction is similar to the one in JSON Schema vs TypeScript types: structure and runtime behavior are related, but they are not the same thing.

State Machines and Distributed Systems

Distributed systems make state harder because events may arrive late, out of order, or more than once.

A payment provider might send a webhook after your own timeout has already marked the order as expired. A worker might retry a job that another worker already completed. A message queue might redeliver an event after a crash.

State machines help by making transitions idempotent and explicit:

paid --PAYMENT_SUCCEEDED--> paid
expired --PAYMENT_SUCCEEDED--> manual_review
shipped --CANCEL_REQUESTED--> return_requested

The state machine does not solve distributed systems by itself, but it gives you a place to define what late, duplicate, and conflicting events mean.

This is especially useful when combined with observability. If a workflow moves through several services, correlation IDs and trace IDs can help you follow the events that caused each transition.

State Machines and Orchestration

Some workflows are coordinated by a central component. Others emerge from services reacting to events.

If one service is responsible for deciding the next step, the design starts to resemble the orchestrator pattern. A state machine can be the orchestrator’s internal model: it knows the current state, receives events, and decides which command should happen next.

For example:

order_created -> reserve_inventory
inventory_reserved -> request_payment
payment_succeeded -> create_shipment
shipment_created -> complete_order

The orchestrator handles the sequence. The state machine defines which sequence is legal.

Guards and Actions

Real state machines often need more than “event means next state.”

A guard is a condition that must be true before a transition can happen.

cart_ready --CHECKOUT [cart has items]--> awaiting_payment
cart_ready --CHECKOUT [cart empty]--> cart_ready

An action is work performed during a transition.

awaiting_payment --PAYMENT_SUCCEEDED--> paid
  action: send confirmation email

Guards decide whether a transition is allowed. Actions perform side effects after a transition is accepted.

Keeping those concepts separate helps avoid messy logic. The machine can say what should happen, while the surrounding application performs the actual IO.

Testing State Machines

State machines are naturally testable because they define a set of states, events, and expected transitions.

Instead of writing only scenario tests like “user clicks these five buttons,” you can test the model directly:

Given state = awaiting_payment
When event = PAYMENT_SUCCEEDED
Then state = paid

You can also test invalid events:

Given state = delivered
When event = CANCEL
Then state = delivered
And no cancellation command is issued

This is valuable because many state bugs hide in unusual paths: retries, cancellations, timeouts, double-clicks, duplicate webhooks, and partial failures.

For UI and browser workflows, model clarity also pairs well with tools like Playwright, since end-to-end tests can focus on representative paths while lower-level tests cover the transition table.

A Small TypeScript Example

You do not need a library to understand the idea. A tiny reducer can act like a state machine:

type State = 'idle' | 'loading' | 'success' | 'error';
type Event = 'FETCH' | 'RESOLVE' | 'REJECT' | 'RETRY';

function transition(state: State, event: Event): State {
  switch (state) {
    case 'idle':
      return event === 'FETCH' ? 'loading' : state;
    case 'loading':
      if (event === 'RESOLVE') return 'success';
      if (event === 'REJECT') return 'error';
      return state;
    case 'error':
      return event === 'RETRY' ? 'loading' : state;
    case 'success':
      return state;
  }
}

This example is deliberately small, but it shows the pattern. The transition logic lives in one place. Invalid events do not accidentally create impossible combinations.

For richer machines, especially nested or parallel states, a dedicated library can be worth it. That is where tools such as XState become useful.

Common Mistakes

Modeling every tiny UI toggle as a machine. State machines are best when behavior has meaningful modes and transitions. Use ordinary state for simple local toggles.

Keeping the diagram separate from the code. A stale diagram can be worse than no diagram. The closer the model is to executable behavior, the more useful it becomes.

Mixing side effects into transition rules. The transition should decide what happens next. Network calls, emails, database writes, and timers should be handled carefully around the machine.

Ignoring invalid events. The value of a state machine comes partly from defining what cannot happen. Invalid events should be intentional, not accidental.

Letting the state names become vague. Names like processing can hide too much. If different behavior happens inside a state, it may need more specific states.

If you are thinking about state machines, try these topics:

For external references, start with Martin Fowler’s State Machine, the XState documentation, and Harel’s original statecharts paper.

Frequently Asked Questions

What is a state machine? A state machine is a model that describes a system as a set of states, events, and transitions. It defines what state the system is in, which events are valid from that state, and what state comes next.

What is a finite state machine? A finite state machine is a state machine with a finite number of possible states. Most everyday software state machines are finite because the lifecycle has a limited set of named modes such as idle, loading, success, and error.

When should I use a state machine? Use one when behavior depends heavily on the current state, when transitions have business rules, or when boolean flags are starting to create impossible combinations. Workflows, checkout flows, approvals, background jobs, and complex UI states are common examples.

Are state machines only for frontend applications? No. They are useful anywhere lifecycle behavior matters. Backend workflows, payment systems, queues, API resources, distributed jobs, embedded systems, and UI components can all benefit from explicit state modeling.

Do I need a state machine library? Not always. Small machines can be implemented with a reducer or transition function. Libraries become useful when you need visualization, nested states, parallel states, timers, actors, tooling, or a shared model across a larger application.

Conclusion

A state machine makes system behavior explicit. Instead of allowing state to emerge from scattered flags and conditionals, it names the possible states, defines the valid events, and controls how the system can move from one state to another.

That structure is most valuable when invalid transitions matter: payments, approvals, jobs, uploads, authentication, checkout, and other workflows where “what happens next” depends on “where we are now.”

You do not need a state machine for every toggle or tiny component. But when the rules are growing, the flags are multiplying, and edge cases keep appearing in strange order, a state machine can turn a messy set of conditionals into a model people can read, test, and trust.

Written by the Workshelve team, who write practical explainers on data integrity, networking, and developer tooling.