Almost every candidate can rattle off what SOLID stands for. Far fewer can look at a 40-line class and tell you which letter it breaks. That second skill is the one interviewers score, especially in a low-level design round where they slide a messy class across the shared editor and ask you to make it better.
The acronym is a warm-up. Answer “what does SOLID stand for” in fifteen seconds and move on, because the scoring questions come right after: here’s a class, what’s wrong with it, how would you change it. All five principles push toward the same outcome, which is code you can change in one spot without a chain reaction everywhere else.
Where you meet these questions matters. In a low-level design or machine-coding round, common at Amazon, Uber, and most Indian product companies, you get 60 to 90 minutes to build something like a parking lot or a rate limiter, and SOLID is the lens the interviewer uses to judge how you carved up the classes. In a shorter screen you might get the acronym plus one spot-the-violation snippet. Reciting definitions gets you to the floor and no further.
The checklist the interviewer is grading you against
Every principle has a matching code smell, and spotting the smell fast is most of the game. This is the mapping worth holding in your head when a class lands in front of you.
| Principle | What it asks of your code | Code smell that signals a violation | Typical refactor |
|---|---|---|---|
| Single Responsibility (SRP) | A class has one reason to change | One class computes, persists, and notifies | Split each concern into its own class |
| Open/Closed (OCP) | Extend behavior without editing existing code | An if/elif chain that grows a branch per new type | Move behavior into subtypes behind a common method |
| Liskov Substitution (LSP) | A subtype works anywhere its base type does | A subclass overrides a method and breaks the parent’s contract | Drop the false is-a and share an interface instead of inheriting |
| Interface Segregation (ISP) | Clients depend only on methods they use | A class implements interface methods with NotImplementedError | Split the fat interface into role-specific ones |
| Dependency Inversion (DIP) | Policy and detail both depend on an abstraction | A class instantiates its own database or API client | Depend on an interface and inject the concrete type |
Single responsibility: the class that does three jobs
SRP says a class should have one reason to change. The interview version hands you a class that computes something, saves it, and emails someone, then asks why that hurts.
class Invoice:
def __init__(self, items):
self.items = items
def total(self):
return sum(i.price * i.qty for i in self.items)
def save(self, db):
db.execute("INSERT INTO invoices ...")
def send_email(self, smtp):
smtp.send("[email protected]", f"You owe {self.total()}")
Three reasons to change live in one place: the pricing rule, the database schema, and the mail format. Swap the SMTP library and you are editing the same file that holds your money math. Pull them apart.
class Invoice:
def __init__(self, items):
self.items = items
def total(self):
return sum(i.price * i.qty for i in self.items)
class InvoiceRepository:
def save(self, invoice, db):
db.execute("INSERT INTO invoices ...")
class InvoiceMailer:
def send(self, invoice, smtp):
smtp.send("[email protected]", f"You owe {invoice.total()}")
Narrate it while you type. “The Invoice knows how to price itself. Persistence and notification are different concerns, so they get their own classes.” That sentence scores more than the mechanical refactor does.
Open/closed: the if-else that grows every sprint
Open for extension, closed for modification. In real code this is the branch statement that gains one more case every time product invents a type. The tell is a function you keep reopening to add cases.
def area(shape):
if shape.kind == "circle":
return 3.14159 * shape.r * shape.r
elif shape.kind == "square":
return shape.side * shape.side
Add a triangle and you reopen area(), risking the circle and square code that already worked. Push the behavior into the shapes so each owns its own formula.
class Circle:
def __init__(self, r):
self.r = r
def area(self):
return 3.14159 * self.r * self.r
class Square:
def __init__(self, side):
self.side = side
def area(self):
return self.side * self.side
A triangle is now a new class, not an edit to old ones. The catch, and interviewers like when you raise it yourself, is that this only pays off if you picked the right extension point. Abstract the wrong axis and you just relocate the pain.
Liskov substitution: the subclass that lies
A subtype has to work anywhere its parent does. The canonical trap is Square inheriting from Rectangle, and it is canonical because it looks completely reasonable until you write one test.
class Rectangle:
def __init__(self, w, h):
self.w = w
self.h = h
def set_width(self, w):
self.w = w
def set_height(self, h):
self.h = h
def area(self):
return self.w * self.h
class Square(Rectangle):
def set_width(self, w):
self.w = w
self.h = w
def set_height(self, h):
self.w = h
self.h = h
A square is a rectangle in geometry class. In code it breaks any function that sets width and height independently and expects them to stay put.
def stretch(rect):
rect.set_width(5)
rect.set_height(4)
assert rect.area() == 20 # holds for Rectangle, fails for Square
Hand stretch() a Square and the assertion blows up, because setting the height quietly reset the width. The fix is to stop pretending the is-a relationship holds. Square and Rectangle each implement a Shape interface and neither inherits the other. If you keep one LSP example in your back pocket, keep this one.
Interface segregation: the interface nobody fully implements
Clients should not be forced to depend on methods they never call. The smell is a class satisfying an interface by stubbing methods with NotImplementedError.
class Worker:
def work(self): ...
def eat(self): ...
class RobotWorker(Worker):
def work(self):
return "welding"
def eat(self):
raise NotImplementedError # robots skip lunch
The robot is stuck carrying a method that means nothing for it, and any code calling eat() on a Worker now has to know which concrete type it holds. Cut the fat interface along the lines that actually vary.
class Workable:
def work(self): ...
class Eatable:
def eat(self): ...
class HumanWorker(Workable, Eatable): ...
class RobotWorker(Workable): ...
Nothing implements a method it cannot honor. Smaller interfaces also make test doubles trivial to write, which is a payoff worth naming out loud.
Dependency inversion: wiring the database into your logic
High-level policy should not depend on low-level detail; both depend on an abstraction. The interview shape is a service that constructs its own database and is therefore impossible to test without a real one.
class OrderService:
def __init__(self):
self.db = MySQLDatabase() # hard-wired
def place(self, order):
self.db.save(order)
OrderService is welded to MySQL. Want to unit test place() without a live database, or move to Postgres later? You are back in the service file. Depend on an abstraction and pass the concrete type in.
class OrderRepository: # the abstraction
def save(self, order): ...
class OrderService:
def __init__(self, repo):
self.repo = repo
def place(self, order):
self.repo.save(order)
The service now takes any OrderRepository, so a test hands it an in-memory fake and production hands it the MySQL one. This is the idea behind every dependency-injection container, so if the conversation drifts toward Spring or ASP.NET, that link is worth drawing.
The follow-ups that separate juniors from seniors
Strong interviewers rarely stop at the clean refactor. They probe whether you understand the cost. The questions that tend to do the sorting sound like:
- “This class passes review today. What future change request would force you to touch three unrelated methods?”
- “You have an interface with exactly one implementer. Why does it exist?”
- “Where in this design does a new payment provider slot in without editing existing code?”
- “Can I substitute this subclass everywhere the base type appears? Show me it doesn’t break.”
The trap answer treats SOLID as five rules you apply on sight. Run all five through a 50-line script and you get five files and three interfaces for something that fit on one screen. Every indirection is a jump the next reader has to follow, and an interface with a single implementation is usually a bet on a future that may never arrive.
Treat the principles as pressure that builds rather than boxes to tick. When a class starts collecting unrelated reasons to change, split it. When you are editing the same branch statement for the fourth time, invert it. Reach for the abstraction before the second real use case shows up and you end up with a factory that builds exactly one thing. Saying that in the room is what the acronym recall can’t show: judgment about when the principle earns its keep.
Drill the patterns next:
