The utils oriented no design architecture : A java Rant

Moroccan software developer, Java/Spring. Love to learn, eager to write.
Search for a command to run...

Moroccan software developer, Java/Spring. Love to learn, eager to write.
The parameter-plumbing section is the sharpest diagnosis in here, because it names exactly why "stateless" gets weaponized as a defense. Passing includeRawDiagnostics through three methods that don't use it isn't statelessness, it's state with the ownership deliberately erased, so nobody has to admit a concept exists. The DeploymentRequest record fix isn't "adding complexity back," it's just naming the thing that was always there and forcing it to validate itself once instead of trusting every caller to assemble it correctly by hand.
The AI-generated JavaDoc bit is the funniest and truest part, and it generalizes past Java entirely, documentation that restates a method signature in prose ("calculates the total by calculating the total") gives the illusion that a design review happened without touching the actual problem, which is that there was never an architectural boundary to document in the first place. Nine thousand lines of grammatically correct filler is strictly worse than four thousand lines of undocumented mess, because it now looks reviewed.
"Static mocking exists, so do chainsaws" is the line I'll be stealing. The FraudAnalyzer interface example makes the actual point clearly, needing bytecode tricks to fake a collaborator isn't a testing sophistication problem, it's the codebase confessing that a dependency got hidden instead of designed, and the fix is never more mocking machinery, it's an interface.
Java’s threading model has long been a cornerstone of its concurrency capabilities. However, the limitations of traditional platform threads (wrappers around OS threads) have persisted for decades—until the release of java 21. Virtual threads, repres...

Introduction Hey there! Let's dive into design patterns—specifically, the Strategy Pattern. Once you get the hang of it, you'll start seeing it everywhere. The Strategy Pattern is all about defining a group of algorithms, wrapping each one up, and ma...

When evaluating the quality of your tests, most developers turn to code coverage tools. These tools provide a percentage indicating how much of your code is executed by your test suite. At first glance, this seems like the ultimate metric: higher cov...

Introduction In modern software development, data validation is important for keeping data correct and stopping incorrect input from entering a system. Spring Boot offers different ways to do data validation. However, with Java 14's new records featu...

Let’s get one thing straight: I do not hate utility classes.
A small utility class containing three genuinely reusable, pure functions is perfectly fine. Converting bytes to hex does not require a rich domain model. Neither does trimming a string or calculating a checksum.
What I hate is when developers take an entire business workflow, remove every trace of meaningful structure, dump forty methods into a class, and proudly announce:
“It’s stateless, so it’s clean.”
No. It's not clean.
It is a procedural landfill wearing a Java class as a disguise.
You did not simplify the system. You took the complexity, smashed it into a pile of static methods, scattered its state across twenty parameters, and made every caller responsible for reconstructing the business process from memory.
Then you called it architecture.
The first warning sign is the size of the public API.
public final class OrderUtils {
public static Order validate(Order order) { ... }
public static Order normalize(Order order) { ... }
public static BigDecimal calculateSubtotal(Order order) { ... }
public static BigDecimal calculateTax(
Order order,
TaxRules taxRules) { ... }
public static BigDecimal calculateDiscount(
Order order,
Customer customer,
DiscountRules discountRules) { ... }
public static BigDecimal calculateShipping(
Order order,
Address address,
ShippingRules shippingRules) { ... }
public static Invoice createInvoice(
Order order,
Customer customer,
Address address,
TaxRules taxRules,
DiscountRules discountRules,
ShippingRules shippingRules) { ... }
public static Receipt createReceipt(
Invoice invoice,
Payment payment) { ... }
private OrderUtils() {
}
}
At first, this looks harmless.
Every method is small. Every method is stateless. Every method can supposedly be tested independently.
Then you open the test class and discover the consequences of this brilliant decision.
class OrderUtilsTest {
@Test
void validate_rejectsOrderWithoutItems() { ... }
@Test
void validate_rejectsNegativeQuantity() { ... }
@Test
void normalize_trimsProductCodes() { ... }
@Test
void calculateSubtotal_handlesMultipleItems() { ... }
@Test
void calculateSubtotal_handlesEmptyOrder() { ... }
@Test
void calculateTax_handlesTaxExemptCustomer() { ... }
@Test
void calculateTax_handlesDifferentRegions() { ... }
@Test
void calculateDiscount_handlesPremiumCustomer() { ... }
@Test
void calculateDiscount_handlesExpiredPromotion() { ... }
@Test
void calculateShipping_handlesRemoteAddress() { ... }
@Test
void createInvoice_combinesAllCalculations() { ... }
@Test
void createInvoice_handlesTaxAndDiscountInteraction() { ... }
@Test
void createInvoice_handlesFreeShippingPromotion() { ... }
// Another 2,000 lines of setup, mocks, fixtures,
// duplicated test data, and existential despair.
}
The problem is not that there are many tests. Tests are good.
The problem is that this glorious OrderUtils masterpiece exposes half the application while representing absolutely nothing.
There is no abstraction, lifecycle, or responsibility. Just a bucket of methods that all happen to mention Order, which is apparently enough to qualify as a valid architecture (Yaay!).
Every public helper becomes another contract. Every rule combination becomes another test. Every method needs its own charming mix of builders, mocks, flags, nulls, and configuration objects.
Eventually, the test class is larger than production, but nobody can split it because the production code has no boundaries to split along.
You do not have a utility class.
You have an entire subsystem hiding in OrderUtils.java, wearing “stateless” as a fake moustache.
Statelessness often turns dependencies and temporary workflow data into method parameters.
The result is parameter plumbing: values get passed through five methods even though only the final method needs them.
public final class ReportUtils {
public static Report generate(
Data data,
ReportFormat format,
boolean includeRawDiagnostics /*focus on this*/) {
return prepare(
data,
format,
includeRawDiagnostics // passed here
);
}
private static Report prepare(
Data data,
ReportFormat format,
boolean includeRawDiagnostics) {
Data normalized = normalize(data);
return build(
normalized,
format,
includeRawDiagnostics // again
);
}
private static Report build(
Data data,
ReportFormat format,
boolean includeRawDiagnostics) {
Report report =
createBasicReport(data, format);
return enrich(
report,
data,
format,
includeRawDiagnostics // another one
);
}
private static Report enrich(
Report report,
Data data,
ReportFormat format,
boolean includeRawDiagnostics) {
if (includeRawDiagnostics) { // finally used
report.addDiagnostics(
createDiagnostics(data, format)
);
}
return report;
}
private ReportUtils() {
}
}
includeRawDiagnostics is passed through generate, prepare, and build.
None of those methods gives a damn about it.
They are glorified Uber drivers transporting a boolean through the call stack so that one method at the bottom can finally use it.
Then the advanced feature grows.
public static Report generate(
Data data,
ReportFormat format,
boolean includeRawDiagnostics,
boolean includePerformanceMetrics,
boolean anonymizeUserData,
boolean includeInternalIds,
boolean useExperimentalLayout,
ZoneId reportingZone,
Locale locale,
Duration timeout) {
...
}
Now every intermediate method needs every argument.
Does it use them? Of course not.
Does it understand them? Absolutely not.
Its only job is to drag them through the call stack like unpaid luggage handlers for bad architecture.
Add one option to the final method and suddenly six signatures change. Then every caller changes. Then every test changes. Then every mock changes. Then someone swaps two booleans and ships it.
Congratulations.
You reinvented state—only without a name, an owner, validation, invariants, or even the basic dignity of being an object.
Long parameter lists are often a missing object desperately trying to crawl out of the codebase.
public static DeploymentResult deploy(
Application application,
Environment environment,
Credentials credentials,
boolean dryRun,
boolean skipTests,
boolean forceRestart,
Duration timeout,
int retryCount,
String releaseLabel,
Map<String, String> metadata) {
...
}
Look at this marvellous monstrosity.
It has required values, optional values, flags, metadata, configuration, and validation rules. At this point, the only parameters missing are my life story, my wife’s approval, and my annual salary.
This is obviously an object, but apparently giving that object a name was too much architecture for one day, so every caller gets to rebuild the same argument pile by hand.
And because Java happily permits this nonsense, someone will inevitably write:
deploy(
application,
environment,
credentials,
false,
true,
false,
Duration.ofSeconds(30),
3,
"release-2026-07",
metadata
);
What do those booleans mean?
Nobody knows.
You need to open the method declaration, count the argument positions with your finger, and pray that the second false was not supposed to be true. Three months later, production breaks, and there you are on a Sunday emergency call, digging through ten identical booleans trying to figure out which one quietly ruined your weekend.
A real object can make the concept explicit and validate itself:
public record DeploymentRequest(
Application application,
Environment environment,
Credentials credentials,
boolean dryRun,
boolean skipTests,
boolean forceRestart,
Duration timeout,
int retryCount,
String releaseLabel,
Map<String, String> metadata) {
public DeploymentRequest {
Objects.requireNonNull(application);
Objects.requireNonNull(environment);
Objects.requireNonNull(credentials);
Objects.requireNonNull(timeout);
Objects.requireNonNull(releaseLabel);
Objects.requireNonNull(metadata);
if (retryCount < 0) {
throw new IllegalArgumentException(
"retryCount cannot be negative"
);
}
metadata = Map.copyOf(metadata);
}
}
Now the workflow receives a meaningful input:
public DeploymentResult deploy(
DeploymentRequest request) {
...
}
That is not “adding unnecessary state.”
That is giving related data a #!!#"#?! name.
Suppose business code directly calls a utility:
public final class CheckoutService {
public Receipt checkout(Order order) {
FraudResult result =
FraudUtils.analyze(order);
if (result.isSuspicious()) {
throw new FraudException();
}
return completePayment(order);
}
}
CheckoutService is now welded directly to FraudUtils.
Not associated with it.
Not depending on an abstraction.
Welded to it, because apparently flexibility is a luxury feature.
You cannot naturally swap in a fake implementation. You cannot choose another fraud strategy. You cannot wrap it with metrics, retries, caching, tracing, or logging without editing the caller or cramming even more responsibilities into the utility class.
But do not worry.
Static mocking exists.
So do chainsaws. That does not make them the correct tool for opening a package.
If replacing a collaborator requires special mocking machinery, bytecode tricks, and a small ritual before every test, that is not advanced testing.
That is the codebase politely informing you that the dependency was hidden instead of designed.
Use an interface:
public interface FraudAnalyzer {
FraudResult analyze(Order order);
}
public final class CheckoutService {
private final FraudAnalyzer fraudAnalyzer;
public CheckoutService(
FraudAnalyzer fraudAnalyzer) {
this.fraudAnalyzer = fraudAnalyzer;
}
public Receipt checkout(Order order) {
FraudResult result =
fraudAnalyzer.analyze(order);
if (result.isSuspicious()) {
throw new FraudException();
}
return completePayment(order);
}
}
Now the test is straightforward:
@Test
void rejectsSuspiciousOrders() {
FraudAnalyzer analyzer =
order -> FraudResult.suspicious();
CheckoutService service =
new CheckoutService(analyzer);
assertThrows(
FraudException.class,
() -> service.checkout(testOrder())
);
}
No static mocking framework.
No global interception.
No magical test runner mutilating bytecode just so the test can pretend this design is acceptable.
No praying that another test left behind some static garbage and poisoned the entire suite.
Just a dependency admitting that it is a dependency.
A concept so painfully obvious that apparently we needed years of mocking hacks to avoid doing it properly.
Utility classes grow through laziness disguised as convenience.
A developer needs a new operation and asks:
“Where should this method go?”
The answer becomes:
“Just put it in
OrderUtils.”
Why?
Because it takes an Order.
Apparently, that is enough architectural reasoning for the day.
Six months later:
public final class OrderUtils {
public static boolean isValid(Order order) { ... }
public static BigDecimal calculateTotal(Order order) { ... }
public static String serialize(Order order) { ... }
public static Order deserialize(String json) { ... }
public static void sendConfirmationEmail(Order order) { ... }
public static String generateCsv(Order order) { ... }
public static void saveToDatabase(Order order) { ... }
public static boolean isFraudulent(Order order) { ... }
public static void publishCreatedEvent(Order order) { ... }
private OrderUtils() {
}
}
Validation, pricing, serialization, email delivery, CSV generation, persistence, fraud detection, and event publishing all live together because they happen to mention the same type.
That is not cohesion.
That is keyword-based architecture.
By that logic, every method containing a String should live in StringUtils, and half the application should be located there.
The utility class becomes a garbage magnet. Every operation that does not have an obvious home gets thrown into it. Nobody wants to create a proper abstraction because adding one more static method is faster and requieres less active neurons.
Eventually, the class is 4,000 lines long, imports half the codebase, and changes for fifteen unrelated reasons.
Then someone discovers generative tooling (claude I hate you!) and decides the real problem is not the garbage design.
The problem is apparently that every garbage method needs fifteen lines of JavaDoc explaining what the garbage does.
/**
* Calculates the total monetary value of the supplied order.
*
* <p>This method processes the provided order and calculates
* its total based on the items currently associated with it.
* It may be used in checkout, invoicing, reporting, or other
* order-related workflows where the total value is required.</p>
*
* @param order the order whose total should be calculated;
* must not be {@code null}
* @return the calculated total for the supplied order
* @throws IllegalArgumentException if the order is invalid
*/
public static BigDecimal calculateTotal(Order order) { ... }
Fantastic.
Now the 4,000-line utility class is a 9,000-line utility class because every painfully obvious method has an AI-generated essay attached to it.
The documentation does not explain why pricing, persistence, fraud detection, serialization, CSV generation, and email delivery live in the same class.
It does not explain the architectural boundary, because there is no architectural boundary.
It does not explain when this method should be used instead of the other six nearly identical methods, because nobody actually knows.
It just translates the method signature into prose:
“Calculates the total of the supplied order by calculating the total of the supplied order.”
Thank God we generated fourteen lines to clear that up.
The class is still incoherent. The responsibilities are still tangled. The abstraction is still nonexistent.
But now the mess has professionally formatted JavaDoc, so apparently the design review is complete.
The problem is not statelessness itself.
Pure functions are excellent. I love pure functions. I am one conference talk away from becoming a full-blown pure-function bro—God forbid.
Small utility classes can be excellent. Stateless services with explicit dependencies can be excellent.
The problem begins when “stateless” stops being a design choice and becomes an excuse to avoid modeling the domain.
You start with one innocent helper:
StringUtils.capitalize(value);
Okay, I like it Picasso.
Then somebody decides the entire billing system should follow the same architecture:
BillingUtils.calculateEverything(
customer,
account,
invoice,
products,
region,
currency,
rules,
options,
flags,
context,
metadata,
true,
false,
true
);
That is not simplicity.
That is a hostage situation with parentheses.
You have not eliminated state.
You took its name, its owner, its validation, and its lifecycle, then scattered the remains across twelve parameters and called the result “clean.”
At this point, codebase is standing over the wreckage saying:
“You took everything from me.”
And SomethingUtils replies:
“I don’t even know who you are.”
You have not eliminated complexity either.
You just hid it in method signatures, call chains, test fixtures, magic strings, configuration maps, duplicated workflows, and the occasional true, false, true ritual—because apparently every developer deserves a small taste of the gambling rush before calling a business-critical method.
Place your bets !!
Does the second false disable caching, skip validation, suppress notifications, or accidentally cancel production invoicing?
Nobody knows.
Spin the boolean roulette wheel, hit deploy, and discover the answer during Sunday’s emergency call.
But at least the class has no fields, so clearly the architecture is flawless.
You have not made the code more functional.
You have made it procedural, lifeless, and exhausting to understand, then sprinkled static on top like seasoning.
So yes, use utility classes for actual utilities: small, stable, context-free operations that genuinely belong nowhere else.
But when a class has forty methods, twelve dependencies disguised as parameters, seven booleans, a 3,000-line test file, and half the application has memorized the sacred order in which its methods must be called, stop calling it a utility.
It is not a helper.
It is not clean.
It is not simple.
It is a missing object model buried under a mountain of static methods while the team proudly adds processAdvancedV2() to SomethingUtils, because processAdvanced() has become such a fragile archaeological site that changing one line can break invoicing, reporting, authentication, and three completely unrelated UI features nobody knew depended on it.
Naturally, nobody fixes the design.
They add V2. Brilliant innit ?