DEV Community

Cover image for I Broke One E-Commerce App Five Times to Finally Understand SOLID (Part 1: S, O, L)
Aswini S M
Aswini S M

Posted on

I Broke One E-Commerce App Five Times to Finally Understand SOLID (Part 1: S, O, L)

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
    }
}
Enter fullscreen mode Exit fullscreen mode

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 */ }
}
Enter fullscreen mode Exit fullscreen mode

📦 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
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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");
    }
}
Enter fullscreen mode Exit fullscreen mode

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");
    }
}
Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode
Discount discount = new PercentageDiscount();
double result = discount.getDiscount(1000);
Enter fullscreen mode Exit fullscreen mode

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");
    }
}
Enter fullscreen mode Exit fullscreen mode

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)