- Low Level Design
- /
- SOLID Principles in a Real Project
Course content
Every concept in this module refactors a slice of the same codebase, so it is worth meeting that codebase once, whole. It is the checkout backend of a small online store. It registers customers, charges them through a growing list of payment methods, tracks store-credit balances, fulfils physical and digital products, and records orders. It is roughly the service you inherit at every startup: it works, it earns money, and every change hurts a little more than the last one did.
None of the five concept pages depend on this one. Each restates its own slice and stands alone, so you can go straight to any principle. What this page adds is the map: five kinds of pain that look unrelated day to day trace back to five specific design decisions, and each principle in SOLID names one of them.
Nobody wrote this service badly on purpose. Each file made sense when it was small and grew one reasonable commit at a time. Here is what daily life with it looks like now.
Changing the wording of the welcome email means editing UserService, the same file that writes to the database and the audit log. A copy tweak rides the same review, build, and deploy as a schema change. One Friday it shipped next to an unrelated bug in the same class and took registration down for an hour.
PaymentProcessor.process is the most-edited method in the repository. Every new payment method adds a branch to it, every edit puts the neighbouring branches at risk, and one careless merge dropped a surcharge multiplier and undercharged every card transaction by 2.5% for two days.
The nightly job that settles fees calls withdraw on every account. The week the first fixed-deposit account appeared, the job crashed halfway through: the base class promises withdraw, and the new subtype throws.
Half of the catalogue's methods are stubs that exist only to satisfy an interface. The overnight restock job walked the full product list during a flash sale, hit a digital gift card, and aborted on an UnsupportedOperationException while physical stock counts went stale.
And nothing in orders/ can be unit tested without a live MySQL instance, because OrderService constructs its own repository. The validation rules mostly went untested, a zero-amount order shipped unnoticed, and a later database migration rippled through every service that had done the same thing.
| The pain | The design decision behind it | The principle that names it |
|---|---|---|
| A copy tweak deploys the persistence path | One class owns saving, emailing, and auditing | Single Responsibility |
| The most-edited method in the repo; merges break payments | One method branches on every payment type by name | Open/Closed |
| A nightly job crashes on a new account type | A subtype throws on a method its base type promises | Liskov Substitution |
| Products full of methods that only throw | One fat interface describes every kind of product | Interface Segregation |
| Business rules untestable without MySQL | High-level code constructs its own low-level details | Dependency Inversion |
The fixes, in reading order: Single Responsibility splits UserService by reason to change. Open/Closed makes new payment types additive instead of another branch. Liskov Substitution narrows the account contract so subtypes stop lying to callers. Interface Segregation breaks the catalogue interface into roles owned by their callers. Dependency Inversion points orders/ at an abstraction and hands the details in from outside.
How to read this module
Each concept page restates its slice of this project and stands alone: the smell, what it costs in production, the refactor, where else the principle applies, and what the fix saves when the next requirement lands. Read in order, the five pages progressively clean up the service you just toured.
By the end of the module the service looks like the tree below. There are more files and smaller pieces, capabilities live on their own interfaces, and every arrow points at an abstraction instead of a concrete class.
Count the cost honestly: the refactored tree has seven more files than the original. That overhead is real, and it buys one specific property: changes that used to mean editing shared, hardened code become additive. Across the module you will watch four future requirements land in this project (wallet payments, a recurring-deposit account, print-on-demand posters, a DynamoDB migration), and each one arrives as a new file plus at most a line of wiring, while everything that already worked ships untouched.
The other honest note is that none of these abstractions was worth creating before the second variant existed. Every concept page ends with a warning against over-applying its principle, and this project is not an instruction to build interfaces for futures you cannot see. The store earned each abstraction on the day a requirement demonstrated the need. The principles are how you recognise that day when it arrives.
Start with Single Responsibility, or jump to whichever row of the pain table looks most like your codebase. Every page ends with a hands-on refactoring exercise whose validator checks your solution the way a reviewer would.