Avoiding Unnecessary Utility Classes

19 min read

Object-Oriented Design and SOLID Review — review Java utility classes that accumulate unrelated static methods and replace them with focused, properly owned behavior.

1. Introduction

Utility classes are common in Java projects.

A utility class usually contains reusable static methods such as:

  • String conversion
  • Date conversion
  • Collection processing
  • Validation
  • Formatting
  • Number conversion
  • Encoding
  • File handling

A small and focused utility class can be useful.

The problem starts when developers use utility classes as a default destination for code that does not seem to fit anywhere else.

Classes such as:

JAVA
CommonUtils
AppUtils
GeneralUtils
Helper
Utility
CommonHelper

often become dumping grounds for unrelated business logic.

Over time, these classes accumulate dozens or hundreds of static methods covering completely different responsibilities.

This creates hidden coupling, weak ownership, difficult testing, duplicated business logic, and unclear architecture.

During code review, the question should not be:

"Are utility classes bad?"

The correct question is:

"Does this logic genuinely represent a stateless, generic utility, or does it belong to a domain/service/component with clear responsibility?"

2. What This Topic Means

Avoiding unnecessary utility classes means not converting every reusable piece of Java code into a static helper method.

A utility class is appropriate when the operation is:

  • Stateless
  • Generic
  • Deterministic
  • Independent of application business rules
  • Independent of database access
  • Independent of external systems
  • Unlikely to require injected dependencies

For example:

JAVA
public final class StringNormalizer {
    private StringNormalizer() {
    }
    public static String trimToNull(String value) {
        if (value == null) {
            return null;
        }
        String trimmed = value.trim();
        return trimmed.isEmpty() ? null : trimmed;
    }
}

This can reasonably be considered utility behavior.

However, consider:

JAVA
public static BigDecimal calculateOrderDiscount(
        Customer customer,
        BigDecimal total) {
    if (customer.isPremium()) {
        return total.multiply(new BigDecimal("0.10"));
    }
    return BigDecimal.ZERO;
}

This is not generic utility logic.

It is business logic related to pricing or discounts.

A better location could be:

JAVA
DiscountService
PricingService
DiscountPolicy

The key principle is ownership.

Business behavior should live in a component whose name expresses the business responsibility.

3. Why It Matters in Real Projects

Readability

A method such as:

JAVA
OrderUtils.calculateDiscount(...)

does not clearly communicate who owns the business rule.

A method such as:

JAVA
pricingService.calculateDiscount(...)

provides much stronger architectural meaning.

Maintainability

Generic utility classes tend to grow continuously because developers repeatedly add unrelated methods.

One class may eventually contain:

  • Date parsing
  • Authentication checks
  • Order discounts
  • Employee calculations
  • JSON conversion
  • Database-related helpers
  • Email generation

Changes become harder to reason about.

Testability

Static business logic may be harder to replace or isolate when a test needs different behavior.

Modern mocking frameworks can mock static methods, but requiring static mocking for normal business behavior is often a design warning.

Debugging

When many workflows depend on a generic utility class, production debugging becomes harder because the same generic class appears throughout stack traces.

Reliability

Business rules duplicated across utility methods can produce inconsistent application behavior.

Team Development

Generic helpers make responsibility ownership unclear.

Different developers may add slightly different versions of the same logic because they cannot easily determine which helper is authoritative.

4. Core Concept

The core distinction is between:

Generic Technical Utility

A generic utility solves a technical problem without knowing application-specific business rules.

Examples:

JAVA
String normalization
Byte conversion
URL encoding
Hash generation
Collection chunking

Domain or Business Logic

Domain logic understands concepts such as:

JAVA
Customer
Order
Payment
Employee
Policy
Invoice
Product
Subscription

It normally belongs in:

  • Domain services
  • Application services
  • Entities
  • Value objects
  • Strategy components
  • Dedicated Spring beans

Infrastructure Logic

Logic involving:

  • Databases
  • HTTP APIs
  • Email
  • Messaging
  • Cloud storage
  • Secrets

should normally belong behind dedicated infrastructure components rather than static utility methods.

Static Does Not Mean Reusable Design

A common misconception is:

"If several classes need this method, make it static."

Reuse alone does not determine ownership.

A shared pricing rule is reusable, but it is still pricing logic.

