State Machine Diagrams in UML Explained

Updated 2026-03-11

TL;DR

State Machine Diagrams model how objects change state in response to events. They show all possible states, transitions between states, and what triggers those transitions. Essential for modeling workflows, UI behavior, and any system where objects have distinct modes of operation.

Prerequisites: Basic understanding of classes and objects, familiarity with UML notation basics, understanding of events and methods in OOP

After this topic: Design state machine diagrams for real-world objects, identify states and transitions in existing systems, implement state patterns in code from diagrams, and explain state behavior during technical interviews

Core Concept

What is a State Machine Diagram?

A state machine diagram (also called a state diagram or statechart) shows how an object transitions through different states during its lifetime. A state represents a condition or situation during which an object satisfies some condition, performs some activity, or waits for some event.

Think of a traffic light: it’s always in one of three states (Red, Yellow, Green), and it transitions between these states in a predictable pattern. The traffic light doesn’t exist in multiple states simultaneously — it’s always in exactly one state at any given time.

Core Components

States are represented by rounded rectangles with the state name inside. An object remains in a state until an event triggers a transition to another state.

Transitions are arrows connecting states, showing how an object moves from one state to another. Each transition is labeled with the event that triggers it, optionally followed by a guard condition in brackets and an action after a slash.

Initial state is shown as a filled black circle with an arrow pointing to the first state. Final state is shown as a bullseye (circle within a circle).

Events are occurrences that trigger transitions. They can be method calls, time passing, or external signals.

Why State Machines Matter

State machines prevent invalid state transitions. Without explicit state modeling, objects can end up in inconsistent states (like a door that’s both open and locked). State machines make valid transitions explicit and prevent impossible ones.

They also clarify complex behavior. When an object’s behavior depends on its history or current condition, state machines make this logic visible and testable. This is crucial for UI components, network protocols, game characters, and business workflows.

When to Use State Machine Diagrams

Use state machines when:

  • An object has distinct modes of behavior
  • The object’s response to events depends on its current state
  • You need to prevent invalid state transitions
  • The system has complex conditional logic based on history

Don’t use them for simple objects with no behavioral changes or for modeling data structure relationships (use class diagrams instead).

Visual Guide

Basic State Machine: Door

stateDiagram-v2
    [*] --> Closed
    Closed --> Open: open()
    Open --> Closed: close()
    Closed --> Locked: lock()
    Locked --> Closed: unlock()
    Open --> [*]

A door has three states. Note that you cannot lock an open door — there’s no transition from Open to Locked. This constraint is enforced by the state machine structure.

State Machine with Guards: ATM Withdrawal

stateDiagram-v2
    [*] --> Idle
    Idle --> CardInserted: insertCard()
    CardInserted --> Authenticated: enterPIN() [valid PIN]
    CardInserted --> Idle: enterPIN() [invalid PIN] / ejectCard()
    Authenticated --> Processing: requestWithdrawal()
    Processing --> Dispensing: [balance sufficient]
    Processing --> Authenticated: [insufficient funds] / showError()
    Dispensing --> Idle: dispenseCash() / ejectCard()
    CardInserted --> Idle: cancel() / ejectCard()
    Authenticated --> Idle: cancel() / ejectCard()

Guards (conditions in brackets) determine which transition to take. Actions after the slash (/) execute during the transition. This ATM only dispenses cash if the balance is sufficient.

Composite States: Media Player

stateDiagram-v2
    [*] --> Stopped
    Stopped --> Playing: play()
    
    state Playing {
        [*] --> NormalSpeed
        NormalSpeed --> FastForward: ffwd()
        FastForward --> NormalSpeed: play()
        NormalSpeed --> Rewind: rewind()
        Rewind --> NormalSpeed: play()
    }
    
    Playing --> Paused: pause()
    Paused --> Playing: play()
    Playing --> Stopped: stop()
    Paused --> Stopped: stop()

Composite states contain substates. When in Playing state, the player is always in one of its substates (NormalSpeed, FastForward, or Rewind). This models hierarchical behavior.

State Machine with Self-Transitions: Order Processing

stateDiagram-v2
    [*] --> New
    New --> Confirmed: confirm()
    Confirmed --> Confirmed: updateQuantity()
    Confirmed --> Processing: startProcessing()
    Processing --> Shipped: ship()
    Shipped --> Delivered: deliver()
    Delivered --> [*]
    Confirmed --> Cancelled: cancel()
    Processing --> Cancelled: cancel()

