Code Rule: #05: Utility Classes Are Not Your Friend
- Mateusz Roguski
- Apr 26
- 5 min read
Updated: May 3

Table of Contents
Examples in Java
Introduction
We’ve all done it. You need to reuse a bit of logic. It doesn’t seem to belong to any particular object. So you throw it in a Helper, Utils, or Tools class and call it a day.
Neat. DRY. Done. Utility classes feel good because they promise instant reuse and tidiness — a quick way to avoid duplication without having to rethink your design.
Except… you’ve just broken your architecture.
What Is a Utility Class?
A utility class is usually a collection of unrelated methods grouped together simply for convenience. They tend to be:
Stateless.
Procedural.
Loosely or not at all tied to domain concepts.
Placed in static classes (StringHelper, DateUtils) or as modules (utils.js).
They aren’t inherently evil, but they often lack one critical thing: structure with meaning. That’s where the trouble starts.
Why Utility Classes Are a Problem
Poor Testability: You can’t mock static methods. You can’t inject dependencies. This leads to harder tests and more rigid designs.
No Domain Semantics: TextUtils.toSlug() doesn’t express a domain concept. Is it part of content management? User profiles? Logging? It tells you nothing.
Hidden Dependencies: Utility methods often access other services, configuration, or environment state with no transparency. They hide their needs.
Hard to Evolve: Once a method is static and widely used, you’re stuck. Adding a dependency? You have to rewrite everything. Adding complexity? Your once-simple method becomes a God function.
Of course, not every small utility function is a crime. The real danger starts when convenience replaces structure — when helper classes quietly grow into places where unrelated behaviors pile up, making the system harder to understand and evolve.
If you stay intentional and limit pure utility code to very small, well-scoped helpers (preferably outside the domain), you can enjoy the benefits without falling into the trap.
The DRY Trap
"Don't Repeat Yourself" (DRY) is one of the most quoted programming principles — but also one of the most misunderstood. Developers often use utility classes to avoid code duplication, believing it's the DRY thing to do.
But DRY isn't just about avoiding repetition of logic — it's about avoiding repetition of meaning. When different parts of your system share a utility method for different reasons, you're not unifying knowledge — you're coupling unrelated concepts.
public class DateHelper {
public static String formatShort(LocalDate date) {
return date.format(
DateTimeFormatter.ofPattern("dd.MM.yyyy")
);
}
}
Used in:
Invoice PDFs.
UI deadline badges.
API log timestamps.
Change the format for invoices? You risk breaking everything else. This isn't DRY — it's accidental coupling.
DRY is a powerful principle, but it deserves a full treatment of its own. For now, remember: duplication of behavior isn't always duplication of knowledge.
Design Principles Utility Classes Often Break
Single Responsibility Principle (SRP): Utility classes tend to group unrelated logic. A StringHelper might handle casing, slugging, and truncation — all different responsibilities.
Dependency Inversion Principle (DIP): Static utility classes can’t be injected or mocked. You depend directly on low-level implementations.
Tell, Don’t Ask: They operate on data instead of encapsulating behavior, which leads to procedural, not object-oriented design.
Encapsulation: Utility methods often require internal data access from other objects, breaking encapsulation boundaries.
Ubiquitous Language (from DDD): Names like Helper or Util reveal nothing about their purpose or domain. They don’t express meaning.
Design Principles This Approach Promotes
Following the “no utility classes” rule doesn’t just prevent bad practices — it actively promotes stronger design habits based on well-known principles:
Single Responsibility Principle (SRP): Each class or component has a clear, focused reason to change. Instead of a bag of unrelated functions, you have cohesive, intentional behavior.
Open/Closed Principle (OCP): Behavior is easier to extend (by creating new services, strategies, or decorators) without modifying existing utility classes.
Dependency Inversion Principle (DIP): You depend on abstractions (interfaces, injected services) rather than static implementations. This makes your system flexible and easier to test.
Tell, Don’t Ask: Behavior belongs to the objects that own the data. You model concepts — not just procedures.
Ubiquitous Language and Rich Domain Modeling: Your classes and methods express domain intent clearly, making the code understandable and aligned with the business.
Good architecture isn’t just about avoiding mess. It's about building a system where change feels natural, not dangerous.
Design Patterns: What This Rule Promotes and Prevents
Following this rule encourages the use of thoughtful, well-structured design patterns — and avoids those that can lead to architectural decay.
Promotes:
Strategy Pattern: Encapsulate interchangeable behavior in composable classes (e.g. different formatters or calculators).
Service Layer: Centralize logic in testable, injectable services with clear roles.
Value Object: Model domain concepts that combine data and behavior.
Command Pattern: Represent actions and operations as objects rather than method calls.
Prevents:
God Object / God Class: Utility classes often grow uncontrollably and try to “do everything.”
Anemic Domain Model: Behavior is pulled out of domain objects into detached utilities, breaking cohesion.
Procedural Programming in OO systems: Static utilities simulate procedural code in a system that should use encapsulation and polymorphism.
Good design isn’t just about avoiding mistakes — it’s about making change easy, safe, and intentional.
What to Do Instead
Use Service
Inject dependencies. Compose behavior. Services clarify intent and responsibility.
Use Value Objects
Let behavior live close to the data it modifies. Model concepts, not tools.
Use Contextual Components
Replace Utils with classes like InvoiceNumberGenerator, ReportHeaderBuilder, or TaxCalculator. Be specific.
Allow Rare Utility Functions
Pure functions with no dependencies can live in infrastructure:
public final class MathUtils {
public static int clamp(int value, int min, int max) {
return Math.max(min, Math.min(max, value));
}
}
Even here: keep it narrow, obvious, and testable.
What If You Already Have Utility Classes?
If your codebase already has utility classes — and most do — don’t panic.
Refactor gradually:
Extract cohesive groups of methods into focused services.
Move domain-specific behavior into proper value objects.
Leave only rare, pure, domain-agnostic functions behind in the infrastructure layer.
Small, intentional steps over time will lead to a clearer, more maintainable system — without breaking everything at once.
Impact on AI-Driven Development
When you let utility classes spread across your codebase, you make it harder for both humans and AI systems to understand your architecture.
Language models and AI tools work by recognizing patterns and meaning. Utility classes, by their nature, blur meaning:
They group unrelated behaviors without domain context.
They hide real relationships between objects and responsibilities.
They produce ambiguous method names that AI cannot easily infer.
In AI-assisted development — whether code generation, refactoring, or static analysis — clarity is critical. When behavior lives in focused services, value objects, and contextual components, AI tools can better assist you by:
Generating meaningful suggestions.
Refactoring without unintended side effects.
Identifying clear extension points in your system.
Utility classes confuse AI just as much as they confuse people.
Structured, intentional architecture helps both human developers and AI collaborators work smarter and safer.
Summary: Utility Classes Are Not Your Friend
Utility classes are easy to create and hard to kill. They start with good intentions but often lead to rigid code, weak boundaries, and accidental coupling.
Don’t just remove duplication. Remove ambiguity. Code is not just logic — it’s communication. The best code isn’t just DRY. It’s meaningful, modular, and maintainable.
Utility classes are not your friend. Clarity is.