State Design Pattern using Spring Boot

Introduction
It was a typical Monday morning at E-Shop Inc., and Sarah, the lead developer, was facing a familiar challenge. The company’s order processing system had become a tangled mess of conditional statements. Each time the business wanted to add a new order status or modify the rules for transitioning between states, the entire codebase needed to be modified, tested, and redeployed. The risk of introducing bugs was high, and the development team was spending more time maintaining the system than adding new features.
“There must be a better way to handle this,” Sarah thought as she sipped her coffee. That’s when she remembered learning about the State Design Pattern during her software design course. Could this be the solution to their problems?
Understanding the Problem
Before diving into the solution, let’s understand the problem Sarah was facing. E-Shop’s order processing system needed to track orders through various states:
Created: When a customer places an order
Paid: After payment is processed
Shipped: When the order leaves the warehouse
Delivered: When the customer receives the order
Each state had its own rules for what could happen next. For example, an order couldn’t be shipped before it was paid, and a delivered order couldn’t go back to being created.
The existing code was a mess of if-else statements:
public class Order {
private String status;
public void nextState() {
if (status.equals("CREATED")) {
status = "PAID";
} else if (status.equals("PAID")) {
status = "SHIPPED";
} else if (status.equals("SHIPPED")) {
status = "DELIVERED";
} else if (status.equals("DELIVERED")) {
System.out.println("Order already delivered.");
}
}
public void previousState() {
if (status.equals("CREATED")) {
System.out.println("Already in initial state.");
} else if (status.equals("PAID")) {
status = "CREATED";
} else if (status.equals("SHIPPED")) {
status = "PAID";
} else if (status.equals("DELIVERED")) {
status = "SHIPPED";
}
}
}
This approach had several problems:
Violation of the Open/Closed Principle: Adding a new state required modifying existing code.
Poor Maintainability: The logic for each state was scattered throughout the class.
Difficult Testing: Testing all possible state transitions was complex.
Prone to Bugs: It was easy to miss a condition or introduce logical errors.
Enter the State Design Pattern
Sarah decided to refactor the system using the State Design Pattern. This pattern allows an object to alter its behavior when its internal state changes, making it appear as if the object’s class has changed.
The key idea is to represent each state as a separate class that implements a common interface. The context (in this case, the Order) delegates state-specific behavior to the current state object.
Implementing the Solution
Sarah started by defining the OrderState
interface:
public interface OrderState {
void next(Order order);
void previous(Order order);
String getStatus();
}
This interface defined the contract that all concrete state classes would follow. Each state would know how to transition to the next or previous state and provide its status.
Next, she created concrete implementations for each state:
CreatedState
public class CreatedState implements OrderState {
@Override
public void next(Order order) {
order.setState(new PaidState());
}
@Override
public void previous(Order order) {
log.info("Already in Created state.");
}
@Override
public String getStatus() {
return "Order Created";
}
}
PaidState
public class PaidState implements OrderState {
@Override
public void next(Order order) {
order.setState(new ShippedState());
}
@Override
public void previous(Order order) {
order.setState(new CreatedState());
}
@Override
public String getStatus() {
return "Order Paid";
}
}
ShippedState
public class ShippedState implements OrderState {
@Override
public void next(Order order) {
order.setState(new DeliveredState());
}
@Override
public void previous(Order order) {
order.setState(new PaidState());
}
@Override
public String getStatus() {
return "Order Shipped";
}
}
DeliveredState
public class DeliveredState implements OrderState {
@Override
public void next(Order order) {
log.info("Order already delivered.");
}
@Override
public void previous(Order order) {
order.setState(new ShippedState());
}
@Override
public String getStatus() {
return "Order Delivered";
}
}
Finally, she refactored the Order
class to use these state objects:
public class Order {
private OrderState state;
public Order() {
this.state = new CreatedState(); // initial state
}
public void setState(OrderState state) {
this.state = state;
}
public void nextState() {
state.next(this);
}
public void previousState() {
state.previous(this);
}
public String getStatus() {
return state.getStatus();
}
}
To make it easy to use this in a Spring Boot application, Sarah created an OrderService
:
@Service
public class OrderService {
private Order order = new Order();
public String getStatus() {
return order.getStatus();
}
public void next() {
order.nextState();
}
public void previous() {
order.previousState();
}
}
Testing the Implementation
To test the implementation, Sarah created a simple Spring Boot application that demonstrated the state transitions:
@SpringBootApplication
@Slf4j
public class SpringBootStateDesignPatternApplication implements CommandLineRunner {
@Autowired
private OrderService service;
public static void main(String[] args) {
SpringApplication.run(SpringBootStateDesignPatternApplication.class, args);
}
@Override
public void run(String... args) throws Exception {
log.info("Order Status: {}", service.getStatus());
service.next();
log.info("Order Status: {}", service.getStatus());
service.next();
log.info("Order Status: {}", service.getStatus());
service.next();
log.info("Order Status: {}", service.getStatus());
service.next();
log.info("Order Status: {}", service.getStatus());
service.previous();
log.info("Order Status: {}", service.getStatus());
service.previous();
log.info("Order Status: {}", service.getStatus());
service.previous();
log.info("Order Status: {}", service.getStatus());
service.previous();
log.info("Order Status: {}", service.getStatus());
}
}
When she ran the application, the console output showed the order transitioning through all the states:
2025-08-06T02:11:27.375+05:30 INFO 8028 [main] .SpringBootStateDesignPatternApplication : Order Status: Order Created
2025-08-06T02:11:27.376+05:30 INFO 8028 [main] .SpringBootStateDesignPatternApplication : Order Status: Order Paid
2025-08-06T02:11:27.376+05:30 INFO 8028 [main] .SpringBootStateDesignPatternApplication : Order Status: Order Shipped
2025-08-06T02:11:27.376+05:30 INFO 8028 [main] .SpringBootStateDesignPatternApplication : Order Status: Order Delivered
2025-08-06T02:11:27.376+05:30 INFO 8028 [main] c.v.p.s.state.DeliveredState : Order already delivered.
2025-08-06T02:11:27.376+05:30 INFO 8028 [main] .SpringBootStateDesignPatternApplication : Order Status: Order Delivered
2025-08-06T02:11:27.376+05:30 INFO 8028 [main] .SpringBootStateDesignPatternApplication : Order Status: Order Shipped
2025-08-06T02:11:27.376+05:30 INFO 8028 [main] .SpringBootStateDesignPatternApplication : Order Status: Order Paid
2025-08-06T02:11:27.376+05:30 INFO 8028 [main] .SpringBootStateDesignPatternApplication : Order Status: Order Created
2025-08-06T02:11:27.376+05:30 INFO 8028 [main] c.v.p.s.state.CreatedState : Already in Created state.
2025-08-06T02:11:27.376+05:30 INFO 8028 [main] .SpringBootStateDesignPatternApplication : Order Status: Order Created
The Benefits
After implementing the State Design Pattern, Sarah noticed several benefits:
Cleaner Code: Each state’s behavior was encapsulated in its own class, making the code more organized and easier to understand.
Easier Maintenance: Adding a new state or modifying an existing one was as simple as creating or modifying a single class, without touching the rest of the codebase.
Better Testability: Each state could be tested in isolation, making it easier to ensure that all state transitions worked correctly.
Reduced Bugs: The pattern eliminated the complex conditional logic that was prone to errors.
Flexibility: The system could now handle more complex state transitions and rules without becoming unwieldy.
Future Enhancements
With the foundation in place, Sarah planned several enhancements for the future:
REST API: Implementing a REST controller to expose the order state management functionality as an API.
Persistence: Adding a database to store orders and their states.
Multiple Orders: Extending the system to handle multiple orders simultaneously.
Complex Rules: Implementing more complex business rules for state transitions, such as validation or notifications.
Conclusion
The State Design Pattern provided an elegant solution to E-Shop’s order processing challenges. By encapsulating state-specific behavior in separate classes and using composition over inheritance, Sarah created a system that was more maintainable, flexible, and robust.
As software systems grow in complexity, design patterns like the State Pattern become invaluable tools in a developer’s toolkit. They provide proven solutions to common problems, allowing developers to create code that is both elegant and practical.
The next time you find yourself drowning in conditional logic or struggling to manage complex state transitions, remember Sarah’s story and consider whether the State Design Pattern might be the solution you’re looking for.
Key Takeaways
Identify State-Dependent Behavior: Look for objects that behave differently based on their internal state.
Encapsulate States: Create separate classes for each state to encapsulate state-specific behavior.
Define a Common Interface: Ensure all state classes implement the same interface to make them interchangeable.
Use Composition: The context (e.g., Order) should contain a reference to the current state object and delegate behavior to it.
Consider State Transitions: Decide whether state transitions should be handled by the context, the state objects, or a combination of both.
Test Thoroughly: Verify that all state transitions work correctly, including edge cases.
Document the State Machine: Create a diagram or documentation that clearly shows all possible states and transitions.
By following these principles, you can create flexible, maintainable systems that elegantly handle complex state-dependent behavior.
That takes to the end of this guide. Hope you loved it and will be using it in your code. You can find the source code of this guide on my github account.
Subscribe to my newsletter
Read articles from Vipul Kumar directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
