Skip to content

Lifecycle Management & State Transitions

Behaviour changes when internal state changes

Builds on Strategy Pattern: Dynamic Runtime Interchangeability and Enums

Stage 3core3 min read

In this design, the Order class itself holds all the rules for the order's lifecycle. It stores the state as a plain string and moves between states with a big switch block. The code is stiff, and each update can break something that used to work (a regression).


1. Understanding the Issue

In an e-commerce or delivery domain, an Order moves through a fixed lifecycle (e.g., placed, preparing, shipping, delivered). Each phase has its own rules about which actions are allowed (such as canceling an order) and which phase comes next.

Instead of giving each phase its own class, the Order class manages every rule for every state. The state logic gets mixed into the order's ordinary methods.


2. Issues In The Code

Cumbersome State Transition Management

  • The Issue: The steps from one state to the next are written inside the nextState() method as a switch on text values.
  • The Impact: To add a new stage or change the order of steps, you must open up this central method. That can break the transitions that already work.

Lack of Encapsulation & SRP Violation

  • The Issue: The Order class stores the order's data and also holds the rules for every phase (like checking, for each status, whether cancelling is allowed).
  • The Impact: This violates the Single Responsibility Principle (SRP), which says a class should have one reason to change. Here the class has two. It changes when the stored order data changes, and again whenever a workflow rule changes.

Code Duplication Risks

  • The Issue: The state checks are spread over separate methods like cancelOrder() and nextState().
  • The Impact: Each new action (such as refundOrder(), holdOrder(), or returnOrder()) will need the same multi-branch checks again. The copies drift apart, and fixing one can leave the others wrong.

Missing Flexibility for Future Changes

  • The Issue: The class depends on magic strings ("ORDER_PLACED", "PREPARING"), which are plain text values with no checking.
  • The Impact: A typo is not caught when the code is compiled. It only shows up when the program runs, if at all. A misspelled state simply fails every comparison without any error, so growing the system is risky.

3. Structural Vulnerabilities

Operational MetricNaive Design StatusArchitectural Target
State OrganizationMonolithic: Trapped inside big conditional blocks.Isolated: Encapsulated in separate objects.
ExtensibilityRigid: Adding a state forces changes across multiple methods.Flexible: Just add a new state class.
Validation SafetyFragile: Controlled by magic strings that nothing checks.Safe: Driven by typed objects, so mistakes show up at compile time.

4. Summary of the "Smell"

A "smell" is a sign in the code that something is off. Suppose an object behaves very differently depending on its condition, and its code is full of large conditional blocks that check a status field. That is a State Mutation Smell.

The State Design Pattern resolves this problem by creating a separate class for each lifecycle phase. The main entity passes each action to its current state object (delegating). The state checks turn into plain method calls, and the right class runs each one (polymorphism).

In this topic

Before code1 file

Before/Main.java77 linesGitHub
package DesignPatterns.BehaviouralPatterns.StatePattern.Before;import java.util.*;/** * PROBLEM: Fragmented State Transition Mechanics. * The 'Order' class is acting as both the data entity and the controller for  * every single state rule. It breaks encapsulation principles by explicitly handling * string state flags across all of its operations. */class Order {    // ISSUE 1: MAGIC STRINGS AND RUNTIME FRAGILITY    // State is managed via string literals. A single typo like "PREPAREING"     // will silently break state comparisons without raising compile-time errors.    private String state;    public Order() {        this.state = "ORDER_PLACED";    }    /**     * ISSUE 2: CONDITIONAL SCATTERING     * For every single action (cancel, refund, ship, assign), you must write      * complex if-else conditions checking every permissible state permutation.     */    public void cancelOrder() {        if (state.equals("ORDER_PLACED") || state.equals("PREPARING")) {            state = "CANCELLED";            System.out.println("Order has been cancelled.");        } else {            System.out.println("Cannot cancel the order now.");        }    }    /**     * ISSUE 3: OCP VIOLATION & MONOLITHIC SWITCH BLOCKS     * If you want to introduce a new intermediate state (e.g., "DISPATCHED" or "RETURNED"),     * you have to modify the switch block here, the conditional loops in cancelOrder(),      * and every other state-dependent method. This opens up massive regression risks.     */    public void nextState() {        switch (state) {            case "ORDER_PLACED":                state = "PREPARING";                break;            case "PREPARING":                state = "OUT_FOR_DELIVERY";                break;            case "OUT_FOR_DELIVERY":                state = "DELIVERED";                break;            default:                System.out.println("No next state from: " + state);                return;        }        System.out.println("Order moved to: " + state);    }    public String getState() {        return state;    }}class Main {    public static void main(String[] args) {        Order order = new Order();                System.out.println("Initial State: " + order.getState());        order.nextState(); // ORDER_PLACED -> PREPARING        order.nextState(); // PREPARING -> OUT_FOR_DELIVERY        order.nextState(); // OUT_FOR_DELIVERY -> DELIVERED        order.cancelOrder(); // Validates delivery block        System.out.println("Final State: " + order.getState());    }}

Done with this topic?

How well do you know it?

PreviousEncapsulating Requests