Difference between State and Strategy
From a class diagram perspective, the State and Strategy patterns look almost identical: both rely on composition and delegation, where a context object holds an interface refer...
🟢 Junior Level
From a class diagram perspective, the State and Strategy patterns look almost identical: both rely on composition and delegation, where a context object holds an interface reference and delegates work to a polymorphic implementation.
However, their architectural intent and control flow are completely different:
- Strategy Pattern (External Control): The algorithm is selected and injected by an external client. Strategies are typically independent of each other and represent alternative ways to accomplish the same task (e.g., different payment or sorting algorithms).
- State Pattern (Internal Control): The object’s behavior changes internally based on its current position in its lifecycle. The object functions as a Finite State Machine (FSM), where states are interconnected and govern valid transitions.
Strategy Pattern State Pattern
(Client Injects Algorithm) (Internal State Transition FSM)
┌────────┐ ┌──────────────┐ ┌────────┐ ┌─────────────┐
│ Client │───>│ Context │ │ Client │───>│ Context │
└────────┘ └──────┬───────┘ └────────┘ └──────┬──────┘
│ delegates │ delegates
▼ ▼
┌───────────────┐ ┌───────────────┐
│PaymentStrategy│ │ OrderState │
└───────┬───────┘ └───────┬───────┘
┌──────────┴──────────┐ ┌──────────┴──────────┐
▼ ▼ ▼ ▼
┌───────────┐ ┌───────────┐ ┌───────────┐ ┌───────────┐
│CreditCard │ │ Crypto │ │ DraftState│ ──────> │ PaidState │
└───────────┘ └───────────┘ └───────────┘ (Next) └───────────┘
(Completely unaware of each other) (Aware of transition rules & invariants)
Real-World Analogy
- Strategy: Driving a car with selectable drive modes: “Eco”, “Comfort”, or “Sport”. The driver explicitly picks which engine mapping to use.
- State: A traffic light. It cycles automatically through “Red” $\rightarrow$ “Yellow” $\rightarrow$ “Green” according to internal rules and timers. The driver cannot arbitrarily force a green light.
Code Comparison
// Strategy: Client explicitly injects the algorithm
Order order = new Order();
order.setPaymentStrategy(new CreditCardPayment()); // Injected from outside
order.pay();
// State: Client invokes business actions; the context transitions internally
Order order = new Order(); // Internal state: NEW
order.processPayment(); // Transitions internally to: PAID
order.ship(); // Transitions internally to: SHIPPED
order.processPayment(); // ❌ Throws IllegalStateException: Already paid!
🟡 Middle Level
The State Pattern: Finite State Machine Implementation
The State pattern models a Finite State Machine where behavior changes dynamically based on internal lifecycle transitions:
// State Interface
public interface DocumentState {
void edit(Document doc, String text);
void publish(Document doc);
}
// Context
public class Document {
private DocumentState state;
private String content = "";
public Document() {
this.state = new DraftState(); // Initial state
}
public void setState(DocumentState state) {
this.state = state;
}
public void edit(String text) { state.edit(this, text); }
public void publish() { state.publish(this); }
public void setContent(String content) { this.content = content; }
}
// Concrete State: Draft
public class DraftState implements DocumentState {
public void edit(Document doc, String text) {
doc.setContent(text);
System.out.println("Draft content updated.");
}
public void publish(Document doc) {
System.out.println("Submitting draft for moderation.");
doc.setState(new ModerationState()); // State manages transition!
}
}
// Concrete State: Published
public class PublishedState implements DocumentState {
public void edit(Document doc, String text) {
throw new IllegalStateException("Published documents cannot be edited!");
}
public void publish(Document doc) {
System.out.println("Document is already published.");
}
}
Direct Comparison Matrix
| Dimension | Strategy Pattern | State Pattern |
|---|---|---|
| Transition Controller | External Client: Injects strategy via constructor/setter. | Internal Context / States: Managed automatically by transition rules. |
| Inter-Class Awareness | None: Strategies are completely isolated and unaware of one another. | High: States know about valid predecessor and successor states. |
| Statefulness | Usually Stateless (can be shared as Singletons). | Typically Stateful (captures current process stage and invariants). |
| Architectural Purpose | Eliminate algorithm branching and duplicate code. | Eliminate sprawling status flag conditionals (if (status == PAID)). |
Mitigating Tight Coupling in State Transitions
When DraftState directly invokes new ModerationState(), concrete state classes become tightly coupled. Common solutions include:
- Transitions Managed by Context: The context acts as the state transition controller; states simply return an event or outcome trigger (
Result.SUCCESS). - Enum State Machine: When states are finite and require minimal dependencies, an
enumwith abstract methods provides a clean, zero-allocation state machine:
public enum OrderStatus {
NEW {
@Override
public OrderStatus next() { return PAID; }
},
PAID {
@Override
public OrderStatus next() { return DELIVERED; }
},
DELIVERED {
@Override
public OrderStatus next() { throw new IllegalStateException("Order is already completed."); }
};
public abstract OrderStatus next();
}
🔴 Senior Level
Concurrency Hazards & Race Conditions in State Transitions
In multi-threaded architectures, changing state on a shared entity is a common source of Double-Spending and state corruption bugs:
- Check-Then-Act Race Condition:
If Thread 1 and Thread 2 concurrently check
if (order.getStatus() == NEW)without atomic synchronization, both threads can execute payments simultaneously, corrupting domain invariants. - Concurrency Mitigations:
AtomicReference<State>: Employs non-blockingcompareAndSet(expectedState, newState)for in-memory state machines.- Database Optimistic Locking (
@Versionin JPA / Hibernate): Any concurrent transition attempt increments the entity’s version column; the losing thread receives anOptimisticLockExceptionand aborts. - Pessimistic Locking (
SELECT ... FOR UPDATE): Locks the row during payment processing.
Modern Java (17–21): Sealed Interfaces & Pattern Matching
Modern Java allows modeling state machines immutably using Sealed Interfaces and Java Records with compile-time exhaustive pattern matching:
// Sealed State Hierarchy (Java 17+)
public sealed interface OrderState permits DraftState, PaidState, ShippedState, CancelledState {}
public record DraftState(BigDecimal amount) implements OrderState {}
public record PaidState(BigDecimal amount, Instant paidAt, String txId) implements OrderState {}
public record ShippedState(String trackingNumber) implements OrderState {}
public record CancelledState(String reason) implements OrderState {}
// Pure Functional State Machine Dispatcher (Java 21 Pattern Matching)
public class OrderWorkflowEngine {
public OrderState transition(OrderState current, String payload) {
return switch (current) {
case DraftState draft -> new PaidState(draft.amount(), Instant.now(), payload);
case PaidState paid -> new ShippedState(payload);
case ShippedState _ -> throw new IllegalStateException("Order already shipped!");
case CancelledState _ -> throw new IllegalStateException("Cannot advance cancelled order!");
};
}
}
The Java compiler guarantees exhaustiveness: if a new state (e.g., RefundedState) is added to the sealed interface, all switch expressions fail to compile until the new state is handled.
Enterprise Solution: Spring Statemachine
For complex enterprise workflows (loan approvals, banking scoring, order fulfillment pipelines with dozens of states and rollbacks), manual State patterns become unmaintainable. Enterprises adopt Spring Statemachine:
@Configuration
@EnableStateMachine
public class OrderStateMachineConfig extends EnumStateMachineConfigurerAdapter<OrderStatus, OrderEvent> {
@Override
public void configure(StateMachineTransitionConfigurer<OrderStatus, OrderEvent> transitions) throws Exception {
transitions
.withExternal()
.source(OrderStatus.NEW).target(OrderStatus.PAID).event(OrderEvent.PAY)
.guard(paymentValidationGuard()) // Guard condition
.action(sendPaymentReceiptAction()) // Transition side-effect
.and()
.withExternal()
.source(OrderStatus.PAID).target(OrderStatus.SHIPPED).event(OrderEvent.SHIP);
}
}
Key Advantages:
- Guards: Transitions are blocked unless preconditions evaluate to
true. - Actions: Built-in hooks for
entry,exit, andtransitionside-effects. - Persistence: Natively serializes state machine execution state to PostgreSQL or Redis.
4 Tricky Questions
1. Can state classes in the State pattern be implemented as thread-safe Singletons?
Answer: Yes. If the concrete state classes do not store mutable instance state (i.e., they are stateless) and operate entirely on the context passed as a method argument (state.handle(Context ctx)), they can be shared as stateless Singletons or cached enum constants. This eliminates object allocation overhead during state transitions and reduces Garbage Collection pressure.
2. How do you prevent race conditions when multiple concurrent threads attempt to trigger state transitions on the same entity?
Answer: In-memory, use an AtomicReference<State> and execute transitions using compare-and-swap (compareAndSet()) in a retry loop. In distributed enterprise systems with database persistence, the industry standard is Optimistic Locking via JPA’s @Version annotation. When two concurrent requests attempt to transition an entity simultaneously, one commits and increments the version number; the other fails with an OptimisticLockException and rolls back, preventing double transitions (such as double payments).
3. How can you eliminate the tight coupling between concrete state classes when transitions require navigating to the next state?
Answer: By shifting transition rules out of concrete state classes and into a Transition Table / State Registry or the Context class. Instead of DraftState instantiating new ModerationState(), the state class returns a domain event or status code (e.g., Event.SUBMIT). The Context or a dedicated transition manager looks up the (CurrentState, Event) -> NextState mapping in a pre-configured table, completely decoupling individual state classes from one another.
4. Why is Spring Statemachine preferred over a manual GoF State pattern in complex enterprise workflows?
Answer: A manual GoF State pattern degrades when workflows include complex requirements such as hierarchical states (sub-states), parallel regions, automated compensation/rollback on timeout, persistent state rehydration across distributed nodes, and audit trails. Spring Statemachine provides these features out of the box through declarative configuration, declarative guards (preconditions), transactional actions, event-driven transitions, and native persistence into Redis/RDBMS.
🎯 Interview Cheat Sheet
30-Second Elevator Pitch
“While Strategy and State share identical class structures, their architectural intent is opposite. Strategy is an algorithm chosen from the outside by the client to achieve interchangeable execution, and strategies remain completely decoupled and stateless. State is an internal Finite State Machine where the object changes its behavior from the inside as its lifecycle progresses, with states governing valid transition rules. Simple state machines can be implemented cleanly via
enumor sealed interfaces, while complex enterprise business workflows leverage Spring Statemachine.”
Key Architectural Takeaways
- Strategy = External Algorithm Choice: Injected by caller, stateless, independent.
- State = Internal FSM Lifecycle: Managed by object/rules, tracks process progression.
- Client Role: In Strategy, the client selects the implementation; in State, the client invokes business actions and the object transitions automatically.
- Modern Java: Use Sealed Interfaces + Pattern Matching for type-safe, exhaustive functional state machines.
- Concurrency: Guard state transitions with optimistic locking (
@Version) orAtomicReference.
Red Flags & Anti-Patterns
- 🚩 Claiming State and Strategy are the same: Demonstrates a lack of architectural comprehension.
- 🚩 Allowing external clients to directly force state changes: E.g.,
order.setState(new DeliveredState())breaks the encapsulation of the finite state machine. - 🚩 Instantiating heavy state objects on every transition: Use stateless singletons or enums instead.
- 🚩 Ignoring multithreaded race conditions on status transitions in payment/order workflows.
Related Topics
- When to use Strategy — Strategy pattern deep-dive and Spring auto-registry
- Advantage of Decorator over inheritance — Composition fundamentals
- What pattern categories exist — Behavioral pattern taxonomy
- What are design patterns — Pattern fundamentals and levels