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 developer who has just learned patterns tends to apply them everywhere — and over-applied patterns are worse than none.
The over-engineering trap
A pattern adds structure: an interface, a class, an indirection, a layer. Structure has a cost — more code to read, more files to navigate, more distance 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 TypeScript, where the temptation is worse because the type system enables cleverness:
// over-engineered: an interface, a factory, a strategy... to add two numbers
interface Calculator {
calculate(a: number, b: number): number;
}
class AdditionCalculator implements Calculator {
calculate(a: number, b: number): number { return a + b; }
}
class CalculatorFactory {
static create(): Calculator { return new AdditionCalculator(); }
}
const result = CalculatorFactory.create().calculate(2, 3); // 5
An interface, a class, and a factory to compute 2 + 3. The honest version:
const add = (a: number, b: number) => a + b;
const 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 learning that the code adds two numbers. Patterns are tools with a cost, not points you score.
The type-system version of the trap
TypeScript adds a second trap the Kotlin course did not have: over-clever types. Just as you can over-apply the Factory pattern, you can over-apply the type system — a baroque conditional-and-mapped-type where a simple type (or a plain runtime check) would do (the building-utility-types lesson's warning). A codebase where simple things are typed with elaborate type-level machinery is harder to work in, produces incomprehensible error messages, and slows the compiler. The over-engineering principle applies to types as much as to patterns: use the simplest type that solves the problem.
YAGNI — you are not going to need it
The instinct behind 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 now for a benefit that never arrives. 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 — a small, safe refactor at that point, with the compiler's help. Speculative abstraction is a debt you pay now for a benefit that rarely arrives.
This is the same lesson the course reached about advanced types ("mastery is restraint") and about any
("do not add structure to silence a worry"). Do the simple thing until a real reason to do the complex
thing appears — measured or met, not imagined.
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.
Abstracting too early — after one or two cases — usually produces the wrong abstraction, because you guessed at the pattern before you could see it. And a wrong abstraction is worse than duplication: it forces everything into a bad shape, and un-abstracting is harder than abstracting. 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 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 concrete type until a second implementation exists. (DI in a small app is the common culprit — an interface per class with one implementation each.)
- A factory that only ever constructs one type. It is a constructor with extra steps.
- A class with a single method and no state. That is a function wearing a costume.
- A layer that only forwards — a class whose every method just calls another's, adding nothing.
- An over-clever type whose error messages are pages long and that nobody can explain in a sentence.
- You cannot say what problem the pattern solves here. If the honest answer is "it is good practice" or "we might need it", that is a reflex, not a reason.
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 a discriminated union for real variants, a function for a strategy, a factory function when construction needs a name or a choice, Dependency Injection when you need testability and decoupling (and a container when the app is big enough).
- Do not add an interface, a factory, a builder, or an elaborate type speculatively, for flexibility you do not have a concrete need for.
The judgement is always the same question: what problem does this solve, and do I actually have that problem? If yes, reach for the lightest tool that solves it — usually a language feature (a function, a union, an object). If no, 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. That is the mark this module — and this whole course — has been pointing at: not the most clever code you can write, but the simplest code that solves the problem, typed honestly.
Check your work
The over-engineering trap. Applying patterns (or types) speculatively adds structure/cost without solving a real problem — worse than none.
Why the simple version is better, not just shorter. Every extra type is something a reader must understand before learning what the code does.
The TypeScript-specific trap. Over-clever types — elaborate type-level machinery where a simple type or a runtime check would do.
YAGNI. 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 varies. Premature abstraction is worse than duplication.
Six signs of over-patterning. A one-implementation interface, a one-type factory, a single-method stateless class, a forwarding-only layer, an unexplainable over-clever type, and being unable to say what problem it solves.
The balanced view. Use patterns (in their light TypeScript 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
- Reduce the over-engineered
Calculatorto the honest one-function version. Count the types you deleted. - Find (or recall) an interface with exactly one implementation. Decide whether it earns its keep or should be the concrete type.
- Find an over-clever type whose error message is incomprehensible, and simplify it (or replace it with a runtime check).
- Apply the rule of three: find two similar pieces of code and resist abstracting. Describe what evidence a third case would give.
- Write a small feature the over-engineered way and the simple way. Ask which a colleague would rather maintain.
- For three abstractions in code you have, answer: what problem does each solve, and do you have that problem?
- Write down the deciding question and keep it beside you — it is the whole module in one sentence.
Official documentation
- Martin Fowler — YAGNI — Why speculative flexibility rarely pays off.
- TypeScript — Do's and Don'ts — Including keeping types simple.
- Refactoring Guru — Design Patterns — Know them so you recognise when not to use them.
Next module — TypeScript in Real Projects: config, declaration files, and code that was not written in TypeScript.
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