SOLID principles in practice
SOLID is five principles for object-oriented design. They're not gospel — sometimes a pragmatic violation is the right answer — but they're the shared vocabulary for design conversations. You should know what each means, when it applies, and when violating it is OK.
S: Single Responsibility Principle
Every class should have one reason to change. Or: every class should have one well-defined responsibility.
Bad: a User class that has methods save() (persistence), sendEmail() (communication), validatePassword() (security), calculateLoyaltyPoints() (business logic). Four reasons to change; four responsibilities.
Good: User holds user data and core invariants. UserRepository persists. EmailService sends emails. PasswordValidator validates. LoyaltyCalculator computes points.
Caveat: don't over-fragment. UserNameValidator + UserEmailValidator + UserPhoneValidator is too granular. Group related responsibilities.
O: Open-Closed Principle
Classes should be open for extension but closed for modification. Adding new behavior shouldn't require editing existing classes — only adding new ones.
Bad: a PaymentProcessor class with a giant processPayment() method containing if (type == "card") ... else if (type == "upi") ... else if (type == "wallet"). Adding a new method requires editing this class.
Good: a PaymentMethod interface; implementations CardPayment, UpiPayment, WalletPayment. Each is independent. Adding crypto payment = new CryptoPayment class. No existing code modified.
This is essentially the Strategy pattern in service of OCP.
L: Liskov Substitution Principle
Subclasses should be substitutable for their parent classes without breaking the program. If Bird has a fly() method, every subclass must be able to fly — or the abstraction is wrong (Penguin can't fly).
This is about behavioral subtyping, not just interface compatibility. The classic violation: Rectangle with setWidth() and setHeight(); Square extends Rectangle forces width = height. Now any code that assumes Rectangle's behavior (setting width independently) breaks when given a Square.
Fix: don't use inheritance just because two things look similar. Sometimes the right model is two unrelated classes implementing a common interface.
I: Interface Segregation Principle
Clients shouldn't be forced to depend on interfaces they don't use. Split fat interfaces into smaller, focused ones.
Bad: an Animal interface with fly(), swim(), walk(), burrow(). A class for Fish must implement fly() (returning nothing useful) just to satisfy the interface.
Good: separate Flyable, Swimmable, Walkable interfaces. Each animal implements what it actually does.
D: Dependency Inversion Principle
High-level modules should depend on abstractions, not concrete implementations. Low-level modules should also depend on abstractions.
Bad: NotificationService class directly instantiates SmtpEmailSender. To swap email senders (or unit-test the service), you must edit the class.
Good: NotificationService depends on an EmailSender interface. SmtpEmailSender is one implementation; SendGridEmailSender is another; MockEmailSender for tests. The actual sender is injected (Dependency Injection).
DI containers (Spring, Guice) automate this. Even without a container, manual DI by passing collaborators through constructors achieves it.
A practical sanity check
You don't need to recite SOLID in an interview. You need to apply it. Symptoms that one of them is being violated:
- "To add this new feature, I need to modify 5 existing classes" → likely OCP violation
- "This class has 800 lines and 30 methods" → likely SRP violation
- "I can't unit-test this because it depends on the database" → likely DIP violation
- "This subclass throws UnsupportedOperationException for some inherited methods" → likely LSP violation