Skip to content

๐Ÿ“‘ State Pattern: Enterprise Document Workflow

๐Ÿ“ Overview

The State Pattern allows an object to change its behavior when its internal state changes. It is particularly useful for managing complex lifecycles (like document workflows or order processing) where the same action (e.g., publish()) must behave differently depending on the current stage of the object.

Core Concepts

  • Context: The main object (e.g., Document) that holds a reference to a state object.
  • State Interface: A common interface for all concrete states.
  • Encapsulated Behavior: Each state class implements behavior specific to that state and handles transitions to the next state.

๐Ÿญ The Engineering Story & Problem

๐Ÿ˜ก The Villain (The Problem)

You're building a Document Management System. A document has stages: Draft, Moderation, and Published.
The rules are complex:
- In Draft, you can edit and publish.
- In Moderation, you can't edit, and only an admin can publish. - In Published, you can't edit or publish.
The "Workflow Spaghetti" code looks like this:

class Document:
    def publish(self, user):
        if self.state == "DRAFT":
            self.state = "MODERATION"
        elif self.state == "MODERATION":
            if user.is_admin:
                self.state = "PUBLISHED"
        # ... and so on for every method ...
Every time you add a new state (like Archived or Rejected), you have to go into every single method and add more if/elif blocks. The Document class becomes a giant, unreadable mess.

๐Ÿฆธ The Hero (The Solution)

The State Pattern introduces "State Delegates." Instead of one giant class, we create DraftState, ModerationState, and PublishedState classes.
The Document class is now simple. When you call doc.publish(), it just says: self.state.publish(self).
- If it's in DraftState, the publish() method moves it to Moderation.
- If it's in PublishedState, the publish() method does nothing.
Each state knows its own rules. To add an Archived state, you just create one new class and point the PublishedState to it. You don't touch the Document or DraftState at all.

๐Ÿ“œ Requirements & Constraints

  1. (Functional): Manage a workflow: Draft -> Moderation -> Published.
  2. (Technical): Transitions must be handled by state objects, not the Document class.
  3. (Technical): Prevent illegal actions (like editing a published document) polymorphically.

๐Ÿ—๏ธ Structure & Blueprint

Class Diagram

classDiagram
    direction TB
    class State {
        <<interface>>
        +render(doc)
        +publish(doc)
    }
    class DraftState {
        +render(doc)
        +publish(doc)
    }
    class ModerationState {
        +render(doc)
        +publish(doc)
    }
    class PublishedState {
        +render(doc)
        +publish(doc)
    }
    class Document {
        -state: State
        +change_state(state)
        +render()
        +publish()
    }

    State <|.. DraftState
    State <|.. ModerationState
    State <|.. PublishedState
    Document o-- State

Runtime Context (Sequence)

sequenceDiagram
    participant User
    participant Doc as Document
    participant Draft as DraftState
    participant Mod as ModerationState

    User->>Doc: publish()
    Doc->>Draft: publish(doc)
    Draft->>Doc: change_state(ModerationState)

    User->>Doc: publish()
    Doc->>Mod: publish(doc)
    note right of Mod: Check if Admin...
    Mod->>Doc: change_state(PublishedState)

๐Ÿ’ป Implementation & Code

๐Ÿง  SOLID Principles Applied

  • Single Responsibility: Each state class handles logic for exactly one stage.
  • Open/Closed: Add new states (e.g., Rejected) without changing existing state classes or the Document.

๐Ÿ The Code

The Villain's Code (Without Pattern)
class Document:
    def __init__(self):
        self.state = "DRAFT"

    def publish(self):
        # ๐Ÿ˜ก Nested if-else nightmare
        if self.state == "DRAFT":
            print("Moving to moderation")
            self.state = "MODERATION"
        elif self.state == "MODERATION":
            print("Already in moderation")
        elif self.state == "PUBLISHED":
            print("Already published")
The Hero's Code (With Pattern)
from abc import abstractmethod


class User:
    def __init__(self, is_admin: bool) -> None:
        self.is_admin : bool = is_admin

class State:
    @abstractmethod
    def currentState(self) -> None:
        pass

    @abstractmethod
    def nextState(self, user: User) -> "State":
        pass


class Draft(State):
    def currentState(self) -> None:
        print("Current State is Draft...")

    def nextState(self, user: User) -> State:
        print("Moving from Draft state to Moderation state")
        return Moderation()


class Moderation(State):
    def currentState(self) -> None:
        print("Current State is Moderation...")

    def nextState(self, user: User) -> State:
        if user.is_admin:
            print("User is Admin so publishing it!")
            return Published()
        print("User is not Admin so cannot publish it!")
        return self


class Published(State):
    def currentState(self) -> None:
        print("Current State is Published...")

    def nextState(self, user: User) -> State:
        print("Already Published!")
        return self


class Document:
    def __init__(self, state: State) -> None:
        self.state : State = state

    def publish(self, user: User) -> None:
        self.state = self.state.nextState(user=user)


draft = Draft()
user = User(is_admin=False)
admin_user = User(is_admin=True)

my_document = Document(state=draft)

my_document.state.currentState()
my_document.publish(user=user)
my_document.state.currentState()
my_document.publish(user=user)

my_document.publish(user=admin_user)
my_document.state.currentState()

my_document.publish(user=user)

โš–๏ธ Trade-offs & Testing

Pros (Why it works) Cons (The Twist / Pitfalls)
Clean Logic: No giant if/else or switch blocks. Class Explosion: One class for every state.
Explicit Transitions: The workflow is clearly defined in classes. State Awareness: States often need to know about each other to transition.
Scalability: Easy to add complex new stages. Overhead: Might be overkill for a simple "Enabled/Disabled" toggle.

๐Ÿงช Testing Strategy

  1. Unit Test States: Test DraftState.publish() in isolation. Verify it calls doc.change_state with ModerationState.
  2. Workflow Test: Start a document in Draft, call publish twice, and verify the final state is Published.

๐ŸŽค Interview Toolkit

  • Interview Signal: mastery of Finite State Machines (FSM) and polymorphic behavior.
  • When to Use:
    • "An object's behavior changes based on its status..."
    • "Implement a multi-step checkout or wizard..."
    • "Handle complex permissions that change per stage..."
  • Scalability Probe: "What if you have 100 states?" (Answer: Use a state-transition table/matrix or a dedicated Workflow Engine like Temporal or Airflow.)
  • Design Alternatives:
    • Strategy: Very similar, but Strategy is about how to do something, while State is about what phase you are in (and phases change).
  • Strategy โ€” States can be seen as strategies that change over time.
  • Singleton โ€” State objects are often Singletons if they don't hold instance data.
  • Flyweight โ€” Shared state objects can save memory.