• Shift Elevate
  • Posts
  • State Pattern: Manage Object Behaviour Through State Transitions

State Pattern: Manage Object Behaviour Through State Transitions

The Pain of Complex State-Dependent Behaviour

Picture this: You're building a document workflow system where documents can be in different states: Draft, Approved, or Published. Your initial approach seems logical: use a status field and conditional logic to handle state specific behaviour.

Then reality hits. Your code becomes a tangled web of if-else statements and switch cases scattered throughout your classes. Every time you add a new state or modify behaviour, you're hunting through multiple methods, breaking existing functionality, and violating the Open Closed Principle.

Suddenly, you're dealing with code like this:

public void processDocument() {
    if (status == DRAFT) {
        // Draft-specific logic
    } else if (status == APPROVED) {
        // Approved-specific logic
    } else if (status == PUBLISHED) {
        // Published-specific logic
    }
}

Looking familiar? The State pattern solves this challenge by encapsulating state specific behaviour in separate state objects, making your code more maintainable and extensible.

Understanding the State Pattern

The State pattern allows an object to alter its behaviour when its internal state changes. The object will appear to change its class by delegating state specific behaviour to separate state objects.

Think of it like a document workflow system: a document behaves differently depending on its current state (Draft, Approved, Published). Instead of cluttering the document class with conditional logic, each state encapsulates its own behaviour and transitions.

This pattern promotes Single Responsibility, Open Closed Principle, and Clean State Management while enabling complex state machines to be built and maintained easily.

State Pattern Components

Core Components

  • State Interface: Defines the interface for state specific behaviour and transitions

  • Context Class: Maintains current state and delegates operations to state objects

  • Concrete States: Implement behaviour for specific states and handle transitions

  • Client: Interacts with the context without knowing about state implementations

Complete Java Implementation

Let's build a document workflow system that demonstrates the State pattern's power in managing complex state dependent behaviour.

State Interface

public interface DocumentState {
    void edit(DocumentWorkflow document);
    void submitForApproval(DocumentWorkflow document);
    void releaseToPublic(DocumentWorkflow document);
    String getStateName();
}

Context Class

public class DocumentWorkflow {
    private DocumentState currentState;
    private String title;
    private String content;
    private String author;

    public DocumentWorkflow(String title, String author) {
        this.title = title;
        this.author = author;
        this.content = "";
        this.currentState = new DraftState();

        System.out.println("Document '" + title + "' created in DRAFT state");
    }

    public void edit() {
        currentState.edit(this);
    }

    public void submitForApproval() {
        currentState.submitForApproval(this);
    }

    public void releaseToPublic() {
        currentState.releaseToPublic(this);
    }

    public void setState(DocumentState state) {
        System.out.println("Document '" + title + "' transitioning from " +
            currentState.getStateName() + " to " + state.getStateName());
        this.currentState = state;
    }

    public void setContent(String content) {
        this.content = content;
    }

    public String getTitle() { return title; }
    public String getContent() { return content; }
    public String getAuthor() { return author; }
    public String getCurrentState() { return currentState.getStateName(); }
}

Concrete States

public class DraftState implements DocumentState {

    @Override
    public void edit(DocumentWorkflow document) {
        System.out.println("✏️ Editing document '" + document.getTitle() + "' in DRAFT state");
        document.setContent("Updated content in draft...");
    }

    @Override
    public void submitForApproval(DocumentWorkflow document) {
        System.out.println("✅ Submitting document '" + document.getTitle() + "' for approval");
        document.setState(new ApprovedState());
    }

    @Override
    public void releaseToPublic(DocumentWorkflow document) {
        System.out.println("❌ Cannot release document in DRAFT state. Must be approved first.");
    }

    @Override
    public String getStateName() {
        return "DRAFT";
    }
}

public class ApprovedState implements DocumentState {

    @Override
    public void edit(DocumentWorkflow document) {
        System.out.println("❌ Cannot edit approved document. Create new version instead.");
    }

    @Override
    public void submitForApproval(DocumentWorkflow document) {
        System.out.println("✅ Document '" + document.getTitle() + "' is already approved");
    }

    @Override
    public void releaseToPublic(DocumentWorkflow document) {
        System.out.println("🚀 Releasing document '" + document.getTitle() + "' to public");
        document.setState(new PublishedState());
    }

    @Override
    public String getStateName() {
        return "APPROVED";
    }
}

public class PublishedState implements DocumentState {

    @Override
    public void edit(DocumentWorkflow document) {
        System.out.println("❌ Cannot edit published document. Create new version instead.");
    }

    @Override
    public void submitForApproval(DocumentWorkflow document) {
        System.out.println("❌ Published document is already approved.");
    }

    @Override
    public void releaseToPublic(DocumentWorkflow document) {
        System.out.println("📖 Document '" + document.getTitle() + "' is already published");
    }

    @Override
    public String getStateName() {
        return "PUBLISHED";
    }
}

Client

public class DocumentWorkflowDemo {
    public static void main(String[] args) {
        System.out.println("=== Document Workflow State Pattern Demo ===\n");

        DocumentWorkflow document = new DocumentWorkflow("Design Patterns Guide", "John Doe");

        System.out.println("\n=== Phase 1: Draft Phase ===");
        document.edit();
        document.edit();

        System.out.println("\n=== Phase 2: Approval ===");
        document.submitForApproval();
        document.edit(); // Should fail

        System.out.println("\n=== Phase 3: Publishing ===");
        document.releaseToPublic();
        document.edit(); // Should fail

        System.out.println("\n=== Document Status ===");
        System.out.println("Current State: " + document.getCurrentState());

        System.out.println("\n=== Testing Invalid Transitions ===");
        DocumentWorkflow newDoc = new DocumentWorkflow("Another Doc", "Jane Smith");
        newDoc.releaseToPublic(); // Should fail - can't release draft directly
    }
}

