- 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=StatePatternTestAdvanced 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:
Identify State-Dependent Behaviour: Look for objects whose behaviour changes based on internal state.
Define State Interface: Create a common interface for all state specific operations.
Implement Concrete States: Build state classes that encapsulate specific behaviour and transitions.
Create Context Class: Build a class that maintains current state and delegates operations.
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]