5. Important Rules

  • Do not create Utils classes by default.
  • Determine the responsibility before choosing the class.
  • Keep generic utilities independent of business concepts.
  • Do not put repository calls inside utility classes.
  • Do not inject Spring beans into static utility classes.
  • Avoid static business rules when dependency injection would provide clearer ownership.
  • Prefer domain-specific class names.
  • Keep utility classes focused on one technical concern.
  • Make pure utility classes stateless.
  • Prevent accidental instantiation of true utility classes with a private constructor.
  • Do not use utility classes to hide poor object design.
  • Do not create a helper merely to reduce another class's line count.
  • Prefer existing JDK or trusted library functionality before creating custom utilities.
  • Avoid duplicating common utilities already available in frameworks such as Spring or Apache Commons.
  • Reconsider a utility class when it starts accumulating unrelated methods.

6. Bad Code Example

Consider the following Spring Boot application:

JAVA
public final class ApplicationUtils {
    private ApplicationUtils() {
    }
    public static boolean isValidEmail(String email) {
        return email != null && email.contains("@");
    }
    public static BigDecimal calculateDiscount(
            Customer customer,
            BigDecimal orderAmount) {
        if (customer.isPremium()) {
            return orderAmount.multiply(new BigDecimal("0.15"));
        }
        return BigDecimal.ZERO;
    }
    public static boolean canCancelOrder(Order order) {
        return order.getStatus() == OrderStatus.CREATED
                || order.getStatus() == OrderStatus.CONFIRMED;
    }
    public static String buildOrderReference(Long orderId) {
        return "ORD-" + orderId;
    }
    public static boolean isAdmin(User user) {
        return user != null
                && user.getRoles().contains("ADMIN");
    }
    public static String formatCustomerName(Customer customer) {
        return customer.getFirstName().trim()
                + " "
                + customer.getLastName().trim();
    }
}

This class looks convenient because the methods are reusable.

However, it mixes several unrelated responsibilities:

  • Email validation
  • Pricing rules
  • Order lifecycle rules
  • Identifier formatting
  • Authorization logic
  • Customer display formatting

The class has no meaningful cohesive responsibility other than "miscellaneous reusable methods."

7. Problems in the Bad Code

Generic Naming

ApplicationUtils does not communicate what the class actually owns.

A developer must inspect the entire class to discover its purpose.

Unrelated Responsibilities

The class contains logic related to:

  • Customers
  • Orders
  • Pricing
  • Security
  • Validation

This creates very low cohesion.

Business Logic Hidden as Utility Logic

The following methods contain business rules:

JAVA
calculateDiscount(...)
canCancelOrder(...)
isAdmin(...)

These rules should have clear business ownership.

Weak Extensibility

Suppose discount behavior changes to include:

  • Premium customers
  • Promotional campaigns
  • Product categories
  • Coupon codes
  • Geographic rules

The static utility method quickly becomes difficult to maintain.

Hard-Coded Authorization Logic

Checking for a literal role such as:

JAVA
"ADMIN"

inside a generic utility class may bypass the application's actual authorization mechanism.

Poor Testing Boundaries

Static business methods encourage tests that directly test utility implementation rather than business components with meaningful contracts.

High Coupling

Many unrelated parts of the application may begin depending on the same class.

As a result, ApplicationUtils becomes globally coupled to the codebase.

8. Code Review Findings

During Pull Request review, a senior Java reviewer should notice:

  • The class name is too generic.
  • Methods are unrelated to each other.
  • Several methods contain domain-specific business rules.
  • Pricing behavior is hidden inside static helper code.
  • Authorization checks should not be implemented as general-purpose utilities.
  • Order lifecycle behavior should have a clear domain owner.
  • Email validation is too simplistic if it is intended for production validation.
  • The class will likely continue growing because it has no defined responsibility boundary.
  • Static methods may make future dependency requirements difficult.
  • The code duplicates responsibilities that may already belong to existing services or domain objects.

The reviewer should recommend moving logic according to responsibility rather than merely splitting the file arbitrarily.

9. Reviewer Comment Example

ApplicationUtils currently contains pricing, authorization, order-state, validation, and customer-formatting logic. These concerns change independently. Could we move the business rules to domain-specific components and keep utilities limited to genuinely generic technical operations?

Another useful review comment:

calculateDiscount() represents pricing policy rather than generic utility behavior. A dedicated pricing/discount component would provide clearer ownership and allow future rules or dependencies to evolve without expanding this global helper.