Self-transitions (loops back to same state) handle events that don’t change state. Updating quantity keeps the order Confirmed but may trigger internal actions like recalculating total.

Examples

Example 1: Implementing a Connection State Machine

from enum import Enum

class ConnectionState(Enum):
    DISCONNECTED = 1
    CONNECTING = 2
    CONNECTED = 3
    DISCONNECTING = 4

class Connection:
    def __init__(self):
        self.state = ConnectionState.DISCONNECTED
        self.retry_count = 0
    
    def connect(self):
        if self.state == ConnectionState.DISCONNECTED:
            print("Initiating connection...")
            self.state = ConnectionState.CONNECTING
            self._attempt_connection()
        else:
            print(f"Cannot connect from {self.state.name} state")
    
    def _attempt_connection(self):
        # Simulate connection logic
        if self.retry_count < 3:
            print("Connection established")
            self.state = ConnectionState.CONNECTED
        else:
            print("Connection failed")
            self.state = ConnectionState.DISCONNECTED
    
    def disconnect(self):
        if self.state == ConnectionState.CONNECTED:
            print("Disconnecting...")
            self.state = ConnectionState.DISCONNECTING
            self._close_connection()
        elif self.state == ConnectionState.DISCONNECTED:
            print("Already disconnected")
        else:
            print(f"Cannot disconnect from {self.state.name} state")
    
    def _close_connection(self):
        print("Connection closed")
        self.state = ConnectionState.DISCONNECTED
    
    def send_data(self, data):
        if self.state == ConnectionState.CONNECTED:
            print(f"Sending: {data}")
            return True
        else:
            print(f"Cannot send data in {self.state.name} state")
            return False

# Usage
conn = Connection()
print(f"Initial state: {conn.state.name}")  # Output: DISCONNECTED

conn.send_data("Hello")  # Output: Cannot send data in DISCONNECTED state

conn.connect()  
# Output: Initiating connection...
#         Connection established

print(f"Current state: {conn.state.name}")  # Output: CONNECTED

conn.send_data("Hello")  # Output: Sending: Hello

conn.connect()  # Output: Cannot connect from CONNECTED state

conn.disconnect()
# Output: Disconnecting...
#         Connection closed

print(f"Final state: {conn.state.name}")  # Output: DISCONNECTED

Key Points:

  • Each method checks the current state before executing
  • Invalid transitions are rejected with error messages
  • State changes are explicit and controlled
  • The enum makes states type-safe and prevents typos

Java equivalent: Use an enum for states and switch statements in methods. C++ can use enum class for type safety.

Try it yourself: Add a RECONNECTING state that’s entered when connection fails but retry_count < 3. Add an auto_reconnect() method.


Example 2: Document Workflow with Guards

from enum import Enum
from datetime import datetime

class DocumentState(Enum):
    DRAFT = 1
    PENDING_REVIEW = 2
    APPROVED = 3
    REJECTED = 4
    PUBLISHED = 5

class Document:
    def __init__(self, title, author):
        self.title = title
        self.author = author
        self.state = DocumentState.DRAFT
        self.reviewer = None
        self.content = ""
        self.review_comments = []
    
    def submit_for_review(self, reviewer):
        # Guard: can only submit from DRAFT or REJECTED
        if self.state not in [DocumentState.DRAFT, DocumentState.REJECTED]:
            print(f"Cannot submit from {self.state.name} state")
            return False
        
        # Guard: must have content
        if not self.content:
            print("Cannot submit empty document")
            return False
        
        self.reviewer = reviewer
        self.state = DocumentState.PENDING_REVIEW
        print(f"Document submitted to {reviewer} for review")
        return True
    
    def approve(self, reviewer):
        # Guard: must be in PENDING_REVIEW
        if self.state != DocumentState.PENDING_REVIEW:
            print(f"Cannot approve from {self.state.name} state")
            return False
        
        # Guard: only assigned reviewer can approve
        if reviewer != self.reviewer:
            print(f"Only {self.reviewer} can review this document")
            return False
        
        self.state = DocumentState.APPROVED
        print(f"Document approved by {reviewer}")
        return True
    
    def reject(self, reviewer, comments):
        if self.state != DocumentState.PENDING_REVIEW:
            print(f"Cannot reject from {self.state.name} state")
            return False
        
        if reviewer != self.reviewer:
            print(f"Only {self.reviewer} can review this document")
            return False
        
        self.state = DocumentState.REJECTED
        self.review_comments.append({
            'reviewer': reviewer,
            'comments': comments,
            'timestamp': datetime.now()
        })
        print(f"Document rejected with comments: {comments}")
        return True
    
    def publish(self):
        # Guard: must be approved
        if self.state != DocumentState.APPROVED:
            print(f"Cannot publish from {self.state.name} state")
            return False
        
        self.state = DocumentState.PUBLISHED
        print(f"Document '{self.title}' published")
        return True
    
    def edit_content(self, new_content):
        # Guard: can only edit in DRAFT or REJECTED states
        if self.state not in [DocumentState.DRAFT, DocumentState.REJECTED]:
            print(f"Cannot edit in {self.state.name} state")
            return False
        
        self.content = new_content
        print("Content updated")
        return True

