Skip to content
intermediatePhase 49 · Low-Level Design

SOLID Principles

Master Single Responsibility, Open-Closed, Liskov, Interface Segregation, Dependency Inversion.

1h 30m
0 problems
Topic Progress0%

Single Responsibility

A class should have only one reason to change. Each class should have one job and one responsibility.

The Principle

SRP: One class = One responsibility = One reason to change

If a class has multiple responsibilities, changes to one
responsibility might break the other responsibility.

Violation Example

// BAD: Multiple responsibilities
class User {
    private String name;
    private String email;
    
    // Responsibility 1: User data
    public String getName() { return name; }
    
    // Responsibility 2: Persistence
    public void save() {
        database.insert(this);
    }
    
    // Responsibility 3: Email
    public void sendEmail(String message) {
        emailService.send(email, message);
    }
    
    // Responsibility 4: Validation
    public boolean validate() {
        return email.contains("@");
    }
}

SRP Applied

// GOOD: Each class has one responsibility
class User {
    private String name;
    private String email;
    
    public String getName() { return name; }
    public String getEmail() { return email; }
}

class UserRepository {
    public void save(User user) {
        database.insert(user);
    }
    
    public User findById(String id) {
        return database.query(id);
    }
}

class EmailService {
    public void sendEmail(String to, String message) {
        // Email logic
    }
}

class UserValidator {
    public boolean validate(User user) {
        return user.getEmail().contains("@");
    }
}

Benefits

  1. Easier to understand: Each class does one thing
  2. Easier to test: Test one responsibility at a time
  3. Easier to modify: Changes are isolated
  4. Better reuse: Focused classes are more reusable

Warning Signs

  • Class name contains "And" or "Or"
  • Class has more than 7-10 methods
  • Changing one feature breaks unrelated features
  • Class has multiple types of data (user data + email + database)

Open-Closed

Software entities should be open for extension but closed for modification. You should be able to add new behavior without changing existing code.

The Principle

OCP: Add new features by adding new code, NOT by modifying existing code

Open for extension: New behavior can be added
Closed for modification: Existing code doesn't change

Violation Example

// BAD: Must modify existing code to add new discount type
class DiscountCalculator {
    public double calculateDiscount(String type, double amount) {
        if (type.equals("PERCENTAGE")) {
            return amount * 0.1;
        } else if (type.equals("FIXED")) {
            return 10.0;
        } else if (type.equals("BUY_ONE_GET_ONE")) {
            return amount / 2;
        }
        // Must modify this class for every new discount type!
    }
}

OCP Applied

// GOOD: New discount types added without modifying existing code
public interface DiscountStrategy {
    double calculate(double amount);
}

public class PercentageDiscount implements DiscountStrategy {
    public double calculate(double amount) {
        return amount * 0.1;
    }
}

public class FixedDiscount implements DiscountStrategy {
    public double calculate(double amount) {
        return 10.0;
    }
}

public class BuyOneGetOneFree implements DiscountStrategy {
    public double calculate(double amount) {
        return amount / 2;
    }
}

// Calculator doesn't change when new discounts are added
public class DiscountCalculator {
    public double calculateDiscount(DiscountStrategy strategy, double amount) {
        return strategy.calculate(amount);
    }
}

// Adding a new discount: just create a new class!
public class LoyaltyDiscount implements DiscountStrategy {
    public double calculate(double amount) {
        return amount * 0.2;
    }
}

OCP Patterns

Pattern How it applies OCP
Strategy Swap algorithms without modifying context
Observer Add listeners without modifying subject
Decorator Add behavior without modifying original
Template Method Override steps without changing algorithm
Plugin Architecture Add plugins without modifying core

Warning Signs

  • Adding a new feature requires modifying existing switch/if-else chains
  • Every new type requires changing a factory or calculator
  • "If we add one more, we need to change..." is said often

Liskov Substitution

Subtypes must be substitutable for their base types without altering the correctness of the program.

The Principle

LSP: If S is a subtype of T, then objects of type T can be
replaced with objects of type S without breaking the program.

In plain English: Subclasses should honor the parent's contract.

Violation Example

// BAD: Ostrich violates Bird's contract
public class Bird {
    public void fly() {
        System.out.println("Flying");
    }
}

public class Ostrich extends Bird {
    @Override
    public void fly() {
        throw new UnsupportedOperationException("Ostriches can't fly!");
    }
}

// This code breaks with Ostrich
void makeBirdFly(Bird bird) {
    bird.fly(); // Throws exception for Ostrich!
}

LSP Applied

// GOOD: Proper abstraction
class Bird { }

