Library
9 min read

Strategy vs State

Published

Draw Strategy and draw State and you get the same picture: a main object (the context) holds a reference to an interface, and hands work to whichever object is plugged in. If you learned them from class diagrams, there is no visible difference. The difference is in who does the swapping.

Take two pictures from the LLD course. Strategy is choosing car, bike or walk in Maps: you pick the mode, and the "car" option has no idea the "walk" option exists. The route will never decide on its own to become a walking route. State is a traffic signal: nobody outside picks the next colour. Green knows that yellow comes after it, and yellow knows red comes next. The signal changes itself.

Your answer

An order goes Placed, Paid, Out for delivery, Delivered, and can be Cancelled from some of those. Separately, checkout lets the customer pick Standard, Express or Free delivery. Which of these is Strategy and which is State? What is the one difference you used to decide?

Watch who changes it#

Try both halves. On the left you are the caller and you swap the rule. On the right you can only press actions, and the current state decides what the order becomes.

Same shape, different hands on the switch
Strategy: shipping rule
Cart of ₹350. You pick the rule.
fee = ₹40 (₹40 flat)
The three rules never mention each other.
    State: order #4471
    You only press actions. Current state:
    PlacedState
    Each state knows which states can come after it.
      Both sides hold one object and hand work to it. On the left, change the shipping rule. On the right, press actions and watch who decides what happens next.

      Strategy: the caller picks how#

      A strategy is one way of doing a job, chosen from outside. The options are interchangeable, independent, and usually picked once per job: per request, per customer, per experiment.

      ts
      interface ShippingRule {
        fee(cartTotal: number): number
      }
       
      class Standard implements ShippingRule {
        fee() {
          return 40
        }
      }
      class FreeAbove499 implements ShippingRule {
        fee(cartTotal: number) {
          return cartTotal >= 499 ? 0 : 40
        }
      }
       
      class Checkout {
        constructor(private shipping: ShippingRule) {}
        setShipping(rule: ShippingRule) {
          this.shipping = rule
        }
        total(cartTotal: number) {
          return cartTotal + this.shipping.fee(cartTotal)
        }
      }
       
      const checkout = new Checkout(new Standard())
      console.log(checkout.total(600)) // 640
      checkout.setShipping(new FreeAbove499()) // the caller swaps it
      console.log(checkout.total(600)) // 600

      Standard and FreeAbove499 never mention each other. Checkout never decides which one to use. Someone outside, the customer's choice or a config flag, makes that decision.

      State: the object changes itself#

      A state is the behaviour of an object at one point in its life. Each state answers every action, and the answer often includes "and now you are in this other state". The caller only says what happened (pay, cancel), never which state comes next.

      ts
      interface OrderState {
        readonly name: string
        pay(order: Order): void
        cancel(order: Order): void
      }
       
      class Order {
        state: OrderState = new Placed()
        pay() {
          this.state.pay(this)
        }
        cancel() {
          this.state.cancel(this)
        }
      }
       
      class Placed implements OrderState {
        readonly name = 'Placed'
        pay(order: Order) {
          order.state = new Paid()
        }
        cancel(order: Order) {
          order.state = new Cancelled()
        }
      }
      class Paid implements OrderState {
        readonly name = 'Paid'
        pay() {
          console.log('already paid')
        }
        cancel(order: Order) {
          order.state = new Cancelled()
        }
      }
      class Cancelled implements OrderState {
        readonly name = 'Cancelled'
        pay() {
          console.log('order was cancelled')
        }
        cancel() {
          console.log('already cancelled')
        }
      }
       
      const order = new Order()
      order.pay()
      console.log(order.state.name) // Paid
      order.cancel()
      console.log(order.state.name) // Cancelled
      order.pay()
      // > order was cancelled

      Placed knows that paying leads to Paid. Nobody calls order.setState(new Paid()) from outside. That one detail, the states choosing their successors, is what makes this State and not Strategy.

      The differences, side by side#

      StrategyState
      Who swaps itThe caller, from outsideThe current state, from inside
      Do the options know each otherNo, they are independentYes, each knows which states can follow
      What variesUsually one algorithm (a fee, a sort, a route)The whole behaviour of several methods at once
      How often it changesOnce per job, or per configMany times in one object's life
      The smell it replacesif (mode === 'express') in one functionif (status === 'paid') repeated in every method
      Lighter TypeScript formA functionA union type and a transition table

      The "smell" row is the practical one. If one function has a growing switch on a mode, that is a Strategy waiting to happen. If five methods each have the same if (status === ...) ladder, and someone forgot to update one of them (the State chapter opens with exactly that bug), that is a State machine waiting to happen.

      The lighter forms in TypeScript#

      Both patterns are smaller in TypeScript than in a textbook. A strategy is often just a function:

      ts
      type ShippingRule = (cartTotal: number) => number
       
      const standard: ShippingRule = () => 40
      const express: ShippingRule = () => 90
      const freeAbove499: ShippingRule = (t) => (t >= 499 ? 0 : 40)
       
      function checkout(total: number, shipping: ShippingRule) {
        return total + shipping(total)
      }
       
      console.log(checkout(350, standard)) // 390
      console.log(checkout(350, express)) // 440
      console.log(checkout(600, freeAbove499)) // 600

      And a state machine is often a union type plus a table of allowed moves. Every rule is in one place, and the compiler checks that you spelled every status correctly:

      ts
      type Status = 'placed' | 'paid' | 'delivered' | 'cancelled'
      type Action = 'pay' | 'deliver' | 'cancel'
       
      const next: Record<Status, Partial<Record<Action, Status>>> = {
        placed: { pay: 'paid', cancel: 'cancelled' },
        paid: { deliver: 'delivered', cancel: 'cancelled' },
        delivered: {},
        cancelled: {},
      }
       
      function act(status: Status, action: Action): Status {
        const to = next[status][action]
        if (!to) throw new Error(`cannot ${action} when ${status}`)
        return to
      }
       
      console.log(act('placed', 'pay')) // paid
      console.log(act('paid', 'cancel')) // cancelled

      Use the class form when each state has real behaviour of its own (different validation, different side effects). Use the table when states differ only in which moves are allowed, which is most order, ticket and booking flows.

      When a strategy turns into a state

      Watch for strategies that start swapping themselves. A retry policy that is "fast" for three attempts and then calls client.setPolicy(new SlowPolicy()) is no longer a strategy, because the option now picks its successor. That is fine, but call it a state machine and draw its transitions, or the next person will not find the rule that changed the policy.

      A state machine for two states

      If an order can only be open or closed, a boolean and one if is clearer than three classes. Reach for State when the same status checks appear in several methods, or when a missed combination has already caused a bug.

      ChoiceWhat you gainWhat you payPick it when
      StrategyNew algorithms are new code, chosen at runtime, testable on their own.The caller has to know which strategies exist and pick one.Several interchangeable ways to do one job: pricing, shipping fees, rider assignment.
      State (classes)Each state's behaviour is in one class, and invalid moves are handled in one place.One class per state, and the transitions are spread across them.States with genuinely different behaviour, not only different allowed moves.
      State (union and table)Every transition visible in one object, checked by the compiler.Per state behaviour has to live in switch statements elsewhere.Most order, payment, ticket and booking lifecycles.
      TypeScript you just picked up
      type Status = 'placed' | 'paid'
      A union of string literals: Status can only be one of these exact strings.
      Record<Status, X>
      An object with exactly one key per Status, each holding an X. Miss a status and it does not compile.
      Partial<Record<Action, Status>>
      Partial makes every key optional, so a state lists only the actions it allows.
      readonly name = 'Placed'
      A field that cannot be reassigned after the object is created.
      type ShippingRule = (t: number) => number
      A function type. Any function with this signature is a valid strategy.

      In the interview#

      Interviewer
      Strategy and State have the same UML. What is actually different?
      You
      Who does the swapping. With Strategy the client picks the algorithm from outside, and the strategies are independent of each other, like shipping rules. With State the object moves itself: each state handles every action and decides which state comes next, like an order going from paid to out for delivery. So strategies do not know each other and states do. Strategy usually varies one algorithm; State varies the behaviour of several methods together.
      Leads with the one difference, then the consequence, with an example of each.
      Interviewer
      How would you model the order lifecycle in TypeScript?
      You
      If states only differ in which actions are allowed, a union type for the status and a Record of allowed transitions, so every rule is in one place and the compiler checks the status names. If each state has its own validation or side effects, one class per state behind an OrderState interface. Either way, I would store the status in the database as the union value, not as a class name.
      Picks the lighter form first and says when to upgrade.
      Strategy or State?
      Rider assignment can use nearest, top rated or least busy, chosen per city by the ops team
      A ticket goes Open, In progress, Waiting on customer, Resolved, and reopens if the customer replies
      A sort function takes a comparator
      A media player where the play button means pause while playing and play while paused

      Checkpoint#

      Checkpoint

      1. What is the most reliable way to tell Strategy from State in code?

      2. Five methods on Booking each start with the same if (status === ...) ladder. Which pattern is this asking for?

      3. A strategy object starts calling context.setStrategy(new OtherStrategy()). What has happened?

      Worth memorising
      • Same shape, different hands on the switch.
      • Strategy: the caller picks, the options do not know each other. Maps: car, bike, walk.
      • State: the object moves itself, each state knows what can follow. The traffic signal.
      • Smell: one growing switch on a mode is Strategy; the same status if in many methods is State.
      • In TypeScript: a strategy is often a function; a state machine is often a union plus a Record.
      Say this in 60 seconds

      Strategy and State have the same structure, a context holding an interface and delegating to it, so I tell them apart by who swaps the object. In Strategy the caller chooses from outside, the options are independent and interchangeable, and it usually varies one algorithm, like shipping fees or rider assignment. In State the object changes itself: each state handles every action and decides which state comes next, so the states know each other, and it varies the behaviour of several methods over the object's life, like an order going from placed to paid to delivered. The smell for Strategy is one growing switch on a mode; for State it is the same status checks repeated in many methods. In TypeScript a strategy is often just a function, and most lifecycles fit a union type with a Record of allowed transitions. If a strategy starts choosing its successor, it has become a state machine and should be drawn as one.