DEV Community

Cover image for I Ban the Second Way of Doing Anything: What That Costs
HideyukiMORI
HideyukiMORI

Posted on

I Ban the Second Way of Doing Anything: What That Costs

For a long time I believed a good reviewer was enough to keep a codebase honest. Then I counted how many times I had written the same review comment.

"There's already a function for that." "We read the clock through the port, not directly." "That's the third way we format a date."

None of these were bugs. All of them were the same failure: a meaning that had grown a second implementation path. And every one of them had passed review, because reviewers hold rules in memory, and memory is exactly the thing that doesn't scale.

So I adopted one rule and made it the constitution of everything I build since: every meaning gets exactly one implementation path, and a machine keeps it that way.

Not "prefer". Not "should". One path, and the build fails if you add another.

what the rule actually says

It sounds like a style preference. It isn't. It's a claim about where truth lives.

If there is one way to read the current time, then "does this code depend on the wall clock?" is a question with a mechanical answer: does it import the one adapter that's allowed to? If there are two ways, the question needs a human, and the human needs to remember both ways, and next month there are three.

The rule cascades. One way to read time means one adapter that's allowed to. One adapter means a build check that rejects now() everywhere else. A build check means someone has to prove it fires. A proof means a document that quotes real output. Every step is boring. Every step is the thing that was missing the last time I found a second path in review.

I tested how far this goes by starting five small repositories in six days — Kotlin, C#, Go, Rust, Java — each with the same constitution, the same rule IDs, the same demand that every rule declare whether a machine enforces it or a person does. Five languages, one rule, 159 merged pull requests. That's the evidence behind everything below.

what it costs

Be honest about this part or the rule is just a slogan.

Adding a rule touches three places. The rule text, the enforcement matrix that says how it's held, and the actual check. Change one and the other two drift. So I wrote a check for that too — the docs and the matrix are validated against each other in the build. Now adding a rule is slower than not adding it. That's the point, but it's also the cost.

The checks themselves become a codebase. The Java clock has about 1,800 lines of dependency-free conformance checking to guard maybe 5,000 lines of application. For a desktop clock. I am not going to pretend that ratio is comfortable.

You have to prove the machine is watching. A check you have never seen fail is not a check. I learned this the hard way this week: my determinism gate rejected the first violation I planted, I wrote "active" in the log, and a second probe walked straight through. The config file behind the gate was being loaded by no build script. A different, bundled rule had caught my first probe by accident. Proving each gate fires — and reading which rule fired — is a real tax, and you pay it every time you add one.

Some of it never becomes mechanical. Across the five repos, the rules that stayed with humans were always the same kind: "this name is too generic", "this invariant is now written in two places", "this shortcut bypasses the boundary". Rules about meaning. Every language's toolchain could hold structure — dependency direction, module boundaries, forbidden calls — and none of them could hold meaning. I write those rules down as "planned" or "impossible" and leave them to review. The rule about one path applies to the enforcement too: don't claim the machine holds what it doesn't.

what I got for it

The second path stopped appearing. Not because people got better — because the build got red.

And something I didn't expect: the rule catches me. The five repositories were built by one person with an AI agent. The AI drifts toward the second path constantly, because the second path is usually the locally obvious one. The machine doesn't care who wrote it. In one afternoon the checks caught a half-wired gate, a test suite that had never run, and a proof document that quoted expected output instead of real output. All three were mine.

where this is wrong

This is the part I'd want to read if I were skeptical, so here it is.

Throwaway code. If the repository will not exist in a month, the three-place sync is pure overhead. I don't do this for prototypes, and I don't do it for scripts.

Teams that can't agree on which way is the one way. The rule assumes a decision. If two people each believe their path is canonical, mechanical enforcement just automates the argument. Decide first, then enforce. I have the easy version of this problem: it's one person and an agent.

The rule can't fully hold itself. When I put the five repositories side by side, the word "active" — as in "this rule is actively enforced by a machine" — meant something different in each one. The C# repo counted "tests and review" as enforcement. The Java repo would have called the same thing "planned". The Kotlin repo left 41 of 56 rules out of its matrix entirely, as review obligations. Same author, same week, five definitions. The principle demands one path for every meaning, and the meaning of "enforced" had five. I only found that out by counting. So: the rule is still held by a person at the top, and that person drifts too.

It's six days, not six years. Everything above is what it looks like when you start this way. I don't yet have evidence for what happens when a codebase built like this is three years old and the checks have to change. My older PHP tools have a lighter version of the same idea and it has held, but that's a different rule set and a different claim.

the shape of it

If you take one thing: the rule isn't "one way". Everybody says that. The rule is one way, held by something that doesn't forget — and then being honest, in writing, about every place where that something is still a human.

The five repos and their enforcement matrices are public: nene-clock (Java) is the most complete, and its QUALITY_GATES.md is the matrix I mean — 46 rows, 31 fully mechanical, 8 partially, 6 planned, 1 impossible. The half-wired gate story is its own post.

Where does your codebase have two ways of doing the same thing right now — and who is holding the rule that says there should be one?

── Hideyuki Mori (Ayane International) 🔗 hideyuki-mori.com

Top comments (1)

Collapse
 
beusebiu profile image
Eusebiu Balan

"That's the third way we format a date" is the line that got me. I have written that review comment about my own code, months apart, and both times I was sure I had already fixed it.

Moving it from a rule people are supposed to remember into a build step that fails is the part most teams skip, usually because it feels heavy on the day you set it up.