Part 01 · Low Level Design

Behavioral Patterns: Strategy, State, Command

StrategyStateTemplate MethodCommandChain of Responsibility

Same job, different rules: swap an algorithm, let status drive behaviour, reuse the steps but change the details, queue and undo actions, and pass a request down a chain.

⏱ 2-3 hours · Station 8 of 80

The fare function that ate the app

A ride app's CalculateFare started as three lines. Then came bikes, then CNGs, then cars, then a night surcharge, then a Puja discount, then "premium cars in Gulshan". Today it's one function with 14 if/else branches, and every change to car fares has a chance of breaking bike fares.

The same team wants "undo" in the shopping cart of their food section, and a refund flow where small refunds are automatic, medium ones need a supervisor and large ones a manager. The first draft of both is, again, a growing if/else.

Behavioral patterns are standard ways to decide who does what when behaviour varies. Each one turns a pile of branches into small objects with one job.

The problem: behaviour that varies, written as branches

What you seePattern
if rideType == ... else if ... that grows with every productStrategy
if status == ... else if ... repeated in many methodsState
Several classes repeat the same steps with small differencesTemplate Method
You need undo, redo, queueing or logging of actionsCommand
A request should be handled by the first of several handlers that canChain of Responsibility

Think of a ride app, a notebook and an office

Choosing bike, CNG or car in a ride app changes how the fare is worked out; the trip itself doesn't care. That's Strategy: swap the algorithm, keep the caller.

Writing in pencil and keeping a list of what you wrote, so you can rub out the last thing: that's Command. Each action is written down as a thing that can be undone.

A refund request at an office goes to the clerk; if it's too big, to the supervisor; then to the manager. Each person handles what they're allowed to and passes the rest up. That's a Chain of Responsibility.

autoup to 500supervisorup to 5,000managerup to 50,000end of chainfinance review3003,00012,00080,000each refund stops at the first approver whose limit covers it
Press play. Four refunds walk the same chain. 300 stops at auto-approval, 3,000 at the supervisor, 12,000 at the manager, and 80,000 falls off the end and is sent to finance review.

Where the comparison breaks down. In an office, the supervisor might bend a rule. In code, each link does exactly what it says, so the order of the chain and each link's limit are the policy; changing them is a business decision, not a refactor.

Predict first

The refund chain is auto (≤ 500) → supervisor (≤ 5,000) → manager (≤ 50,000). Finance adds a new step: a 'regional head' who can approve up to 200,000, between the manager and the end. What changes?

Five patterns for varying behaviour

  1. Strategy: put each variant of an algorithm behind one interface (FareStrategy.Fare(km)), and give the caller the one it needs. New variant = new type. The caller has no branches. Perfect for pricing rules, sorting rules, routing rules.
  2. State: when an object's behaviour depends on its state and many methods would branch on it, make each state a class with the same interface (Placed.Cancel(), Shipped.Cancel()). The object delegates to its current state object. It's the object-oriented version of the transition table from chapter 4; use the table until the states really have different behaviour.
  3. Template Method: a base defines the fixed steps of a process (validate → charge → notify), and variants fill in specific steps. Useful, but it relies on inheritance; in Go, Rust and modern TypeScript the same idea is usually a function that takes the variable steps as parameters (composition).
  4. Command: turn an action into an object with Do and Undo (and maybe a name). Then you can keep a history for undo, queue actions for later, retry them, or log exactly what happened.
  5. Chain of Responsibility: link handlers so each one either handles a request or passes it to the next. Adding, removing or reordering steps doesn't touch the others. HTTP middleware (auth → rate limit → logging → handler) is the most common chain in real code.
  6. The common idea: find the part that varies, give it an interface, and move each variant into its own small object. The code that uses the behaviour stops changing.

Undo, step by step

Step through it

Two adds and three undos

CallerCartHistory stackAddItem

Step 1/5 Run a command, not a method

The caller makes AddItem{tea, 2} and hands it to cart.Run. The cart applies it and pushes it on the history.

cart
2 items, 60 tk
history
[add tea x2]

When branches pile up

