keepcalculator

06 Numbers and Powers

State Machines: Preventing Impossible Changes

Card No. 06·16912 · Updated · Standard formula, shown below

The calculator on this card is switched off while it is checked. The formula is below, and the worked example shows how to do the sum by hand.

State machines let you define exactly which transitions are allowed between states, so impossible changes are rejected by design rather than by error-checking code. You model your system as a set of states, a set of events, and a transition table that says what happens when an event occurs in a given state. This guide explains how to apply that idea in practice, with concrete examples and common pitfalls.

Formula

The calculator above is a toy that illustrates the core idea: a transition is allowed only if a precondition (here, time in state > 0) holds. In a real state machine, the precondition is a boolean expression over the current state and the event. The general formula is:

Allowed(event, state) = precondition(state, event)

If the precondition is true, the transition fires; if false, it is rejected. The variables are:

  • state – the current state of the system (e.g., “draftâ€�, “publishedâ€�, “archivedâ€�).
  • event – an action or input that may cause a change (e.g., “submitâ€�, “approveâ€�, “deleteâ€�).
  • precondition – a boolean function that returns true only when the transition is legal.

In practice, you implement this as a lookup table or a set of guarded rules, not as a mathematical function, but the principle is identical.

Worked Example

Using the default values from the calculator: time in state = 0 hours, events attempted = 0. The precondition is t > 0, which is false, so allowed = 0 * (0 > 0) = 0, and blocked = 0 * (0 <= 0) = 0. If you set time to 5 hours and events to 10, allowed = 10 * (5 > 0) = 10, blocked = 10 * (5 <= 0) = 0. The calculator simply counts how many attempts would pass the guard. In a real system, each attempt would be a specific event like “publish� or “cancel�.

Background: Why State Machines Exist

State machines have been used in computer science since the 1950s, originally in automata theory and later in compilers, network protocols, and user interfaces. The key insight is that many bugs come from allowing operations that should be impossible in a given context. For example, a document that is already published should not be published again. A state machine makes that rule explicit and enforced by the structure of the code, not by scattered if-statements.

In software engineering, the term “state machine� usually refers to a finite-state machine (FSM) or a statechart (UML state machine). The latter adds hierarchy, concurrency, and history, but the basic idea is the same: you enumerate all valid states and all valid transitions, and anything else is automatically invalid.

How It Works: Core Components

To implement a state machine, you need three things:

  • States – distinct conditions the system can be in. For an order, states might be “pendingâ€�, “paidâ€�, “shippedâ€�, “deliveredâ€�, “cancelledâ€�.
  • Events – the triggers that cause a transition. For an order: “payâ€�, “shipâ€�, “deliverâ€�, “cancelâ€�.
  • Transition table – a matrix that maps (current state, event) to a new state, or to “invalidâ€�.

In code, you can represent this as a dictionary where the key is a tuple (state, event) and the value is the next state. If the key is missing, the transition is rejected. For example (in Python-like pseudocode):

transitions = {
    ('pending', 'pay'): 'paid',
    ('paid', 'ship'): 'shipped',
    ('shipped', 'deliver'): 'delivered',
    ('pending', 'cancel'): 'cancelled',
    ('paid', 'cancel'): 'refund_pending',
}
def transition(current, event):
    next_state = transitions.get((current, event))
    if next_state is None:
        raise InvalidTransition(f"Cannot {event} from {current}")
    return next_state

This is the simplest form. For real-world systems, you often add guards (additional conditions besides the state), actions (side effects that run when a transition fires), and entry/exit hooks.

How to Choose the Right State Machine Approach

There are several implementation strategies. The choice depends on your language, team, and complexity.

  • Enum + switch – Use an enum for states and a switch statement for events. Good for small, simple machines with a handful of states.
  • Transition table – As shown above, a lookup table. Easy to read and modify, but can become unwieldy with many states.
  • State pattern – Use classes for each state, with methods for events. More object-oriented, allows behavior per state, but more boilerplate.
  • Statechart libraries – Libraries like XState (JavaScript), Spring StateMachine (Java), or Boost.Statechart (C++) provide hierarchical states, guards, actions, and visual tools. Best for complex systems with nested states.

For most business applications, a transition table with a guard function is sufficient and keeps the logic centralized. If you need to model concurrent states or history, consider a statechart library.

Common Problems and Misconceptions

One common mistake is thinking that a state machine is only for UI wizards or game characters. In reality, it applies to any system where the validity of an operation depends on the current condition – order processing, user accounts, workflow approvals, device state, and even network connections.

Another misconception is that state machines are rigid and hard to change. In practice, the transition table is usually a single data structure that is easy to update. The difficulty comes when you have hidden dependencies – for example, a transition that should also update a database record. You need to ensure that the side effect happens only when the transition is valid, and that it is part of the transition action, not scattered in the caller.

