The File That Should’ve Stayed Boring
A harmless little helper that became a logic hydra. You know this one.
It started as helpers.ts
.
Just a couple of string functions: slugify, capitalize, maybe something to format currency. You know, the kind of low-commitment one-liners that feel too pure to hurt you. Then came the date formatting. Then the ID generator. Then a function to normalize order items—but just the formatting, we said.
Six months later, helpers.ts
was 800 lines, had three different behaviors behind getFormattedAmount
, and included a “temporary” fix for tax rules in Germany.
When it broke, checkout broke.
When I traced the bug, I ended up in a function literally called isActiveCustomerLike
. I say “literally” not for emphasis, but because that’s the actual name we shipped to production. It returned a boolean. It also triggered a database fetch.
No one knew where to fix things anymore. The “logic” was scattered across helpers, utils, factories, and files with names like coreUtils
, sharedUtils
, and, I kid you not, utilsUtils
.
It took two engineers, one product manager, and a desperate Slack message to “whoever added this tax logic” to fix a bug that boiled down to: we’d built a second app, and hidden it inside the first one.
The Architecture That Felt So Grown-Up
When “modular” meant putting everything in neat little buckets—with no lids.
We were being responsible. Reusable. DRY, but not religiously. Just enough to avoid copy-pasting that formatPrice
function everywhere.
We wanted composability without commitment. A toolkit, not a framework. Helpers for common logic. Utils for shared tasks. Factories for object setup. Each with its own tidy file, decoupled and dependency-free.
We’d read enough blog posts to know the dangers of fat services and god objects. We were breaking things up. Modularizing. Keeping our domain logic clean and uncluttered.
It felt mature. Under-engineered, even. Pragmatic.
And in fairness—it started clean.
But clean doesn’t mean safe.
The Secret Life of Utilities
How the real business logic slipped sideways into files no one owns.
It’s easy to spot when your models are bloated or your services too abstract. What’s harder to catch is the slow gravitational pull of logic drifting sideways—into the shadows between your files.
Helpers didn’t stay small. They started answering “just this one business need.” Then another. Then someone needed to check discount eligibility—quickly, no time to refactor the cart service—so they stuffed it into discountUtils
.
Eventually, that file had a half-implemented loyalty engine living in it.
Meanwhile, the domain stayed pristine. Untouched. Empty.
The logic was somewhere else now—scattered across helpers and utils and factories that technically worked across the app, but only if you understood the exact combination of arguments and side effects. Calling one meant loading five implicit assumptions and hoping nothing changed upstream.
We built abstraction with no ownership. Encapsulation with no borders. A hall of mirrors where every function seemed useful, until you realized none of them were reliable.
Even the tests lied. Mock-heavy, low-coverage, and subtly obsolete. We were asserting behavior that no longer made sense, in files named things like formatHelper.test
.
It wasn’t tech debt. It was tech drift.
And it was everywhere.
Welcome to the Utility Sink
The anti-pattern with charm, convenience, and a body count.
I call it the Utility Sink.
It looks like a grab bag, but it’s a gravitational well. Logic flows into it because it’s easy. Fast. Unopinionated. A helper can live anywhere, touch anything. It doesn’t need to understand context. That’s the appeal.
And the danger.
The Utility Sink has no identity. So it absorbs all of them. Your business rules, formatting logic, feature flags, database queries—all hiding behind verbs like “get” and “is” and “format.”
There’s no owner, because helpers belong to “everyone.” So no one sees when they start returning different results depending on the quarter, the client, or the moon phase.
The Utility Sink feels like reuse. What it delivers is indirection. The worst kind: invisible, inconsistent, and wrapped in good intentions.
It’s how you end up with a normalizeCustomerData
function that applies different phone number logic depending on the country code and whether the customer is B2B or B2C—but only if the skipValidation
flag isn’t set.
It doesn’t throw. It just returns slightly wrong data.
Until your email system starts calling everyone “Valued Client” because their names went missing.
Like a Sith apprentice, it always starts with good intentions—power without responsibility, shortcuts without context.
And before you know it, the logic has turned.
If It Gets Feature Requests, It’s Not a Helper
A brutal litmus test for architectural lies we tell ourselves.
Here’s the gut check:
If your helper gets feature requests, it’s not a helper anymore.
If product cares about it, if stakeholders ask about it, if QA writes edge cases for it—you’ve crossed the threshold. It’s no longer utility. It’s domain. You just parked it in a folder named shared
.
Your helpers shouldn’t know about your product tiers. Your utils shouldn’t fetch from the database. Your factories shouldn’t contain logic branches for enterprise clients.
And if they do? Don’t rename them. Relocate them.
Put them where they belong. Even if it feels redundant. Even if it means your pristine service class gains weight. Logic needs a home—and a name that admits what it does.
Because there’s no such thing as “just” a helper.
That word? It’s code for “I don’t want to think too hard about where this goes.”
Which is fair. Until it breaks prod. Then you’ll think real hard, real fast.
The Callback You Didn’t Ask For
Yes, we fixed it. No, it didn’t stay fixed. Entropy always gets a sequel.
We fixed the tax bug. Eventually. We extracted the German VAT rules into the TaxPolicyService
, gave it a clear contract, and wrote tests that didn’t involve mocking half the internet.
But the legacy remained.
Six months after the fix, someone opened a PR: “refactor formatInvoice
into invoiceUtils
.”
I saw it, sighed, and left a comment I’ve reused ever since:
“Only move it if you’re ready to own it.”
They replied with a thumbs-up. And merged it anyway.
The file is now 400 lines long. I haven’t opened it in weeks.
I assume it’s doing fine.
Probably.
Maybe.
God help us if it starts getting feature requests.
Top comments (0)
Some comments may only be visible to logged-in visitors. Sign in to view all comments.