For authorization:

Please avoid implementing role authorization inside a generic static helper. We should use the application's established Spring Security authorization mechanism so permission rules remain centralized and consistent.

10. Improved Code

Email Validation

For request validation, use Bean Validation where appropriate.

JAVA
public record CreateCustomerRequest(
        @NotBlank
        @Email
        String email,
        @NotBlank
        String firstName,
        @NotBlank
        String lastName) {
}

Pricing Responsibility

JAVA
@Service
public class DiscountService {
    private static final BigDecimal PREMIUM_DISCOUNT_RATE =
            new BigDecimal("0.15");
    public BigDecimal calculateDiscount(
            Customer customer,
            BigDecimal orderAmount) {
        Objects.requireNonNull(customer, "customer cannot be null");
        Objects.requireNonNull(orderAmount, "orderAmount cannot be null");
        if (orderAmount.signum() < 0) {
            throw new IllegalArgumentException(
                    "orderAmount cannot be negative");
        }
        if (!customer.isPremium()) {
            return BigDecimal.ZERO;
        }
        return orderAmount.multiply(PREMIUM_DISCOUNT_RATE);
    }
}

Order Lifecycle Behavior

If the rule naturally belongs to the domain model:

JAVA
public class Order {
    private Long id;
    private OrderStatus status;
    public boolean canBeCancelled() {
        return status == OrderStatus.CREATED
                || status == OrderStatus.CONFIRMED;
    }
    public void cancel() {
        if (!canBeCancelled()) {
            throw new InvalidOrderStateException(
                    "Order cannot be cancelled from status " + status);
        }
        status = OrderStatus.CANCELLED;
    }
}

Alternatively, if cancellation rules become complex, move them into a dedicated policy or service.

Order Reference Formatting

If reference generation is a specific business responsibility:

JAVA
@Component
public class OrderReferenceGenerator {
    public String generate(Long orderId) {
        Objects.requireNonNull(orderId, "orderId cannot be null");
        return "ORD-" + orderId;
    }
}

Authorization

Use Spring Security rather than a generic static method.

JAVA
@PreAuthorize("hasRole('ADMIN')")
public void deleteCustomer(Long customerId) {
    customerRepository.deleteById(customerId);
}

Customer Display Name

If the behavior naturally belongs to the domain object:

JAVA
public class Customer {
    private String firstName;
    private String lastName;
    public String getDisplayName() {
        return Stream.of(firstName, lastName)
                .filter(Objects::nonNull)
                .map(String::trim)
                .filter(value -> !value.isEmpty())
                .collect(Collectors.joining(" "));
    }
}

The result is not more complicated because each rule now has an owner that reflects its meaning.

11. Improved Code Explanation

Validation Uses the Existing Framework

Instead of maintaining a custom email helper, Bean Validation provides declarative validation.

This reduces custom code and integrates naturally with Spring MVC validation.

Pricing Logic Has Explicit Ownership

DiscountService clearly owns discount calculations.

Future changes such as coupon or campaign rules can evolve around the pricing domain.

Order State Rules Stay Near the Order

canBeCancelled() describes order behavior.

Keeping this rule near the domain state improves discoverability.

Authorization Uses Spring Security

Authorization should be enforced by the application's security mechanism rather than a manually called helper method.

Reference Generation Is Explicit

OrderReferenceGenerator communicates intent better than:

JAVA
ApplicationUtils.buildOrderReference(...)

Static Global Dependency Is Removed

Different application components now depend only on responsibilities they actually need.

12. Bad Code vs Improved Code

AreaGeneric Utility ApproachResponsibility-Based Approach
OwnershipUnclearExplicit
NamingGenericDomain-specific
CohesionLowHigh
Business RulesHidden in helpersLocated in business components
TestabilityStatic/global behaviorFocused components
ExtensibilityStatic methods grow in complexityComponents can evolve
SecurityManual helper checksFramework-based authorization
DiscoverabilityDevelopers search utility filesLogic located by domain
CouplingMany classes depend on one helperDependencies remain targeted
MaintenanceUtility class grows continuouslyChanges remain localized

13. Real Project Scenario

Consider a healthcare backend application.

Initially, the development team creates:

JAVA
HealthcareUtils

It contains only:

JAVA
formatPatientName(...)
calculateAge(...)

Later developers add:

