Design patternBehavioral Patterns
State
Move each mode of an object into its own class so behavior and transitions change with the mode instead of branching on a status field.
- Complexity
- Medium
- Seen in the wild
- Medium
- Also known as
- Objects for States
Intent
Let an object change its behavior when its internal mode changes, as if it had switched class. Each mode becomes a separate object that implements the same interface, and the owning object delegates to whichever mode is current. Transitions become the mode objects' job, not a status check repeated in every method.
Forces
- Several methods vs one status field: every public method starts with the same
switch (status), and the branches drift apart. - Legal transitions vs scattered assignments:
status = "canceled"can appear anywhere, so nobody can list which moves are allowed. - Mode-specific data vs one flat object: a retry counter that only means something while a payment is overdue still sits on the object in every mode.
- Adding a mode vs editing every method: a new status should not mean touching each
switchin the file.
Therefore
Give each mode its own class behind one interface, have the context forward every mode-dependent call to the current mode object, and let mode objects decide which mode comes next.
The context holds one reference to the current state. A state method does its work and returns (or sets) the next state. Mode-specific data lives in the state object that needs it. If states carry no data, you can share one instance of each.
Structure
Diagram source (Mermaid)
classDiagram
class Subscription {
-SubscriptionState state
+paymentSucceeded() void
+paymentFailed() void
+cancel() void
+canUseProduct() bool
}
class SubscriptionState {
<<interface>>
+paymentSucceeded() SubscriptionState
+paymentFailed() SubscriptionState
+cancel() SubscriptionState
+canUseProduct() bool
}
class Trialing {
+canUseProduct() bool
}
class Active {
+canUseProduct() bool
}
class PastDue {
-int failedCharges
+paymentFailed() SubscriptionState
}
class Canceled {
+canUseProduct() bool
}
Subscription o-- SubscriptionState
SubscriptionState <|.. Trialing
SubscriptionState <|.. Active
SubscriptionState <|.. PastDue
SubscriptionState <|.. Canceled
PastDue ..> Canceled : third failureThe lifecycle the example implements:
Diagram source (Mermaid)
stateDiagram-v2 [*] --> Trialing Trialing --> Active : paymentSucceeded Trialing --> PastDue : paymentFailed Active --> PastDue : paymentFailed PastDue --> Active : paymentSucceeded PastDue --> Canceled : third failure Trialing --> Canceled : cancel Active --> Canceled : cancel PastDue --> Canceled : cancel Canceled --> Active : paymentSucceeded
Participants
- Context (
Subscription): the object clients use. It holds the current state, forwards calls to it, and stores the state it gets back. - State (
SubscriptionState): the interface for every mode-dependent operation, including the events that cause transitions. - Concrete states (
Trialing,Active,PastDue,Canceled): one mode each. They implement that mode's behavior, hold that mode's data, and pick the next state.
Use it when
- Several methods of one class branch on the same status field, and the branches disagree about edge cases.
- You need a reviewable answer to "which transitions are legal", and today the answer is a grep for assignments.
- Some fields are meaningful only in one mode, such as an attempt counter, a lock holder or a deadline.
- Modes keep being added, such as a new paused or suspended status, and each one touches every method.
Do not use it when
- The machine is small: three or four states, one or two events, little behavior per state. Use an enum and a single
switch(ormatch) in onetransitionfunction. In TypeScript, Rust, Kotlin, Swift or Java 21, an exhaustive switch over a closed type makes the compiler flag every unhandled state, which a class hierarchy does not. Past roughly a dozen states, or once states carry behavior that calls back into the context, the switch becomes the maintenance problem and per-state classes pay off. - States differ only in which state comes next, not in behavior. Use a table-driven FSM: a map from
(state, event)to the next state. It is data, so you can print it, validate it and draw it. - The workflow is long-running, persisted and audited, such as orders or approvals. Store the state as a column and drive it with a workflow engine or a transition table. Rebuild state objects from the column, not the other way around.
- The behavior varies by a choice made from outside and never changes on its own. That is Strategy.
Application grill
Inspect first: the status fields branching across methods and every site that switches on them; the transition rules as they exist scattered in conditionals; where the state lives (a database column, memory) and who guards invalid transitions today.
Missing pieces: none beyond the language: a transition table or one object per state. If states are long-running and must survive process death (an order lifecycle over days), the infrastructure answer is a state table with a CHECK constraint, or a workflow engine - decide by whether the state outlives the process.
Ask before you build:
- Scale: how many states and transitions - past roughly six, a table beats one class per state?
- Performance: is the state checked per request (both forms are cheap; measure only in tight loops)?
- Reliability: is an invalid transition a loud failure or a silent coercion, and does the database enforce it too?
- Budget: N state classes versus one transition table - which does the team maintain?
- Implementation: how are legacy rows migrated to the new state model, and who owns the transition tests?
Implementation steps
- List every state and every event. Draw the transition table or a
stateDiagram-v2first; it exposes missing and illegal moves. - Write the state interface with one method per event plus each query whose answer depends on the mode.
- Create one class per state. Move the matching branch of each
switchinto it, and move mode-only fields into the state that uses them. - Make each event method return the next state. Returning a value keeps states free of a back-reference to the context.
- Replace the status field in the context with a state reference, and make each public method forward to the state and store the result.
- Decide what an illegal event does in each state: ignore it, return
this, or throw. Make it consistent and test it. - If you persist the context, map the state to a stable string or enum at the storage boundary.
Example
A SaaS billing service tracks a subscription from trial to paid, through failed card charges, to cancellation. Access to the product depends on where the subscription is in that lifecycle.
const MAX_FAILED_CHARGES = 3;
interface SubscriptionState {
readonly name: string;
canUseProduct(): boolean;
paymentSucceeded(): SubscriptionState;
paymentFailed(): SubscriptionState;
cancel(): SubscriptionState;
}
class Trialing implements SubscriptionState {
readonly name = "trialing";
canUseProduct() { return true; }
paymentSucceeded() { return new Active(); }
paymentFailed() { return new PastDue(1); }
cancel() { return new Canceled(); }
}
class Active implements SubscriptionState {
readonly name = "active";
canUseProduct() { return true; }
paymentSucceeded() { return this; }
paymentFailed() { return new PastDue(1); }
cancel() { return new Canceled(); }
}
class PastDue implements SubscriptionState {
constructor(private readonly failedCharges: number) {}
get name() { return `past_due(${this.failedCharges})`; }
canUseProduct() { return this.failedCharges < 2; } // one-charge grace period
paymentSucceeded() { return new Active(); }
paymentFailed(): SubscriptionState {
const failed = this.failedCharges + 1;
return failed >= MAX_FAILED_CHARGES ? new Canceled() : new PastDue(failed);
}
cancel() { return new Canceled(); }
}
class Canceled implements SubscriptionState {
readonly name = "canceled";
canUseProduct() { return false; }
paymentSucceeded() { return new Active(); } // reactivation
paymentFailed() { return this; }
cancel() { return this; }
}
class Subscription {
private state: SubscriptionState = new Trialing();
constructor(readonly accountId: string) {}
paymentSucceeded(): void { this.moveTo(this.state.paymentSucceeded()); }
paymentFailed(): void { this.moveTo(this.state.paymentFailed()); }
cancel(): void { this.moveTo(this.state.cancel()); }
canUseProduct(): boolean { return this.state.canUseProduct(); }
private moveTo(next: SubscriptionState): void {
if (next !== this.state) console.log(`${this.accountId}: ${this.state.name} -> ${next.name}`);
this.state = next;
}
}
const sub = new Subscription("acct_42");
sub.paymentSucceeded(); // trialing -> active
sub.paymentFailed(); // active -> past_due(1)
console.log(sub.canUseProduct()); // true
sub.paymentFailed(); // past_due(1) -> past_due(2)
console.log(sub.canUseProduct()); // false
sub.paymentFailed(); // past_due(2) -> canceled
sub.paymentSucceeded(); // canceled -> activeTrade-offs
Gains
- Each mode's behavior sits in one place, and the repeated
switchdisappears from every method. - Transitions are explicit return values, so you can list and test every legal move.
- Mode-only data lives only in the mode that uses it, so it can't be read in the wrong mode.
- A new mode is a new class plus the transitions into it.
Costs
- More types, and the full transition graph is spread across classes. Without a diagram, nobody can see it in one view.
- Adding an event means touching every state class, the same cost Visitor pays for new element types.
- States that need the context's data either receive it as arguments or hold a back-reference, which couples them to the context.
- Persisting the context needs a mapping between state objects and a stored value.
- For small machines, an enum with an exhaustive switch is shorter and gives better compiler checks.
Relations
- Strategy has the same delegation shape. Strategies are picked from outside and ignore each other; states pick their successors and know about each other.
- Flyweight applies when states hold no data: share one instance per state class instead of allocating on every transition.
- Memento can snapshot the current state object to support undo or rollback of a transition.
- Singleton is the usual way stateless states get shared, though a module-level constant does the same with less ceremony.
Agent checklist
- You drew or wrote the transition table before creating classes, and it has no missing
(state, event)pairs. - You compared the result against an enum plus exhaustive
switchand a transition table, and the class version is clearly smaller or clearer. - Only state objects decide the next state; no code outside them assigns the state field directly.
- Behavior for illegal events is consistent across states and covered by tests.
- Persisted state round-trips through a stable stored value.
- The tests that covered the old
switchstill pass, and each state has its own transition tests.
See also
- StrategyPut each interchangeable algorithm behind one interface so the caller picks a behavior at runtime without branching on it.
- FlyweightShare one immutable object for state that many objects have in common, and pass the per-object rest in, to cut memory when instances number in the millions.
- MementoCapture an object's internal state in an opaque snapshot so it can be restored later without exposing that state to the code holding the snapshot.
- SingletonGuarantee one instance of a class per process with a global access point. Usually better replaced by one instance created at startup and injected.
Original writing and examples. Topic coverage follows Refactoring.Guru, which explains this topic with its own illustrations.