On this page
What you need first
This lesson uses ideas from these earlier lessons. Read them first if any idea is new to you.
01Why interviewers ask this
In a machine-coding round, you write a small program, and then the interviewer adds a new requirement (lesson 105). The new requirement is often one of three kinds. "Add another way to work this out." "Tell someone when this changes." "It must behave differently depending on its status."
A weak answer grows an if and elif chain with each new requirement. Another weak answer uses a big name from a book and builds many classes that nobody needs. A strong answer sees the shape of the change, adds one small class, and can say what it costs.
At SDE-1 level you should write each pattern on a small example. At SDE-2 level you should pick the right one from the wording of the requirement and keep the code small. At SDE-3 level you should also say when not to use one, and how an observer can fail.
02The idea in plain words
A design pattern is a name for a shape of code that solves a problem that comes again and again. It is not a library. You write it fresh each time. The name lets you say in one word what you did. In lesson 106 the Discount classes already had one such shape. This lesson names three. We use one food order for all three.
Strategy: swap how one job is done
A maps app offers "fastest", "shortest" and "avoid tolls". The trip is the same. Only the way of working out the route changes. You tap another choice and the app itself does not change.
Strategy works the same way. Put each way of doing one job in its own class. All the classes have the same method. The caller holds one of them and calls that method. To change the way, you pass in another object. A code smell is a sign in the code that a better shape may help. The smell that points to Strategy is a chain of if kind == ... checks in one method. Strategy is lesson 106's open-closed and dependency-inversion rules at work.
Here is the delivery fee. The first class is an abstract class: it lists a method and does not fill it in.
from abc import ABC, abstractmethod
class FeeRule(ABC):
@abstractmethod
def fee(self, subtotal, km):
"""Return the delivery fee in rupees."""
class FlatFee(FeeRule):
def __init__(self, amount):
self.amount = amount
def fee(self, subtotal, km):
return self.amount
class PerKmFee(FeeRule):
def __init__(self, per_km):
self.per_km = per_km
def fee(self, subtotal, km):
return self.per_km * kmclass FreeAbove(FeeRule):
def __init__(self, minimum, otherwise):
self.minimum, self.otherwise = minimum, otherwise
def fee(self, subtotal, km):
return 0 if subtotal >= self.minimum else self.otherwiseThe same shape fits the rate limiter of lesson 56. Fixed window, sliding window and token bucket are three strategies with one method, allow.
Observer: tell everyone who asked
Think of a teacher's WhatsApp group for her students. The teacher posts once. Every member gets the message. The teacher does not know what each student does with it, and a student can leave the group.
Observer works the same way. The thing that changes is the subject. It keeps a list of observers. Each observer has one method. The subject calls that method on every observer when something happens. To subscribe is to add yourself to the list. The subject does not know what an observer does with the news.
class StatusObserver(ABC):
@abstractmethod
def on_status(self, order_id, old, new):
"""Called after the order moves from old to new."""
class CustomerSms(StatusObserver):
def on_status(self, order_id, old, new):
print(f"SMS to customer: order {order_id} is now {new}")Three things can go wrong:
- Observers run one after another, inside the call that changed the subject. One slow observer delays all the others and the caller.
- One observer may fail. It must not stop the rest, so the subject catches the error for each observer and continues with the next one.
- An observer that is no longer needed must unsubscribe (leave the list). Otherwise the subject keeps it in memory and keeps calling it.
Observer looks like publish and subscribe in lesson 58, but it is smaller. It lives inside one program, in memory, and the subject waits for each observer. If the program stops, the news is lost. For slow work, or work that must survive a restart, use a queue (lesson 58).
State: behaviour that depends on the status
On a phone, one power button does two jobs. When the screen is off, it wakes the phone. When the phone is on, it locks the phone. The same press does a different thing, because of the state.
In lesson 66 a state machine is a list of states and the legal moves between them. The State pattern is one way to write that in code. Each state is its own class. The object holds the current state object. It passes each action to that object. The state object does the action and returns the next state. If the state does not allow the action, it raises an error. The smell that points to State is the same if status == ... chain repeated in many methods.
The parent class below raises NotAllowed for every action. A state allows an action by filling it in. So every caller must expect NotAllowed from any state.
class NotAllowed(Exception):
pass
COOKING_CHARGE = 80
class State:
name = ""
def advance(self):
raise NotAllowed(f"cannot move on when {self.name}")
def cancel(self, paid):
raise NotAllowed(f"cannot cancel when {self.name}")
class Placed(State):
name = "PLACED"
def advance(self):
return Preparing()
def cancel(self, paid):
return paid, Cancelled()class Preparing(State):
name = "PREPARING"
def advance(self):
return OutForDelivery()
def cancel(self, paid):
return paid - COOKING_CHARGE, Cancelled()
class OutForDelivery(State):
name = "OUT_FOR_DELIVERY"
def advance(self):
return Delivered()
class Delivered(State):
name = "DELIVERED"
class Cancelled(State):
name = "CANCELLED"Here cancel does different work in each state. When the order is placed, the refund is full. When it is being prepared, the refund is less a cooking charge. After that, cancel gives an error. This is why classes are worth it here. If the states differed only in which moves are legal, a small table of legal moves would be simpler.
One order that uses all three
class Order:
def __init__(self, order_id, subtotal, km, fee_rule):
self.order_id = order_id
self.paid = subtotal + fee_rule.fee(subtotal, km)
self.state = Placed()
self.observers = []
def subscribe(self, observer):
self.observers.append(observer)
def unsubscribe(self, observer):
self.observers.remove(observer)
def advance(self):
self._move_to(self.state.advance())
def cancel(self):
refund, next_state = self.state.cancel(self.paid)
self._move_to(next_state)
return refundThe same class goes on with the method that moves to a new state and tells the observers:
def _move_to(self, new_state):
old = self.state.name
self.state = new_state
for observer in self.observers:
try:
observer.on_status(self.order_id, old, new_state.name)
except Exception as error:
print(f"observer failed: {error}")The fee rule is a Strategy that the order is given. The observers list makes the order a subject. The state object decides what advance and cancel do.
Stuck on something? The AI tutor has read this lesson. Ask it with a free account
03Pause and predict
Pause. Guess first.
A teammate must add three things to the food order. (1) A new way to work out the delivery fee. (2) A message to the rider app whenever the status changes. (3) A rule that an order cannot be cancelled after the rider has the food. For each one, which pattern fits, and what is the smallest new piece of code?
Write your guess before you read on. The answer is in the worked example.
04Worked example
Assumptions (made up to teach with):
- An order of 2 biryani at Rs 250 and 1 lassi at Rs 60. The subtotal is 2 x 250 + 60 = Rs 560.
- The distance is 6 km.
- The cooking charge is Rs 80.
The fee, three ways.
FlatFee(40): Rs 40, so the customer pays 560 + 40 = Rs 600.PerKmFee(8): 8 x 6 = Rs 48, so the customer pays 560 + 48 = Rs 608.FreeAbove(500, 40): the subtotal 560 is at least 500, so the fee is Rs 0 and the customer pays Rs 560.
We use FreeAbove(500, 40), so the order is created as Order(1, 560, 6, FreeAbove(500, 40)) and paid is 560. We subscribe three observers. The first is the SMS to the customer. The second is a board in the kitchen, written like CustomerSms, which prints Kitchen board: order 1, PLACED -> PREPARING. The third is a counter that adds 1 for each move.
class Counter(StatusObserver):
def __init__(self):
self.moves = 0
def on_status(self, order_id, old, new):
self.moves += 1A normal run. The order calls advance(). The state Placed returns Preparing. The order tells its three observers. The output is:
SMS to customer: order 1 is now PREPARINGKitchen board: order 1, PLACED -> PREPARING
The counter prints nothing and now holds 1. Now the customer cancels. The state Preparing returns a refund of 560 - 80 = Rs 480, and the next state is Cancelled. The observers hear about the move again, and the counter holds 2. A second cancel() raises NotAllowed: cannot cancel when CANCELLED, because the class Cancelled fills in no actions. If the customer had cancelled while the order was still Placed, the refund would be the full Rs 560.
The three new requirements (the predict box).
- A new fee rule is a new child class of
FeeRule. This is Strategy.Orderdoes not change. - The rider app is a new observer class, plus one
subscribecall. This is Observer.Orderdoes not change.
class RiderApp(StatusObserver):
def on_status(self, order_id, old, new):
print(f"Rider app: order {order_id} is now {new}")- No cancel after pickup needs no new code.
OutForDeliverydoes not fill incancel, so the parent class refuses it. This is State. A test confirms it. Order 2 moves twice. The rider app printsRider app: order 2 is now PREPARINGandRider app: order 2 is now OUT_FOR_DELIVERY. Thencancel()raisesNotAllowed: cannot cancel when OUT_FOR_DELIVERY.
So the number of changed lines in Order is 0 for all three.
A failing observer. We subscribe an observer that raises an error ("sms down") and then the counter. The order prints observer failed: sms down, and the counter still counts the move. Without the try and except, the counter would never hear about it.
05Trade-offs
- Strategy solves adding a new way without editing old code. It costs one class for each way and one more step when you read. I would use it when there are two real ways, or the interviewer says another is coming. I would use a plain function when there is one way and no change is expected.
- Observer solves the subject having to know who listens. It costs calls that a reader cannot see, a fixed order of calls, and a risk that a slow or failing observer hurts the others. I would change to a queue when the work is slow or must survive a restart.
- State solves the same
ifchain repeated in many methods. It costs many small classes. I would change to a table of legal moves when the states differ only in which moves are allowed.
06Common misconceptions
"Patterns make code better, so I should use all three in every problem." A pattern is a cost. It adds classes. Use one when its shape appears in the requirement. A rate limiter with one fixed rule does not need Strategy.
"Observer is the same as a queue." An observer is called at once, in the same program, and the caller waits for it. If the program stops, the news is lost. A queue holds the message and a worker handles it later (lesson 58).
"Strategy and State are the same, because both swap a class." In Strategy the caller chooses the way, and it stays until the caller changes it. In State the object moves itself to the next state as actions happen. The actions allowed change with the state.
07How to say it in the interview
Say it like this
I look for the shape in the requirement. A rule that can change becomes a Strategy, something that must tell others becomes an Observer, and behaviour that depends on status becomes a State. Each is a small new class, so a new requirement does not edit working code.
08Check your understanding
- Name the three patterns. For each one, name the smell in code that points to it.
- An order has a subtotal of Rs 450 and a distance of 3 km. (a) What does the customer pay with
FreeAbove(500, 40)and withPerKmFee(8)? (b) WithFreeAbove(500, 40), what is the refund if the customer cancels while the order isPREPARING, with a cooking charge of Rs 80? - An order has 3 observers: an SMS that takes 2,000 ms, an email that takes 5 ms and a counter that takes 5 ms. They run in that order. How long does
advance()take, and what do you change?
09Answers
Show the answers
- Strategy: a chain of
if kind == ...checks that grows with each new way. Observer: the subject's code calls SMS, email and other code by name, so each new listener edits it. State: the sameif status == ...chain repeated in many methods. - (a)
FreeAbove: 450 is below 500, so the fee is Rs 40 and the customer pays 450 + 40 = Rs 490.PerKmFee(8): 8 x 3 = Rs 24, so the customer pays 450 + 24 = Rs 474. (b) The customer paid Rs 490, so the refund is 490 - 80 = Rs 410. - The observers run one after another: 2,000 + 5 + 5 = 2,010 ms, so the caller waits about 2 seconds. Make the SMS observer only put a message in a queue (lesson 58), which takes a few milliseconds. A worker then sends the SMS.
10Sources
- The three patterns are named in Gamma, Helm, Johnson and Vlissides, "Design Patterns: Elements of Reusable Object-Oriented Software" (Addison-Wesley, 1994). Summary: https://en.wikipedia.org/wiki/Design_Patterns (opens in a new tab) (checked 20 Oct 2026).
- Abstract classes in Python: https://docs.python.org/3/library/abc.html (opens in a new tab) (checked 20 Oct 2026). The code in this lesson was run with Python 3.
- The food order, the fees, the prices and the cooking charge are made up for teaching.
Try it now, no sign-up
- Play Level 1: one server and 100 users. Free, no sign-up.
- Read today’s Drill: one real situation, answered in about 7 minutes.
More practice, in the app (free account)
Related lessons and practice
Ask the AI tutor about this lesson
The tutor has read this lesson. Ask in your own words, any time. Free account, 20 questions a day.
Reading never needs an account. The account is for the tutor, the quiz, your notes and your progress.
All lessons in Module 17: Low-level design and machine coding