Home/SOLID Principles

SOLID Principles

Master the five fundamental principles of object-oriented design for writing maintainable, scalable software

S - Single Responsibility Principle (SRP)

A class should have only one reason to change, meaning it should have only one job or responsibility.

❌ Bad Example: Multiple Responsibilities

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// Bad: UserManager class has too many responsibilities
public class UserManager {
// Responsibility 1: User data management
public void createUser(String name, String email) {
// Create user logic
}
// Responsibility 2: Email sending
public void sendWelcomeEmail(String email) {
// Email sending logic
System.out.println("Sending email to: " + email);
}
// Responsibility 3: Database operations
public void saveToDatabase(User user) {
// Database save logic
System.out.println("Saving to database: " + user);
}
// Responsibility 4: Report generation
public void generateUserReport(User user) {
// Report generation logic
System.out.println("Generating report for: " + user);
}
}

✅ Good Example: Separated Responsibilities

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// Good: Each class has a single responsibility
public class User {
private String name;
private String email;
public User(String name, String email) {
this.name = name;
this.email = email;
}
// Getters and setters
}
public class UserService {
// Only manages user business logic
public User createUser(String name, String email) {
return new User(name, email);
}
}
public class EmailService {
// Only handles email operations
public void sendWelcomeEmail(String email) {
System.out.println("Sending welcome email to: " + email);
}
}
public class UserRepository {
// Only handles database operations
public void save(User user) {
System.out.println("Saving user to database: " + user);
}
}
public class ReportService {
// Only handles report generation
public void generateUserReport(User user) {
System.out.println("Generating report for: " + user);
}
}

O - Open/Closed Principle (OCP)

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

❌ Bad Example: Modifying Existing Code

java
1
2
3
4
5
6
7
8
9
10
11
12
13
// Bad: Must modify the class to add new payment methods
public class PaymentProcessor {
public void processPayment(String paymentType, double amount) {
if (paymentType.equals("CREDIT_CARD")) {
System.out.println("Processing credit card payment: $" + amount);
} else if (paymentType.equals("PAYPAL")) {
System.out.println("Processing PayPal payment: $" + amount);
} else if (paymentType.equals("BITCOIN")) {
// Need to modify this class to add Bitcoin support!
System.out.println("Processing Bitcoin payment: $" + amount);
}
}
}

✅ Good Example: Extension Without Modification

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// Good: Can add new payment methods without modifying existing code
public interface PaymentMethod {
void processPayment(double amount);
}
public class CreditCardPayment implements PaymentMethod {
@Override
public void processPayment(double amount) {
System.out.println("Processing credit card payment: $" + amount);
}
}
public class PayPalPayment implements PaymentMethod {
@Override
public void processPayment(double amount) {
System.out.println("Processing PayPal payment: $" + amount);
}
}
public class BitcoinPayment implements PaymentMethod {
@Override
public void processPayment(double amount) {
System.out.println("Processing Bitcoin payment: $" + amount);
}
}
public class PaymentProcessor {
public void processPayment(PaymentMethod paymentMethod, double amount) {
// No need to modify this class when adding new payment methods!
paymentMethod.processPayment(amount);
}
}

L - Liskov Substitution Principle (LSP)

Objects of a superclass should be replaceable with objects of its subclasses without breaking the application. Subtypes must be substitutable for their base types.

❌ Bad Example: Violation of LSP

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// Bad: Square changes the behavior of Rectangle in unexpected ways
public class Rectangle {
protected int width;
protected int height;
public void setWidth(int width) {
this.width = width;
}
public void setHeight(int height) {
this.height = height;
}
public int getArea() {
return width * height;
}
}
public class Square extends Rectangle {
@Override
public void setWidth(int width) {
// Violates LSP: Unexpected behavior
this.width = width;
this.height = width; // Forces height to equal width
}
@Override
public void setHeight(int height) {
// Violates LSP: Unexpected behavior
this.width = height; // Forces width to equal height
this.height = height;
}
}
// This will fail for Square (violates LSP)
public void testRectangle(Rectangle rect) {
rect.setWidth(5);
rect.setHeight(4);
// Expected: 20, but for Square: 16!
assert rect.getArea() == 20; // Fails for Square
}

✅ Good Example: Proper Substitution

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// Good: Use composition or separate hierarchies
public interface Shape {
int getArea();
}
public class Rectangle implements Shape {
protected int width;
protected int height;
public Rectangle(int width, int height) {
this.width = width;
this.height = height;
}
public void setWidth(int width) {
this.width = width;
}
public void setHeight(int height) {
this.height = height;
}
@Override
public int getArea() {
return width * height;
}
}
public class Square implements Shape {
private int side;
public Square(int side) {
this.side = side;
}
public void setSide(int side) {
this.side = side;
}
@Override
public int getArea() {
return side * side;
}
}
// Now both can be used through Shape interface without issues
public void printArea(Shape shape) {
System.out.println("Area: " + shape.getArea());
}

I - Interface Segregation Principle (ISP)