JAVA
isPatientEligible(...)
maskPolicyNumber(...)
calculateClaimAmount(...)
canAccessMedicalRecord(...)
buildClaimReference(...)
validateProvider(...)
convertAppointmentTime(...)
determineClaimStatus(...)

After several years, the utility class contains more than 100 methods.

Now different teams depend on it:

  • Claims team
  • Patient team
  • Provider team
  • Security team
  • Appointment team

The result is serious architectural confusion.

For example, the security team changes:

JAVA
canAccessMedicalRecord(...)

but another endpoint continues using different authorization logic.

The claims team modifies:

JAVA
calculateClaimAmount(...)

and unintentionally affects several workflows because the static method is used across unrelated services.

The appropriate design would separate responsibilities such as:

JAVA
ClaimPricingService
PatientEligibilityService
MedicalRecordAuthorizationService
ClaimReferenceGenerator
PatientNameFormatter

This makes ownership and testing much clearer.

14. Production Impact

Unnecessary utility classes do not usually cause production incidents simply because they exist.

The risk comes from hidden business logic and widespread dependency.

Inconsistent Business Rules

Different utility methods may implement similar rules differently.

For example:

JAVA
calculateDiscount(...)
calculatePremiumDiscount(...)
getCustomerDiscount(...)

may eventually return different results.

Security Errors

Authorization hidden in helper methods can be skipped accidentally.

Developers must remember to call the helper at every appropriate location.

Regression Risk

Changing a globally used static method may affect many application areas.

Difficult Incident Analysis

A utility method may be called from dozens of workflows.

Reviewers and support engineers must determine which caller triggered incorrect behavior.

Maintenance Cost

The utility class grows because there is no clear rule about what it should contain.

Tight Coupling

A widely used generic helper becomes difficult to remove because many modules depend on it.

15. Common Developer Mistakes

Creating CommonUtils at Project Start

A generic utility class is created before there is any actual need.

It then becomes the default dumping ground.

Moving Business Logic to Helpers

Developers sometimes move complicated code out of a service into a helper and assume the design is improved.

The service becomes shorter, but ownership remains unclear.

Making Everything Static

Static methods are chosen simply because they are easy to call.

This makes future dependency injection harder.

Injecting Spring Beans Into Static Fields

Example:

JAVA
@Component
public class UserUtils {
    private static UserRepository userRepository;
    public UserUtils(UserRepository repository) {
        UserUtils.userRepository = repository;
    }
}

This should generally be rejected.

It combines static global state with dependency injection and produces difficult lifecycle and testing behavior.

Reimplementing Existing Library Methods

Projects often create custom helpers for behavior already provided by:

  • Objects
  • Collections
  • Optional
  • String
  • java.time
  • Spring utilities
  • Apache Commons

Using Helpers to Hide Domain Behavior

Example:

JAVA
OrderUtils.canCancel(order)

may be less expressive than:

JAVA
order.canBeCancelled()

when the rule belongs directly to the order domain.

Giant Constants Utility Classes

Developers may create:

JAVA
ConstantsUtils

containing unrelated constants for security, orders, APIs, dates, and messages.

Constants should normally stay close to the component that owns them.

16. Edge Cases

Null Handling

Utility methods often silently introduce inconsistent null semantics.

One method may return null, another empty string, another throw an exception.

The contract should be explicit.

Empty Collections

Generic collection utilities should define behavior for empty and null collections separately.

Localization

Formatting helpers may become incorrect when the application later supports multiple locales.

Time Zones

Date utilities are especially dangerous when they hide:

  • Default time zone
  • System locale
  • Date formatting assumptions

Prefer explicit ZoneId, Locale, and java.time APIs.

Concurrent Usage

Pure stateless utility methods are generally thread-safe if they use only local variables.

However, utility classes containing mutable static fields can create serious concurrency issues.

Stateful Dependencies

If a supposedly generic utility begins requiring:

  • Repository access
  • Cache access
  • Configuration
  • HTTP clients

it is probably no longer a utility.

17. Performance Considerations

The existence of a utility class itself normally has no meaningful performance impact.

Static method calls are not a significant production optimization compared with normal instance methods in typical Spring applications.

Performance concerns arise from what developers hide inside utilities.

Expensive Operations

Avoid helpers such as:

JAVA
ApplicationUtils.getCustomer(...)

if they secretly perform database access.