When you see the same if/else growing, ask:

  • Does the algorithm vary? → Strategy.
  • Does behaviour depend on the object's state, in many methods? → State (or a transition table).
  • Do I need to store, undo or queue actions? → Command.
  • Should the first capable handler take it? → Chain of Responsibility.
  • Same steps, different details? → Template Method, or a function that takes the details as parameters.

Try it: which pattern is this?

Try each case

Name the pattern

sorter := sorters[userChoice]; sorter.Sort(list)
works: Strategy. Same job, swappable algorithm.

Fares, undo and refunds in code

Strategy for fares, Command for an undoable cart, and Chain of Responsibility for refund approval.

package main

import "fmt"

// ---- Strategy: the fare rule is a swappable object; the ride doesn't care which.

type FareStrategy interface{ Fare(km int) int }

type PerKm struct{ Base, PerKm int }

func (p PerKm) Fare(km int) int { return p.Base + p.PerKm*km }

// ---- Command: each change to the cart is an object that knows how to undo itself.

var prices = map[string]int{"tea": 30, "biscuit": 20}

type Cart struct {
	qty     map[string]int
	history []Command
}

type Command interface {
	Do(c *Cart)
	Undo(c *Cart)
	Name() string
}

type AddItem struct {
	Item string
	Qty  int
}

func (a AddItem) Do(c *Cart)   { c.qty[a.Item] += a.Qty }
func (a AddItem) Undo(c *Cart) { c.qty[a.Item] -= a.Qty }
func (a AddItem) Name() string { return fmt.Sprintf("add %s x%d", a.Item, a.Qty) }

func (c *Cart) Run(cmd Command) { cmd.Do(c); c.history = append(c.history, cmd) }

func (c *Cart) UndoLast() bool {
	if len(c.history) == 0 {
		return false
	}
	last := c.history[len(c.history)-1]
	c.history = c.history[:len(c.history)-1]
	last.Undo(c)
	return true
}

func (c *Cart) Summary() string {
	items, total := 0, 0
	for name, q := range c.qty {
		items += q
		total += q * prices[name]
	}
	return fmt.Sprintf("%d items, %d tk", items, total)
}

// ---- Chain of Responsibility: each approver handles what it can, or passes it on.

type Approver interface{ Approve(amount int) string }

type Limit struct {
	Who  string
	Max  int
	Next Approver
}

func (l Limit) Approve(amount int) string {
	if amount <= l.Max {
		return l.Who
	}
	if l.Next == nil {
		return "rejected, needs finance review"
	}
	return l.Next.Approve(amount)
}

func main() {
	strategies := []struct {
		name string
		s    FareStrategy
	}{{"bike", PerKm{40, 10}}, {"car", PerKm{100, 30}}, {"cng", PerKm{40, 20}}}
	for _, st := range strategies {
		fmt.Printf("fare %-5s 8 km: %d tk\n", st.name, st.s.Fare(8))
	}

	cart := &Cart{qty: map[string]int{}}
	for _, cmd := range []Command{AddItem{"tea", 2}, AddItem{"biscuit", 1}} {
		cart.Run(cmd)
		fmt.Printf("cart: %s -> %s\n", cmd.Name(), cart.Summary())
	}
	for range 3 {
		if cart.UndoLast() {
			fmt.Println("cart: undo ->", cart.Summary())
		} else {
			fmt.Println("cart: undo -> nothing to undo")
		}
	}

	chain := Limit{"auto-approved", 500,
		Limit{"approved by supervisor", 5000,
			Limit{"approved by manager", 50000, nil}}}
	for _, amount := range []int{300, 3000, 12000, 80000} {
		fmt.Printf("refund %d tk: %s\n", amount, chain.Approve(amount))
	}
}

All six print exactly this:

fare bike  8 km: 120 tk
fare car   8 km: 340 tk
fare cng   8 km: 200 tk
cart: add tea x2 -> 2 items, 60 tk
cart: add biscuit x1 -> 3 items, 80 tk
cart: undo -> 2 items, 60 tk
cart: undo -> 0 items, 0 tk
cart: undo -> nothing to undo
refund 300 tk: auto-approved
refund 3000 tk: approved by supervisor
refund 12000 tk: approved by manager
refund 80000 tk: rejected, needs finance review

