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
// Bad: UserManager class has too many responsibilitiespublic 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
// Good: Each class has a single responsibilitypublic 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
// Bad: Must modify the class to add new payment methodspublic 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
// Good: Can add new payment methods without modifying existing codepublic 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
// Bad: Square changes the behavior of Rectangle in unexpected wayspublic 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
// Good: Use composition or separate hierarchiespublic 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 issuespublic 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
// Bad: All workers must implement methods they don't needpublic 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
// Good: Split into multiple specific interfacespublic 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
// Bad: High-level class depends directly on low-level classespublic 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
// Good: Both high-level and low-level depend on abstractionpublic 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 databasespublic 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