# Usage
doc = Document("API Design Guide", "Alice")
print(f"State: {doc.state.name}")  # Output: DRAFT

doc.submit_for_review("Bob")  # Output: Cannot submit empty document

doc.edit_content("# API Design\n\nBest practices...")  # Output: Content updated

doc.submit_for_review("Bob")  # Output: Document submitted to Bob for review
print(f"State: {doc.state.name}")  # Output: PENDING_REVIEW

doc.edit_content("More content")  # Output: Cannot edit in PENDING_REVIEW state

doc.approve("Charlie")  # Output: Only Bob can review this document

doc.reject("Bob", "Needs more examples")  
# Output: Document rejected with comments: Needs more examples

print(f"State: {doc.state.name}")  # Output: REJECTED

doc.edit_content("# API Design\n\nBest practices...\n\nExamples:")  
# Output: Content updated

doc.submit_for_review("Bob")  # Output: Document submitted to Bob for review

doc.approve("Bob")  # Output: Document approved by Bob

doc.publish()  # Output: Document 'API Design Guide' published

print(f"Final state: {doc.state.name}")  # Output: PUBLISHED

Key Points:

  • Guards (conditional checks) prevent invalid transitions
  • Multiple guards can apply to a single transition
  • Actions (like storing comments) execute during transitions
  • State-dependent behavior is explicit in each method

Try it yourself: Add a ARCHIVED state that can only be reached from PUBLISHED. Add a revision_number that increments each time the document is rejected and resubmitted.


Example 3: State Pattern Implementation

from abc import ABC, abstractmethod

# State interface
class VendingMachineState(ABC):
    @abstractmethod
    def insert_coin(self, machine):
        pass
    
    @abstractmethod
    def select_item(self, machine, item):
        pass
    
    @abstractmethod
    def dispense(self, machine):
        pass

# Concrete states
class NoCoinState(VendingMachineState):
    def insert_coin(self, machine):
        print("Coin inserted")
        machine.state = machine.has_coin_state
    
    def select_item(self, machine, item):
        print("Insert coin first")
    
    def dispense(self, machine):
        print("Insert coin first")

class HasCoinState(VendingMachineState):
    def insert_coin(self, machine):
        print("Coin already inserted")
    
    def select_item(self, machine, item):
        if item in machine.inventory and machine.inventory[item] > 0:
            print(f"Selected: {item}")
            machine.selected_item = item
            machine.state = machine.dispensing_state
        else:
            print(f"{item} not available")
            print("Returning coin")
            machine.state = machine.no_coin_state
    
    def dispense(self, machine):
        print("Select item first")

class DispensingState(VendingMachineState):
    def insert_coin(self, machine):
        print("Please wait, dispensing item")
    
    def select_item(self, machine, item):
        print("Please wait, dispensing item")
    
    def dispense(self, machine):
        item = machine.selected_item
        print(f"Dispensing {item}")
        machine.inventory[item] -= 1
        machine.selected_item = None
        machine.state = machine.no_coin_state