class FlyingBird extends Bird {
    public void fly() {
        System.out.println("Flying");
    }
}

class Ostrich extends Bird {
    // Ostrich IS-A Bird but NOT a FlyingBird
    public void run() {
        System.out.println("Running fast");
    }
}

// Now the code is safe
void makeBirdFly(FlyingBird bird) {
    bird.fly(); // Only accepts flying birds
}

Real-World LSP

// Rectangle/Square problem
class Rectangle {
    protected int width, height;
    public void setWidth(int w) { width = w; }
    public void setHeight(int h) { height = h; }
    public int getArea() { return width * height; }
}

// Square violates LSP!
class Square extends Rectangle {
    @Override
    public void setWidth(int w) {
        width = w; height = w; // Changes both!
    }
}

// This test fails for Square
Rectangle r = new Square();
r.setWidth(5);
r.setHeight(4);
// Expected area: 20, Actual: 16 (Square changed both!)

Warning Signs

  • Subclass throws UnsupportedOperationException
  • Subclass overrides change parent behavior
  • is-instance-of checks needed before using subtypes
  • Subclass requires different input validation

Interface Segregation

Clients should not be forced to depend on interfaces they don't use. Prefer small, specific interfaces over large, general ones.

The Principle

ISP: Many specific interfaces > One general interface

Don't force classes to implement methods they don't need.

Violation Example

// BAD: Fat interface forces unnecessary implementations
public interface Worker {
    void work();
    void eat();
    void sleep();
    void takeBreak();
}

// Robot is forced to implement eat() and sleep()!
class Robot implements Worker {
    public void work() { /* Robot works */ }
    public void eat() { /* Robots don't eat! */ }     // Forced
    public void sleep() { /* Robots don't sleep! */ }  // Forced
    public void takeBreak() { /* Robots don't take breaks! */ }
}

ISP Applied

// GOOD: Segregated interfaces
public interface Workable {
    void work();
}

public interface Feedable {
    void eat();
}

public interface Sleepable {
    void sleep();
}

// Each class implements only what it needs
class Human implements Workable, Feedable, Sleepable {
    public void work() { /* Human works */ }
    public void eat() { /* Human eats */ }
    public void sleep() { /* Human sleeps */ }
}

class Robot implements Workable {
    public void work() { /* Robot works */ }
    // No eat() or sleep() needed!
}

Interface Design Guidelines

Guideline Description
Small 1-3 methods per interface
Focused One concept per interface
Cohesive Methods relate to each other
Stable Don't change frequently

Common Fat Interfaces

// BAD: Too many responsibilities
public interface UserService {
    User getUser(String id);
    void createUser(User user);
    void deleteUser(String id);
    void sendEmail(String to, String msg);
    void generateReport();
    void backupData();
}

// GOOD: Segregated
public interface UserReader {
    User getUser(String id);
}

public interface UserWriter {
    void createUser(User user);
    void deleteUser(String id);
}

public interface UserNotifier {
    void sendEmail(String to, String msg);
}

Warning Signs

  • Interface has more than 7 methods
  • Implementations have empty method bodies
  • Adding methods breaks existing implementations
  • "I don't need this method" is common

Dependency Inversion

High-level modules should not depend on low-level modules. Both should depend on abstractions. Abstractions should not depend on details. Details should depend on abstractions.

The Principle

DIP: Depend on interfaces, not concrete implementations

High-level ──▶ Abstraction ◀── Low-level

Both depend on the abstraction, not on each other.

Violation Example

// BAD: High-level depends on low-level
class OrderService {
    private MySQLDatabase database;  // Depends on concrete class!
    private SmtpEmailService email;  // Depends on concrete class!
    
    public OrderService() {
        this.database = new MySQLDatabase();  // Tight coupling
        this.email = new SmtpEmailService();  // Tight coupling
    }
    
    public void createOrder(Order order) {
        database.save(order);
        email.sendConfirmation(order);
    }
}

DIP Applied

// GOOD: Both depend on abstractions
public interface Database {
    void save(Order order);
    Order findById(String id);
}

public interface EmailService {
    void sendConfirmation(Order order);
}

// High-level module depends on abstractions
class OrderService {
    private Database database;
    private EmailService email;
    
    // Dependencies injected, not created
    public OrderService(Database database, EmailService email) {
        this.database = database;
        this.email = email;
    }
    
    public void createOrder(Order order) {
        database.save(order);
        email.sendConfirmation(order);
    }
}

// Low-level modules implement abstractions
class MySQLDatabase implements Database {
    public void save(Order order) { /* MySQL logic */ }
}

