If someone told you SOLID principles once and it went in one ear and out the other — same. The definitions are dry. The examples in most tutorials are toy examples that don't survive contact with a real codebase.
So instead, let's break (and fix) one e-commerce application — the kind with orders, payments, notifications, deliveries, and discounts — five separate times. One break per principle. This is Part 1, covering S, O, and L. Part 2 covers I and D.
🟢 S — Single Responsibility Principle
"One class = One responsibility"
A class should have only one reason to change.
❌ The mess
class OrderManager {
void placeOrder() {
// place order
}
void processPayment() {
// process payment
}
void sendEmail() {
// send email
}
}
Payment logic changes → touch OrderManager. Email provider changes → touch OrderManager. Order flow changes → touch OrderManager again. One class, three unrelated reasons to break.
✅ The fix
class OrderService {
void placeOrder() { /* order logic */ }
}
class PaymentService {
void processPayment() { /* payment logic */ }
}
class NotificationService {
void sendEmail() { /* email logic */ }
}
📦 OrderService → orders. 💳 PaymentService → payments. 📧 NotificationService → notifications.
Remember it as: one class, one job.
🔵 O — Open/Closed Principle
"Open for extension, closed for modification"
Add new functionality without repeatedly editing existing, tested code.
❌
class PaymentService {
void pay(String type, double amount) {
if (type.equals("CARD")) {
// card payment
}
else if (type.equals("UPI")) {
// UPI payment
}
else if (type.equals("WALLET")) {
// wallet payment
}
}
}
Add Net Banking. Add PayPal. Add crypto. Every new payment method means reopening a class that already works and already shipped.
✅ The fix
interface Payment {
void pay(double amount);
}
class CardPayment implements Payment {
public void pay(double amount) {
System.out.println("Processing card payment");
}
}
class UPIPayment implements Payment {
public void pay(double amount) {
System.out.println("Processing UPI payment");
}
}
Adding Net Banking now means adding a new class — not editing the ones already in production:
class NetBankingPayment implements Payment {
public void pay(double amount) {
System.out.println("Processing net banking payment");
}
}
Remember it as: new features shouldn't require reopening old, tested code.
🟣 L — Liskov Substitution Principle
"Child classes should behave like their parent"
If B is a subtype of A, you should be able to swap B in wherever A is expected — without breaking anything.
✅ Playing by the rules
interface Discount {
double getDiscount(double amount);
}
class PercentageDiscount implements Discount {
public double getDiscount(double amount) {
return amount * 0.10;
}
}
class FlatDiscount implements Discount {
public double getDiscount(double amount) {
return 100;
}
}
Discount discount = new PercentageDiscount();
double result = discount.getDiscount(1000);
Swap in FlatDiscount or SeasonalDiscount — the app doesn't care, because both honor the same contract.
Breaking the rules
class NoDiscount implements Discount {
public double getDiscount(double amount) {
throw new UnsupportedOperationException("Discount not available");
}
}
Anywhere the app expects a Discount to calculate a number, this one throws instead. It's technically a Discount — it's just not a trustworthy substitute for one.
Remember it as: a child class should be a true stand-in for its parent, not a landmine.
That's S, O, and L down — three ways an e-commerce codebase quietly turns into a maintenance nightmare, and three small fixes that keep it flexible. Part 2 wraps up with I and D, including the one that Spring Boot's dependency injection is basically built on.
Question for the comments: which of these three have you actually seen go wrong in a real codebase — the god-class (S), the endless if-else chain (O), or the subtype that lies about its contract (L)?
Top comments (0)