The patterns you do not need
This lesson closes the patterns module, and it is the most important one: the patterns you do not need. The biggest architectural risk in a Spring codebase is not too few patterns — it is too many. Enterprise Java culture, and pattern textbooks, push developers to add abstraction, indirection and "flexibility" that the application never uses, producing code that is harder to read, change and debug than the plain version. This lesson is how to recognise over-engineering and resist it — the honest counterweight to the whole "design patterns" topic.
Over-engineering is the common failure, not under-engineering
In most real Spring codebases, the problems are added complexity, not missing structure:
- Interfaces with a single implementation (
FooService/FooServiceImpl), added "in case". - A repository interface wrapping Spring Data (which is already a repository).
- Layers of DTO ↔ entity ↔ domain-object mappers where one mapping would do.
- A generic "framework" or base class built for a single use case.
- Configuration and strategy hooks for flexibility no one asked for.
- A
Manager,Helper,Util,Handler,Processorfor everything, each a thin wrapper. - Elaborate factories where the container already creates the object.
Each was added by someone being "thorough" or "future-proof", and each is a cost paid now for a benefit that usually never arrives. The senior instinct is the opposite of the junior one: not "what pattern should I add?" but "what can I remove while keeping this correct?"
Why over-engineering hurts
Added abstraction is not free — it has real, ongoing costs:
- Harder to read. Every layer of indirection is another hop to follow. Tracing a simple operation through interface → impl → mapper → helper → factory is far harder than reading one clear method.
- Harder to change. More moving parts means a change touches more files; the "flexibility" you added for an imagined change often does not fit the change that actually comes, so you refactor through the abstraction anyway.
- Harder to debug. Indirection (especially dynamic — factories, strategies chosen at runtime, deep inheritance) makes it harder to see what actually runs.
- More to test and maintain. Every abstraction is more code to keep working.
The plain version — a concrete service, a direct mapping, the framework's own facilities — is usually easier on every one of these axes. Simple is a feature. Complexity must earn its place by solving a real problem you have now.
The rules of thumb
Practical tests to apply before adding a pattern or abstraction:
- YAGNI. You Aren't Gonna Need It — do not build for an imagined future; build for the present need and refactor when the future actually arrives (with the real requirement, you will design better anyway).
- Rule of three. Do not abstract on the first or second occurrence; wait until you have three real cases, then extract the abstraction that fits all three. Abstracting from one guess usually produces the wrong abstraction.
- Prefer the framework's facility. Before building your own (singleton, factory, event bus, repository), check whether Spring already provides it — it almost always does (the "Spring is the pattern" lesson).
- The plainest thing that works. Reach for a concrete class before an interface, a method before a class, a direct call before an indirection — add structure only when the plain version has a concrete problem.
- Delete-ability. If you cannot name the concrete problem an abstraction solves today, it is a candidate for deletion.
Running these before adding machinery prevents most over-engineering.
The honest caveat: this is not "never abstract"
Balance matters — the counterweight to over-engineering is not under-engineering. There are real abstractions worth having: a genuine Strategy interface with multiple implementations, a service layer that gives cohesive operations a home, a well-placed aspect for a truly cross-cutting concern, DTOs at the API boundary. These solve present, real problems and earn their keep. The skill is telling the two apart:
- Justified abstraction solves a problem you have now, is used by multiple real cases, and makes the code clearer.
- Over-engineering solves a problem you imagine, is used by one case, and makes the code harder to follow.
The test that cuts through it: can you point at the concrete, present problem this abstraction solves, and is the code clearer with it than without? If yes, keep it; if you are reaching for "flexibility", "future-proofing", or "best practice" without a present problem, that is over-engineering. This is the judgement the whole module has been building towards — Spring gives you most patterns already, the few you add should each earn their place, and the mark of a good engineer is code that is as simple as the problem allows and no simpler, not code that shows off every pattern they know.
Check your work
The common failure is too many patterns. Single-implementation interfaces, a repository over Spring
Data, layers of mappers, generic frameworks for one use case, Manager/Helper/Util wrappers,
unnecessary factories — complexity added for imagined futures.
Why it hurts. Harder to read (more indirection to follow), change (more files, wrong-fit flexibility), debug (dynamic indirection), and maintain (more code). The plain version is usually easier on all axes; simple is a feature.
Rules of thumb. YAGNI (build for the present); rule of three (abstract on the third real case, not the first); prefer the framework's facility; the plainest thing that works; if you cannot name the problem it solves today, delete it.
Not "never abstract". Justified abstraction solves a present problem, serves multiple real cases, and makes code clearer (a real Strategy, the service layer, a true cross-cutting aspect, DTOs). The test: can you name the concrete present problem, and is the code clearer with it? If not, it is over-engineering.
Practice
- Find three over-engineered constructs in a codebase (a single-impl interface, a Spring-Data wrapper, a
thin
Helper) and, for each, write the simpler version. - For an abstraction you are tempted to add, apply the test: name the concrete present problem it solves and whether the code is clearer with it. Add it only if both hold.
- Apply the rule of three: find a place you abstracted from one case and reason about whether the abstraction actually fits later cases.
- Take a chain of indirection (interface → impl → mapper → helper) and count the hops to follow one operation; collapse the unjustified ones.
- Contrast a justified abstraction (a two-implementation Strategy) with an unjustified one (a one-impl interface) in the same codebase.
- Review your own recent code and find one abstraction you can delete while keeping it correct and clear.
Official documentation
- Martin Fowler — YAGNI — Building for the present, not the imagined future.
- Martin Fowler — Is Design Dead? (rule of three, simplicity) — On simplicity and when to abstract.
- Spring — Core technologies — The facilities Spring already provides, so you do not rebuild them.
Next: configuration, profiles and environment variables (deployment).
Stuck on this lesson?
Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.
About the internship