Clients should not be forced to depend on interfaces they don't use. Many specific interfaces are better than one general-purpose interface.

❌ Bad Example: Fat Interface

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
// Bad: All workers must implement methods they don't need
public interface Worker {
void work();
void eat();
void sleep();
void getPaid();
void attendMeeting();
}
public class HumanWorker implements Worker {
@Override
public void work() {
System.out.println("Human is working");
}
@Override
public void eat() {
System.out.println("Human is eating");
}
@Override
public void sleep() {
System.out.println("Human is sleeping");
}
@Override
public void getPaid() {
System.out.println("Human gets paid");
}
@Override
public void attendMeeting() {
System.out.println("Human attends meeting");
}
}
public class RobotWorker implements Worker {
@Override
public void work() {
System.out.println("Robot is working");
}
@Override
public void eat() {
// Robots don't eat! Forced to implement unnecessary method
throw new UnsupportedOperationException("Robots don't eat");
}
@Override
public void sleep() {
// Robots don't sleep! Forced to implement unnecessary method
throw new UnsupportedOperationException("Robots don't sleep");
}
@Override
public void getPaid() {
// Robots don't get paid! Forced to implement unnecessary method
throw new UnsupportedOperationException("Robots don't get paid");
}
@Override
public void attendMeeting() {
// Most robots don't attend meetings
throw new UnsupportedOperationException("Robots don't attend meetings");
}
}

✅ Good Example: Segregated Interfaces

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
// Good: Split into multiple specific interfaces
public interface Workable {
void work();
}
public interface Eatable {
void eat();
}
public interface Sleepable {
void sleep();
}
public interface Payable {
void getPaid();
}
public interface MeetingAttendable {
void attendMeeting();
}
public class HumanWorker implements Workable, Eatable, Sleepable, Payable, MeetingAttendable {
@Override
public void work() {
System.out.println("Human is working");
}
@Override
public void eat() {
System.out.println("Human is eating");
}
@Override
public void sleep() {
System.out.println("Human is sleeping");
}
@Override
public void getPaid() {
System.out.println("Human gets paid");
}
@Override
public void attendMeeting() {
System.out.println("Human attends meeting");
}
}
public class RobotWorker implements Workable {
// Only implements what it needs!
@Override
public void work() {
System.out.println("Robot is working");
}
}

D - Dependency Inversion Principle (DIP)

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.

❌ Bad Example: Tight Coupling

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Bad: High-level class depends directly on low-level classes
public class MySQLDatabase {
public void save(String data) {
System.out.println("Saving to MySQL: " + data);
}
}
public class UserService {
// Tightly coupled to MySQL
private MySQLDatabase database = new MySQLDatabase();
public void saveUser(String userData) {
// Cannot easily switch to another database
database.save(userData);
}
}
// If we want to switch to PostgreSQL, we must modify UserService!
public class PostgreSQLDatabase {
public void save(String data) {
System.out.println("Saving to PostgreSQL: " + data);
}
}

✅ Good Example: Dependency on Abstraction

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
// Good: Both high-level and low-level depend on abstraction
public interface Database {
void save(String data);
}
public class MySQLDatabase implements Database {
@Override
public void save(String data) {
System.out.println("Saving to MySQL: " + data);
}
}
public class PostgreSQLDatabase implements Database {
@Override
public void save(String data) {
System.out.println("Saving to PostgreSQL: " + data);
}
}
public class MongoDBDatabase implements Database {
@Override
public void save(String data) {
System.out.println("Saving to MongoDB: " + data);
}
}
public class UserService {
// Depends on abstraction, not concrete implementation
private Database database;
// Dependency injection
public UserService(Database database) {
this.database = database;
}
public void saveUser(String userData) {
// Works with any database implementation!
database.save(userData);
}
}
// Usage - can easily switch databases
public class Main {
public static void main(String[] args) {
// Use MySQL
UserService mysqlService = new UserService(new MySQLDatabase());
mysqlService.saveUser("user1");
// Switch to PostgreSQL without changing UserService
UserService postgresService = new UserService(new PostgreSQLDatabase());
postgresService.saveUser("user2");
// Switch to MongoDB without changing UserService
UserService mongoService = new UserService(new MongoDBDatabase());
mongoService.saveUser("user3");
}
}

Benefits of SOLID Principles

Maintainability

Code is easier to understand, modify, and maintain over time

Flexibility

Easy to extend functionality without breaking existing code

Testability

Loosely coupled code is easier to unit test with mocks

Reusability

Well-designed components can be reused in different contexts

Scalability

Systems built on SOLID principles scale better

Reduced Bugs

Changes in one part don't unexpectedly affect other parts

Interview Tips

  • 💡Be able to explain each principle with real-world examples
  • 💡Know how to identify SOLID violations in code
  • 💡Practice refactoring code to follow SOLID principles
  • 💡Understand that SOLID principles work together, not in isolation
  • 💡Be prepared to discuss trade-offs (over-engineering vs. under-engineering)
  • 💡Connect SOLID principles to design patterns you know