Hirestack
Hirestack
Chapter 2

SOLID principles in practice

📖 2 min read · 6 sections · May 2026

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: