
A few weeks ago, I was working on a large refactor of our business logic. The idea was simple: clean up the code, remove some duplication, and make things easier to maintain.
I wasn’t changing any feature.
Just moving logic around — what could possibly go wrong?
Turns out, a lot.
The Breakage
Despite all tests passing locally, things started breaking in production. Certain flows stopped working. Side behaviors disappeared. Internal teams raised tickets.
And I had no clue why.
That’s when I came across Hyrum’s Law:
“With a sufficient number of users of an API, it does not matter what you promise in the contract: all observable behaviors of your system will be depended on by somebody.”
And suddenly, it all made sense.
A Simple Code Example
Let’s take a basic function:
function calculatePrice(cart) {
const total = cart.items.reduce((sum, item) => sum + item.price, 0);
logEvent('CALCULATION_DONE');
return total;
}
Now imagine I “refactor” it like this:
function calculatePrice(cart) {
return cart.items.reduce((sum, item) => sum + item.price, 0);
}
Cleaner, right? But I just removed:
logEvent('CALCULATION_DONE');
To me, it was just a side effect. To some background service relying on that log event… it was everything.
Porting vs Refactoring
This experience made me clearly see the difference between two approaches:
Porting
Moving logic from one place to another without changing behavior, even if it’s messy.
Refactoring
Changing the internal structure to improve readability or performance — and potentially removing or changing observable behavior.
Porting tries to preserve everything, including the quirks. Refactoring assumes you can safely clean things up.
But Hyrum’s Law warns: even accidental behaviors might be used somewhere.
My New Checklist
Whenever I’m about to “clean up” legacy logic, I ask:
- Are there side effects (logs, timing, order of calls) that might matter?
- Who uses this function? Are there any downstream dependencies I’m unaware of?
- Am I porting or truly refactoring?
Final Thought
In complex systems, no behavior is truly private. If it's visible, someone somewhere is depending on it — even if unintentionally.
Refactoring isn’t just about better code. It’s about understanding contracts, expectations, and impact.
So next time you're about to refactor legacy code, remember: It’s not just your code. It’s everyone’s expectations of that code.
Have you experienced this before? I’d love to hear how you caught invisible dependencies during a refactor.