The caller may assume it is a cheap local operation.

Repeated Object Creation

Formatting or parsing utilities may repeatedly create expensive objects unnecessarily.

Modern java.time formatters such as DateTimeFormatter are immutable and thread-safe and can often be reused safely.

Collection Copies

A utility method may defensively copy large collections without callers realizing the cost.

Reflection

Generic reflection-heavy utility methods may introduce unnecessary complexity and runtime overhead.

External Calls

A method named:

JAVA
UserUtils.validateUser(...)

should not secretly invoke an external API.

The performance characteristics should remain obvious from the architecture.

18. Security Considerations

Security becomes relevant when utility classes contain authentication or authorization behavior.

Do Not Use Generic Helpers as Security Enforcement

Avoid relying on code such as:

JAVA
if (!UserUtils.isAdmin(user)) {
    throw new AccessDeniedException();
}

when Spring Security can enforce authorization centrally.

Avoid Logging Sensitive Data

Formatting utilities should not expose:

  • Passwords
  • Tokens
  • Payment data
  • Health records
  • Authentication headers

Avoid Homegrown Cryptography

Do not create generic methods such as:

JAVA
SecurityUtils.encryptPassword(...)

using custom encryption logic.

Use established security libraries and password encoders.

Avoid Weak Validation Assumptions

Security validation must not rely on simplistic string checks when stronger framework support exists.

Keep Secrets Out of Static Fields

Do not place credentials or API keys in utility constants.

Secrets should come from secure configuration or secret-management systems.

19. Testing Considerations

Test True Utilities Directly

A pure deterministic utility can usually be tested with straightforward unit tests.

Example:

JAVA
class StringNormalizerTest {
    @Test
    void shouldReturnNullForNullInput() {
        assertNull(StringNormalizer.trimToNull(null));
    }
    @Test
    void shouldReturnNullForBlankInput() {
        assertNull(StringNormalizer.trimToNull("   "));
    }
    @Test
    void shouldTrimNonBlankInput() {
        assertEquals(
                "Java",
                StringNormalizer.trimToNull(" Java "));
    }
}

Test Business Components Through Their Public Contracts

For DiscountService, test:

  • Standard customer
  • Premium customer
  • Zero amount
  • Negative amount
  • Null customer
  • Null amount

Example:

JAVA
class DiscountServiceTest {
    private final DiscountService discountService =
            new DiscountService();
    @Test
    void shouldCalculatePremiumDiscount() {
        Customer customer = new Customer();
        customer.setPremium(true);
        BigDecimal discount =
                discountService.calculateDiscount(
                        customer,
                        new BigDecimal("1000.00"));
        assertEquals(
                new BigDecimal("150.0000"),
                discount);
    }
}

Integration Tests

Integration testing is usually unnecessary for true pure utilities.

It becomes necessary for extracted components involving:

  • Repositories
  • Spring Security
  • REST clients
  • Messaging
  • Configuration

This distinction itself is valuable: if the class requires heavy integration testing, it may not actually be a utility.

20. Refactoring Guidelines

Step 1: Inspect the Utility Class

List all public methods.

Group them by responsibility.

For example:

JAVA
Pricing
Security
Formatting
Order state
Date handling

Step 2: Identify True Utilities

Keep only methods that are genuinely:

  • Generic
  • Stateless
  • Infrastructure-independent
  • Business-independent

Step 3: Move Business Logic

Move domain-specific rules into:

  • Entities
  • Domain services
  • Spring services
  • Policies
  • Dedicated components

Step 4: Replace Calls Incrementally

Do not rewrite the entire application in one Pull Request.

Replace usages responsibility by responsibility.

Step 5: Preserve Behavior

Add tests before moving complex rules.

Ensure:

  • Returned values remain identical
  • Exceptions remain compatible where required
  • Null handling remains correct
  • Transaction behavior is unchanged

Step 6: Remove Unused Static Methods

After migrating all consumers, delete obsolete utility methods.

Step 7: Rename Remaining Utilities

Replace broad names such as:

JAVA
CommonUtils

with focused names such as:

JAVA
StringNormalizer
FileNameSanitizer
CollectionChunker

Step 8: Prevent Future Dumping

Define clear team review rules around utility classes.

21. Best Practices

Prefer Intent-Revealing Names

Prefer:

JAVA
PriceCalculator
OrderReferenceGenerator
TokenHasher
FileNameSanitizer

