When a pattern is the wrong answer
This is the most important lesson in the module, and the one most pattern courses never teach. Knowing the patterns is useful; knowing when not to use them is what separates a good engineer from one who makes every codebase more complicated. The enthusiastic junior who has just learned the patterns tends to apply them everywhere — and over-applied patterns are worse than no patterns at all.
The over-engineering trap
A pattern adds structure: an interface, a class, an indirection. Structure has a cost — more code to read, more files to navigate, more indirection between the reader and what the code actually does. That cost is worth paying only when the pattern solves a real problem you actually have. Applied speculatively — "we might need to swap this out one day" — it is pure overhead.
Here is the trap in miniature. Someone learns the Factory pattern and writes:
// over-engineered: a factory, an interface, and a strategy... to add two numbers
interface Calculator { fun calculate(a: Int, b: Int): Int }
class AdditionCalculator : Calculator { override fun calculate(a: Int, b: Int) = a + b }
object CalculatorFactory { fun create(): Calculator = AdditionCalculator() }
val result = CalculatorFactory.create().calculate(2, 3) // 5
Three types and a factory to compute 2 + 3. The honest version:
fun add(a: Int, b: Int) = a + b
val result = add(2, 3) // 5
The second is not "less sophisticated" — it is better, because it does exactly what is needed and no more. Every extra type in the first version is something a future reader must understand before they learn that the code adds two numbers. Patterns are not points you score; they are tools with a cost.
YAGNI — you are not going to need it
The instinct that leads to over-engineering is anticipating flexibility you do not yet need: "let me make this an interface with a factory, so we can add other implementations later." Almost always, "later" never comes, and you have paid the complexity cost for a flexibility nobody used. The principle is YAGNI — You Are Not Going To Need It. Build for the requirements you have, not the ones you imagine. When a second implementation genuinely appears, then introduce the abstraction — and it is a small, safe refactor at that point, with the IDE's help. Speculative abstraction is a debt you pay now for a benefit that rarely arrives.
This is the same lesson the database course reached about choosing infrastructure for imagined scale,
and the same one the collections module reached about asSequence "for performance": do the simple
thing until you have measured or met a real reason to do the complex thing.
The rule of three
A practical heuristic for when to abstract: the rule of three. The first time you write something, just write it. The second time you need something similar, note the duplication but resist abstracting — two examples are not enough to see the right shape. The third time, you have enough evidence to abstract well, because you can see what genuinely varies and what stays the same.
Abstracting too early — after one or two cases — usually produces the wrong abstraction, because you guessed at the pattern before you could see it. A wrong abstraction is worse than duplication: it is a shape everything must now be forced into, and un-abstracting is harder than abstracting. Duplication is cheap to fix later; a bad abstraction is expensive. So: prefer a little duplication to a premature abstraction, and let the third occurrence tell you the real shape.
Signs you are over-patterning
Concrete smells that you have reached for a pattern you did not need:
- An interface with exactly one implementation, and no plan for a second. The interface adds indirection for nothing — use the class directly until a second implementation exists.
- A factory that only ever constructs one type. It is a constructor with extra steps.
- A layer that only forwards — a class whose every method just calls another object's method, adding nothing. (Genuine decorators add behaviour; empty pass-throughs do not.)
- Names like
AbstractBaseManagerFactoryImpl. A pile of pattern-suffix words usually signals structure piled on structure with little real work underneath. - You cannot explain what problem the pattern solves here. If the honest answer is "it is good practice" or "we might need it", that is not a problem — that is a reflex.
When you see these in your own code, the fix is to delete structure until only what does real work remains. Removing an unnecessary abstraction almost always makes code clearer.
The balanced view
None of this means "never use patterns". It means use them when they earn their place:
- Do use
objectfor a genuine singleton, a factory function when construction needs a name or a choice, a sealed class for real state, a lambda strategy for behaviour that genuinely varies,Flowfor real observation. - Do not add an interface, a factory, or a builder speculatively, for a flexibility you do not have a concrete need for.
The judgement is always the same question from the first lesson: what problem does this solve, and do I actually have that problem? If you have it, reach for the lightest tool that solves it — usually a Kotlin language feature. If you do not have it, write the simple, direct code. An engineer who writes the simplest thing that works, and adds structure only when a real need appears, produces codebases that are a pleasure to work in — and that is the mark this whole module, and this whole course, has been pointing at.
Check your work
The over-engineering trap. Applying patterns speculatively adds structure (cost) without solving a real problem — worse than no pattern.
Why the honest simple version is better, not just shorter. Every extra type is something a reader must understand before learning what the code does.
YAGNI. You Are Not Going To Need It — build for the requirements you have, not imagined ones; add the abstraction when a real second case appears.
The rule of three. Write it plainly the first and second time; abstract on the third, when you can see what genuinely varies.
Why premature abstraction is worse than duplication. A wrong abstraction forces everything into a bad shape and is hard to undo; duplication is cheap to fix later.
Five signs of over-patterning. A one-implementation interface, a one-type factory, a forwarding-only layer, pattern-suffix name pile-ups, and being unable to say what problem it solves.
The balanced view. Use patterns (in their light Kotlin form) when they earn their place; do not add them speculatively.
The deciding question. What problem does this solve, and do I actually have that problem?
Practice
- Take the over-engineered
Calculatorexample and reduce it to the honest one-function version. Note how many types you deleted. - Find (or recall) an interface with exactly one implementation. Decide whether it earns its keep or should be the class directly.
- Apply the rule of three: find two similar pieces of code and resist abstracting them. Describe what evidence a third case would give you.
- Write a small feature the over-engineered way (interface + factory) and the simple way. Ask a friend which they would rather maintain.
- For three abstractions in code you have, answer honestly: what problem does each solve, and do you have that problem?
- Delete one unnecessary layer or interface from your code and confirm it reads more clearly.
- Write down the deciding question and keep it beside you — it is the whole module in one sentence.
Official documentation
- Kotlin — Coding conventions — The style that favours simple, direct code.
- Martin Fowler — YAGNI — Why speculative flexibility usually does not pay off.
- Refactoring Guru — Design Patterns — Know them so you can recognise when not to use them.
- Kotlin — Idioms — The simple, direct Kotlin forms to reach for first.
Next module — Testing and Tooling: Gradle, tests, and structuring a real project.
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