Design and code quality
Patterns, coupling, and technical debt as a deliberate choice.
Coupling and cohesion
intermediateCoupling is how much modules depend on one another. Cohesion is how related the contents of a single module are. The target is low coupling and high cohesion, and the reason is entirely practical: it decides how far a change spreads, which is the thing that makes a codebase pleasant or exhausting to work in. Tight coupling means a change here forces a change there, and the effect compounds. A module depended on by twenty others cannot be changed without twenty conversations, so it stops being changed, and workarounds accumulate around it instead. Codebases that resist every modification are usually not badly written line by line; they are wired together too tightly.
Cohesion is the same idea from the other side. Organising by technical layer, all controllers here, all models there, all services somewhere else, scatters every feature across the tree, so adding a field means editing six files in six directories. Organising by feature puts what changes together in one place, and the test is simply how many directories a typical change touches.
Coupling comes in degrees worth naming. Depending on a module's published interface is loose and fine. Depending on its internal structure, its database tables, or its file layout is tight and brittle. Two services sharing a database table is the tightest coupling most systems contain, and it is usually invisible on the architecture diagram, which draws them as separate boxes.
The useful heuristic is to ask what would have to change together. Things that change for the same reason belong together; things that change for different reasons belong apart. That is the single responsibility principle stated in the form that is actually actionable, since almost anything can be described as one responsibility if the description is vague enough.
Both are means rather than ends. The goal is that a change is small, local and safe, and if a low-coupling design makes a common change require touching five modules, the design is wrong regardless of how well it scores. Optimise for the changes you actually make, which is a question with an answer in the commit history.
A design has admirably low coupling, and a common change touches five modules. What now?
Answers are generated and can be wrong. The topic above is the reviewed version.
Premature abstraction
advancedAbstracting after one use is a guess at what varies, and the guess is usually wrong. The abstraction then obstructs the very change it was built to accommodate: the second case needs a flag, the third needs another, and within a year the shared function has six parameters, four of which are only used by one caller each.
Duplication is cheaper to fix than a wrong abstraction, and the asymmetry is the whole argument. Copies are visible: you can find them, compare them and merge them mechanically once you can see what they have in common. A wrong abstraction hides the differences behind parameters and has callers that have adapted to its shape, so unwinding it means untangling every one of them.
Waiting for the third occurrence is a heuristic rather than a law, and its value is that by then the axis of variation is usually visible. Two examples look identical because you have only seen two; the third tells you which part was incidental. Where the pattern is genuinely obvious from the first, abstract, and where it is not, copy and wait.
There is a related failure that is harder to see: an abstraction that is correct but adds a layer nobody needed. Indirection has a cost paid by every reader, who must now hold two files in their head to understand one behaviour, and the cost is invisible to the person who wrote it because they already have both files in their head.
The honest signals that an abstraction has gone wrong are specific. Parameters that only exist to switch behaviour for one caller. A name that had to be vague because the thing does several jobs. Callers passing arguments they do not care about. Anyone finding it easier to bypass than to use. Any of those is a prompt to inline it and start again from what the code actually does.
Underneath is a rule about the cost of being wrong. Reversible decisions deserve speed and irreversible ones deserve deliberation, and duplication is reversible while a widely adopted abstraction is not. That asymmetry, rather than any aesthetic preference, is why the advice is to wait.
Which is the clearest sign an abstraction has gone wrong?
Evidence · 1 source
- 01Sandi Metz, The wrong abstraction
The claim that duplication is far cheaper than the wrong abstraction, and the mechanism by which callers adapt to a bad shape until it cannot be removed.
Answers are generated and can be wrong. The topic above is the reviewed version.
Technical debt as a decision
intermediateThe metaphor is precise and worth using precisely. Deliberate debt is a shortcut taken knowingly to hit a date, recorded, with an intended repayment: we are hard-coding this for launch and will generalise it in March. That is a legitimate engineering trade, and it is the only kind that can be argued for in a planning meeting.
Accidental debt is what accumulates from not knowing better at the time, and calling it debt flatters it. Nobody chose it and there is no plan, so it is closer to damage than to a loan. The distinction matters because the conversations are different: one is a decision to revisit and the other is work to schedule.
The interest is real and compounds. Every change in that area costs more, and the rate rises as more code comes to depend on the shortcut, so the cheapest moment to repay is always now and it gets worse monotonically. That is also why a shortcut in a rarely touched corner may be worth leaving forever, since interest is only paid where change happens.
Which makes the useful question where the debt is rather than how much there is. Debt in code nobody touches costs nothing. Debt in the module every feature passes through is taxing every piece of work the team does, and the same amount of untidiness produces wildly different costs depending on where it sits. Change frequency is the multiplier.
Making it visible is what turns it into a decision. A note in the code with a date, an issue linked from the shortcut, a section in the design document listing what was skipped and why. Debt nobody can point at cannot be argued for or against, and it turns into a general sense that the codebase is bad, which persuades nobody and prioritises nothing. Repayment works best as continuous rather than as a project. A rewrite proposal competes with features and loses; improving the code you are already touching does not, because it is part of the work rather than an alternative to it. That habit is also why the areas of a codebase that get worked on most tend to be the ones in the best condition, provided the team has the discipline to leave them slightly better each time.
What makes technical debt deliberate rather than accidental?
Answers are generated and can be wrong. The topic above is the reviewed version.