SOLID is an acronym for five design principles introduced by Robert C. Martin ("Uncle Bob") that guide developers in writing clean, maintainable, and robust object-oriented code. These principles are considered the gold standard in professional software engineering and are a very common topic in technical interviews.
"A class should have one, and only one, reason to change."
Every class should be responsible for a single piece of functionality. If a class handles user authentication AND email sending AND logging, it has three reasons to change. A bug fix in the email logic could accidentally break the authentication logic.
// BAD: One class doing everything
class UserService {
void registerUser() { /* ... */ }
void sendWelcomeEmail() { /* ... */ }
void logRegistration() { /* ... */ }
}
// GOOD: Each class has a single responsibility
class UserRegistration { void register() { /* ... */ } }
class EmailService { void sendWelcome() { /* ... */ } }
class AuditLogger { void log() { /* ... */ } }
"Software entities should be open for extension, but closed for modification."
You should be able to add new functionality to a system by writing new code, not by modifying existing, tested, and working code. This is typically achieved through Inheritance and Interfaces.
// GOOD: Adding a new shape does NOT require modifying existing code
interface Shape { double area(); }
class Circle implements Shape { /* ... */ }
class Rectangle implements Shape { /* ... */ }
class Triangle implements Shape { /* ... */ } // NEW! No existing code modified.
double totalArea(List<Shape> shapes) {
return shapes.stream().mapToDouble(Shape::area).sum();
}
"Objects of a superclass should be replaceable with objects of its subclasses without breaking the application."
If a function accepts a Bird object, it should work perfectly fine if you pass it a Sparrow (a subclass). The subclass must not weaken the guarantees of the parent class.
The classic violation is the Square-Rectangle problem. A Square IS-A Rectangle mathematically, but if Rectangle.setWidth() and Rectangle.setHeight() are independent operations, a Square overriding setWidth() to also set the height breaks the parent's contract.
"A client should never be forced to implement an interface that it doesn't use."
Fat interfaces with dozens of methods force implementing classes to provide stub implementations for methods they don't care about. Instead, break large interfaces into smaller, more specific ones.
// BAD: A fat interface
interface Worker {
void work();
void eat();
void sleep();
}
// A Robot implements Worker but CANNOT eat or sleep!
// GOOD: Segregated interfaces
interface Workable { void work(); }
interface Eatable { void eat(); }
interface Sleepable { void sleep(); }
class HumanWorker implements Workable, Eatable, Sleepable { /* ... */ }
class RobotWorker implements Workable { /* Only implements what it needs */ }
"High-level modules should not depend on low-level modules. Both should depend on abstractions."
Instead of a PaymentService class directly creating and using a StripeGateway object (tight coupling), both should depend on a PaymentGateway interface. This allows you to easily swap Stripe for PayPal without touching the PaymentService code.
// High-level module depends on an ABSTRACTION, not a concrete class
class PaymentService {
private PaymentGateway gateway; // Interface, not Stripe!
PaymentService(PaymentGateway gateway) {
this.gateway = gateway; // Injected via constructor
}
void processPayment(double amount) {
gateway.charge(amount);
}
}