# Context class
class VendingMachine:
    def __init__(self):
        # Initialize states
        self.no_coin_state = NoCoinState()
        self.has_coin_state = HasCoinState()
        self.dispensing_state = DispensingState()
        
        # Set initial state
        self.state = self.no_coin_state
        
        # Machine data
        self.inventory = {'Coke': 5, 'Pepsi': 3, 'Water': 10}
        self.selected_item = None
    
    def insert_coin(self):
        self.state.insert_coin(self)
    
    def select_item(self, item):
        self.state.select_item(self, item)
    
    def dispense(self):
        self.state.dispense(self)

# Usage
machine = VendingMachine()

machine.select_item('Coke')  # Output: Insert coin first

machine.insert_coin()  # Output: Coin inserted

machine.insert_coin()  # Output: Coin already inserted

machine.select_item('Sprite')  
# Output: Sprite not available
#         Returning coin

machine.insert_coin()  # Output: Coin inserted

machine.select_item('Coke')  # Output: Selected: Coke

machine.dispense()  
# Output: Dispensing Coke

print(f"Coke remaining: {machine.inventory['Coke']}")  # Output: 4

Key Points:

  • State Pattern encapsulates state-specific behavior in separate classes
  • The context (VendingMachine) delegates to the current state object
  • Adding new states doesn’t require modifying existing state classes
  • Each state class handles all events, making behavior explicit

Design Pattern Connection: This is the classic State Pattern from the Gang of Four. It’s the OOP implementation of a state machine.

Try it yourself: Add a MaintenanceState that can be entered from any state and prevents all operations except returning to NoCoinState.

Common Mistakes

1. Missing State Validation

Mistake: Allowing operations without checking current state.

# Bad: No state checking
class Door:
    def __init__(self):
        self.is_open = False
        self.is_locked = False
    
    def lock(self):
        self.is_locked = True  # Can lock an open door!

Why it’s wrong: This allows impossible states (locked and open simultaneously). State machines exist to prevent this.

Fix: Always validate state before transitions.

# Good: Explicit state checking
class Door:
    def __init__(self):
        self.state = 'closed'
    
    def lock(self):
        if self.state == 'closed':
            self.state = 'locked'
        else:
            raise ValueError(f"Cannot lock door in {self.state} state")

2. Using Boolean Flags Instead of States

Mistake: Representing states with multiple boolean flags.

# Bad: Boolean soup
class Order:
    def __init__(self):
        self.is_confirmed = False
        self.is_processing = False
        self.is_shipped = False
        self.is_cancelled = False

Why it’s wrong: With 4 booleans, you have 16 possible combinations, but only 5 are valid states. This creates ambiguity and bugs.

Fix: Use an enum or state objects.

# Good: Explicit states
from enum import Enum

class OrderState(Enum):
    NEW = 1
    CONFIRMED = 2
    PROCESSING = 3
    SHIPPED = 4
    CANCELLED = 5

class Order:
    def __init__(self):
        self.state = OrderState.NEW

3. Forgetting Guard Conditions

Mistake: Not documenting or implementing conditions that control transitions.

# Bad: No guards
def withdraw(self, amount):
    self.state = 'dispensing'
    self.balance -= amount  # What if balance is insufficient?

Why it’s wrong: Guard conditions are essential business logic. Without them, invalid transitions occur.

Fix: Explicitly check guards before transitioning.

# Good: Guards enforced
def withdraw(self, amount):
    if self.state != 'authenticated':
        raise ValueError("Must be authenticated")
    if self.balance < amount:
        return False  # Guard failed
    self.state = 'dispensing'
    self.balance -= amount
    return True

4. Mixing State Logic with Business Logic

Mistake: Putting complex business logic inside state transition methods.

# Bad: Business logic mixed with state logic
def approve_order(self):
    self.state = 'approved'
    # Calculate discounts
    # Update inventory
    # Send emails
    # Generate invoice
    # ... 50 more lines

Why it’s wrong: State transitions should be simple. Complex logic makes states hard to understand and test.

Fix: Separate state transitions from business logic.

# Good: Separation of concerns
def approve_order(self):
    if self.state != 'pending':
        return False
    self.state = 'approved'
    self._on_approval()  # Separate method for business logic
    return True

def _on_approval(self):
    self.apply_discounts()
    self.update_inventory()
    self.send_notifications()

5. Not Handling All Events in All States

Mistake: Forgetting to define behavior for events that can occur in any state.

# Bad: Missing event handlers
class MediaPlayer:
    def play(self):
        if self.state == 'stopped':
            self.state = 'playing'
        # What if state is 'paused'? 'error'? Undefined behavior!

Why it’s wrong: Undefined behavior leads to crashes or inconsistent states.

Fix: Handle every event in every state, even if the action is “do nothing” or “show error.”

# Good: Complete event handling
def play(self):
    if self.state == 'stopped':
        self.state = 'playing'
    elif self.state == 'paused':
        self.state = 'playing'
    elif self.state == 'playing':
        pass  # Already playing, do nothing
    else:
        raise ValueError(f"Cannot play from {self.state} state")

Interview Tips

1. Start with States, Then Add Transitions

When asked to design a state machine, don’t jump into transitions immediately. First, identify all possible states by asking: “What are the distinct modes this object can be in?” Write them down. Then ask: “How does the object move between these states?” This systematic approach shows structured thinking.

Example response: “For an elevator, I’d first identify states: Idle, MovingUp, MovingDown, DoorsOpen, EmergencyStop. Then I’d map transitions: Idle → MovingUp when floor requested above, MovingUp → DoorsOpen when destination reached.”


2. Always Mention Guard Conditions

Interviewers look for candidates who think about edge cases. When describing transitions, explicitly mention guards: “This transition only happens if [condition].” This shows you understand that not all transitions are unconditional.

Example: “The order moves from Confirmed to Processing only if payment is verified AND inventory is available. If either guard fails, we transition to a different state.”


3. Explain Why State Machines Over If-Else Chains

Be ready to justify using a state machine. The key points:

  • Prevents invalid states: Impossible to be in two states simultaneously
  • Makes transitions explicit: Clear what events trigger what changes
  • Easier to test: Each state can be tested independently
  • Easier to extend: Adding a new state doesn’t require modifying all existing logic

Interview question you might hear: “Why not just use a bunch of if statements?” Answer: “If statements can create invalid combinations. With 5 boolean flags, you have 32 possible combinations but maybe only 8 valid states. State machines make valid states explicit.”


4. Connect to Design Patterns

Mention the State Pattern when discussing implementation. Show you know the difference between the diagram (design tool) and the pattern (implementation approach). “I’d implement this using the State Pattern, where each state is a class implementing a common interface. This makes the code match the diagram structure.”


5. Draw the Diagram

If given a whiteboard, draw the state machine. Visual representation:

  • Shows you can communicate complex ideas clearly
  • Makes it easier to spot missing transitions
  • Helps the interviewer follow your thinking

Use clear notation: circles for states, arrows for transitions, labels for events. Don’t forget initial and final states.


6. Discuss Real-World Examples

Have 2-3 real-world state machine examples ready:

  • TCP connection: CLOSED → LISTEN → SYN_RECEIVED → ESTABLISHED → CLOSE_WAIT → CLOSED
  • Order processing: NEW → CONFIRMED → PROCESSING → SHIPPED → DELIVERED
  • Authentication: LOGGED_OUT → AUTHENTICATING → LOGGED_IN → SESSION_EXPIRED

Being able to reference these shows practical experience.


7. Address Concurrency Concerns

For senior positions, mention thread safety: “In a multi-threaded environment, I’d need to synchronize state transitions to prevent race conditions. Two threads shouldn’t be able to transition the object simultaneously.”


8. Know When NOT to Use State Machines

Show judgment by knowing limitations:

  • Too few states: If an object only has 2 states, a boolean might suffice
  • Too many states: If you have 50+ states, consider hierarchical state machines or a different approach
  • No behavioral changes: If state doesn’t affect behavior, you don’t need a state machine

Example: “For a simple on/off switch, a state machine is overkill. But for a microwave with states like Idle, Cooking, Paused, DoorOpen, and different behavior in each, it’s appropriate.”

Key Takeaways

  • State machines model object behavior through explicit states and transitions, preventing invalid state combinations and making complex logic manageable
  • Every state machine has states, transitions, events, guards, and actions — understand each component and how they work together to control object behavior
  • Use enums or the State Pattern for implementation — enums for simple cases, State Pattern for complex behavior where each state has significantly different logic
  • Guard conditions control when transitions occur — always validate preconditions before state changes to maintain system integrity
  • State machines are design tools and implementation patterns — draw them to communicate design, implement them to enforce valid behavior at runtime