Two bills. Same customer, same consumption, same published rates from the same utility.
One says 6,927. The other says 11,145.
A 61% difference — and both were produced by our own code, months apart, from inputs that hadn't changed. The gap came from a single assumption nobody on the team had ever written down, because it never looked like a decision.
What the assumption was
Electricity is usually priced in slabs. The first hundred units cost one rate, the next hundred cost more, and everything above that costs more again.
There are two ways a utility can apply those slabs, and the difference is enormous.
The first way is marginal: each band is charged at its own rate. You pay the cheap rate on your first hundred units no matter how much you consume overall. This is how most people intuitively assume it works. It's how income tax brackets work in most countries, which is probably why the intuition is so strong.
The second way is cumulative: crossing a threshold re-prices your entire consumption at the higher rate. Use one unit too many, and every unit you consumed that month gets billed at the top rate.
We had built it for the first. The utility used the second.
Why nobody caught it
Three reasons, and I think they generalize.
It was never a decision. Nobody sat in a meeting and chose marginal over cumulative. Someone wrote a pricing function, wrote it the way slabs work in most of the world, and moved on. An assumption that was never made explicitly can't be reviewed, because there's nothing to review.
Every test passed. Our tests checked that the function returned a number, that the number went up with consumption, and that boundaries didn't throw. All true. All completely blind to the thing that was wrong. We were testing the shape of the output, not its correctness against the domain.
The output was plausible. This is the part that still bothers me. If the bug had produced a negative number or a NaN, someone would have caught it in a day. Instead, it produced a believable figure that was believably wrong. A plausible output is the most expensive kind of bug, because it survives every casual review by people who don't know the domain deeply enough to smell it.
What it actually cost
Our tool estimates how much a household would save by installing rooftop solar. Under cumulative slabs, a high-consumption household is paying the top rate on everything, which means their savings from going solar are dramatically larger than a marginal model predicts.
So we weren't just wrong. We were wrong in the direction that made the whole proposition look worse than it was, to exactly the customers for whom it made the most sense.
We can't know how many people looked at a number, decided it wasn't worth it, and closed the tab. That's the uncomfortable part. Bugs that make you fail loudly get fixed. Bugs that make you quietly less useful can run for a very long time.
The fix wasn't code
The instinct is to add a second pricing function and a flag. That fixes this instance and leaves the actual problem in place.
The actual problem was that a domain rule was living inside an implementation detail. It had no name, no home, and no owner. Nobody could point at it, question it, or check it against a source document—because it wasn't anywhere. It was implied by the shape of an if-statement.
So we moved it out. Tariffs became stored documents rather than code paths—each one naming its own pricing mode, its own effective dates, and its own charges and taxes in the order they apply. Adding a new utility became a data-entry task that a non-engineer could verify against a published tariff notification.
The mode is now the most prominent field in the document, precisely because it's the one that costs the most when it's wrong.
The lesson I keep coming back to
The dangerous assumptions in a codebase aren't the ones you argued about. Those got written down, reviewed, and probably commented on.
The dangerous ones are the ones that never felt like assumptions—where you knew how the world worked, and it happened to work differently here. They're invisible because they're not marked as choices. They're just how you wrote it.
The practical test I've started using: if this were wrong, would anything break loudly? If the answer is no — if the code would keep running and keep returning something that looks fine — then that's a place where the assumption needs to become explicit data, with a name and a source you can check it against.
Getting a number wrong is recoverable. Not being able to find where the number came from is the real failure.
We're building this at solarbazaar.io, where being wrong about an electricity bill is the whole product failing rather than a rounding error.
Top comments (0)