Five Things I Was Confidently Wrong About

Positions I argued for, sometimes loudly, that turned out to be mistaken. What changed my mind in each case.

Share
Five Things I Was Confidently Wrong About. Abstract opinion illustration in orange and dark grey on debugly.dev

A blog about debugging should occasionally debug its author. These are positions I held firmly and was wrong about, roughly in order of how long it took me to notice.

1. That microservices were about scale

For a long time I understood microservices as a scaling technique. Split the monolith, scale the hot service independently, done.

That is a real benefit and it is rarely the reason teams actually need them, and optimising for it led me to split services along the wrong lines.

Splitting by technical layer, so an API service, a worker service, and a data service, gives you independent scaling and no independence at all. Every feature touches all three. Every deploy is coordinated. You have distributed a monolith and added network calls between its function calls.

The thing microservices are actually good for is letting teams deploy without coordinating. That means the boundary should follow team ownership and rate of change, not the technical stack. A service owned by one team, deployable without asking anybody, is the unit that pays off.

What changed my mind was watching a three service split make deploys slower rather than faster, because every change needed all three released in order.

Related realisation: most teams asking about microservices should be asking about modular monoliths. Same boundaries, same discipline about interfaces, no network in between. You can always extract later, and extracting a well bounded module is much easier than un-distributing a badly split system.

2. That test coverage percentage meant something

I pushed for 80 percent coverage on a team once. We got there. The bug rate did not change.

Coverage measures which lines executed during the test run. It does not measure whether anything was verified. A test that calls a function and asserts it does not throw covers every line and checks nothing.

Once you have a target number, people optimise for the number, and the cheapest way to raise coverage is to write tests for the easy code: getters, mappers, simple utilities. The hard code, the concurrency, the error paths, the boundary conditions, stays untested because testing it is difficult and it barely moves the percentage.

I now care about a different question: would this test fail if the behaviour were wrong? Mutation testing answers it directly, and even without the tooling, mentally inverting an implementation and asking whether any test notices is a fast check.

This one connects to something I write about often now: tests generated from an implementation certify the implementation, including its bugs, and they show up as coverage.

3. That the debugger was strictly better than print statements

I spent years quietly thinking that print debugging was what you did before you learned to use a debugger properly. I was wrong in a way that cost me real time on real bugs.

A debugger shows you state at one moment. A log shows you the history of a system across time. Those answer different questions, and a large share of hard bugs are questions about sequence: how many times did this run, in what order did these fire, which of four thousand iterations was the bad one.

Worse, for concurrency bugs the debugger is actively counterproductive. Attaching one changes the timing enough that races stop reproducing, and I have watched a bug vanish under a breakpoint and concluded it was fixed.

What changed my mind was learning the mechanism. Once you know a breakpoint rewrites machine code and traps into the kernel, the observer effect stops being mysterious and starts being predictable. I have argued the full case elsewhere.

4. That good code needs few comments

I believed the strong version of self documenting code: if you need a comment, the code is not clear enough, so improve the code.

This is right about a specific kind of comment and wrong as a general rule, and the distinction took me too long to see.

Comments explaining what the code does are usually a smell. // increment counter above counter++ is noise, and if a function needs a paragraph to explain what it does, rename it.

Comments explaining why are irreplaceable, and no amount of naming captures them:

// The provider double-encodes the payload. Confirmed with their support
// on 2026-03-14, ticket #88231. Remove when they fix it.
const data = JSON.parse(JSON.parse(body));

There is no variable name that carries that. The information is not in the code, it is in the history of an interaction with another company, and deleting the comment because "the code should be self documenting" destroys it permanently.

I have written a whole post around a related idea: abstractions are expensive when they hide the reason something exists, and the cheapest fix for that is often a comment.

The rule I use now: comment the decisions and the surprises. If somebody might reasonably think "why is this written this strange way", answer them.

5. That performance work should wait for measurement

"Premature optimisation is the root of all evil" is correct and I applied it too broadly.

The quote is about micro-optimisation, about contorting code for speed you have not measured. It is not an argument against thinking about performance during design.

Some decisions are extremely expensive to reverse: your data model, your consistency model, whether an operation is synchronous, whether a boundary is a network call. Getting those wrong and discovering it in a profiler six months later means a rewrite, not an optimisation.

What I got wrong specifically: I let a design go through with a per-item network call inside a loop, on the reasoning that we should measure before optimising. It was fine at ten items. Production had four thousand. The fix was not a tuning change, it was restructuring the API and the callers.

The distinction I use now: structural performance decisions belong in design, micro-optimisation belongs after measurement. If a choice determines the complexity class of an operation, or whether something crosses a network, think about it up front. If it is about which loop construct is faster, measure or ignore it.

The tell for a structural decision is that changing it later requires changing callers.

The pattern

Looking at these together, four of the five have the same shape: I took a heuristic that was correct in its original context and applied it as a rule outside that context.

Premature optimisation, self documenting code, coverage targets, debuggers over prints. Each is good advice somewhere. Each becomes bad advice when it stops being a heuristic and starts being an identity.

The microservices one is different and more embarrassing, because it was simply not understanding what the tool was for.

I assume there are five more of these that I currently hold and cannot see, which is the uncomfortable but probably correct conclusion. The best defence I have found is noticing when I am arguing from a principle rather than from a specific observation, because that is usually where a heuristic has quietly become a belief.