Use with judgement
Everything in this repo – the patterns, SOLID, DRY – is a tool for a specific problem, not a rule to comply with. That framing matters, because every one of these tools has a cost, and paying the cost without having the problem makes code worse. Design maturity isn’t knowing the patterns; it’s knowing when not to use them.
Every tool has a price
The benefit column in this repo’s tables is real, but so is the invoice:
| Tool | Solves | Costs when the problem isn’t there |
|---|---|---|
| Builder | Many optional fields | Fifty lines of ceremony for a two-field class – a constructor was clearer |
| Simple Factory | Callers coupled to concrete types | An indirection layer over a single new that never varies |
| Strategy | Multiple interchangeable algorithms | An interface, a context, and one lonely implementation – an if was clearer |
| Interfaces everywhere | Multiple implementations / substitution in tests | BookService + BookServiceImpl pairs that only ever have one member |
| SRP | Unrelated reasons-to-change tangled together | Logic shattered into confetti – ten two-line classes nobody can follow |
| DRY | One piece of knowledge in several places | Coincidentally-similar code welded together, coupling rules that change separately |
| Observer | Many independent reactions to an event | Control flow nobody can trace – “what happens when I restock?” has no findable answer |
Signals you’re overdoing it
- More structure than logic: files, interfaces and indirections outnumber the lines that actually do something.
- Abstractions with exactly one caller or one implementation, kept “because we might need it”. You aren’t gonna need it (YAGNI) – and when you are, that’s the day to add it.
- You can’t answer “what problem does this layer solve?” with something that has already happened at least once.
- Reading a two-line change requires opening five files.
Signals you’re underdoing it
- Shotgun surgery: one business change means edits scattered across many files (missing DRY/SRP).
- An
if/elseladder that grows every sprint (Strategy/OCP’s problem has arrived). - Business logic you can’t test without credentials, networks, or a database (DIP’s problem).
- A class everyone edits and nobody understands (the God Class – see anti-patterns).
How to calibrate
- Start with the simplest thing that works. A constructor, a plain method, one class.
- Let the problem show up before the solution. The second or third time a change hurts, the real shape of the abstraction is visible – extract it then (the “rule of three”). Patterns applied speculatively usually guess the wrong axis of change.
- Refactor toward patterns, don’t start from them. Their biggest everyday value is vocabulary: “this wants to be a Strategy” is a one-sentence design review.
- Match the effort to the mattering. The order-money path deserves DIP and careful seams; a one-off report script does not.
The tests in this repo model this deliberately: each one demonstrates a pattern earning its keep against a concrete problem – telescoping constructors, drifting VAT copies, an untestable payment call. If your code doesn’t have the problem, it shouldn’t have the pattern.