The State Pattern is a behavioral design pattern that allows an object to alter its behavior when its internal state changes. The object will appear to change its class, providing clean state-specific behavior without complex conditional logic.
What is the State Pattern?
The State Pattern encapsulates state-specific behavior in separate state objects and delegates behavior to the current state. This makes state transitions explicit and behavior changes clean and organized.
Key Benefits:
- Eliminates complex conditionals - No large switch statements or if-else chains
- Clean state transitions - Each state knows what states it can transition to
- Open/Closed Principle - Easy to add new states without modifying existing code
- Single Responsibility - Each state class handles one state's behavior
- Explicit state modeling - States become first-class objects
Structure
Context ──────> State Interface ↑ ↑ | | Current State Concrete States - StateA - StateB - StateC
Implementation Examples
Example 1: Vending Machine State Management
import java.util.*;
// State interface
interface VendingMachineState {
void insertMoney(double amount);
void selectProduct(String productCode);
void dispenseProduct();
void cancel();
void refill();
}
// Concrete States
class ReadyState implements VendingMachineState {
private final VendingMachine machine;
public ReadyState(VendingMachine machine) {
this.machine = machine;
}
@Override
public void insertMoney(double amount) {
machine.addMoney(amount);
System.out.printf("Inserted $%.2f. Total: $%.2f%n", amount, machine.getCurrentMoney());
machine.setState(new HasMoneyState(machine));
}
@Override
public void selectProduct(String productCode) {
System.out.println("Please insert money first");
}
@Override
public void dispenseProduct() {
System.out.println("Please insert money and select a product first");
}
@Override
public void cancel() {
System.out.println("No transaction to cancel");
}
@Override
public void refill() {
System.out.println("Machine refilled");
}
@Override
public String toString() {
return "READY";
}
}
class HasMoneyState implements VendingMachineState {
private final VendingMachine machine;
public HasMoneyState(VendingMachine machine) {
this.machine = machine;
}
@Override
public void insertMoney(double amount) {
machine.addMoney(amount);
System.out.printf("Inserted $%.2f. Total: $%.2f%n", amount, machine.getCurrentMoney());
}
@Override
public void selectProduct(String productCode) {
Product product = machine.getProduct(productCode);
if (product == null) {
System.out.println("Invalid product code: " + productCode);
return;
}
if (product.getStock() <= 0) {
System.out.println("Product out of stock: " + product.getName());
return;
}
if (machine.getCurrentMoney() < product.getPrice()) {
System.out.printf("Insufficient funds. Need $%.2f, have $%.2f%n",
product.getPrice(), machine.getCurrentMoney());
return;
}
machine.setSelectedProduct(product);
machine.setState(new ProductSelectedState(machine));
System.out.println("Selected: " + product.getName());
}
@Override
public void dispenseProduct() {
System.out.println("Please select a product first");
}
@Override
public void cancel() {
double refund = machine.getCurrentMoney();
machine.setCurrentMoney(0);
machine.setState(new ReadyState(machine));
System.out.printf("Transaction cancelled. Refunded: $%.2f%n", refund);
}
@Override
public void refill() {
System.out.println("Cannot refill while transaction in progress");
}
@Override
public String toString() {
return "HAS_MONEY";
}
}
class ProductSelectedState implements VendingMachineState {
private final VendingMachine machine;
public ProductSelectedState(VendingMachine machine) {
this.machine = machine;
}
@Override
public void insertMoney(double amount) {
System.out.println("Product already selected. Please complete or cancel current transaction");
}
@Override
public void selectProduct(String productCode) {
System.out.println("Product already selected. Please complete or cancel current transaction");
}
@Override
public void dispenseProduct() {
Product product = machine.getSelectedProduct();
double change = machine.getCurrentMoney() - product.getPrice();
product.reduceStock(1);
machine.setCurrentMoney(0);
machine.setSelectedProduct(null);
System.out.printf("Dispensing: %s%n", product.getName());
if (change > 0) {
System.out.printf("Returning change: $%.2f%n", change);
}
machine.setState(new ReadyState(machine));
System.out.println("Thank you for your purchase!");
}
@Override
public void cancel() {
Product product = machine.getSelectedProduct();
double refund = machine.getCurrentMoney();
machine.setCurrentMoney(0);
machine.setSelectedProduct(null);
machine.setState(new ReadyState(machine));
System.out.printf("Transaction cancelled. Refunded: $%.2f%n", refund);
}
@Override
public void refill() {
System.out.println("Cannot refill while transaction in progress");
}
@Override
public String toString() {
return "PRODUCT_SELECTED";
}
}
class OutOfServiceState implements VendingMachineState {
private final VendingMachine machine;
public OutOfServiceState(VendingMachine machine) {
this.machine = machine;
}
@Override
public void insertMoney(double amount) {
System.out.println("Machine is out of service. Please try again later");
}
@Override
public void selectProduct(String productCode) {
System.out.println("Machine is out of service. Please try again later");
}
@Override
public void dispenseProduct() {
System.out.println("Machine is out of service. Please try again later");
}
@Override
public void cancel() {
System.out.println("Machine is out of service");
}
@Override
public void refill() {
machine.setState(new ReadyState(machine));
System.out.println("Machine refilled and back in service");
}
@Override
public String toString() {
return "OUT_OF_SERVICE";
}
}
// Context class
class VendingMachine {
private VendingMachineState state;
private double currentMoney;
private Product selectedProduct;
private final Map<String, Product> products;
public VendingMachine() {
this.products = new HashMap<>();
this.state = new ReadyState(this);
initializeProducts();
}
private void initializeProducts() {
products.put("A1", new Product("A1", "Coke", 1.50, 10));
products.put("A2", new Product("A2", "Pepsi", 1.50, 8));
products.put("B1", new Product("B1", "Chips", 1.00, 15));
products.put("B2", new Product("B2", "Chocolate", 1.25, 12));
}
// Delegate methods to current state
public void insertMoney(double amount) {
state.insertMoney(amount);
}
public void selectProduct(String productCode) {
state.selectProduct(productCode);
}
public void dispenseProduct() {
state.dispenseProduct();
}
public void cancel() {
state.cancel();
}
public void refill() {
state.refill();
}
public void setOutOfService() {
this.state = new OutOfServiceState(this);
System.out.println("Machine set to out of service");
}
// Getters and setters
public void setState(VendingMachineState state) {
System.out.printf("State changed: %s → %s%n", this.state, state);
this.state = state;
}
public VendingMachineState getState() { return state; }
public double getCurrentMoney() { return currentMoney; }
public void setCurrentMoney(double money) { this.currentMoney = money; }
public void addMoney(double amount) { this.currentMoney += amount; }
public Product getSelectedProduct() { return selectedProduct; }
public void setSelectedProduct(Product product) { this.selectedProduct = product; }
public Product getProduct(String code) { return products.get(code); }
public Collection<Product> getProducts() { return products.values(); }
}
// Product class
class Product {
private final String code;
private final String name;
private final double price;
private int stock;
public Product(String code, String name, double price, int stock) {
this.code = code;
this.name = name;
this.price = price;
this.stock = stock;
}
public void reduceStock(int quantity) {
this.stock -= quantity;
}
// Getters
public String getCode() { return code; }
public String getName() { return name; }
public double getPrice() { return price; }
public int getStock() { return stock; }
}
// Demo
public class VendingMachineDemo {
public static void main(String[] args) {
VendingMachine machine = new VendingMachine();
System.out.println("=== Vending Machine Demo ===\n");
// Display available products
System.out.println("Available Products:");
machine.getProducts().forEach(p ->
System.out.printf(" %s: %s - $%.2f (Stock: %d)%n",
p.getCode(), p.getName(), p.getPrice(), p.getStock()));
System.out.println();
// Test normal workflow
System.out.println("1. Normal purchase workflow:");
machine.insertMoney(2.00);
machine.selectProduct("A1");
machine.dispenseProduct();
System.out.println("\n2. Insufficient funds:");
machine.insertMoney(1.00);
machine.selectProduct("B1"); // Needs $1.00, has $1.00
machine.selectProduct("A1"); // Needs $1.50, has $1.00
machine.cancel();
System.out.println("\n3. Multiple money insertion:");
machine.insertMoney(1.00);
machine.insertMoney(0.50);
machine.selectProduct("A1");
machine.dispenseProduct();
System.out.println("\n4. Out of service test:");
machine.setOutOfService();
machine.insertMoney(1.00);
machine.refill(); // Back in service
System.out.println("\n5. Invalid operations:");
machine.selectProduct("A1"); // No money inserted
machine.dispenseProduct(); // No product selected
}
}
Output:
=== Vending Machine Demo === Available Products: A1: Coke - $1.50 (Stock: 10) A2: Pepsi - $1.50 (Stock: 8) B1: Chips - $1.00 (Stock: 15) B2: Chocolate - $1.25 (Stock: 12) 1. Normal purchase workflow: State changed: READY → HAS_MONEY Inserted $2.00. Total: $2.00 State changed: HAS_MONEY → PRODUCT_SELECTED Selected: Coke Dispensing: Coke Returning change: $0.50 State changed: PRODUCT_SELECTED → READY Thank you for your purchase! 2. Insufficient funds: State changed: READY → HAS_MONEY Inserted $1.00. Total: $1.00 Selected: Chips Insufficient funds. Need $1.50, have $1.00 State changed: HAS_MONEY → READY Transaction cancelled. Refunded: $1.00 3. Multiple money insertion: State changed: READY → HAS_MONEY Inserted $1.00. Total: $1.00 Inserted $0.50. Total: $1.50 State changed: HAS_MONEY → PRODUCT_SELECTED Selected: Coke Dispensing: Coke State changed: PRODUCT_SELECTED → READY Thank you for your purchase! 4. Out of service test: Machine set to out of service Machine is out of service. Please try again later State changed: OUT_OF_SERVICE → READY Machine refilled and back in service 5. Invalid operations: Please insert money first Please insert money and select a product first
Example 2: Document Workflow Management
import java.util.*;
// State interface
interface DocumentState {
void edit(String content);
void review(boolean approved);
void publish();
void archive();
String getStatus();
}
// Concrete States
class DraftState implements DocumentState {
private final Document document;
public DraftState(Document document) {
this.document = document;
}
@Override
public void edit(String content) {
document.setContent(content);
System.out.println("Document edited in draft state");
}
@Override
public void review(boolean approved) {
if (approved) {
document.setState(new ReviewedState(document));
System.out.println("Document approved, moved to reviewed state");
} else {
System.out.println("Document rejected, remains in draft");
}
}
@Override
public void publish() {
System.out.println("Cannot publish document in draft state. Please review first.");
}
@Override
public void archive() {
document.setState(new ArchivedState(document));
System.out.println("Document archived from draft");
}
@Override
public String getStatus() {
return "DRAFT";
}
}
class ReviewedState implements DocumentState {
private final Document document;
public ReviewedState(Document document) {
this.document = document;
}
@Override
public void edit(String content) {
System.out.println("Cannot edit document in reviewed state. Move back to draft first.");
}
@Override
public void review(boolean approved) {
if (approved) {
System.out.println("Document already reviewed");
} else {
document.setState(new DraftState(document));
System.out.println("Document sent back to draft for revisions");
}
}
@Override
public void publish() {
document.setState(new PublishedState(document));
System.out.println("Document published successfully");
}
@Override
public void archive() {
document.setState(new ArchivedState(document));
System.out.println("Document archived from reviewed state");
}
@Override
public String getStatus() {
return "REVIEWED";
}
}
class PublishedState implements DocumentState {
private final Document document;
public PublishedState(Document document) {
this.document = document;
}
@Override
public void edit(String content) {
System.out.println("Cannot edit published document. Create new version.");
}
@Override
public void review(boolean approved) {
System.out.println("Document already published. Review not applicable.");
}
@Override
public void publish() {
System.out.println("Document already published");
}
@Override
public void archive() {
document.setState(new ArchivedState(document));
System.out.println("Published document archived");
}
@Override
public String getStatus() {
return "PUBLISHED";
}
}
class ArchivedState implements DocumentState {
private final Document document;
public ArchivedState(Document document) {
this.document = document;
}
@Override
public void edit(String content) {
document.setState(new DraftState(document));
document.setContent(content);
System.out.println("Document unarchived and edited");
}
@Override
public void review(boolean approved) {
System.out.println("Cannot review archived document");
}
@Override
public void publish() {
System.out.println("Cannot publish archived document");
}
@Override
public void archive() {
System.out.println("Document already archived");
}
@Override
public String getStatus() {
return "ARCHIVED";
}
}
// Context class
class Document {
private DocumentState state;
private String content;
private final String title;
private final String author;
private final Date createdDate;
private Date modifiedDate;
public Document(String title, String author, String content) {
this.title = title;
this.author = author;
this.content = content;
this.createdDate = new Date();
this.modifiedDate = new Date();
this.state = new DraftState(this);
}
// Delegate methods to current state
public void edit(String content) {
state.edit(content);
this.modifiedDate = new Date();
}
public void review(boolean approved) {
state.review(approved);
this.modifiedDate = new Date();
}
public void publish() {
state.publish();
this.modifiedDate = new Date();
}
public void archive() {
state.archive();
this.modifiedDate = new Date();
}
public void restore() {
if (state instanceof ArchivedState) {
state.edit(this.content); // This will move to draft state
}
}
// Getters and setters
public void setState(DocumentState state) {
System.out.printf("Document state changed: %s → %s%n", this.state.getStatus(), state.getStatus());
this.state = state;
}
public DocumentState getState() { return state; }
public String getContent() { return content; }
public void setContent(String content) { this.content = content; }
public String getTitle() { return title; }
public String getAuthor() { return author; }
public Date getCreatedDate() { return createdDate; }
public Date getModifiedDate() { return modifiedDate; }
public void printStatus() {
System.out.printf("Document: %s | Status: %s | Author: %s%n",
title, state.getStatus(), author);
System.out.printf("Created: %s | Modified: %s%n", createdDate, modifiedDate);
System.out.println("---");
}
}
// Demo
public class DocumentWorkflowDemo {
public static void main(String[] args) throws InterruptedException {
System.out.println("=== Document Workflow Demo ===\n");
Document document = new Document("Project Proposal", "John Doe", "Initial proposal content");
document.printStatus();
// Normal workflow
System.out.println("1. Editing in draft:");
document.edit("Updated proposal content with more details");
System.out.println("\n2. First review (rejected):");
document.review(false);
document.printStatus();
System.out.println("3. Editing after rejection:");
document.edit("Revised content based on feedback");
System.out.println("\n4. Second review (approved):");
document.review(true);
document.printStatus();
System.out.println("5. Publishing:");
document.publish();
document.printStatus();
System.out.println("6. Archiving:");
document.archive();
document.printStatus();
System.out.println("7. Restoring from archive:");
document.restore();
document.printStatus();
// Test invalid operations
System.out.println("8. Testing invalid operations:");
document.publish(); // Should fail - in draft state
document.review(true);
document.publish();
document.edit("Trying to edit published doc"); // Should fail
}
}
Example 3: TCP Connection State Management
import java.util.*;
// State interface
interface TCPState {
void open();
void close();
void acknowledge();
void send(String data);
}
// Concrete States
class TCPClosedState implements TCPState {
private final TCPConnection connection;
public TCPClosedState(TCPConnection connection) {
this.connection = connection;
}
@Override
public void open() {
connection.setState(new TCPListenState(connection));
System.out.println("Connection opened, now in LISTEN state");
}
@Override
public void close() {
System.out.println("Connection already closed");
}
@Override
public void acknowledge() {
System.out.println("Cannot acknowledge - connection closed");
}
@Override
public void send(String data) {
System.out.println("Cannot send data - connection closed");
}
@Override
public String toString() {
return "CLOSED";
}
}
class TCPListenState implements TCPState {
private final TCPConnection connection;
public TCPListenState(TCPConnection connection) {
this.connection = connection;
}
@Override
public void open() {
System.out.println("Connection already open");
}
@Override
public void close() {
connection.setState(new TCPClosedState(connection));
System.out.println("Connection closed from LISTEN state");
}
@Override
public void acknowledge() {
connection.setState(new TCPEstablishedState(connection));
System.out.println("Connection acknowledged, now ESTABLISHED");
}
@Override
public void send(String data) {
System.out.println("Cannot send data - connection not established");
}
@Override
public String toString() {
return "LISTEN";
}
}
class TCPEstablishedState implements TCPState {
private final TCPConnection connection;
public TCPEstablishedState(TCPConnection connection) {
this.connection = connection;
}
@Override
public void open() {
System.out.println("Connection already established");
}
@Override
public void close() {
connection.setState(new TCPClosedState(connection));
System.out.println("Connection closed from ESTABLISHED state");
}
@Override
public void acknowledge() {
System.out.println("Connection already acknowledged");
}
@Override
public void send(String data) {
System.out.println("Sending data: " + data);
connection.addToSentData(data);
}
@Override
public String toString() {
return "ESTABLISHED";
}
}
// Context class
class TCPConnection {
private TCPState state;
private final List<String> sentData;
private final String remoteHost;
private final int remotePort;
public TCPConnection(String remoteHost, int remotePort) {
this.remoteHost = remoteHost;
this.remotePort = remotePort;
this.sentData = new ArrayList<>();
this.state = new TCPClosedState(this);
}
// Delegate methods to current state
public void open() {
System.out.printf("Attempting to open connection to %s:%d%n", remoteHost, remotePort);
state.open();
}
public void close() {
state.close();
}
public void acknowledge() {
state.acknowledge();
}
public void send(String data) {
state.send(data);
}
// Getters and setters
public void setState(TCPState state) {
System.out.printf("TCP State changed: %s → %s%n", this.state, state);
this.state = state;
}
public TCPState getState() { return state; }
public void addToSentData(String data) { sentData.add(data); }
public List<String> getSentData() { return new ArrayList<>(sentData); }
public String getRemoteHost() { return remoteHost; }
public int getRemotePort() { return remotePort; }
public void printStatus() {
System.out.printf("Connection: %s:%d | State: %s | Data Sent: %d packets%n",
remoteHost, remotePort, state, sentData.size());
}
}
// Demo
public class TCPConnectionDemo {
public static void main(String[] args) {
System.out.println("=== TCP Connection State Demo ===\n");
TCPConnection connection = new TCPConnection("example.com", 8080);
connection.printStatus();
System.out.println("\n1. Normal connection workflow:");
connection.open();
connection.acknowledge();
connection.send("Hello Server!");
connection.send("How are you?");
connection.close();
connection.printStatus();
System.out.println("\n2. Invalid operations:");
connection.send("This should fail"); // Closed connection
connection.acknowledge(); // Closed connection
System.out.println("\n3. Reopening connection:");
connection.open();
connection.send("Trying to send before established"); // Should fail
connection.acknowledge();
connection.send("Now this should work");
connection.printStatus();
System.out.println("\nSent data: " + connection.getSentData());
}
}
Advanced State Pattern
Example 4: State Pattern with State Transitions Map
import java.util.*;
// Enhanced state with transition rules
interface EnhancedState {
void handle();
boolean canTransitionTo(String nextState);
Set<String> getPossibleTransitions();
}
class ConcreteStateA implements EnhancedState {
private final StateContext context;
private final Set<String> possibleTransitions = Set.of("STATE_B", "STATE_C");
public ConcreteStateA(StateContext context) {
this.context = context;
}
@Override
public void handle() {
System.out.println("Handling in State A");
// Auto-transition based on business rules
if (someCondition()) {
context.transitionTo("STATE_B");
}
}
@Override
public boolean canTransitionTo(String nextState) {
return possibleTransitions.contains(nextState);
}
@Override
public Set<String> getPossibleTransitions() {
return possibleTransitions;
}
private boolean someCondition() {
return Math.random() > 0.5;
}
@Override
public String toString() {
return "STATE_A";
}
}
// State context with transition management
class StateContext {
private EnhancedState currentState;
private final Map<String, EnhancedState> states = new HashMap<>();
private final List<String> transitionHistory = new ArrayList<>();
public StateContext() {
// Initialize states
states.put("STATE_A", new ConcreteStateA(this));
// Add other states...
currentState = states.get("STATE_A");
transitionHistory.add(currentState.toString());
}
public void transitionTo(String stateName) {
EnhancedState nextState = states.get(stateName);
if (nextState != null && currentState.canTransitionTo(stateName)) {
System.out.printf("Transition: %s → %s%n", currentState, nextState);
currentState = nextState;
transitionHistory.add(stateName);
} else {
System.out.printf("Invalid transition: %s → %s%n", currentState, stateName);
}
}
public void handle() {
currentState.handle();
}
public void printTransitionHistory() {
System.out.println("Transition History: " + transitionHistory);
}
}
Best Practices
- Use Enums for State Identification
public enum DocumentStateType {
DRAFT, REVIEWED, PUBLISHED, ARCHIVED
}
- Make States Stateless When Possible
// Reusable state instance
class PublishedState implements DocumentState {
private static final PublishedState INSTANCE = new PublishedState();
public static PublishedState getInstance() {
return INSTANCE;
}
}
- Use State Pattern with Strategy Pattern
// State defines what you can do, Strategy defines how you do it
interface ProcessingStrategy {
void process(String data);
}
class StateWithStrategy {
private DocumentState state;
private ProcessingStrategy strategy;
// ...
}
- Implement State Transition Validation
public boolean isValidTransition(DocumentState current, DocumentState next) {
Map<DocumentState, Set<DocumentState>> validTransitions = Map.of(
DRAFT, Set.of(REVIEWED, ARCHIVED),
REVIEWED, Set.of(DRAFT, PUBLISHED, ARCHIVED)
// ...
);
return validTransitions.get(current).contains(next);
}
When to Use the State Pattern
Use State Pattern when:
- An object's behavior depends on its state
- You have complex conditional state checks
- States have well-defined transitions
- You need to add new states frequently
- You want to avoid large conditional statements
Common Use Cases:
- Workflow systems - Document approval processes
- Game development - Character states (idle, walking, attacking)
- UI components - Button states (enabled, disabled, loading)
- Network protocols - Connection state management
- Order processing - Order status workflows
Benefits and Trade-offs
Benefits:
- ✅ Clean code - No complex conditionals
- ✅ Easy extensibility - Add new states without modifying existing
- ✅ Single Responsibility - Each state handles its own behavior
- ✅ Explicit state transitions - Clear state flow
Trade-offs:
- ❌ Increased number of classes - More classes to manage
- ❌ Overkill for simple state machines - Use enums for simple cases
- ❌ Runtime state transitions - May be less efficient than conditionals
Conclusion
The State Pattern is a powerful tool for managing object behavior that changes with state:
Key Benefits:
- Eliminates Complex Conditionals - Replaces switch/case and if/else chains
- Clean State Management - Each state is encapsulated in its own class
- Easy Maintenance - States can be modified independently
- Explicit Transitions - State flow is clear and maintainable
Implementation Approach:
- Identify States - List all possible states
- Define State Interface - Common operations for all states
- Create Concrete States - Implement state-specific behavior
- Delegate in Context - Context delegates to current state
- Manage Transitions - States or context handles transitions
Real-World Impact:
- Workflow systems become more maintainable
- Complex business rules are easier to manage
- New states can be added with minimal risk
- Code becomes more testable and readable
By using the State Pattern, you can create systems that are flexible, maintainable, and easy to understand, especially when dealing with complex state-dependent behavior.
Remember: The State Pattern shines when you have complex state-dependent behavior. For simpler cases with few states and simple transitions, consider using enums or simple conditionals to avoid unnecessary complexity.