# 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.

- kind: pattern
- category: behavioral
- aliases: Objects for States
- canonical: https://aipatterns.dev/design-patterns/state
- inspired-by: https://refactoring.guru/design-patterns/state

## 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 `switch` in 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

```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 failure
```

The lifecycle the example implements:

```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` (or `match`) in one `transition` function. 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](https://aipatterns.dev/design-patterns/strategy.md).

## 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

1. List every state and every event. Draw the transition table or a `stateDiagram-v2` first; it exposes missing and illegal moves.
2. Write the state interface with one method per event plus each query whose answer depends on the mode.
3. Create one class per state. Move the matching branch of each `switch` into it, and move mode-only fields into the state that uses them.
4. Make each event method return the next state. Returning a value keeps states free of a back-reference to the context.
5. Replace the status field in the context with a state reference, and make each public method forward to the state and store the result.
6. Decide what an illegal event does in each state: ignore it, return `this`, or throw. Make it consistent and test it.
7. 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.

```ts title="subscription.ts"
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 -> active
```

```python title="subscription.py"
from __future__ import annotations

MAX_FAILED_CHARGES = 3

class Trialing:
    name = "trialing"

    def can_use_product(self) -> bool: return True
    def payment_succeeded(self) -> State: return Active()
    def payment_failed(self) -> State: return PastDue(1)
    def cancel(self) -> State: return Canceled()

class Active:
    name = "active"

    def can_use_product(self) -> bool: return True
    def payment_succeeded(self) -> State: return self
    def payment_failed(self) -> State: return PastDue(1)
    def cancel(self) -> State: return Canceled()

class PastDue:
    def __init__(self, failed_charges: int) -> None:
        self.failed_charges = failed_charges
        self.name = f"past_due({failed_charges})"

    def can_use_product(self) -> bool:
        return self.failed_charges < 2  # one-charge grace period

    def payment_succeeded(self) -> State: return Active()

    def payment_failed(self) -> State:
        failed = self.failed_charges + 1
        return Canceled() if failed >= MAX_FAILED_CHARGES else PastDue(failed)

    def cancel(self) -> State: return Canceled()

class Canceled:
    name = "canceled"

    def can_use_product(self) -> bool: return False
    def payment_succeeded(self) -> State: return Active()  # reactivation
    def payment_failed(self) -> State: return self
    def cancel(self) -> State: return self

State = Trialing | Active | PastDue | Canceled

class Subscription:
    def __init__(self, account_id: str) -> None:
        self.account_id = account_id
        self._state: State = Trialing()

    def payment_succeeded(self) -> None: self._move_to(self._state.payment_succeeded())
    def payment_failed(self) -> None: self._move_to(self._state.payment_failed())
    def cancel(self) -> None: self._move_to(self._state.cancel())
    def can_use_product(self) -> bool: return self._state.can_use_product()

    def _move_to(self, next_state: State) -> None:
        if next_state is not self._state:
            print(f"{self.account_id}: {self._state.name} -> {next_state.name}")
        self._state = next_state

if __name__ == "__main__":
    sub = Subscription("acct_42")
    sub.payment_succeeded()  # trialing -> active
    sub.payment_failed()  # active -> past_due(1)
    print(sub.can_use_product())  # True
    sub.payment_failed()  # past_due(1) -> past_due(2)
    print(sub.can_use_product())  # False
    sub.payment_failed()  # past_due(2) -> canceled
    sub.payment_succeeded()  # canceled -> active
```

```go title="subscription.go"
package main

import "fmt"

const maxFailedCharges = 3

type State interface {
	Name() string
	CanUseProduct() bool
	PaymentSucceeded() State
	PaymentFailed() State
	Cancel() State
}

type Trialing struct{}

func (Trialing) Name() string            { return "trialing" }
func (Trialing) CanUseProduct() bool     { return true }
func (Trialing) PaymentSucceeded() State { return Active{} }
func (Trialing) PaymentFailed() State    { return PastDue{FailedCharges: 1} }
func (Trialing) Cancel() State           { return Canceled{} }

type Active struct{}

func (Active) Name() string              { return "active" }
func (Active) CanUseProduct() bool       { return true }
func (a Active) PaymentSucceeded() State { return a }
func (Active) PaymentFailed() State      { return PastDue{FailedCharges: 1} }
func (Active) Cancel() State             { return Canceled{} }

type PastDue struct{ FailedCharges int }

func (p PastDue) Name() string          { return fmt.Sprintf("past_due(%d)", p.FailedCharges) }
func (p PastDue) CanUseProduct() bool   { return p.FailedCharges < 2 } // one-charge grace period
func (PastDue) PaymentSucceeded() State { return Active{} }
func (PastDue) Cancel() State           { return Canceled{} }
func (p PastDue) PaymentFailed() State {
	if p.FailedCharges+1 >= maxFailedCharges {
		return Canceled{}
	}
	return PastDue{FailedCharges: p.FailedCharges + 1}
}

type Canceled struct{}

func (Canceled) Name() string            { return "canceled" }
func (Canceled) CanUseProduct() bool     { return false }
func (Canceled) PaymentSucceeded() State { return Active{} } // reactivation
func (c Canceled) PaymentFailed() State  { return c }
func (c Canceled) Cancel() State         { return c }

type Subscription struct {
	AccountID string
	state     State
}

func NewSubscription(id string) *Subscription { return &Subscription{AccountID: id, state: Trialing{}} }

func (s *Subscription) PaymentSucceeded()   { s.moveTo(s.state.PaymentSucceeded()) }
func (s *Subscription) PaymentFailed()      { s.moveTo(s.state.PaymentFailed()) }
func (s *Subscription) Cancel()             { s.moveTo(s.state.Cancel()) }
func (s *Subscription) CanUseProduct() bool { return s.state.CanUseProduct() }

func (s *Subscription) moveTo(next State) {
	if next != s.state { // value structs compare by content
		fmt.Printf("%s: %s -> %s\n", s.AccountID, s.state.Name(), next.Name())
	}
	s.state = next
}

func main() {
	sub := NewSubscription("acct_42")
	sub.PaymentSucceeded()           // trialing -> active
	sub.PaymentFailed()              // active -> past_due(1)
	fmt.Println(sub.CanUseProduct()) // true
	sub.PaymentFailed()              // past_due(1) -> past_due(2)
	fmt.Println(sub.CanUseProduct()) // false
	sub.PaymentFailed()              // past_due(2) -> canceled
	sub.PaymentSucceeded()           // canceled -> active
}
```

```java title="Billing.java"
public class Billing {
    static final int MAX_FAILED_CHARGES = 3;

    sealed interface SubscriptionState permits Trialing, Active, PastDue, Canceled {
        String name();
        boolean canUseProduct();
        SubscriptionState paymentSucceeded();
        SubscriptionState paymentFailed();
        SubscriptionState cancel();
    }

    record Trialing() implements SubscriptionState {
        public String name() { return "trialing"; }
        public boolean canUseProduct() { return true; }
        public SubscriptionState paymentSucceeded() { return new Active(); }
        public SubscriptionState paymentFailed() { return new PastDue(1); }
        public SubscriptionState cancel() { return new Canceled(); }
    }

    record Active() implements SubscriptionState {
        public String name() { return "active"; }
        public boolean canUseProduct() { return true; }
        public SubscriptionState paymentSucceeded() { return this; }
        public SubscriptionState paymentFailed() { return new PastDue(1); }
        public SubscriptionState cancel() { return new Canceled(); }
    }

    record PastDue(int failedCharges) implements SubscriptionState {
        public String name() { return "past_due(" + failedCharges + ")"; }
        public boolean canUseProduct() { return failedCharges < 2; } // one-charge grace period
        public SubscriptionState paymentSucceeded() { return new Active(); }
        public SubscriptionState paymentFailed() {
            int failed = failedCharges + 1;
            return failed >= MAX_FAILED_CHARGES ? new Canceled() : new PastDue(failed);
        }
        public SubscriptionState cancel() { return new Canceled(); }
    }

    record Canceled() implements SubscriptionState {
        public String name() { return "canceled"; }
        public boolean canUseProduct() { return false; }
        public SubscriptionState paymentSucceeded() { return new Active(); } // reactivation
        public SubscriptionState paymentFailed() { return this; }
        public SubscriptionState cancel() { return this; }
    }

    static final class Subscription {
        private final String accountId;
        private SubscriptionState state = new Trialing();

        Subscription(String accountId) { this.accountId = accountId; }

        void paymentSucceeded() { moveTo(state.paymentSucceeded()); }
        void paymentFailed() { moveTo(state.paymentFailed()); }
        void cancel() { moveTo(state.cancel()); }
        boolean canUseProduct() { return state.canUseProduct(); }

        private void moveTo(SubscriptionState next) {
            if (!next.equals(state)) {
                System.out.println(accountId + ": " + state.name() + " -> " + next.name());
            }
            state = next;
        }
    }

    public static void main(String[] args) {
        var sub = new Subscription("acct_42");
        sub.paymentSucceeded(); // trialing -> active
        sub.paymentFailed(); // active -> past_due(1)
        System.out.println(sub.canUseProduct()); // true
        sub.paymentFailed(); // past_due(1) -> past_due(2)
        System.out.println(sub.canUseProduct()); // false
        sub.paymentFailed(); // past_due(2) -> canceled
        sub.paymentSucceeded(); // canceled -> active
    }
}
```

## Trade-offs

**Gains**
- Each mode's behavior sits in one place, and the repeated `switch` disappears 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](https://aipatterns.dev/design-patterns/strategy.md) has the same delegation shape. Strategies are picked from outside and ignore each other; states pick their successors and know about each other.
- [Flyweight](https://aipatterns.dev/design-patterns/flyweight.md) applies when states hold no data: share one instance per state class instead of allocating on every transition.
- [Memento](https://aipatterns.dev/design-patterns/memento.md) can snapshot the current state object to support undo or rollback of a transition.
- [Singleton](https://aipatterns.dev/design-patterns/singleton.md) 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 `switch` and 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 `switch` still pass, and each state has its own transition tests.