instead of:

JAVA
AppUtils
CommonHelper

Use Domain Objects Where Appropriate

Behavior strongly related to an object's state may belong on that object.

For example:

JAVA
order.canBeCancelled()

can be more expressive than:

JAVA
OrderUtils.canCancel(order)

Prefer Dependency Injection for Business Services

If a component may eventually need:

  • Configuration
  • Repository
  • Feature flags
  • External integration

an injectable component is generally more flexible.

Keep Real Utilities Pure

A utility method should ideally behave like:

JAVA
output = function(input)

without hidden external side effects.

Prefer Standard Libraries

Before writing a utility method, check whether the functionality already exists.

Keep Utilities Focused

Good examples include:

JAVA
CsvEscaper
HashingUtils
CollectionPartitioner

provided their responsibilities remain narrow.

22. Practices to Avoid

CommonUtils

Avoid classes whose names communicate no responsibility.

Static Repository Access

Never hide database calls behind generic static helpers.

Static API Clients

External calls should be handled by explicit integration components.

Business Rules in Helpers

Pricing, permissions, workflow decisions, and eligibility rules deserve domain ownership.

Mutable Static State

Avoid:

JAVA
private static Map<String, Object> cache;

unless there is a carefully designed and justified concurrency model.

Manual Service Locator Patterns

Avoid utilities that retrieve Spring beans through ApplicationContext.

Example:

JAVA
SpringContextUtils.getBean(...)

This hides dependencies and weakens testability.

Utility Chains

Avoid code such as:

JAVA
OrderUtils.process(
        CustomerUtils.validate(
                PaymentUtils.prepare(...)));

The workflow becomes difficult to understand and maintain.

23. Code Review Checklist

  • Is this method genuinely generic utility behavior?
  • Does this utility understand application-specific business concepts?
  • Would a domain-specific class provide clearer ownership?
  • Is the utility class name meaningful and focused?
  • Does this class contain unrelated methods?
  • Does any utility method access a database?
  • Does any utility method call an external API?
  • Does the utility depend on Spring-managed components?
  • Is mutable static state present?
  • Could this behavior belong to an existing domain object?
  • Is authorization being implemented manually?
  • Is the method duplicating functionality already available in Java or Spring?
  • Are hidden side effects present?
  • Are null-handling rules explicit?
  • Could future business changes make this static method difficult to evolve?
  • Is the utility being created only to reduce another class's size?
  • Will many unrelated modules become dependent on this helper?
  • Can the remaining utility methods be grouped around one technical responsibility?
  • Is static mocking required because business behavior has been implemented statically?
  • Would a named component make the code easier to understand during PR review?

24. Common Pull Request Review Comments

  1. > This looks like pricing business logic rather than generic utility behavior. Could we move it to the pricing component so the rule has a clear owner?
  1. > CommonUtils is becoming a collection of unrelated methods. Please place this method in a class that represents its actual responsibility.
  1. > This helper performs a repository call, which makes the cost and side effect difficult to see from callers. I suggest moving it to a dedicated service/repository-backed component.
  1. > We should avoid storing the injected dependency in a static field. Please keep this as a normal Spring bean with constructor injection.
  1. > Spring already provides validation support for this case. Can we use Bean Validation instead of introducing another custom utility method?
  1. > This authorization check should use our Spring Security configuration rather than requiring each caller to remember to invoke a helper.
  1. > The method is reusable, but it is still domain logic. Reuse alone does not require making it static.
  1. > Could this behavior live on Order itself? order.canBeCancelled() would make the domain rule easier to discover than OrderUtils.canCancel(order).
  1. > Please confirm whether this utility has hidden I/O. A method with a helper-style name should not unexpectedly perform network or database operations.
  1. > Before adding another method here, I suggest splitting this class by responsibility; the existing methods already cover several unrelated domains.

25. Code Review Exercise

Review the following code:

JAVA
public final class PaymentUtils {
    private static PaymentRepository paymentRepository;
    private static NotificationClient notificationClient;
    private PaymentUtils() {
    }
    public static void initialize(
            PaymentRepository repository,
            NotificationClient client) {
        paymentRepository = repository;
        notificationClient = client;
    }
    public static boolean isPaymentAllowed(Customer customer) {
        return customer != null
                && customer.isActive()
                && !customer.isBlocked();
    }
    public static Payment createPayment(
            Customer customer,
            BigDecimal amount) {
        if (!isPaymentAllowed(customer)) {
            throw new IllegalArgumentException(
                    "Payment not allowed");
        }
        Payment payment = new Payment();
        payment.setCustomerId(customer.getId());
        payment.setAmount(amount);
        payment.setStatus(PaymentStatus.CREATED);
        Payment savedPayment =
                paymentRepository.save(payment);
        notificationClient.send(
                customer.getEmail(),
                "Payment created");
        return savedPayment;
    }
    public static String formatAmount(BigDecimal amount) {
        return "₹" + amount.setScale(
                2,
                RoundingMode.HALF_UP);
    }
}

Identify:

  • Code smells
  • Responsibility problems
  • Static-state problems
  • Testing problems
  • Production risks
  • Better component boundaries

Do not look at the solution until you have reviewed the snippet yourself.

26. Exercise Solution

The class is named PaymentUtils, but it performs much more than utility work.

Problem 1: Mutable Static Dependencies

The class contains:

JAVA
private static PaymentRepository paymentRepository;
private static NotificationClient notificationClient;

This introduces global mutable state.

It can cause:

  • Initialization-order problems
  • Difficult tests
  • Shared-state problems
  • Hidden dependencies

Problem 2: Manual Initialization

The initialize() method acts like a service locator or manual dependency injection mechanism.

Spring already provides dependency injection.

Problem 3: Business Eligibility Rule

isPaymentAllowed() represents payment eligibility logic.

This belongs to a business component or policy.

Problem 4: Database Access

createPayment() saves entities.

A method doing persistence is not a simple utility.

Problem 5: Notification Side Effect

The same helper method sends customer notifications.

This mixes persistence and communication concerns.

Problem 6: Formatting Mixed With Payment Processing

formatAmount() is the only method that resembles generic formatting behavior.

Even this may need locale-aware currency formatting rather than string concatenation.

Improved Payment Policy

JAVA
@Component
public class PaymentEligibilityPolicy {
    public boolean isAllowed(Customer customer) {
        return customer != null
                && customer.isActive()
                && !customer.isBlocked();
    }
}

Improved Payment Service

JAVA
@Service
public class PaymentService {
    private final PaymentRepository paymentRepository;
    private final PaymentEligibilityPolicy eligibilityPolicy;
    private final ApplicationEventPublisher eventPublisher;
    public PaymentService(
            PaymentRepository paymentRepository,
            PaymentEligibilityPolicy eligibilityPolicy,
            ApplicationEventPublisher eventPublisher) {
        this.paymentRepository = paymentRepository;
        this.eligibilityPolicy = eligibilityPolicy;
        this.eventPublisher = eventPublisher;
    }
    @Transactional
    public Payment createPayment(
            Customer customer,
            BigDecimal amount) {
        if (!eligibilityPolicy.isAllowed(customer)) {
            throw new PaymentNotAllowedException();
        }
        if (amount == null || amount.signum() <= 0) {
            throw new IllegalArgumentException(
                    "Payment amount must be positive");
        }
        Payment payment = new Payment();
        payment.setCustomerId(customer.getId());
        payment.setAmount(amount);
        payment.setStatus(PaymentStatus.CREATED);
        Payment savedPayment =
                paymentRepository.save(payment);
        eventPublisher.publishEvent(
                new PaymentCreatedEvent(
                        savedPayment.getId(),
                        customer.getEmail()));
        return savedPayment;
    }
}

Improved Notification Listener

JAVA
@Component
public class PaymentNotificationListener {
    private final NotificationClient notificationClient;
    public PaymentNotificationListener(
            NotificationClient notificationClient) {
        this.notificationClient = notificationClient;
    }
    @TransactionalEventListener(
            phase = TransactionPhase.AFTER_COMMIT)
    public void handle(PaymentCreatedEvent event) {
        notificationClient.send(
                event.customerEmail(),
                "Payment created");
    }
}

Improved Currency Formatter

JAVA
@Component
public class CurrencyFormatter {
    public String format(
            BigDecimal amount,
            Locale locale) {
        Objects.requireNonNull(amount, "amount cannot be null");
        Objects.requireNonNull(locale, "locale cannot be null");
        NumberFormat formatter =
                NumberFormat.getCurrencyInstance(locale);
        return formatter.format(amount);
    }
}

Why This Is Better