A frequent problem is forgetting to handle the “invalid� case. If you use a switch and don’t have a default, an unexpected event will silently do nothing or crash. Always explicitly raise an error or return a failure result when a transition is not allowed.

Another pitfall is using strings for states and events. Typos become runtime errors. Use enums or constants to get compile-time checking.

Comparison: State Machines vs. Other Approaches

You might ask why not just use boolean flags or if-statements. For a couple of states, flags are fine. But as the number of states grows, flags become combinatorial. If you have three boolean flags, you have 2^3 = 8 possible combinations, but many are invalid. A state machine enumerates only the valid ones, so you cannot accidentally reach an invalid combination.

Another alternative is a rules engine or a decision table. Those are more flexible for complex business rules, but they are heavier and harder to debug. State machines are a middle ground: they are structured enough to prevent impossible changes, but simple enough to reason about.

Compared to a simple guard clause (e.g., if (status == 'paid') then ship()), a state machine centralizes all guards in one place, making it easier to audit and to see all possible transitions at a glance.

Costs and Trade-offs

The main cost of a state machine is upfront design. You must enumerate all states and transitions before coding, which takes time and requires domain knowledge. If you miss a transition, you will get an error at runtime, which is good – it forces you to think about edge cases.

There is also a learning curve for teams not familiar with the pattern. The transition table can become large, but it is usually still more readable than a pile of if-else statements.

Performance is rarely an issue; a lookup table is O(1). The overhead comes from the framework if you use a library, but that is negligible for most applications.

Safety and Correctness

State machines improve safety by making illegal states unrepresentable. This is a form of “making impossible states impossible�, a phrase popularized by functional programming. It reduces the chance of bugs where an operation is performed on an object in the wrong state, such as trying to ship an unpaid order.

For safety-critical systems (like medical devices or avionics), state machines are often required because they are formally verifiable. Even in ordinary web apps, they prevent data corruption and inconsistent records.

One important safety note: a state machine only prevents invalid transitions that you have defined. It does not prevent invalid data inside a state. For example, if a “shipped� state requires a tracking number, you must still validate that the tracking number is present when you enter the state. The state machine ensures the transition, not the data integrity.

Alternatives to Full State Machines

If a full state machine feels overkill, consider these lighter alternatives:

  • Enum with allowed transitions method – Add a method to the enum that returns whether a given event is allowed. This is a mini state machine without a separate table.
  • Guard clauses in services – Check the current state at the beginning of each service method. This is simple but duplicates logic.
  • Event sourcing with a state rebuild – Store events and derive the current state by replaying them. This is a different paradigm, but it also ensures that the state is always consistent with the history.

Choose the simplest thing that works for your context. If you have only two or three states, a guard clause is fine. If you have many states and complex rules, invest in a proper state machine.

Frequently Asked Questions

What is an “impossible change� in this context?

An impossible change is a transition that should never happen because it violates business rules or logic. For example, changing an order from “delivered� back to “pending� is usually impossible. A state machine prevents that by not having a transition for it.

Do I need a library to use state machines?

No. For simple cases, a dictionary or a switch statement is enough. Libraries add features like hierarchy, history, and visualizations, but they also add dependency. Start with a simple implementation and only add a library when you need those features.

How do I handle side effects like sending an email when a transition fires?

You attach the side effect to the transition definition. When the transition is executed, the state changes and the action runs. This ensures the side effect only happens when the transition is valid. Avoid putting side effects in the caller that checks the state, because that duplicates logic.

What if I need to allow a transition only under certain conditions, like a user role?

Add a guard function that takes the current state, event, and context (like the user) and returns a boolean. The transition is allowed only if the guard returns true. This is a common pattern in statechart libraries.

Can state machines be used in front-end applications?

Yes, they are very useful for UI state, like form steps or modal dialogs. Libraries like XState are designed for that. They help manage complex UI flows and prevent invalid interactions.

How do I debug a state machine when something goes wrong?

Log every transition attempt: the current state, the event, the guard result, and the next state. If a transition is rejected, you can see why. Many libraries also offer time-travel debugging or visualizations.

Related

Formula used
in t | Time in state (hours) | h | 0 | 0 | 8760
in e | Events attempted | - | 0 | 0 | 1000000
out allowed | Allowed transitions | - | e * (t > 0) | 0
out blocked | Blocked transitions | - | e * (t

Each line is one input (in, sel, list) or one result (out: label, unit, formula, decimals). How the calculators work

Method: the standard formula for this calculation, rounded as each result states. See how the calculators work or report an error.

More cards in this drawer

The whole Numbers and Powers drawer →