class PostgresDatabase implements Database {
    public void save(Order order) { /* Postgres logic */ }
}

// Can swap implementations without changing OrderService
OrderService service = new OrderService(
    new PostgresDatabase(),  // Changed from MySQL!
    new SmtpEmailService()
);

Dependency Injection

DIP is implemented through dependency injection:

Method Description
Constructor Pass dependencies in constructor
Setter Set dependencies via setters
Interface Provide dependency through interface method

Warning Signs

  • High-level class creates low-level objects with new
  • Swapping implementations requires changing multiple classes
  • Hard-coded class names in import statements
  • Difficult to unit test due to concrete dependencies

Practice Problems

0/3solved
Design SOLID Principles System

Design a scalable SOLID Principles system. Cover high-level architecture, data model, and API design.

Solution
// Complete system design:
// - Functional + Non-functional requirements
// - Capacity estimation
// - Data model (SQL/NoSQL choice)
// - API endpoints
// - Component architecture
// - Scaling strategy
// - Monitoring & reliability
SOLID Principles Scaling

How would you scale SOLID Principles to handle 10x the current load? Identify bottlenecks and solutions.

Solution
// Scaling approach:
// 1. Load balancing
// 2. Database sharding/replication
// 3. Cache layer (Redis)
// 4. CDN for static assets
// 5. Async processing (queues)
// 6. Microservices decomposition
SOLID Principles Failure Modes

Analyze potential failure modes for SOLID Principles and design mitigation strategies.

Solution
// Failure mitigation:
// 1. Redundancy (multi-AZ)
// 2. Circuit breakers
// 3. Retry with backoff
// 4. Dead letter queues
// 5. Health checks
// 6. Graceful degradation

Quiz

1. What does the Single Responsibility Principle state?

Question 1 options

2. How does the Strategy Pattern apply the Open-Closed Principle?

Question 2 options

3. What is a classic LSP violation?

Question 3 options

4. What does Interface Segregation Principle recommend?

Question 4 options

5. What is the key idea of Dependency Inversion?

Question 5 options

Flashcards

Question

What is the Single Responsibility Principle (SRP)?

Answer

A class should have only one reason to change — one job, one responsibility. This makes classes easier to understand, test, and modify.

Question

What is the Open-Closed Principle (OCP)?

Answer

Software entities should be open for extension (add new behavior) but closed for modification (don't change existing code). Achieved through interfaces, strategy, observer, decorator patterns.

Question

What is the Liskov Substitution Principle (LSP)?

Answer

Subtypes must be substitutable for their base types without breaking program correctness. Subclasses must honor the parent's contract.

Question

What is Interface Segregation Principle (ISP)?

Answer

Clients shouldn't be forced to depend on interfaces they don't use. Prefer many small, specific interfaces over one large, general interface.

Question

What is the Dependency Inversion Principle (DIP)?

Answer

High and low-level modules should both depend on abstractions, not on each other. Details should depend on abstractions. Implemented through dependency injection.

Revision Notes

Key Takeaways

  • 1.SRP: Each class should have one responsibility and one reason to change
  • 2.OCP: Add new behavior through extension, not modification of existing code
  • 3.LSP: Subclasses must be usable wherever their parent class is expected
  • 4.ISP: Prefer small, focused interfaces over large, general ones
  • 5.DIP: Depend on abstractions, not concrete implementations

Interview Tips

  • Mention SOLID principles when discussing class design trade-offs
  • Show how Strategy pattern applies OCP in your design
  • Explain LSP when discussing inheritance hierarchies
  • Demonstrate DIP when showing dependency management and testability

Cheat Sheet

SOLID Principles - Cheat Sheet

SRP (Single Responsibility):
One class = One reason to change.
User class + UserRepository + EmailService.

OCP (Open-Closed):
Open for extension, closed for modification.
Add new features by adding new code, not modifying existing.
Use: Strategy, Observer, Decorator, Template Method.

LSP (Liskov Substitution):
Subtypes must be substitutable for base types.
Square/Rectangle problem: Square violates LSP.

ISP (Interface Segregation):
Many specific > One general interface.
1-3 methods per interface. No empty methods.

DIP (Dependency Inversion):
Depend on abstractions, not concrete classes.
Both high and low-level depend on interfaces.
Implement via: Constructor, Setter, Interface injection.

Warning Signs:

  • SRP: Class has "And" in name, >10 methods
  • OCP: Switch/if-else chains for types
  • LSP: UnsupportedOperationException in subclass
  • ISP: Empty method bodies in implementations
  • DIP: new keyword for dependencies