Reading the output:

  • Fares: bike 40 + 10×8 = 120, car 100 + 30×8 = 340, CNG 40 + 20×8 = 200. The loop just calls Fare(8) on whichever strategy it holds.
  • Cart: two commands run, then two undos reverse them in last-in-first-out order. The third undo finds an empty history and says so.
  • Refunds: 300 stops at auto, 3,000 at the supervisor, 12,000 at the manager. 80,000 falls off the end of the chain and is rejected for finance review.
  • What each language changed: Go and Python use tiny value types, and Python's Protocol means AddItem never declares that it's a Command. Rust's Command method is called apply, not do, because do is a reserved word; the chain is Option<Box<Limit>>, so the end of the chain is None rather than a null pointer. Java's commands are records and the history is an ArrayDeque used as a stack. C++ owns each command in a unique_ptr, so popping the history destroys the command right after its Undo.

Behaviour patterns in real systems

  • Choose the strategy once, at the edge. Map the ride type to a strategy when the request arrives (a factory, from chapter 7), then pass the strategy in. Business code never sees the ride type again.
  • Middleware is a chain you already use. Auth, rate limiting, logging and recovery in web frameworks are links in a chain. Keep each link small and its order explicit in one place.
  • Commands are great for audit logs and retries. If every action is an object with a name and data, you can log it, store it and replay it (event sourcing builds on this idea).
  • Undo needs care with real side effects. Undoing "add to cart" is easy; undoing "send SMS" isn't. Commands with irreversible effects need a compensating action (send a correction), not a true undo.
  • Don't wrap a single if in a pattern. Two stable branches are fine as an if. Reach for Strategy when variants keep arriving.

Designs that cause problems later

Try each case

Pick the design that will hurt

func (s Car) Fare() { if isBike { ... } }
breaks: The branch moved inside the strategy. Each strategy should only know its own rule.

Pause and think

Marketing wants a night surcharge of 20% on every ride type between 11 p.m. and 6 a.m. Do you add it inside each fare strategy?

Reveal

No. It applies to every strategy, so it shouldn't be copied into each one. Wrap the chosen strategy instead: a NightSurcharge{inner FareStrategy} that calls inner.Fare(km) and adds 20% when the time is in range (with the time passed in, not read from a clock). That's the Decorator pattern from the next chapter: it adds behaviour around a strategy without changing any of them.

Quick quiz

1. What's the difference between Strategy and State?

Answer

Strategy is chosen from outside and usually stays fixed for the job (which fare rule). State changes from inside as the object moves through its lifecycle, and the current state decides how each method behaves.

2. What does a Command object need so that undo works?

Answer

Enough data to perform the action and to reverse it, plus Do and Undo methods. The invoker keeps a history (a stack) of commands that ran.

3. Why is a chain easier to change than one big if/else?

Answer

Each link knows only its own rule and the next link. Adding, removing or reordering steps changes how the chain is built, not the code of the other links.

4. Why do many Go and Rust codebases prefer functions over Template Method?

Answer

Template Method relies on inheritance. Passing the varying steps as functions (or small interfaces) gives the same "fixed outline, variable details" with composition, which those languages favour.

Your challenge: remove, and a regional head

  • Add a RemoveItem command whose Undo puts the item back. After the two adds, run RemoveItem{tea, 1} and print the cart, then do the three undos.
  • Insert a regional head link (approved by regional head, up to 200,000) after the manager.

Before you run it, predict the new cart lines and the last refund line.

You've succeeded when the cart prints cart: remove tea x1 -> 2 items, 50 tk, the undos then print 3 items, 80 tk, 2 items, 60 tk and 0 items, 0 tk, the last refund line is refund 80000 tk: approved by regional head, and no existing command or approver was edited.

In one line

When behaviour varies, give the varying part an interface and move each variant into its own small object: Strategy swaps algorithms, State swaps behaviour with the lifecycle, Command turns actions into undoable records, and a Chain passes a request along until someone can handle it. The code that uses them stops growing branches.

Station stamp

Finish the chapter

Done reading? Press Complete to save your progress. XP is Pro-only — on Pro this chapter is worth +10 XP.

Go Pro to earn XP →