Nuno Silva

Mar 13, 2026 • 7 min read

The Global Utils Trap: Stop Grouping Code by Data Type

The file belongs to everyone. It is owned by no one.

The Global Utils Trap: Stop Grouping Code by Data Type

Every codebase has one. It might be a utils folder, a Helper.java class, or a common/index.ts file. You know the one. It has 47 functions, no clear theme, and nobody is quite sure who is responsible for it.

It starts with the best of intentions. A developer needs to calculate a date for a UI component. They notice another developer formatting a date elsewhere for a database query. Remembering the golden rule of DRY, they group it all into a shared DateUtils file. Clean. Efficient. Responsible, even.

But as the product scales and deadlines tighten, the discipline slips. The file is right there, it already imports everywhere, and the new function is sort of date-related. What begins as a tidy shared library quietly mutates into a 4,000-line junk drawer of orphaned logic — stuffed with functions that have no home and therefore live here, in the drawer, with everything else.

The problem isn't that utility functions exist. The problem is the principle we use to organise them. When we create global utility files based on data types — Strings, Dates, Arrays — rather than business domains — Billing, HR, Inventory — we accidentally strip our code of its most important structural asset: context.


The Trap: Two Very Different Things in the Same Drawer

Let's look at a classic DateUtils file. It usually contains what appears to be one category of code but is actually two very different things masquerading as the same:

// 1. Pure mechanism — operates on a date, knows nothing about the business
public static String formatToISO(Date date) { ... }

// 2. Business rule — encodes how this specific company operates
public static Date calculateNextInvoiceDate(Date currentDate) { ... }

formatToISO is a tool. It takes a date and returns a string. It has no opinion about invoices, employees, or anything else. It could belong to any codebase on the planet.

calculateNextInvoiceDate is something else entirely. It adds 30 days, skips weekends, and accounts for public holidays — because that is how this company gets paid. It is a business rule disguised as a utility function.

By placing both in a global DateUtils file, we commit a quiet architectural sin. We have taken a critical piece of company logic and buried it next to a string formatter. And because it lives in a global file with no owner, the boundaries disappear.

Six months later, the HR team needs to calculate the date for an employee's 30-day performance review. A developer searches the codebase, finds calculateNextInvoiceDate, sees that it adds 30 days and skips weekends, and repurposes it for the HR module. Sensible — it does exactly what they need.

Then the CFO decides that invoices are now due in 45 days. We update the utility function. Billing is fixed. But HR performance reviews are now firing two weeks late, and nobody immediately knows why — because nothing about DateUtils suggests that changing it would touch HR.

We tried to share code. What we actually did was silently couple two completely unrelated business domains through a file that was supposed to be neutral.


When the Drawer Breaks

When this kind of coupling surfaces, the instinct is usually to swing to the opposite extreme:

  1. Keep the global utils file, but be more disciplined about what goes in it.

  2. Duplicate the logic — give Billing and HR their own separate date calculation functions.

The first option is optimistic. "Be more disciplined" is not an architecture; it is a hope. Without a structural rule that enforces the boundary, the junk drawer fills back up within a quarter.

The second option feels wrong because it is technically duplication. Both functions add 30 days and skip weekends. A linter would flag them. A code reviewer would question them.

But this discomfort is pointing at the right solution for the wrong reasons. The answer is not to duplicate, and it is not to share a business rule. It is to separate the two layers that have been accidentally merged.

What the calculateNextInvoiceDate example is actually doing: it is a business decision (30 days) sitting on top of a pure calculation (add working days to a date). That calculation — same input, always same output, no knowledge of invoices or employees — is a genuine mechanism. It deserves to live in shared utils. The decision built on top of it does not.

These are two different things that ended up in the same function. Pulling them apart is not duplication. It is the fix.


The Fix: Let Ownership Determine Location

Before deciding where a function lives, ask one question: which team would you call if this rule changed?

If the CFO changes payment terms, you call Billing. If HR changes their review cycle, you call HR. The answer tells you exactly where the code belongs — and it works regardless of how your project is structured.

This is the line worth drawing: between mechanism and business rule.

A mechanism operates on data without knowing anything about the business. It belongs to no domain, and a global utilities file is a perfectly reasonable home for it. No business change will ever make a mechanism mean something different.

A business rule encodes a specific decision made by a specific part of the organisation. It has an owner. It should live with that owner. Splitting the two layers makes this concrete:

// Shared utils — a pure function, safe to share across the entire codebase
public static Date addWorkingDays(Date date, int days) { ... }

// Billing — owns its own rule, delegates the arithmetic
public Date calculateNextInvoiceDate(Date current) {
 return DateUtils.addWorkingDays(current, 30);
}

// HR — owns its own rule, delegates the same arithmetic
public Date calculateNextReviewDate(Date current) {
 return DateUtils.addWorkingDays(current, 30);
}

There is no duplication here — because addWorkingDays was always the right thing to share. calculateNextInvoiceDate never was. When the CFO moves to 45 days, only BillingService changes. The pure function is untouched. HR never knows it happened.

The goal is not to avoid sharing code. It is to share code at the right layer of abstraction.

Here is how that ownership shift looks across different architectural styles:

  • Layered / Traditional

    • The Junk Drawer: DateUtils.calculateInvoiceDate()

    • The Ownership Fix: BillingService.calculateInvoiceDate()

  • Feature Folders

    • The Junk Drawer: src/utils/dateUtils.ts

    • The Ownership Fix: src/features/billing/utils.ts

  • Domain-Driven Design

    • The Junk Drawer: DateUtils static method

    • The Ownership Fix: Domain service or entity method

In every case, the change is less about how much code moves and more about where the boundary is drawn. The vocabulary differs by architecture. The principle doesn't.


What Belongs in Global Utils?

This is not an argument for abolishing shared utilities. Pure mechanisms are real, and they deserve a shared home. The question is how to tell the difference.

The Transplant Test

Ask this of every function before deciding where it lives: could this function exist, unchanged, in a completely different product?

A date formatter, a string truncator, a number rounder — yes, easily. These are genuinely domain-agnostic, and a global utils file is exactly the right home for them. No business decision will ever give them a different meaning.

But calculateNextInvoiceDate? That function could not exist in a different product without being completely rewritten. It encodes a pricing model, a payment schedule, a set of calendar rules specific to this business. It is not a utility. It is a policy.

If it cannot survive a transplant, it is not a mechanism. It belongs with its owner — not in the drawer.


The Bottom Line

A global utils file is not inherently bad. What is bad is using data type as a proxy for ownership — because data types do not own anything. They do not make business decisions. They do not have a team behind them. They are just a shape.

The junk drawer is not a consequence of bad developers. It is a consequence of a convenient but shallow organisational principle: it works with dates, so it goes in DateUtils. That principle feels like tidiness, but it quietly erases the most important information in the codebase: who is responsible for this logic, and who has the right to change it.

Data types describe what code operates on. Business domains describe who owns the decision. Those are not the same thing — and the architecture should reflect that difference.


The next time you reach for a global utils file, pause for one second: am I filing this by what it operates on, or by who is responsible for it?

Join Nuno on Peerlist!

Join amazing folks like Nuno and thousands of other builders on Peerlist.

peerlist.io/

It’s available... this username is available! 😃

Claim your username before it's too late!

This username is already taken, you’re a little late.😐

0

6

0