# The utils oriented no design architecture : A java Rant

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 Test Suite Nobody Asked For

The first warning sign is the size of the public API.

```java
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.

```java
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.

## Pass It Down, Nobody Needs It!

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.

```java
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.

```java
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.

## Just Admit It’s an Object

Long parameter lists are often a missing object desperately trying to crawl out of the codebase.

```java
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:

```java
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:

```java
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:

```java
public DeploymentResult deploy(
        DeploymentRequest request) {
    ...
}
```

That is not “adding unnecessary state.”

That is giving related data a #!!#"#?! name.

## Mock This, I Dare You !

Suppose business code directly calls a utility:

```java
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:

```java
public interface FraudAnalyzer {
    FraudResult analyze(Order order);
}
```

```java
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:

```java
@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.

## Who needs cohesion !

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:

```java
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.

```java
/** 
* 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 Real Problem

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:

```java
StringUtils.capitalize(value);
```

Okay, I like it Picasso.

Then somebody decides the entire billing system should follow the same architecture:

```java
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 ?
