State Machine Modeling is a technique for structuring complex workflows and system behaviors around discrete, well-defined states and the transitions between them. It matters when a system must track progress through a sequence of stages—such as a purchase order lifecycle, a user onboarding flow, or a message processing pipeline—and must react precisely to events based on its current state. It excels in environments with asynchronous processing, distributed systems, and long-running processes where state must survive restarts, failures, and coordination across services.
Defining the Core Structure
A state machine is defined by a set of states, events that trigger transitions, and actions associated with transitions or states. The core unit is a *transition*, which specifies:
- The current state
- The event that triggers the move
- The next state
- Optional actions (side effects)
- Optionally, conditions (guards) that must evaluate to true for the transition to fire
Transitions can be *immediate*, *delayed*, or *queued*, and states can be *entry*, *exit*, or *composite*.
Representing States and Transitions
States are typically named strings, often in snake_case or PascalCase. Transitions are described as a list of configuration entries, commonly in a JSON object or a YAML file.
{
"states": {
"idle": {
"entry": "log('Starting idle state')",
"exit": "save_checkpoint()"
},
"processing": {
"entry": "start_worker_pool(8)",
"exit": "stop_worker_pool()"
},
"completed": {
"entry": "send_notification('Job finished')",
"exit": "archive_job()"
}
},
"transitions": [
{
"from": "idle",
"event": "start_job",
"to": "processing",
"actions": [
"assign_job_to_queue(job_id)",
"set_job_status(job_id, 'in_progress')"
],
"guards": [
"can_process(job_id)",
"not_in_maintenance()"
]
},
{
"from": "processing",
"event": "job_completed",
"to": "completed",
"actions": [
"mark_job_as_final(job_id)"
],
"guards": [
"validate_output(job_id)"
]
},
{
"from": "processing",
"event": "job_failed",
"to": "idle",
"actions": [
"retry_job(job_id, max_retries=3)",
"log_error(job_id, 'Processing failed')"
]
}
]
}
This structure is used across systems: a workflow engine reads the definition at startup, a service registers events via an API, and a state store persists current state and transition history.
Managing State Persistence
State machines require durable storage to track which state a process is in across restarts and failures. The minimal required schema is a table with:
id: unique identifier (e.g.,job_id)state: current state (string)data: JSON blob of context (e.g.,{ "attempts": 2, "input": "...", "progress": 0.75 })version: (optional) for conflict resolutionupdated_at: timestamp of last state change
CREATE TABLE job_state (
id TEXT PRIMARY KEY,
state TEXT NOT NULL,
data JSONB NOT NULL,
version BIGINT DEFAULT 1,
updated_at TIMESTAMPTZ DEFAULT NOW()
);
The system reads the current state, applies the transition logic, and writes the updated state back. The most common failure mode is state overwriting due to concurrent updates.
When two processes read the same job, both update state to processing, but only one writes back. The other’s update is silently lost. To avoid this, use optimistic concurrency control:
UPDATE job_state
SET state = 'processing',
data = jsonb_set(data, '{progress}', '0.8'),
version = version + 1
WHERE id = 'j-12345'
AND version = 3; -- expected version
If the UPDATE returns 0 rows, the transaction failed due to a version mismatch. The service must retry the update with the new version.
Triggering Transitions via Events
Events are the primary mechanism for driving state changes. An event is a named input with optional payload. Events may be:
- Synchronous: a direct API call
- Asynchronous: message via a queue (e.g., Kafka, RabbitMQ, SQS)
- Scheduled: time-based triggers
- External: from user actions, system signals, or external APIs
Event Processing Loop
A typical event processor follows this flow:
- Poll for new events (e.g., from a message queue)
- For each event:
- Read current state from store
- Find all transitions matching the event and current state
- Filter transitions with guards that evaluate to
true - Choose the first matching transition (or execute all in sequence)
- Apply actions (execute functions, update state, emit new events)
- Write updated state with incremented version
- Acknowledge the event
The loop is often implemented as a service using curl, docker, and systemctl:
docker run -d \
--name state-machine \
--env DATABASE_URL=postgresql://db:5432/workflow \
--env EVENT_QUEUE=amqp://rabbit:5672/workflow \
--volume /etc/workflow/config.json:/app/config.json \
workflow-engine:latest
Configuration is in config.json:
{
"event_polling_interval": "1s",
"max_concurrent_jobs": 100,
"retry_policy": {
"max_retries": 5,
"backoff": "exponential",
"base_delay": "1s"
}
}
Common Failures in Event Handling
- Event duplication: the same event is processed twice. The system must idempotently apply transitions. A common pattern is to record
processed_event_idsin a set, and skip processing if the event ID was already seen. - Event order loss: due to network delays or multiple producers, events arrive out of order. A state machine may process
job_completedbeforejob_started. The solution is to attach asequence_idortimestampto each event, and use it to enforce ordering. - Missing transitions: a new event is added but the state machine does not define a transition for it. The system logs a warning:
WARN: No transition defined for event 'job_canceled' in state 'processing'
This indicates a configuration drift between the event producer and the state engine.
Handling Complex State: Composite and Nested States
When a state machine grows, states can contain sub-machines. This is especially useful when a process has a hierarchy of phases.
For example, a processing state might contain a sub-machine for data_validation with its own states: validating, valid, invalid, retrying.
This is modeled using nested states:
{
"states": {
"processing": {
"entry": "start_worker_pool(8)",
"exit": "stop_worker_pool()",
"children": {
"validation": {
"initial": "validating",
"states": {
"validating": {
"entry": "start_validation_timer()"
},
"valid": {
"entry": "mark_as_valid()"
},
"invalid": {
"entry": "send_failure_report()"
},
"retrying": {
"entry": "schedule_retry()"
}
},
"transitions": [
{
"from": "validating",
"event": "validation_succeeded",
"to": "valid"
},
{
"from": "validating",
"event": "validation_failed",
"to": "invalid"
},
{
"from": "invalid",
"event": "retry",
"to": "retrying"
}
]
}
}
}
}
}
When the state machine enters processing, it automatically enters the validation sub-machine and sets its initial state to validating. Transitions in the parent machine can trigger sub-machine transitions, and the sub-machine can emit events that propagate upward.
Key Edge Cases
- Entry and exit actions in sub-machines: when a composite state enters, its entry actions run, and when it exits, its exit actions run. But if the sub-machine is already active, entering the composite state may trigger entry actions even though the sub-machine was already in progress.
- Implicit transitions: a transition from
processingtocompletedmay implicitly exit thevalidationsub-machine, but only if the sub-machine is notparallel. The sub-machine may continue running in the background, decoupled. - State merging: when a composite state has multiple active sub-states, the full state is a combination:
processing.validation.valid. The system must track this composite state in thestatefield.
Error Handling and Recovery
State machines must survive failures: a node crashes during a transition, a database becomes unavailable, or a job times out.
Failure Modes
- Transition failure: an action fails during a transition. The system logs the error and may re-queue the event.
- State store failure: when writing the updated state, the database is unreachable. The system retries the update with exponential backoff.
- Event loss: an event is processed but the state update fails. The event is considered processed, but the state is inconsistent. The system may use a compensating transaction or a recovery job that scans for uncommitted events.
Recovery Strategies
- Retryable transitions: define a
retryableflag on transitions. When a transition fails, the system logs the failure and schedules a retry after a delay. - Checkpointing: after each successful transition, the system saves a *checkpoint*—the full state and all context. This allows rolling back to a known point.
- Event sourcing: instead of storing only the current state, the system stores all transitions as events. The state machine can replay all events to reconstruct any past state.
A common recovery command:
# Rebuild state from event log
psql -c "
SELECT
job_id,
state,
data,
updated_at
FROM job_state
WHERE id = 'j-12345'
ORDER BY updated_at;
"
# Replay all events from a Kafka topic
kafka-console-consumer.sh --bootstrap-server kafka:9092 \
--topic job_events \
--partition 0 \
--offset 1000 \
| while read event; do
job_id=$(echo "$event" | jq -r '.job_id')
event_type=$(echo "$event" | jq -r '.type')
payload=$(echo "$event" | jq -c '.payload')
# Apply event to state
psql -c "
UPDATE job_state
SET state = $1,
data = jsonb_set(data, '{events}', jsonb_agg(events), true),
updated_at = now()
WHERE id = $2
RETURNING state, data;
" $event_type $job_id
done
Summary: What to Watch For
- State versioning is non-negotiable: forget versioning, and concurrent updates silently overwrite each other.
- Transitions are not atomic: a failure in the middle of a transition leaves the system in an inconsistent state.
- Events are the only source of truth: if an event is not processed, the state machine is out of sync.
- Nested states add complexity: entry and exit actions must be coordinated across layers.
- Idempotency is critical: event processors must handle duplicates and partial failures gracefully.
- Error logs must include the full state context: when a job fails in
processing.validation.invalid, the logs should showjob_id,data,state, andtransition_history.
A well-modeled state machine is not just a flowchart—it is a resilient, self-documenting system that records its own evolution.
This page was rewritten on 11 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.
Join the Newsletter
Get the latest updates and deep insights shipped directly to your inbox.