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
- Easier to understand: Each class does one thing
- Easier to test: Test one responsibility at a time
- Easier to modify: Changes are isolated
- 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
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 & reliabilityHow 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 decompositionAnalyze 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 degradationQuiz
1. What does the Single Responsibility Principle state?
2. How does the Strategy Pattern apply the Open-Closed Principle?
3. What is a classic LSP violation?
4. What does Interface Segregation Principle recommend?
5. What is the key idea of Dependency Inversion?
Flashcards
Question
What is the Single Responsibility Principle (SRP)?
Click to reveal answer
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)?
Click to reveal answer
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)?
Click to reveal answer
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)?
Click to reveal answer
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)?
Click to reveal answer
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