Limited time: AI code review, hints, mock interviews, whiteboard analysis, and all Pro features are unlocked. Enroll
⏱️ 14 min read

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


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

  1. β€œ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.

  2. β€œHow does OCP relate to the Strategy pattern?” – Strategy is OCP in action. New algorithms are new classes; existing code stays closed.

  3. β€œWhat’s a Liskov violation you’ve seen in practice?” – The classic: Stack extends Vector in Java’s standard library. Stack isn’t really a Vector – you can call get(index) on a Stack, which breaks stack semantics.

  4. β€œWhen is dependency injection overkill?” – For simple utilities that will never have multiple implementations (e.g., a MathUtils class), injecting via interface adds ceremony with no benefit.

  5. β€œ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

Free system design + DSA prep. If it helped you crack an interview, consider supporting.

SensAI SensAI
Beta
Listening...
Tap mic to stop voice mode

Shape what we build next

Every piece of feedback is read by the team and directly influences our roadmap.

What type of feedback?

Install SystemCraft

Add to your home screen for instant access, offline reading, and a distraction-free experience.

Offline reading Faster loads No browser tabs App-like feel

Unlock AI Features

One click to activate - no payment, no credit card. Just sign in and you're in.

AI code review and hints
SensAI chat assistant
AI mock interviews
Whiteboard analysis
100% free during early access