Ask a candidate to define the Single Responsibility Principle in an LLD round and nine times out of ten they nail it. Ask them to point at the exact line in their own design that breaks it, and the confident tone disappears. That gap, reciting a definition versus catching the violation in your own code, is what most SOLID prep misses.
SOLID isn't five rules you memorize the week before a design round at Amazon or Meta. It's a lens for reading code you already wrote and asking what happens when this changes. Landing that design round in the first place usually starts earlier: a resume that gets you shortlisted, which is what LastRoundAI's resume builder is for, and more roles in the pipeline to begin with, which is what our Auto-Apply tool handles. Below are the five principles the way interviewers actually probe them: the smell that signals a violation, a short before/after fix, and where each one quietly resurfaces later in the interview as a named design pattern.
Why interviewers keep coming back to this
Low-level design interviews test two things at once: can you model a domain (a parking lot, a rate limiter, a vending machine), and can you defend the shape of your classes when the interviewer changes a requirement mid-conversation. SOLID is the vocabulary for that second half. When the interviewer says "now add a new payment type" and your design needs a rewrite instead of an extension, that's an Open/Closed violation playing out live, whether or not either of you names it that way.
Robert C. Martin coined the five principles in the early 2000s, bundling ideas that had circulated in the object-oriented programming community for a decade into one acronym. The canonical definitions still hold up two decades later: a class should have "one and only one reason to change," and objects should be "open for extension but closed for modification," per DigitalOcean's summary of Martin's original SOLID definitions. Interviewers rarely ask you to recite these five sentences. They ask you to notice, live, when your own design breaks one of them.
The five principles, the smell, and the fix
Each one below follows the same shape: what the violation looks like in real code, why it hurts later, and the smallest fix that clears it. Skip straight to the one you're shakiest on if you already know the rest.
Single Responsibility Principle
The smell: a class name that needs "and" to describe honestly, or a class that changes for two unrelated reasons, say a formatting tweak and a database migration both touching the same file in the same sprint.
Before, an Invoice class that calculates totals, formats a receipt, and writes to a database, all at once:
class Invoice:
def calculate_total(self):
...
def print_receipt(self):
...
def save_to_database(self):
...
After, split by reason to change. Pricing logic, presentation, and persistence become three classes that can be tested and modified on their own schedule:
class Invoice:
def calculate_total(self):
...
class InvoicePrinter:
def print_receipt(self, invoice):
...
class InvoiceRepository:
def save(self, invoice):
...
Notice the trade-off already showing up: three small classes instead of one convenient one. That's fine here, because pricing, formatting, and storage genuinely change on different timelines. It isn't always fine. More on that below.
Open/Closed Principle
The smell: an if/elif chain that branches on a type field, and grows by one branch every time someone adds a new case.
class AreaCalculator:
def area(self, shape):
if shape.kind == "circle":
return 3.14159 * shape.radius ** 2
elif shape.kind == "square":
return shape.side ** 2
# every new shape means editing this method
After: push area() onto each shape. The calculator calls shape.area() without knowing or caring about the concrete type.
class Shape:
def area(self):
raise NotImplementedError
class Circle(Shape):
def area(self):
return 3.14159 * self.radius ** 2
class Square(Shape):
def area(self):
return self.side ** 2
class AreaCalculator:
def total(self, shapes):
return sum(s.area() for s in shapes)
Adding a Triangle now means adding a class, not editing AreaCalculator. That's the whole principle: extend by adding, not by re-opening a method that already works and already passed its tests.
Liskov Substitution Principle
The smell: a subclass that overrides a method to do less than the parent promised, or throws where the parent quietly wouldn't.
class Rectangle:
def set_width(self, w):
self.width = w
def set_height(self, h):
self.height = h
class Square(Rectangle):
def set_width(self, w):
self.width = self.height = w
def set_height(self, h):
self.width = self.height = h
Any code written against Rectangle assumes width and height move independently. Hand it a Square instead and that assumption breaks silently: rect.set_width(5); rect.set_height(4) should give an area of 20, and on a Square it gives 16. Liskov's own formulation is more abstract, properties provable about a supertype must hold for its subtypes, but this is what it looks like once it hits a test suite.
The fix isn't a smarter Square. It's admitting Square shouldn't extend Rectangle at all, and giving both a shared interface with no mutable setters, only area():
class Shape:
def area(self):
raise NotImplementedError
class Rectangle(Shape):
def __init__(self, w, h):
self.w, self.h = w, h
def area(self):
return self.w * self.h
class Square(Shape):
def __init__(self, side):
self.side = side
def area(self):
return self.side ** 2
Interface Segregation Principle
The smell: an interface with a method most implementers stub out with "not supported" or a bare exception.
class Worker:
def work(self):
raise NotImplementedError
def eat_lunch(self):
raise NotImplementedError
class RobotWorker(Worker):
def work(self):
...
def eat_lunch(self):
raise NotImplementedError # robots don't eat lunch
After: split into two narrow interfaces, and implement only what genuinely applies.
class Workable:
def work(self):
raise NotImplementedError
class Eater:
def eat_lunch(self):
raise NotImplementedError
class HumanWorker(Workable, Eater):
def work(self):
...
def eat_lunch(self):
...
class RobotWorker(Workable):
def work(self):
...
In my experience running technical mock rounds, this is the principle candidates flub most, not Liskov. Most engineers have implemented an interface someone else designed. Fewer have ever had to design one wide enough to hurt somebody downstream.
Dependency Inversion Principle
The smell: a high-level class that says new next to a specific vendor or framework name, instead of accepting an abstraction from the outside.
class MySQLUserRepository:
def save(self, user):
...
class UserService:
def __init__(self):
self.repo = MySQLUserRepository()
def register(self, user):
self.repo.save(user)
After: depend on an abstraction, and inject the concrete implementation from outside the class.
class UserRepository:
def save(self, user):
raise NotImplementedError
class MySQLUserRepository(UserRepository):
def save(self, user):
...
class UserService:
def __init__(self, repo: UserRepository):
self.repo = repo
def register(self, user):
self.repo.save(user)
Swap MySQLUserRepository for an in-memory fake in a unit test, or for a Postgres one in production, and UserService never changes. This is the principle most directly responsible for why dependency injection frameworks got invented in the first place.
SOLID is why design patterns work
Every principle above is also the mechanism behind a named design pattern, which is exactly why interviewers move from "explain SOLID" to "now use the Strategy pattern" inside the same 45 minutes. Refactoring.Guru describes a pattern as "a general concept for solving a particular problem," not a block of code you copy, per its explanation of what a design pattern is. SOLID is the set of concerns those templates were built to satisfy.
Strategy and the Factory family exist mostly to satisfy Open/Closed: swap behavior by adding a class instead of editing a branch. Dependency Inversion is the whole reason constructor injection and the Adapter pattern exist. Interface Segregation shows up directly in the Observer pattern, where each subscriber only implements the update() it actually cares about. Once you can explain SRP, OCP, and DIP cleanly, half the standard pattern catalog stops looking like new material and starts looking like SOLID with a name attached to it. Our design patterns interview questions guide walks through the dozen patterns that come up most often, and our low-level design interview questions post has the parking-lot, elevator, and rate-limiter prompts where these five principles actually get tested end to end.
The trade-off nobody puts on the slide
Here's a genuinely debatable opinion: most mid-level engineers who struggle in an LLD round don't fail for violating SOLID. They fail for over-applying it. I've watched candidates turn a two-class parking lot problem into eleven interfaces because they wanted to prove they knew all five letters. That's not senior judgment. That's anxiety wearing a design pattern.
Every one of these principles buys flexibility at the cost of indirection. A UserRepository interface with exactly one implementation and no planned second one is ceremony, not a safeguard. The skill interviewers are actually grading for isn't "did you apply SOLID." It's "did you apply the right amount of it for a system that might need a second payment provider next quarter, and not one interface more." I don't have a clean formula for where that line sits. It moves depending on the company, the level you're interviewing for, and honestly the interviewer's own taste.
The part that doesn't come through in a written before/after is defending that trade-off out loud, under time pressure, when an interviewer pushes back on why you split a class into three. That's the specific muscle LastRoundAI's mock interview practice is built to train on technical rounds: a live AI interviewer asks the follow-up, scores your answer across six dimensions including structure and specificity, and flags when your explanation ran long or got vague instead of landing on the actual trade-off. Reading this post gets you the vocabulary. Saying it out loud under a clock is the real interview.
Two questions interviewers actually ask
Q: Which SOLID principle is hardest to keep once a codebase is a few years old?
A: Open/Closed, by a wide margin. Every principle degrades under real deadlines, but OCP erodes quietly. Someone adds "just one more if-branch" to a class that used to be closed for modification, nobody notices because the existing tests still pass, and eighteen months later that class has forty branches. SRP violations tend to get caught in code review because the class visibly does too much. OCP violations hide inside a method that looks reasonable one line at a time.
Q: How do you explain Liskov Substitution without the Rectangle/Square example, since every interviewer has already heard it?
A: Use payment methods instead. If RefundableCard and GiftCard both extend PaymentMethod, and GiftCard.refund() throws because gift cards can't be refunded, any code written against PaymentMethod that calls refund() breaks the moment someone hands it a GiftCard. Same violation, different domain, and it signals you understand the principle rather than the mnemonic that goes with it.
None of this replaces reading actual code. The fastest way to internalize where these five break is to take a design you already shipped, real or an old take-home, and ask which principle it would fail first, or run a quick check with LastRoundAI's free tools if you don't have an old design handy. Most engineers can name the principle they'd violate before they can name the one they follow well. That's worth sitting with before your next LLD round, not five definitions memorized the night before.

