SOLID Principles in Software Engineering: A Practical Guide with Java Examples
Writing code that works is relatively easy. Writing code that remains easy to understand, test, modify, and extend as the application grows is much harder. This is where the SOLID principles come in.
SOLID is a collection of five object-oriented design principles introduced and popularized through the work of software engineering pioneers such as Robert C. Martin. These principles help us design software that is easier to maintain and less likely to break when requirements change.
The five principles are:
- S - Single Responsibility Principle
- O - Open/Closed Principle
- L - Liskov Substitution Principle
- I - Interface Segregation Principle
- D - Dependency Inversion Principle
1. Single Responsibility Principle (SRP)
SRP says that a class must have a only one reason to change. This is probably the most misunderstood SOLID principle. It does not mean A class should have only one method.
Instead, it means that a class should have only one responsibility, where the responsibility is closely related for a reason to change.
The problem
Imagine we are building an order management system.
public class OrderService {
public void createOrder(Order order) {
// Save order
}
public void calculateTotal(Order order) {
// Calculate total
}
public void sendEmail(Order order) {
// Send confirmation email
}
public void generateInvoice(Order order) {
// Generate PDF invoice
}
}
This class is doing too many things. It is responsible for:
- Order creation
- Price calculation
- Email communication
- Invoice generation
Now imagine, if the email provider changes we need to modify the order service. Better design would be to separate the responsibilities.
public class OrderService {
private final PricingService pricingService;
private final OrderRepository orderRepository;
public OrderService(
PricingService pricingService,
OrderRepository orderRepository) {
this.pricingService = pricingService;
this.orderRepository = orderRepository;
}
public void createOrder(Order order) {
pricingService.calculateTotal(order);
orderRepository.save(order);
}
}
// Pricing becomes its own responsibility:
public class PricingService {
public BigDecimal calculateTotal(Order order) {
// Pricing logic
}
}
// Email becomes another responsibility:
public class EmailService {
public void sendOrderConfirmation(Order order) {
// Email logic
}
}
// Invoice generation becomes another:
public class InvoiceService {
public byte[] generateInvoice(Order order) {
// PDF generation
}
}
Why SRP matters?
SRP gives us:
- Smaller classes
- Easier testing
- Easier debugging
- Lower coupling
- Safer changes
- Better code organization
A useful question to ask is:
"If this requirement changes, how many unrelated things would I have to modify in this class?"
If the answer is "many", the class may be violating SRP.