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:
CommonUtils
AppUtils
GeneralUtils
Helper
Utility
CommonHelperoften 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:
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:
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:
DiscountService
PricingService
DiscountPolicyThe 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:
OrderUtils.calculateDiscount(...)does not clearly communicate who owns the business rule.
A method such as:
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:
String normalization
Byte conversion
URL encoding
Hash generation
Collection chunkingDomain or Business Logic
Domain logic understands concepts such as:
Customer
Order
Payment
Employee
Policy
Invoice
Product
SubscriptionIt normally belongs in:
- Domain services
- Application services
- Entities
- Value objects
- Strategy components
- Dedicated Spring beans
Infrastructure Logic
Logic involving:
- Databases
- HTTP APIs
- 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
Utilsclasses 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:
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:
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:
"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
ApplicationUtilscurrently 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.
public record CreateCustomerRequest(
@NotBlank
@Email
String email,
@NotBlank
String firstName,
@NotBlank
String lastName) {
}Pricing Responsibility
@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:
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:
@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.
@PreAuthorize("hasRole('ADMIN')")
public void deleteCustomer(Long customerId) {
customerRepository.deleteById(customerId);
}Customer Display Name
If the behavior naturally belongs to the domain object:
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:
ApplicationUtils.buildOrderReference(...)Static Global Dependency Is Removed
Different application components now depend only on responsibilities they actually need.
12. Bad Code vs Improved Code
| Area | Generic Utility Approach | Responsibility-Based Approach |
|---|---|---|
| Ownership | Unclear | Explicit |
| Naming | Generic | Domain-specific |
| Cohesion | Low | High |
| Business Rules | Hidden in helpers | Located in business components |
| Testability | Static/global behavior | Focused components |
| Extensibility | Static methods grow in complexity | Components can evolve |
| Security | Manual helper checks | Framework-based authorization |
| Discoverability | Developers search utility files | Logic located by domain |
| Coupling | Many classes depend on one helper | Dependencies remain targeted |
| Maintenance | Utility class grows continuously | Changes remain localized |
13. Real Project Scenario
Consider a healthcare backend application.
Initially, the development team creates:
HealthcareUtilsIt contains only:
formatPatientName(...)
calculateAge(...)Later developers add:
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:
canAccessMedicalRecord(...)but another endpoint continues using different authorization logic.
The claims team modifies:
calculateClaimAmount(...)and unintentionally affects several workflows because the static method is used across unrelated services.
The appropriate design would separate responsibilities such as:
ClaimPricingService
PatientEligibilityService
MedicalRecordAuthorizationService
ClaimReferenceGenerator
PatientNameFormatterThis 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:
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:
@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:
ObjectsCollectionsOptionalStringjava.time- Spring utilities
- Apache Commons
Using Helpers to Hide Domain Behavior
Example:
OrderUtils.canCancel(order)may be less expressive than:
order.canBeCancelled()when the rule belongs directly to the order domain.
Giant Constants Utility Classes
Developers may create:
ConstantsUtilscontaining 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:
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:
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:
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:
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:
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:
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:
Pricing
Security
Formatting
Order state
Date handlingStep 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:
CommonUtilswith focused names such as:
StringNormalizer
FileNameSanitizer
CollectionChunkerStep 8: Prevent Future Dumping
Define clear team review rules around utility classes.
21. Best Practices
Prefer Intent-Revealing Names
Prefer:
PriceCalculator
OrderReferenceGenerator
TokenHasher
FileNameSanitizerinstead of:
AppUtils
CommonHelperUse Domain Objects Where Appropriate
Behavior strongly related to an object's state may belong on that object.
For example:
order.canBeCancelled()can be more expressive than:
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:
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:
CsvEscaper
HashingUtils
CollectionPartitionerprovided 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:
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:
SpringContextUtils.getBean(...)This hides dependencies and weakens testability.
Utility Chains
Avoid code such as:
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
- > 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?
- >
CommonUtilsis becoming a collection of unrelated methods. Please place this method in a class that represents its actual responsibility.
- > 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.
- > We should avoid storing the injected dependency in a static field. Please keep this as a normal Spring bean with constructor injection.
- > Spring already provides validation support for this case. Can we use Bean Validation instead of introducing another custom utility method?
- > This authorization check should use our Spring Security configuration rather than requiring each caller to remember to invoke a helper.
- > The method is reusable, but it is still domain logic. Reuse alone does not require making it static.
- > Could this behavior live on
Orderitself?order.canBeCancelled()would make the domain rule easier to discover thanOrderUtils.canCancel(order).
- > Please confirm whether this utility has hidden I/O. A method with a helper-style name should not unexpectedly perform network or database operations.
- > 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:
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:
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
@Component
public class PaymentEligibilityPolicy {
public boolean isAllowed(Customer customer) {
return customer != null
&& customer.isActive()
&& !customer.isBlocked();
}
}Improved Payment Service
@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
@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
@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:
CommonUtils
ApplicationUtils
HelperServiceand 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:
CommonUtils
ApplicationUtils
GlobalHelperbecoming 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.