SOLID Principles
SOLID is a set of five design principles that make object-oriented code easier to extend, test, and maintain. Every machine coding interview implicitly evaluates whether your code follows these principles β even if the interviewer never mentions them by name.
Why this matters: SOLID violations are the number one reason candidates get βpoor designβ feedback. If your ParkingLot class handles parking logic, billing, notifications, and reporting, youβve violated at least three of these principles.
Prerequisites
- Basic OOP (classes, interfaces, inheritance)
- Machine Coding Round β context on how these are evaluated
Overview
flowchart LR
S["S - Single Responsibility"]:::client
O["O - Open-Closed"]:::service
L["L - Liskov Substitution"]:::data
I["I - Interface Segregation"]:::client
D["D - Dependency Inversion"]:::service
S --> O
O --> L
L --> I
I --> D
classDef client fill:#4c3a5e,stroke:#818cf8,color:#e2e8f0
classDef service fill:#1a3a2a,stroke:#4ade80,color:#e2e8f0
classDef data fill:#3b3520,stroke:#fbbf24,color:#e2e8f0
S β Single Responsibility Principle
A class should have only one reason to change.
If your class handles both βparking a vehicleβ and βcalculating the bill,β those are two separate reasons to change. Pricing logic changes shouldnβt require touching parking logic.
// VIOLATION: This class has three reasons to change
// 1. Parking logic changes 2. Billing changes 3. Notification changes
public class ParkingLot {
public Ticket park(Vehicle v) { /* find slot, assign */ }
public double calculateBill(Ticket t) {
long hours = Duration.between(t.getEntryTime(), Instant.now()).toHours();
if (t.getVehicleType() == VehicleType.CAR) return hours * 20;
if (t.getVehicleType() == VehicleType.BIKE) return hours * 10;
return hours * 50; // truck
}
public void sendExitSMS(String phone, double amount) {
// SMS gateway integration
}
}
// FIX: Split into focused classes
public class ParkingLot {
private final ParkingStrategy strategy;
public Ticket park(Vehicle v) {
Slot slot = strategy.findSlot(floors, v.getType());
slot.occupy(v);
return new Ticket(v, slot, Instant.now());
}
public Receipt unpark(Ticket t) {
t.getSlot().free();
double amount = billingService.calculate(t);
return new Receipt(t, amount);
}
}
public class BillingService {
private final Map<VehicleType, PricingRule> rules;
public double calculate(Ticket t) {
PricingRule rule = rules.get(t.getVehicleType());
return rule.compute(t.getEntryTime(), Instant.now());
}
}
public class NotificationService {
public void sendExitSMS(String phone, Receipt receipt) { /* ... */ }
}
# VIOLATION: Three responsibilities in one class
class ParkingLot:
def park(self, vehicle):
# parking logic
pass
def calculate_bill(self, ticket):
hours = (datetime.now() - ticket.entry_time).total_seconds() / 3600
rates = {"CAR": 20, "BIKE": 10, "TRUCK": 50}
return hours * rates[ticket.vehicle_type]
def send_exit_sms(self, phone, amount):
# SMS gateway
pass
# FIX: Each class has one job
class ParkingLot:
def __init__(self, strategy: ParkingStrategy, billing: BillingService):
self._strategy = strategy
self._billing = billing
def park(self, vehicle) -> Ticket:
slot = self._strategy.find_slot(self._floors, vehicle.type)
slot.occupy(vehicle)
return Ticket(vehicle, slot)
def unpark(self, ticket) -> Receipt:
ticket.slot.free()
amount = self._billing.calculate(ticket)
return Receipt(ticket, amount)
class BillingService:
def __init__(self, rules: dict):
self._rules = rules
def calculate(self, ticket) -> float:
rule = self._rules[ticket.vehicle_type]
return rule.compute(ticket.entry_time, datetime.now())
class NotificationService:
def send_exit_sms(self, phone: str, receipt: Receipt):
pass
// VIOLATION: Multiple responsibilities
class ParkingLot {
public:
Ticket park(Vehicle* v) { /* ... */ }
double calculateBill(Ticket* t) {
auto hours = duration(t->entryTime, now());
if (t->vehicleType == CAR) return hours * 20;
if (t->vehicleType == BIKE) return hours * 10;
return hours * 50;
}
void sendExitSMS(string phone, double amount) { /* ... */ }
};
// FIX: Separate concerns
class ParkingLot {
unique_ptr<ParkingStrategy> strategy_;
BillingService* billing_;
public:
Ticket park(Vehicle* v) {
auto slot = strategy_->findSlot(floors_, v->getType());
slot->occupy(v);
return Ticket(v, slot);
}
Receipt unpark(Ticket* t) {
t->getSlot()->free();
double amount = billing_->calculate(t);
return Receipt(t, amount);
}
};
class BillingService {
unordered_map<VehicleType, unique_ptr<PricingRule>> rules_;
public:
double calculate(Ticket* t) {
return rules_[t->getVehicleType()]->compute(t->getEntryTime());
}
};
O β Open-Closed Principle
Classes should be open for extension but closed for modification.
Adding a new vehicle type or pricing strategy should not require editing existing, tested code. You extend behavior by adding new classes, not by adding else if branches.
// VIOLATION: Adding a new discount type requires modifying this method
public class DiscountCalculator {
public double apply(Order order, String discountType) {
if (discountType.equals("PERCENTAGE")) {
return order.getTotal() * 0.10;
} else if (discountType.equals("FLAT")) {
return 50.0;
} else if (discountType.equals("BUY_ONE_GET_ONE")) {
// complex logic...
return order.getCheapestItem().getPrice();
}
// Every new discount = edit this class
return 0;
}
}
// FIX: New discounts are new classes -- existing code untouched
public interface DiscountStrategy {
double calculate(Order order);
}
public class PercentageDiscount implements DiscountStrategy {
private final double rate;
public PercentageDiscount(double rate) { this.rate = rate; }
@Override
public double calculate(Order order) {
return order.getTotal() * rate;
}
}
public class FlatDiscount implements DiscountStrategy {
private final double amount;
public FlatDiscount(double amount) { this.amount = amount; }
@Override
public double calculate(Order order) {
return Math.min(amount, order.getTotal());
}
}
// Adding BOGO? Just add a new class. DiscountCalculator never changes.
public class BuyOneGetOneDiscount implements DiscountStrategy {
@Override
public double calculate(Order order) {
return order.getCheapestItem().getPrice();
}
}
# VIOLATION: must edit this function for every new discount
def apply_discount(order, discount_type: str) -> float:
if discount_type == "PERCENTAGE":
return order.total * 0.10
elif discount_type == "FLAT":
return 50.0
elif discount_type == "BOGO":
return order.cheapest_item.price
return 0
# FIX: Strategy-based, open for extension
from abc import ABC, abstractmethod
class DiscountStrategy(ABC):
@abstractmethod
def calculate(self, order) -> float: ...
class PercentageDiscount(DiscountStrategy):
def __init__(self, rate: float):
self._rate = rate
def calculate(self, order) -> float:
return order.total * self._rate
class FlatDiscount(DiscountStrategy):
def __init__(self, amount: float):
self._amount = amount
def calculate(self, order) -> float:
return min(self._amount, order.total)
# New discount? New class. Existing code untouched.
class BuyOneGetOneDiscount(DiscountStrategy):
def calculate(self, order) -> float:
return order.cheapest_item.price
// VIOLATION: switch grows with every new discount
class DiscountCalculator {
public:
double apply(Order* order, const string& type) {
if (type == "PERCENTAGE") return order->getTotal() * 0.10;
if (type == "FLAT") return 50.0;
return 0;
}
};
// FIX: Polymorphism
class DiscountStrategy {
public:
virtual ~DiscountStrategy() = default;
virtual double calculate(const Order& order) const = 0;
};
class PercentageDiscount : public DiscountStrategy {
double rate_;
public:
explicit PercentageDiscount(double rate) : rate_(rate) {}
double calculate(const Order& order) const override {
return order.getTotal() * rate_;
}
};
class FlatDiscount : public DiscountStrategy {
double amount_;
public:
explicit FlatDiscount(double amount) : amount_(amount) {}
double calculate(const Order& order) const override {
return min(amount_, order.getTotal());
}
};
L β Liskov Substitution Principle
Subtypes must be substitutable for their base types without breaking correctness.
If code works with a Bird reference, replacing it with a Penguin (which canβt fly) should not crash or produce nonsensical results. If it does, the hierarchy is wrong.
// VIOLATION: Penguin breaks the contract of Bird
public class Bird {
public void fly() { System.out.println("Flying..."); }
}
public class Penguin extends Bird {
@Override
public void fly() {
throw new UnsupportedOperationException("Penguins can't fly!");
}
}
// Any code doing bird.fly() now blows up with Penguin
public void migrateBirds(List<Bird> birds) {
for (Bird b : birds) b.fly(); // BOOM on Penguin
}
// FIX: Separate capabilities into interfaces
public interface Bird {
String getName();
}
public interface Flyable {
void fly();
}
public class Sparrow implements Bird, Flyable {
public String getName() { return "Sparrow"; }
public void fly() { System.out.println("Sparrow flying"); }
}
public class Penguin implements Bird {
public String getName() { return "Penguin"; }
// No fly() -- Penguin never promises to fly
}
// Migration only takes Flyable birds
public void migrateBirds(List<Flyable> birds) {
for (Flyable b : birds) b.fly(); // Safe -- only flyable birds here
}
# VIOLATION: Square breaks Rectangle's contract
class Rectangle:
def __init__(self, width, height):
self._width = width
self._height = height
def set_width(self, w):
self._width = w
def set_height(self, h):
self._height = h
def area(self):
return self._width * self._height
class Square(Rectangle):
def set_width(self, w):
self._width = w
self._height = w # Silently changes height too!
def set_height(self, h):
self._width = h
self._height = h
# This function breaks with Square
def stretch_width(rect: Rectangle):
rect.set_width(rect._width * 2)
# Expects area to double, but Square quadruples it
# FIX: Don't force Square into Rectangle hierarchy
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self) -> float: ...
class Rectangle(Shape):
def __init__(self, width: float, height: float):
self.width = width
self.height = height
def area(self) -> float:
return self.width * self.height
class Square(Shape):
def __init__(self, side: float):
self.side = side
def area(self) -> float:
return self.side * self.side
// VIOLATION: Square overrides Rectangle in a way that breaks expectations
class Rectangle {
protected:
int width_, height_;
public:
virtual void setWidth(int w) { width_ = w; }
virtual void setHeight(int h) { height_ = h; }
int area() const { return width_ * height_; }
};
class Square : public Rectangle {
public:
void setWidth(int w) override { width_ = w; height_ = w; }
void setHeight(int h) override { width_ = h; height_ = h; }
};
// FIX: Separate hierarchy
class Shape {
public:
virtual ~Shape() = default;
virtual int area() const = 0;
};
class Rectangle : public Shape {
int width_, height_;
public:
Rectangle(int w, int h) : width_(w), height_(h) {}
int area() const override { return width_ * height_; }
};
class Square : public Shape {
int side_;
public:
explicit Square(int s) : side_(s) {}
int area() const override { return side_ * side_; }
};
I β Interface Segregation Principle
Clients should not be forced to depend on methods they donβt use.
A fat interface that forces implementors to write empty or throwing methods is a design smell. Split it into focused interfaces.
// VIOLATION: Robot is forced to implement eat() and sleep()
public interface Worker {
void work();
void eat();
void sleep();
}
public class Robot implements Worker {
public void work() { /* welding */ }
public void eat() { /* ??? */ throw new UnsupportedOperationException(); }
public void sleep() { /* ??? */ throw new UnsupportedOperationException(); }
}
// FIX: Split into focused interfaces
public interface Workable {
void work();
}
public interface Feedable {
void eat();
}
public interface Restable {
void sleep();
}
public class Human implements Workable, Feedable, Restable {
public void work() { /* ... */ }
public void eat() { /* ... */ }
public void sleep() { /* ... */ }
}
public class Robot implements Workable {
public void work() { /* welding -- no fake eat/sleep */ }
}
# VIOLATION: Printer forced to implement fax and scan
class MultiFunctionDevice(ABC):
@abstractmethod
def print(self, doc): ...
@abstractmethod
def scan(self, doc): ...
@abstractmethod
def fax(self, doc): ...
class BasicPrinter(MultiFunctionDevice):
def print(self, doc): pass # works
def scan(self, doc): raise NotImplementedError # forced stub
def fax(self, doc): raise NotImplementedError # forced stub
# FIX: Granular interfaces
class Printable(ABC):
@abstractmethod
def print(self, doc): ...
class Scannable(ABC):
@abstractmethod
def scan(self, doc): ...
class Faxable(ABC):
@abstractmethod
def fax(self, doc): ...
class BasicPrinter(Printable):
def print(self, doc):
# Only implements what it can actually do
pass
class OfficeMachine(Printable, Scannable, Faxable):
def print(self, doc): pass
def scan(self, doc): pass
def fax(self, doc): pass
// VIOLATION: fat interface
class IWorker {
public:
virtual void work() = 0;
virtual void eat() = 0;
virtual void sleep() = 0;
virtual ~IWorker() = default;
};
class Robot : public IWorker {
public:
void work() override { /* welding */ }
void eat() override { /* meaningless */ }
void sleep() override { /* meaningless */ }
};
// FIX: Segregated interfaces
class IWorkable {
public:
virtual void work() = 0;
virtual ~IWorkable() = default;
};
class IFeedable {
public:
virtual void eat() = 0;
virtual ~IFeedable() = default;
};
class Robot : public IWorkable {
public:
void work() override { /* welding */ }
// No eat(), no sleep()
};
class Human : public IWorkable, public IFeedable {
public:
void work() override { /* ... */ }
void eat() override { /* ... */ }
};
D β Dependency Inversion Principle
High-level modules should not depend on low-level modules. Both should depend on abstractions.
Your OrderService should not directly instantiate MySQLRepository. It should depend on a Repository interface. This enables testing with mocks and swapping implementations without touching business logic.
// VIOLATION: High-level OrderService depends on concrete MySQL class
public class OrderService {
private MySQLOrderRepository repo = new MySQLOrderRepository(); // tight coupling
public void placeOrder(Order order) {
repo.save(order); // Can't test without MySQL, can't switch to Postgres
}
}
// FIX: Depend on abstraction, inject implementation
public interface OrderRepository {
void save(Order order);
Optional<Order> findById(String id);
}
public class MySQLOrderRepository implements OrderRepository {
public void save(Order order) { /* MySQL logic */ }
public Optional<Order> findById(String id) { /* ... */ }
}
public class InMemoryOrderRepository implements OrderRepository {
private final Map<String, Order> store = new HashMap<>();
public void save(Order order) { store.put(order.getId(), order); }
public Optional<Order> findById(String id) { return Optional.ofNullable(store.get(id)); }
}
// High-level module depends on interface, not concrete class
public class OrderService {
private final OrderRepository repo; // abstraction
public OrderService(OrderRepository repo) { // injected
this.repo = repo;
}
public void placeOrder(Order order) {
repo.save(order); // Works with MySQL, Postgres, or InMemory
}
}
# VIOLATION: direct dependency on concrete notification channel
class OrderService:
def __init__(self):
self._emailer = SMTPEmailer() # tight coupling
def place_order(self, order):
# ... business logic ...
self._emailer.send(order.user_email, "Order placed!")
# FIX: depend on abstraction
class NotificationChannel(ABC):
@abstractmethod
def notify(self, recipient: str, message: str): ...
class EmailNotifier(NotificationChannel):
def notify(self, recipient, message):
# SMTP logic
pass
class SMSNotifier(NotificationChannel):
def notify(self, recipient, message):
# Twilio logic
pass
class OrderService:
def __init__(self, notifier: NotificationChannel): # injected
self._notifier = notifier
def place_order(self, order):
# ... business logic ...
self._notifier.notify(order.user_contact, "Order placed!")
// VIOLATION: OrderService directly creates MySQLRepo
class OrderService {
MySQLOrderRepo repo_; // concrete dependency
public:
void placeOrder(const Order& order) {
repo_.save(order); // untestable without MySQL
}
};
// FIX: Depend on interface, inject via constructor
class IOrderRepository {
public:
virtual void save(const Order& order) = 0;
virtual optional<Order> findById(const string& id) = 0;
virtual ~IOrderRepository() = default;
};
class OrderService {
unique_ptr<IOrderRepository> repo_;
public:
explicit OrderService(unique_ptr<IOrderRepository> repo)
: repo_(std::move(repo)) {}
void placeOrder(const Order& order) {
repo_->save(order); // works with any implementation
}
};
When to Use vs When to Avoid
| Principle | Apply When | Donβt Over-Apply When |
|---|---|---|
| SRP | Class has 200+ lines or handles unrelated concerns | Youβre splitting a 20-line class into 5 files |
| OCP | New types/variants are expected in the future | The set of options is permanently fixed (2 branches) |
| LSP | Building inheritance hierarchies | Using simple composition (no polymorphism) |
| ISP | Implementors are leaving methods empty or throwing | Interface has 2-3 cohesive methods all clients use |
| DIP | You need testability or swappable implementations | Itβs a simple utility with no reason to mock |
Interview Questions
-
βGive me a real example of SRP violation and how youβd fix it.β β Use the ParkingLot example above: billing + parking + notification in one class.
-
βHow does OCP relate to the Strategy pattern?β β Strategy is OCP in action. New algorithms are new classes; existing code stays closed.
-
βWhatβs a Liskov violation youβve seen in practice?β β The classic:
Stack extends Vectorin Javaβs standard library. Stack isnβt really a Vector β you can callget(index)on a Stack, which breaks stack semantics. -
βWhen is dependency injection overkill?β β For simple utilities that will never have multiple implementations (e.g., a
MathUtilsclass), injecting via interface adds ceremony with no benefit. -
βHow do SOLID principles interact with each other?β β SRP and ISP both push toward smaller units. OCP and DIP both push toward depending on abstractions. Theyβre complementary, not independent.
See It in Action
| Parking Lot | Splitwise | Vending Machine | Rate Limiter |