The improved design makes dependencies explicit.

PaymentService owns payment creation.

PaymentEligibilityPolicy owns payment eligibility.

Notification behavior has a dedicated owner.

Formatting no longer lives beside persistence logic.

The classes are easier to test individually and easier for developers to discover.

27. Interview Perspective

This topic commonly appears indirectly in senior Java and Spring Boot interviews.

The interviewer may show code containing:

JAVA
CommonUtils
ApplicationUtils
HelperService

and ask:

"What problems do you see with this design?"

A strong answer should not claim that all static methods are bad.

Instead, explain the distinction between genuine utilities and hidden business logic.

You should discuss:

  • Cohesion
  • Responsibility ownership
  • Dependency injection
  • Hidden side effects
  • Static state
  • Testability
  • Domain modeling
  • Security
  • Existing library functionality
  • Refactoring strategy

For senior roles, interviewers may also expect discussion about how utility-heavy code can indicate an anemic domain model or procedural design.

28. Interview Questions and Answers

Basic Question

Question: Are utility classes bad in Java?

Answer:

No.

Utility classes are useful for small, stateless, generic technical operations.

The problem occurs when they become containers for business logic, persistence, authorization, or unrelated application behavior.

Intermediate Question

Question: What are signs that a utility class should be refactored?

Answer:

Common signals include:

  • Generic names such as CommonUtils
  • Many unrelated methods
  • Domain-specific parameters
  • Database calls
  • API calls
  • Spring dependencies
  • Mutable static fields
  • Business rules
  • Authorization logic
  • Frequent growth

These indicate that the class probably lacks a cohesive responsibility.

Advanced Question

Question: Why can excessive static utility usage become problematic in a Spring Boot application?

Answer:

Spring applications normally benefit from explicit dependency injection.

Static business utilities can hide dependencies and make future requirements difficult to implement.

For example, a static pricing method may later require:

  • Configuration
  • Feature flags
  • Database data
  • Remote pricing information

A Spring component can receive these dependencies naturally through constructor injection, whereas a static utility often requires awkward global access or redesign.

Scenario-Based Question

Question: A utility method is used by 25 services. Should it remain static because it is highly reusable?

Answer:

Not necessarily.

Reuse does not determine whether something is a utility.

I would evaluate what the method represents.

If it performs generic deterministic transformation, a static utility may be appropriate.

If it represents a shared business rule, I would prefer a business component so the rule has explicit ownership and can evolve safely.

Code-Review Question

Question: You see a new method added to CommonUtils that loads an employee from the database. What would you comment?

Answer:

I would explain that database access has side effects and should be visible through a repository or service dependency.

For example:

This helper now performs database I/O, which is unexpected for a generic utility and hides the dependency from callers. Could we keep this lookup in the employee service/repository layer instead?

Real-Project Question

Question: When is a static utility method acceptable in production Java code?

Answer:

It is reasonable when the operation is:

  • Generic
  • Stateless
  • Deterministic
  • Free of hidden I/O
  • Independent of application business rules
  • Unlikely to need injected dependencies

Examples include focused string normalization, byte conversion, hashing helpers using established APIs, or collection transformations.

29. Quick Rule to Remember

If the method understands your business domain, it probably deserves a business owner—not a generic Utils class.

30. Final Takeaway

Utility classes are not inherently bad.

The problem is using them as a shortcut for deciding where code belongs.

What the Developer Should Remember

Before adding a static helper, ask:

  • Is this generic technical logic?
  • Is it stateless?
  • Does it have hidden side effects?
  • Does it understand business concepts?
  • Could it require dependencies later?
  • Is there already a class that should own this behavior?

Use utility classes only when they improve clarity.

What the Reviewer Should Check

During Pull Request review, check:

  • Whether the utility has a focused responsibility
  • Whether business logic is hidden inside static methods
  • Whether database or API calls are hidden
  • Whether mutable static state exists
  • Whether authorization is being bypassed
  • Whether Java or framework functionality already exists
  • Whether the method belongs to a domain service, entity, policy, formatter, validator, or integration component

What Should Be Avoided in Production Code

Avoid:

JAVA
CommonUtils
ApplicationUtils
GlobalHelper

becoming permanent dumping grounds for application logic.

Do not use static methods merely because they are easy to call.

Prefer code where ownership, dependencies, side effects, and business intent are obvious from the class and method names.