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
A machine-coding round is a timed round in which you write a small working program. Then the interviewer adds a new requirement. In Tradeoff's round, part of the score is extensibility: how easily new rules fit in. Does the new rule fit as a small new piece, or does it break old code?
A weak answer puts everything in one big class. It works for the first version. Then every change means editing working code in many places. A strong answer splits the code so that a change touches one small place. It can also say why.
At SDE-1 level you should split the code into sensible classes. At SDE-2 level you should make the new requirement a small, tidy change. At SDE-3 level you should also say when a principle is not worth its cost.
02The idea in plain words
Think of the sockets in a house. Every appliance has the same plug, so you can add a new fan without opening the wall. Each appliance does one job. A plug that fits the socket works in it, and it has only the pins it needs. The wiring depends on the socket shape, not on one brand of fan.
SOLID is five rules for code that behaves like this. Each letter is one rule. The code below is Python because it is short. The ideas are the same in Java, JavaScript and C++. We use one food-ordering example. Here is the first version, which has problems:
class OrderService:
def place(self, items, discount_type, phone):
total = sum(price * qty for price, qty in items)
if discount_type == "PERCENT":
total = total * 90 // 100
elif discount_type == "FLAT":
if total >= 500:
total = total - 50
print(f"SMS to {phone}: order total {total}")
with open("orders.txt", "a") as f:
f.write(f"{phone},{total}\n")
return totalAn order has 2 biryani at Rs 250 and 1 lassi at Rs 60. The total is 2 x 250 + 60 = Rs 560. With "PERCENT" (10% off) the method returns 560 x 90 // 100 = 504 (// divides and drops the fraction). With "FLAT" (Rs 50 off above Rs 500) it returns 510.
S: single responsibility
A class should have one reason to change. A reason to change is one group of rules that someone may ask you to change. The method above has three: the price rules, the message to the customer, and the place where orders are saved. A change to one can break the other two. Give each job its own class:
class PriceCalculator:
def subtotal(self, items):
return sum(price * qty for price, qty in items)
class SmsNotifier:
def send(self, phone, text):
print(f"SMS to {phone}: {text}")
class FileOrderStore:
def save(self, phone, total):
with open("orders.txt", "a") as f:
f.write(f"{phone},{total}\n")A class can have many methods. What matters is that all of them change for the same reason.
O: open for extension, closed for modification
You should be able to add a new behaviour without editing code that already works. The if and elif chain for discounts breaks this: each new discount edits the method. The fix is an abstract class: a class that lists methods but does not fill them in. Other classes fill them in. Python's abc module makes the list a promise, because you cannot create an object from a class that has an unfilled method.
from abc import ABC, abstractmethod
class Discount(ABC):
@abstractmethod
def apply(self, total):
"""Return the total after the discount."""
class PercentOff(Discount):
def __init__(self, percent):
self.percent = percent
def apply(self, total):
return total * (100 - self.percent) // 100
class FlatOff(Discount):
def __init__(self, amount, minimum):
self.amount, self.minimum = amount, minimum
def apply(self, total):
return total - self.amount if total >= self.minimum else totalPercentOff(10).apply(560) gives 504. FlatOff(50, 500).apply(560) gives 510. A new discount is a new class. No old line changes.
L: Liskov substitution
Code that works with a parent class must also work with each child class, with no surprise. A child may not refuse a job that the parent promises. In everyday words, "a cash payment is a payment" is not enough. The child must also behave like the parent. Here a child breaks the promise:
class Payment:
def pay(self, amount): ...
def refund(self, amount): ...
class UpiPayment(Payment):
def refund(self, amount):
return f"Rs {amount} sent back to UPI"
class CashOnDelivery(Payment):
def refund(self, amount):
raise NotImplementedError("cash cannot be refunded")
def cancel_order(payment, amount):
return payment.refund(amount)cancel_order(UpiPayment(), 460) works. cancel_order(CashOnDelivery(), 460) crashes. Three rules keep a child safe. It must not ask for more than the parent asks. It must not give less than the parent promises. It must not raise a new kind of error that the parent never raises. The fix here: do not promise what you cannot do. Make Payment promise only pay. Add a second class, RefundablePayment(Payment), that also promises refund. UPI extends RefundablePayment. Cash extends only Payment. Now cancel_order is meant only for RefundablePayment. A cash payment has no refund method at all, so the mistake is easy to see when you write the code.
I: interface segregation
A class should not be forced to write methods it does not use. An interface is the list of methods that a class promises. Suppose Notifier lists send_sms, send_email and send_push. A WhatsApp class needs only one of them, and must write two empty or crashing methods. Keep each interface small. The order code needs only one thing: tell the customer.
class Notifier(ABC):
@abstractmethod
def send(self, phone, text): ...D: dependency inversion
Rules of the business, such as "price an order and tell the customer", are high-level code. Details, such as one SMS company or one file, are low-level code. The high-level code should depend on abstract classes, not on the details. Do not create the detail inside the class. Pass it in through the constructor. This is called dependency injection.
class OrderService:
def __init__(self, discount, notifier, store):
self.discount, self.notifier, self.store = discount, notifier, store
def place(self, items, phone):
subtotal = sum(price * qty for price, qty in items)
total = self.discount.apply(subtotal)
self.notifier.send(phone, f"order total {total}")
self.store.save(phone, total)
return totalDependency inversion is the rule. Dependency injection is one way to follow it. The store is an abstract class too, like Notifier, with one method, save. This class does not know which discount, which message channel or which store it gets. A test can pass in a store that keeps rows in memory, with no file.
How to spot each one
| Rule | A smell you can see | A fix |
|---|---|---|
| S | One class changes for many different reasons | One class for each job |
| O | A new case means a new if branch in old code | An abstract class and one new class per case |
| L | A child class raises an error or ignores a promise | Promise less in the parent |
| I | A class has empty or crashing methods | Smaller interfaces |
| D | A class makes its own SMS or file object inside | Pass it in through the constructor |
Stuck on something? The AI tutor has read this lesson. Ask it with a free account
03Pause and predict
Pause. Guess first.
In the first version of
OrderService.place, a teammate wants three changes. (a) A new discount. (b) WhatsApp instead of SMS. (c) A database instead of a file. How many of the three need an edit inside that one method? Which principle says what to do?
Write your guess before you read on. The answer is in the worked example.
04Worked example
The interviewer adds a new requirement: "20% off, but at most Rs 100 off. And send the message on WhatsApp, not SMS." The order is the same. It has 2 biryani at Rs 250 and 1 lassi at Rs 60, which is Rs 560.
Assumption: the first version is the method in the plain-words section. It has 11 lines. The new version is the classes shown above.
First version.
- (a) needs a new
elifbranch: 2 new lines insideplace. - (b) changes the
printline. - (c) would change the
openandwritelines.
So all three changes need an edit inside the same method. That answers the predict box. S says to split the three jobs. Then O (for the discount) and D (for the message and the store) let each job change without an edit in the others. For the new requirement you add 2 lines and change 1 of the 11 lines, in a method that also saves files. Each edit can break the price, the message or the saving. To test it, you must write to a real file.
New version. We add two classes and change one line: the line that builds the service. Before, it passed PercentOff(10), an SMS notifier and a file store.
class CappedPercentOff(Discount):
def __init__(self, percent, cap):
self.percent, self.cap = percent, cap
def apply(self, total):
return total - min(total * self.percent // 100, self.cap)
class WhatsAppNotifier(Notifier):
def send(self, phone, text):
print(f"WhatsApp to {phone}: {text}")For the test we also need a store that keeps rows in memory, with no file. OrderStore is an abstract class with one method, save, like Notifier.
class OrderStore(ABC):
@abstractmethod
def save(self, phone, total): ...
class MemoryOrderStore(OrderStore):
def __init__(self):
self.rows = []
def save(self, phone, total):
self.rows.append((phone, total))
store = MemoryOrderStore()
service = OrderService(CappedPercentOff(20, 100), WhatsAppNotifier(), store)
print(service.place([(250, 2), (60, 1)], "98000"))It prints WhatsApp to 98000: order total 460 and then 460. Now the arithmetic. 20% of 560 = 560 x 20 // 100 = 112. The cap is 100, and min(112, 100) = 100. So the total is 560 - 100 = Rs 460. Before the new requirement, PercentOff(10) gave 504.
OrderService.place has 0 changed lines. Its line count did not move. Only the line that builds the service changed. This is O and D together. The test uses MemoryOrderStore, so it needs no file. It can check that store.rows is [("98000", 460)].
The same order cancelled. The customer paid by UPI, so we refund Rs 460. With the fixed payment classes, cancel_order is written only for RefundablePayment. A cash order has no refund method, so the surprise crash from the L section is gone.
05Trade-offs
- SOLID solves the cost of change: the new requirement took 2 new classes and 1 changed line, and old code stayed safe. It costs more classes, more files and a reader who must follow one more step. I would use it where a change is likely or already asked. I would change to simple code when there is one case and no change is expected.
- An abstract class solves adding a case without editing. It costs a name, a file and an extra jump when you read. I would add one when there are two real cases, or the interviewer has said a change is coming.
- Passing things in solves easy tests and easy swaps. It costs one place that builds all the objects. In a timed round, build that place in the first few minutes and keep it short.
06Common misconceptions
"Single responsibility means a class has only one method." It means one reason to change. PercentOff has __init__ and apply, and both change only when the percent rule changes.
"Open for extension means I may never edit old code." You still fix bugs and change rules that really change. The rule is about adding a new case. Adding a discount should not need an edit in the order flow.
"A cash payment is a payment, so inheritance is fine." In English it is true. In code, CashOnDelivery breaks cancel_order, because it refuses a job that the parent promises. A child class is correct only if every caller of the parent still works.
07How to say it in the interview
Say it like this
Each class has one reason to change, and I pass the discount and the notifier in through the constructor. So a new discount or WhatsApp instead of SMS is a new small class, not an edit inside working code.
08Check your understanding
- What do S, O, L, I and D stand for? Give one plain sentence for each.
- Using
CappedPercentOff(20, 100), what total do you get for an order of (a) Rs 480 and (b) Rs 1,200? - A teammate writes
class ReadOnlyCart(Cart)whereadd_itemraises an error. A functionadd_free_gift(cart)callscart.add_item. What happens when it gets aReadOnlyCart? Which principle is broken, and what do you change?
09Answers
Show the answers
- S: a class has one reason to change. O: add new behaviour without editing old code. L: a child class works wherever its parent works. I: a class is not forced to write methods it does not use, so keep interfaces small. D: high-level code depends on abstract classes, and the details are passed in.
- (a) 20% of 480 = 480 x 20 // 100 = 96. This is below the cap of 100, so the total is 480 - 96 = Rs 384. (b) 20% of 1,200 = 240. This is above the cap, so the discount is 100 and the total is 1,200 - 100 = Rs 1,100.
add_free_giftcrashes, because the child refuses a job that the parent promises. This breaks L. Do not makeReadOnlyCarta child ofCart. Make a small parent, such asCartView, with onlytotal.CartandReadOnlyCartboth extend it, and onlyCarthasadd_item. Thenadd_free_giftaccepts onlyCart.
10Sources
- The five principles were collected by Robert C. Martin in "Design Principles and Design Patterns" (2000). Michael Feathers coined the name SOLID, around 2004. Summary: https://en.wikipedia.org/wiki/SOLID (opens in a new tab) (checked 20 Oct 2026).
- Abstract classes cannot be created until every abstract method is filled in: Python documentation,
abcmodule, 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 example, the prices and the discount rules are made up for teaching. The idea that extensibility is part of the score describes Tradeoff's own machine-coding round, not the rules of any company.
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