Expected Output

=== Document Workflow State Pattern Demo ===

Document 'Design Patterns Guide' created in DRAFT state

=== Phase 1: Draft Phase ===
✏️ Editing document 'Design Patterns Guide' in DRAFT state
✏️ Editing document 'Design Patterns Guide' in DRAFT state

=== Phase 2: Approval ===
✅ Submitting document 'Design Patterns Guide' for approval
Document 'Design Patterns Guide' transitioning from DRAFT to APPROVED
❌ Cannot edit approved document. Create new version instead.

=== Phase 3: Publishing ===
🚀 Releasing document 'Design Patterns Guide' to public
Document 'Design Patterns Guide' transitioning from APPROVED to PUBLISHED
❌ Cannot edit published document. Create new version instead.

=== Document Status ===
Current State: PUBLISHED

=== Testing Invalid Transitions ===
Document 'Another Doc' created in DRAFT state
❌ Cannot release document in DRAFT state. Must be approved first.

🚀 Get the Complete Implementation

The full code with advanced state management and transition validation is available in our Design Patterns Repository.

# Clone and run the complete demo
git clone https://github.com/shift-elevate/design-patterns.git
cd design-patterns
mvn test -Dtest=StatePatternTest

Advanced State Management

The State pattern enables powerful workflow management systems with complex state transitions:

State Machine with Validation

public class StateTransitionValidator {
    private static final Map<String, Set<String>> VALID_TRANSITIONS = Map.of(
        "DRAFT", Set.of("APPROVED"),
        "APPROVED", Set.of("PUBLISHED"),
        "PUBLISHED", Set.of()
    );

    public static boolean isValidTransition(String fromState, String toState) {
        return VALID_TRANSITIONS.getOrDefault(fromState, Set.of()).contains(toState);
    }

    public static Set<String> getValidTransitions(String currentState) {
        return VALID_TRANSITIONS.getOrDefault(currentState, Set.of());
    }
}

State History and Rollback

public class DocumentWorkflowWithHistory extends DocumentWorkflow {
    private Stack<DocumentState> stateHistory = new Stack<>();
    
    @Override
    public void setState(DocumentState state) {
        stateHistory.push(getCurrentStateObject());
        super.setState(state);
    }
    
    public void rollbackToPreviousState() {
        if (!stateHistory.isEmpty()) {
            DocumentState previousState = stateHistory.pop();
            System.out.println("Rolling back to " + previousState.getStateName());
            super.setState(previousState);
        } else {
            System.out.println("No previous state to rollback to");
        }
    }
    
    public List<String> getStateHistory() {
        return stateHistory.stream()
            .map(DocumentState::getStateName)
            .collect(Collectors.toList());
    }
}

Real World Examples

The State pattern is extensively used in real world applications:

1. Media Player Applications

The State pattern can effectively manage playback states like Stopped, Playing, Paused, and Buffering. Each state can handle different operations: the play button could start playback only when stopped or paused, pause could work only when playing, and seeking through content could behave differently when buffering versus playing. This approach can make complex media controls maintainable and prevent invalid operations.

2. Vending Machine Controllers

The State pattern is well suited for managing transaction states like Idle, Product Selected, Payment Processing, and Dispensing. Each state can define valid actions: product selection could work only in Idle state, payment only after selection, and dispensing only after successful payment. This can help ensure the machine handles transactions correctly and prevents issues like dispensing without payment.

3. Thread Lifecycle Management

The State pattern can be applied to thread management with states like New, Runnable, Running, Blocked, and Terminated. Each state can determine what operations are valid. Threads could only start from New state, only running threads could be blocked, and terminated threads cannot be restarted. This pattern can be particularly useful in concurrent programming scenarios.

When to Use State Pattern

Understanding when to apply the State pattern is crucial for making the right architectural decisions. Here's when it shines and when alternatives might be better:

✅ Ideal Scenarios:

  • You have an object whose behaviour changes based on its internal state.

  • You have complex conditional statements that depend on the object's state.

  • You need to add new states without modifying existing code.

  • State transitions are well-defined and follow specific rules.

❌ Skip It When:

  • The state transitions are simple and unlikely to change.

  • You only have a few states with minimal behaviour differences.

  • Performance is critical and the overhead of state objects is too high.

  • The state logic is straightforward and doesn't justify the complexity.

Next Steps: Apply State Pattern in Your Project

Ready to implement the State pattern in your own projects? Here's a structured approach to get you started:

  1. Identify State-Dependent Behaviour: Look for objects whose behaviour changes based on internal state.

  2. Define State Interface: Create a common interface for all state specific operations.

  3. Implement Concrete States: Build state classes that encapsulate specific behaviour and transitions.

  4. Create Context Class: Build a class that maintains current state and delegates operations.

  5. Validate Transitions: Ensure only valid state transitions are allowed in your system.

The State pattern transforms complex conditional logic into clean, maintainable state machines. By encapsulating state specific behaviour in separate objects, you build systems that are easy to extend and modify without breaking existing functionality.

Found this helpful? Share it with a colleague who's struggling with complex state-dependent behaviour. Got questions? We'd love to hear from you at [email protected]