Avoiding Excessive Static Methods

22 min read

Object-Oriented Design and SOLID Review — review Java static method usage and move replaceable or testable behavior into injected services instead.

1. Introduction

Static methods are useful in Java, but they become a design problem when developers use them as the default solution for business logic, infrastructure calls, state management, or application services.

A static method belongs to the class rather than to an object instance.

For example:

JAVA
public class StringUtils {
    public static boolean isBlank(String value) {
        return value == null || value.isBlank();
    }
}

This is a reasonable use because the method:

  • Has no object-specific state
  • Has no dependency
  • Produces a result only from its input
  • Represents simple reusable utility behavior

The problem starts when developers create classes such as:

JAVA
PaymentUtils
OrderUtils
DatabaseUtils
EmailUtils
ApplicationUtils

and place important application behavior inside static methods.

For example:

JAVA
PaymentUtils.processPayment(...)
OrderUtils.createOrder(...)
EmailUtils.sendEmail(...)
DatabaseUtils.saveCustomer(...)

Such code may initially look convenient because no object needs to be created and no dependency needs to be injected.

In real Spring Boot applications, however, excessive static methods can create:

  • Tight coupling
  • Hidden dependencies
  • Difficult unit testing
  • Global mutable state
  • Poor dependency injection
  • Hard-to-replace implementations
  • Unclear ownership of business logic
  • Concurrency problems
  • Configuration problems

During code review, the question should not be:

"Are static methods bad?"

The better question is:

Does this method genuinely represent stateless utility behavior, or is static being used to avoid proper object design and dependency management?

2. What This Topic Means

Avoiding excessive static methods means using static methods only where they naturally fit instead of turning application logic into globally accessible procedural code.

Static methods are appropriate for operations that:

  • Do not depend on instance state
  • Do not require injected collaborators
  • Are deterministic or nearly pure
  • Represent general-purpose utility behavior
  • Do not need runtime implementation substitution

Examples include:

JAVA
Math.max(...)
Collections.emptyList(...)
Objects.requireNonNull(...)
UUID.randomUUID()

Application code can also have appropriate static helpers.

For example:

JAVA
public final class MoneyUtils {
    private MoneyUtils() {
    }

    public static boolean isPositive(BigDecimal amount) {
        return amount != null && amount.signum() > 0;
    }
}

This is very different from:

JAVA
public class PaymentUtils {
    public static PaymentResult process(
            PaymentRepository repository,
            PaymentGateway gateway,
            PaymentRequest request) {
        ...
    }
}

The second example is effectively an application service disguised as a utility method.

3. Why It Matters in Real Projects

Maintainability

Static application logic creates direct class-level coupling.

If code contains:

JAVA
PaymentUtils.processPayment(request);

the consumer is tied directly to PaymentUtils.

Replacing the implementation later requires modifying the caller.

With an injected dependency:

JAVA
paymentProcessor.process(request);

the implementation can change behind the contract.

Testability

Static methods can be harder to replace during unit tests.

A service depending on:

JAVA
PaymentGateway

can receive a mock.

A service calling:

JAVA
PaymentUtils.callGateway()

has a hard-coded dependency.

Although modern mocking frameworks may support static mocking, relying on static mocking for normal application logic is usually a warning sign.

Dependency Injection

Spring manages object lifecycles and dependencies through beans.

Static methods bypass that object model.

They do not naturally participate in:

  • Constructor injection
  • Bean scopes
  • Decorators
  • Proxies
  • AOP
  • Transaction management
  • Runtime implementation selection

Configuration

Static utility classes often accumulate global configuration.

Example:

JAVA
private static String apiUrl;
private static String apiKey;

This introduces mutable global state and makes tests and parallel execution harder.

Reliability

Static mutable state is shared across threads.

In Spring Boot applications, multiple requests may execute concurrently.

Unsafe static state can cause race conditions and cross-request data leakage.

Team Development

Large utility classes often become dumping grounds.

Developers keep adding unrelated functionality because calling a static method is convenient.

Over time:

JAVA
CommonUtils

may contain:

  • Date formatting
  • JSON parsing
  • Email sending
  • Validation
  • Encryption
  • Database access
  • HTTP calls

This creates poor ownership and difficult PR reviews.

4. Core Concept

The key distinction is between:

Stateless utility behavior

and:

Application behavior with dependencies or lifecycle.

Good Static Candidate

JAVA
public final class PercentageUtils {
    private PercentageUtils() {
    }

    public static BigDecimal calculate(
            BigDecimal amount,
            BigDecimal percentage) {
        return amount
                .multiply(percentage)
                .divide(BigDecimal.valueOf(100));
    }
}

The method depends only on its inputs.

Poor Static Candidate

JAVA
public class OrderUtils {
    private static OrderRepository orderRepository;
    private static PaymentGateway paymentGateway;

    public static Order createOrder(OrderRequest request) {
        PaymentResult result = paymentGateway.charge(request);
        Order order = mapOrder(request, result);
        return orderRepository.save(order);
    }
}

This method:

  • Has dependencies
  • Performs business workflow
  • Accesses infrastructure
  • Requires configuration
  • Has side effects
  • Needs testing and substitution

It belongs in a service, not in a utility class.

Java-Specific Consideration

Static methods are resolved through the declaring class.

They are not polymorphic in the same way as instance methods.

You cannot naturally substitute:

JAVA
PaymentUtils

with another implementation using dependency injection.

Instance-based design allows:

JAVA
PaymentProcessor paymentProcessor;

with implementations such as:

JAVA
StripePaymentProcessor
RazorpayPaymentProcessor
MockPaymentProcessor

This flexibility is valuable in real projects.

5. Important Rules

  • Use static methods for genuinely stateless utility behavior.
  • Do not make business services static for convenience.
  • Avoid static mutable fields.
  • Do not store request-specific data in static fields.
  • Do not use static methods to bypass dependency injection.
  • Prefer constructor-injected collaborators for external systems.
  • Avoid static database access methods in Spring applications.
  • Avoid static HTTP client calls for business integrations.
  • Avoid static wrappers around Spring beans.
  • Do not introduce static methods merely because a method does not currently access fields.
  • Consider whether future dependencies are likely.
  • Keep utility classes small and cohesive.
  • Make utility-class constructors private.
  • Prefer pure static methods where possible.
  • Do not hide global state behind static accessors.
  • Avoid static service locators.
  • Avoid static methods where runtime substitution is required.
  • Do not force instance classes when a simple pure utility is clearly sufficient.
  • Review static methods based on responsibility, side effects, and dependencies.

6. Bad Code Example

Consider a Spring Boot customer-registration flow.

JAVA
public class CustomerUtils {
    private static CustomerRepository customerRepository;
    private static JavaMailSender mailSender;

    public static void setCustomerRepository(CustomerRepository repository) {
        customerRepository = repository;
    }

    public static void setMailSender(JavaMailSender sender) {
        mailSender = sender;
    }

    public static Customer register(CustomerRequest request) {
        if (request == null) {
            throw new IllegalArgumentException("Request cannot be null");
        }

        if (request.getEmail() == null || request.getEmail().isBlank()) {
            throw new IllegalArgumentException("Email is required");
        }

        Customer customer = new Customer();
        customer.setName(request.getName());
        customer.setEmail(request.getEmail());

        Customer savedCustomer = customerRepository.save(customer);

        SimpleMailMessage message = new SimpleMailMessage();
        message.setTo(savedCustomer.getEmail());
        message.setSubject("Welcome");
        message.setText("Welcome " + savedCustomer.getName());

        mailSender.send(message);

        return savedCustomer;
    }
}

A caller uses:

JAVA
Customer customer = CustomerUtils.register(request);

At first glance, this reduces object creation and constructor injection.

However, it introduces several design problems.

7. Problems in the Bad Code

Global Mutable Dependencies

These fields:

JAVA
private static CustomerRepository customerRepository;
private static JavaMailSender mailSender;

are shared globally.

Their values can be changed at runtime.

Hidden Dependency Requirements

The method signature is:

JAVA
register(CustomerRequest request)

but the actual operation depends on:

  • CustomerRepository
  • JavaMailSender

The caller cannot see these dependencies.

Initialization Risk

If:

JAVA
setCustomerRepository(...)

or:

JAVA
setMailSender(...)

is not called before register(), the application can fail with NullPointerException.

Difficult Testing

Tests must configure global state before calling the method.

Test order can begin affecting results.

Parallel Test Problems

Two tests may modify the same static dependencies concurrently.

This can create nondeterministic failures.

Tight Coupling

Every caller is directly coupled to:

JAVA
CustomerUtils

Replacing registration behavior requires modifying callers.

Business Logic in Utility Class

Customer registration is a use case, not generic utility behavior.

Infrastructure Mixed with Business Logic

The method handles:

  • Validation
  • Entity creation
  • Persistence
  • Email

This is application-service behavior.

Poor Spring Integration

Static methods bypass normal Spring dependency injection.

8. Code Review Findings

A senior reviewer should notice the following.

Finding 1: Static Dependencies

Any mutable dependency stored in a static field should be questioned.

Repositories and mail senders should usually be injected into managed beans.

Finding 2: Static Setter Injection

Methods such as:

JAVA
setCustomerRepository(...)

are a major design smell in Spring applications.

They effectively create global mutable dependency configuration.

Finding 3: Hidden Side Effects

The name:

JAVA
CustomerUtils.register(...)

looks utility-like, but the method performs database writes and sends email.

Reviewers should question whether the class name accurately communicates behavior.

Finding 4: Test Isolation Risk

Because dependencies are static, one test can affect another.

Finding 5: No Runtime Substitution

Consumers cannot easily switch registration strategies.

Finding 6: Incorrect Responsibility Classification

Registration belongs in an application service such as:

JAVA
CustomerRegistrationService

rather than in a general utility class.

9. Reviewer Comment Example

CustomerUtils keeps Spring dependencies in static mutable fields. Could we move this workflow into a normal Spring service and inject CustomerRepository and JavaMailSender through the constructor? That would remove global state and improve test isolation.

Another review comment:

register() performs persistence and notification side effects, so it does not behave like a utility function. Consider modelling this as an application service rather than a static helper.

10. Improved Code

JAVA
@Service
public class CustomerRegistrationService {
    private final CustomerRepository customerRepository;
    private final CustomerNotificationService notificationService;

    public CustomerRegistrationService(
            CustomerRepository customerRepository,
            CustomerNotificationService notificationService) {
        this.customerRepository = customerRepository;
        this.notificationService = notificationService;
    }

    public Customer register(CustomerRequest request) {
        validate(request);

        Customer customer = new Customer();
        customer.setName(request.getName());
        customer.setEmail(request.getEmail());

        Customer savedCustomer = customerRepository.save(customer);

        notificationService.sendWelcomeMessage(savedCustomer);

        return savedCustomer;
    }

    private void validate(CustomerRequest request) {
        if (request == null) {
            throw new IllegalArgumentException("Customer request cannot be null");
        }

        if (request.getEmail() == null || request.getEmail().isBlank()) {
            throw new IllegalArgumentException("Customer email is required");
        }
    }
}

@Service
public class CustomerNotificationService {
    private final JavaMailSender mailSender;

    public CustomerNotificationService(JavaMailSender mailSender) {
        this.mailSender = mailSender;
    }

    public void sendWelcomeMessage(Customer customer) {
        SimpleMailMessage message = new SimpleMailMessage();
        message.setTo(customer.getEmail());
        message.setSubject("Welcome");
        message.setText("Welcome " + customer.getName());

        mailSender.send(message);
    }
}

A utility method can still be used where appropriate.

JAVA
public final class EmailAddressUtils {
    private EmailAddressUtils() {
    }

    public static String normalize(String email) {
        if (email == null) {
            return null;
        }

        return email.trim().toLowerCase(Locale.ROOT);
    }
}

11. Improved Code Explanation

Dependencies Are Explicit

CustomerRegistrationService declares:

JAVA
CustomerRepository
CustomerNotificationService

through constructor injection.

Anyone reading the class can immediately see what the service needs.

No Global Mutable State

Dependencies belong to the service instance managed by Spring.

There are no static setters.

Testing Becomes Easier

A unit test can construct:

JAVA
CustomerRegistrationService

using mocks.

Each test has its own dependencies.

Business Responsibility Has a Proper Owner

Customer registration is represented by:

JAVA
CustomerRegistrationService

instead of hiding inside:

JAVA
CustomerUtils

Notification Infrastructure Is Encapsulated

The registration service does not know how email is sent.

Static Utility Still Has a Place

EmailAddressUtils.normalize() is reasonable because it:

  • Depends only on input
  • Has no injected dependency
  • Does not access global mutable state
  • Has no external side effect

The goal is not eliminating static methods.

The goal is using them appropriately.

12. Bad Code vs Improved Code

AreaExcessive Static DesignImproved Design
DependenciesHidden in static fieldsExplicit constructor dependencies
StateGlobal mutable stateBean instance state
TestingRequires static setupEasy mock injection
Test isolationRisk of cross-test contaminationIndependent test instances
Spring integrationBypasses DIWorks naturally with Spring
SubstitutionHard-coded class callsReplaceable collaborators
Side effectsHidden inside utility methodOwned by service
ReadabilityUtility naming hides workflowService naming communicates intent
ConcurrencyShared state riskSafer dependency ownership

13. Real Project Scenario

Consider a banking microservice.

A project begins with:

JAVA
BankingUtils

The class initially contains:

JAVA
formatAccountNumber()
calculateInterest()

These are relatively harmless utility functions.

Later developers add:

JAVA
validateCustomer()
fetchAccount()
transferMoney()
sendOtp()
logTransaction()
callFraudService()
saveTransaction()

All methods become static because the class already exists.

Eventually the transfer flow looks like:

JAVA
BankingUtils.transferMoney(request);

Inside this method, the class:

  • Reads account data
  • Calls fraud APIs
  • Updates balances
  • Persists transactions
  • Sends OTP messages
  • Writes audit logs

To support these operations, developers add static fields for:

JAVA
AccountRepository
FraudClient
NotificationClient
TransactionRepository

The class now acts as an entire application layer hidden behind static methods.

Problems appear when:

  • Tests run in parallel.
  • One environment changes static configuration.
  • Another team needs a different fraud provider.
  • Transaction handling must be introduced.
  • Spring retry or AOP needs to be applied.
  • Production incidents require mocking one integration.

A better design separates:

JAVA
MoneyTransferService
FraudChecker
AccountRepository
NotificationService
TransactionAuditService

and keeps only genuine calculation utilities static.

14. Production Impact

Race Conditions

Static mutable data is shared between threads.

If request-specific values are stored statically, one user's request can overwrite another user's data.

Cross-Request Data Leakage

A severe example is:

JAVA
private static String currentCustomerId;

In a multi-threaded web application, multiple requests can access the same value.

This can produce incorrect processing or data exposure.

Configuration Leakage

Tests or runtime components changing static configuration can affect the entire JVM.

Difficult Recovery

Static state can survive much longer than expected.

Recovering from corrupted global state may require restarting the application.

Harder Incident Debugging

Static methods hide dependency relationships and make runtime behavior harder to trace.

Reduced Flexibility

Replacing one provider or implementation can require changes across many callers.

Broken Framework Features

Static self-contained workflows may bypass features developers expect from Spring-managed beans, such as:

  • Transactions
  • Retry
  • Security interceptors
  • Metrics
  • AOP

15. Common Developer Mistakes

Making Methods Static Because They Do Not Currently Use Fields

Example:

JAVA
public static BigDecimal calculatePrice(...)

may be fine.

But developers should ask whether the behavior is a reusable calculation or part of a domain service likely to require dependencies.

Static Repository Access

Avoid:

JAVA
CustomerUtils.findCustomer(id);

when the implementation calls a repository.

Static HTTP Clients

Avoid:

JAVA
PaymentUtils.callGateway(request);

if it performs network operations.

Static Dependency Injection Hacks

Examples:

JAVA
static ApplicationContext context;
static Repository repository;

These usually indicate misuse of the DI framework.

Static Service Locator

Avoid patterns such as:

JAVA
BeanProvider.getBean(PaymentService.class);

inside normal business logic.

Dependencies should be explicit.

Global Configuration

Avoid:

JAVA
public static String API_KEY;

especially when mutable.

Global Cache Without Concurrency Design

A static HashMap used as an application cache can introduce:

  • Race conditions
  • Memory growth
  • Consistency problems

Use a proper cache implementation when needed.

Static Logger Confusion

A static final logger is generally fine:

JAVA
private static final Logger LOGGER = LoggerFactory.getLogger(OrderService.class);

This does not mean every static field is problematic.

Converting Everything to Instance Methods

Not every static method needs dependency injection.

Pure utilities can remain static.

16. Edge Cases

Constants

Static final constants are appropriate:

JAVA
private static final int MAX_RETRY_ATTEMPTS = 3;

when the value truly belongs to the class and is immutable.

Logger Fields

This is a standard and appropriate pattern:

JAVA
private static final Logger LOGGER =
        LoggerFactory.getLogger(PaymentService.class);

Factory Methods

Static factory methods can be useful.

Example:

JAVA
Money.of(amount, currency);

They may improve readability compared with constructors.

Immutable Objects

Static factory methods are common in immutable domain objects.

Stateless Validation Helpers

A small pure validation helper can reasonably be static.

Thread-Safe Shared State

Some static shared objects may be safe when intentionally designed, immutable, or thread-safe.

However, their lifecycle and memory implications must still be reviewed.

Static Initialization

Complex logic inside static initializers should be avoided because failures can cause class-initialization problems.

Legacy Code

Removing static calls from a large legacy codebase may require incremental refactoring rather than a full rewrite.

17. Performance Considerations

Static versus instance method invocation is rarely an important performance concern in normal business applications.

Do not choose static methods because:

"Static calls are faster."

Modern JVM optimizations make such differences irrelevant for most Spring Boot applications.

Actual performance is usually dominated by:

  • Database calls
  • External API calls
  • Serialization
  • Network latency
  • File I/O
  • Large collections
  • Lock contention

Object Creation

Developers sometimes use static methods to avoid object creation.

In Spring applications, services are often singleton beans anyway.

Therefore:

JAVA
@Service
public class PricingService {
}

does not mean Spring creates a new service object for every method call.

Global Caches

Static caches can create memory problems.

For example:

JAVA
private static final Map<String, Customer> CACHE = new HashMap<>();

If entries are never removed, memory consumption can continuously increase.

Contention

Static synchronized methods can become global bottlenecks:

JAVA
public static synchronized void update(...) {
}

Every thread competes for the same class-level lock.

The performance concern is therefore not static invocation itself but shared state and synchronization design.

18. Security Considerations

Static Sensitive Data

Avoid:

JAVA
public static String API_SECRET;

or:

JAVA
private static String accessToken;

if values are mutable or unnecessarily exposed.

Cross-Request Leakage

Never store user-specific information in static fields:

JAVA
currentUser
currentToken
currentAccount

These values are shared by all threads.

Logging

Static utility methods handling sensitive data may introduce hidden logging.

Review for:

  • Passwords
  • Tokens
  • Personal information
  • Card numbers
  • API secrets

Authorization

Business authorization logic should not be hidden inside generic static utility methods.

Security boundaries should remain explicit.

Cryptography

Avoid creating custom static crypto utilities unless they correctly use secure Java libraries and proper key management.

Do not treat cryptographic operations as simple formatting utilities.

19. Testing Considerations

Pure Static Utility

A pure static method is easy to test directly.

Example:

JAVA
@Test
void shouldCalculatePercentage() {
    BigDecimal result = PercentageUtils.calculate(
            new BigDecimal("200"),
            new BigDecimal("10")
    );

    assertEquals(new BigDecimal("20"), result);
}

No mocking is required.

Static Application Logic

Testing becomes harder when the static method calls:

  • Repository
  • HTTP API
  • Mail service
  • Clock
  • File system

Avoid Static Mocking as Default Design

Static mocking can be useful for legacy code, but it should not become the architecture.

If normal unit tests constantly require:

JAVA
mockStatic(...)

review whether the dependency should become injectable.

Concurrency Tests

If static mutable state exists, test:

  • Parallel execution
  • Concurrent updates
  • State visibility
  • Race conditions

Integration Tests

Verify Spring-managed services through normal dependency injection.

Test Isolation

Each test should be capable of running independently.

Tests should not require resetting global static dependencies.

20. Refactoring Guidelines

Legacy code may contain hundreds of static calls.

Refactor incrementally.

Step 1: Identify Static Methods with Side Effects

Prioritize methods that:

  • Write to databases
  • Call external APIs
  • Send messages
  • Access files
  • Modify global state

Pure utility methods can often remain unchanged.

Step 2: Identify Dependencies

For example:

JAVA
PaymentUtils.processPayment()

may internally depend on:

JAVA
PaymentGateway
PaymentRepository

Make those dependencies explicit.

Step 3: Create an Instance Service

Create:

JAVA
PaymentService

with constructor dependencies.

Step 4: Move the Static Implementation

Move behavior without changing business rules.

Step 5: Update One Caller

Replace:

JAVA
PaymentUtils.processPayment(request);

with:

JAVA
paymentService.processPayment(request);

Step 6: Add Tests

Verify behavior remains unchanged.

Step 7: Migrate Remaining Callers

Change them incrementally.

Step 8: Remove Static Global State

Once no callers depend on it, remove the static fields.

Step 9: Keep Valid Utilities

Do not convert unrelated pure static functions unnecessarily.

Step 10: Avoid Large Refactoring PRs

Migrating static architecture incrementally reduces regression risk.

21. Best Practices

Keep Pure Utilities Pure

Good utility classes should generally avoid:

  • Spring dependencies
  • Repository access
  • HTTP calls
  • Mutable shared state

Prefer Constructor Injection

Application services should declare dependencies explicitly.

Use Static Factory Methods Where Appropriate

Static does not automatically mean bad design.

Factory methods such as:

JAVA
PaymentRequest.of(...)

can be clear and safe.

Make Utility Classes Non-Instantiable

Use:

JAVA
private UtilityClass() {
}

when the class intentionally contains only static operations.

Keep Utilities Cohesive

Prefer:

JAVA
DateTimeUtils
MoneyUtils

over:

JAVA
CommonUtils

when static utilities are genuinely necessary.

Keep Side Effects in Managed Components

Database operations, network operations, email, and messaging should normally live in injectable services or adapters.

Prefer Immutability for Shared Static Data

If data is static, immutable values are much safer than mutable values.

22. Practices to Avoid

Static Spring Bean Access

Avoid storing Spring beans in static fields.

ApplicationContext Lookup Everywhere

Avoid:

JAVA
ApplicationContextProvider.getBean(...);

for ordinary application dependencies.

God Utility Classes

Avoid:

JAVA
CommonUtils
ApplicationUtils
GeneralUtils

containing unrelated static methods.

Static Request Context

Avoid storing request information in normal static fields.

Static Repository Wrappers

Avoid:

JAVA
DatabaseUtils.save(...);

when proper repositories already exist.

Static External API Calls

Avoid hiding integration dependencies behind static methods.

Static Mutable Collections

Avoid unbounded globally shared mutable maps and lists.

Making Everything Non-Static

Do not introduce injectable beans for trivial pure calculations merely to follow a rule mechanically.

23. Code Review Checklist

  • Does this static method genuinely represent utility behavior?
  • Does the method depend only on its parameters?
  • Does the method access a repository?
  • Does the method call an external service?
  • Does the method perform business workflow orchestration?
  • Does the class contain mutable static fields?
  • Is request-specific state stored statically?
  • Are dependencies hidden in static fields?
  • Is static setter injection being used?
  • Is ApplicationContext being used as a service locator?
  • Would constructor injection make dependencies clearer?
  • Does the method need runtime implementation substitution?
  • Are tests forced to use static mocking?
  • Could static state leak between tests?
  • Could static state be accessed concurrently?
  • Is a static collection acting as an unmanaged cache?
  • Is sensitive information stored in static fields?
  • Does the utility class contain unrelated methods?
  • Would converting this method to an instance service improve design?
  • Is the static method actually pure and safe enough to remain static?

24. Common Pull Request Review Comments

  1. This static method performs database access, so it behaves more like an application service than a utility. Could we move it into a Spring-managed component with constructor injection?
  1. The repository is stored in a mutable static field. This introduces global state and can make tests interfere with each other. Please consider normal dependency injection.
  1. This method calls an external payment provider but hides that dependency behind a static utility call. An injected PaymentProcessor would make the dependency explicit and replaceable.
  1. Could we avoid static setter injection here? Bean initialization order becomes implicit and a missing initialization can result in runtime null failures.
  1. This cache is a static mutable HashMap shared across request threads. Please review thread safety, eviction, and memory-growth behavior.
  1. This helper is pure and has no dependencies, so keeping it static looks reasonable. I would avoid introducing a Spring bean only for this calculation.
  1. We are using static mocking in several tests for this new code. That may indicate this dependency should be injectable instead.
  1. This CommonUtils class now contains validation, HTTP calls, JSON mapping, and database helpers. Could we move these responsibilities into focused components?
  1. Please avoid storing the current user in a static field. The value would be shared across concurrent requests.
  1. The static call bypasses the normal Spring-managed service boundary. Could we expose this operation through an injected collaborator instead?

25. Code Review Exercise

Review the following implementation.

Identify:

  • Static-related design smells
  • Hidden dependencies
  • Concurrency risks
  • Testing problems
  • Security concerns
  • Refactoring opportunities

public class PaymentUtils { private static PaymentRepository paymentRepository; private static PaymentGatewayClient paymentGatewayClient; private static String currentCustomerId; private static final Map<String, PaymentResult> CACHE = new HashMap<>();

``` public static void initialize( PaymentRepository repository, PaymentGatewayClient client) { paymentRepository = repository; paymentGatewayClient = client; }

public static PaymentResult process( String customerId, PaymentRequest request) { currentCustomerId = customerId;

if (CACHE.containsKey(request.getTransactionId())) { return CACHE.get(request.getTransactionId()); }

PaymentResponse response = paymentGatewayClient.charge(request);

Payment payment = new Payment(); payment.setCustomerId(currentCustomerId); payment.setAmount(request.getAmount()); payment.setStatus(response.getStatus());

paymentRepository.save(payment);

PaymentResult result = new PaymentResult( response.getStatus() );

CACHE.put(request.getTransactionId(), result);

return result; } ```

}

Review questions:

  • Should payment processing be static?
  • What happens when multiple requests execute concurrently?
  • Is currentCustomerId safe?
  • Is the cache thread-safe?
  • Who initializes the dependencies?
  • What happens if initialization is missed?
  • Can the cache grow indefinitely?
  • How easily can the gateway be mocked?
  • Should repository access exist in a utility class?

Do not reveal the solution before completing the review.

26. Exercise Solution

Issue 1: Static Mutable Dependencies

The class stores:

JAVA
paymentRepository
paymentGatewayClient

globally.

Dependencies should be explicit instance dependencies.

Issue 2: currentCustomerId Is Unsafe

This field:

JAVA
private static String currentCustomerId;

is shared by all requests.

Consider two requests:

JAVA
Request A -> customerId = CUST-100
Request B -> customerId = CUST-200

Request B can overwrite the value before Request A creates the payment.

Request A may then save the wrong customer ID.

This is a serious production correctness issue.

Issue 3: Static HashMap Is Not Thread-Safe

JAVA
new HashMap<>()

is accessed by multiple threads without synchronization.

Concurrent access can produce unpredictable behavior.

Issue 4: Unbounded Memory

Entries are never evicted.

The cache may grow for the lifetime of the application.

Issue 5: Hidden Initialization Requirement

Before calling:

JAVA
process(...)

someone must call:

JAVA
initialize(...)

If initialization does not occur, runtime failure is possible.

Issue 6: Difficult Test Isolation

Tests share static dependencies and cache data.

Issue 7: Business Logic Misclassified as Utility

Payment processing is a domain/application operation.

Improved Code

JAVA
@Service
public class PaymentService {
    private final PaymentRepository paymentRepository;
    private final PaymentGatewayClient paymentGatewayClient;
    private final PaymentResultCache paymentResultCache;

    public PaymentService(
            PaymentRepository paymentRepository,
            PaymentGatewayClient paymentGatewayClient,
            PaymentResultCache paymentResultCache) {
        this.paymentRepository = paymentRepository;
        this.paymentGatewayClient = paymentGatewayClient;
        this.paymentResultCache = paymentResultCache;
    }

    public PaymentResult process(
            String customerId,
            PaymentRequest request) {

        return paymentResultCache
                .find(request.getTransactionId())
                .orElseGet(() -> processNewPayment(customerId, request));
    }

    private PaymentResult processNewPayment(
            String customerId,
            PaymentRequest request) {

        PaymentResponse response = paymentGatewayClient.charge(request);

        Payment payment = new Payment();
        payment.setCustomerId(customerId);
        payment.setAmount(request.getAmount());
        payment.setStatus(response.getStatus());

        paymentRepository.save(payment);

        PaymentResult result = new PaymentResult(
                response.getStatus()
        );

        paymentResultCache.put(
                request.getTransactionId(),
                result
        );

        return result;
    }
}

@Component
public class PaymentResultCache {
    private final ConcurrentMap<String, PaymentResult> cache =
            new ConcurrentHashMap<>();

    public Optional<PaymentResult> find(String transactionId) {
        return Optional.ofNullable(cache.get(transactionId));
    }

    public void put(
            String transactionId,
            PaymentResult result) {
        cache.put(transactionId, result);
    }
}

In a real production system, a proper cache implementation such as Caffeine or a distributed cache may be more appropriate depending on the requirements.

Why This Is Better

Dependencies are constructor-injected.

Customer ID remains a local method parameter.

There is no global request state.

The service can be tested using mocks.

Cache behavior is isolated.

A future implementation can replace the cache without changing payment workflow code.

Spring manages the service lifecycle.

27. Interview Perspective

Interview questions about static methods are usually more useful when they focus on design rather than syntax.

A basic interview may ask:

"What is a static method?"

A senior interview may ask:

"You inherit a Spring Boot system where most service methods are static. What problems would you expect?"

A strong answer should discuss:

  • Dependency injection
  • Testability
  • Global state
  • Thread safety
  • Polymorphism
  • Coupling
  • Framework proxies
  • Transactions
  • Configuration
  • Appropriate utility use

Another common scenario is:

"Are static methods bad?"

The correct answer is not simply yes.

Static methods are appropriate for stateless utility operations and factory methods.

The problem is excessive use for behavior that needs dependencies, state management, runtime substitution, or framework integration.

28. Interview Questions and Answers

Basic Question

Question: When is a static method appropriate in Java?

Answer:

A static method is appropriate when the operation logically belongs to the class rather than an instance and does not require instance-specific state.

Good examples include:

  • Pure calculations
  • Stateless transformations
  • Utility operations
  • Factory methods

The method should generally depend only on its parameters or immutable class-level data.

Intermediate Question

Question: Why can excessive static methods make unit testing harder?

Answer:

Static calls create direct coupling to a specific implementation.

With dependency injection, I can provide a mock implementation through the constructor.

With a direct static call, I may need static mocking or other workarounds.

If static methods also use global mutable state, test isolation becomes even more difficult.

Advanced Question

Question: Why are static mutable fields dangerous in a Spring Boot web application?

Answer:

Static fields are shared across all threads in the JVM.

Multiple HTTP requests can read and write the same value simultaneously.

This can cause:

  • Race conditions
  • Cross-request data leakage
  • Incorrect business results
  • Visibility problems
  • Nondeterministic bugs

Request-specific data should never be stored in ordinary static fields.

Scenario-Based Question

Question: A team creates PaymentUtils.processPayment() because they do not want to inject PaymentService into multiple classes. Would you approve it?

Answer:

No, not solely for that reason.

Payment processing has infrastructure dependencies and side effects.

Turning it into a static method hides those dependencies and increases coupling.

I would keep payment processing behind an injected service or interface.

The number of consumers does not justify converting an application service into a utility.

Code-Review Question

Question: You see a static method calling ApplicationContext.getBean() to obtain a repository. What would you review?

Answer:

I would consider it a service-locator smell.

The method hides its dependency and bypasses normal constructor injection.

I would recommend making the containing component Spring-managed and injecting the repository explicitly.

Real-Project Question

Question: Is private static final Logger LOGGER a problem?

Answer:

No.

A static final logger is a normal Java pattern.

It is effectively immutable shared infrastructure associated with the class.

The problem is not the static keyword itself.

The concern is inappropriate shared mutable state or static business dependencies.

Spring Boot Question

Question: Can @Transactional be applied effectively to arbitrary static methods?

Answer:

Spring's normal declarative transaction management works through managed bean proxies and instance method interception.

Static methods do not participate in that model like normal proxied instance methods.

Transaction boundaries should normally be placed on methods of Spring-managed components.

Design Question

Question: Should every utility method become an injectable service for testability?

Answer:

No.

Pure deterministic utility functions often need no mocking.

For example:

JAVA
MoneyUtils.round(amount)

can be directly tested.

Injection is more useful when behavior has dependencies, side effects, configuration, lifecycle, or requires implementation substitution.

29. Quick Rule to Remember

Keep static methods for stateless utilities; keep business workflows, dependencies, and mutable state in normal objects.

30. Final Takeaway

Static methods are a useful Java language feature.

They become problematic when they are used as a shortcut around proper application design.

Developers should remember:

  • Static does not automatically mean bad.
  • Pure utility functions can remain static.
  • Constants and loggers are common valid static members.
  • Static factory methods can improve API design.
  • Business workflows should normally live in objects or Spring-managed services.
  • External dependencies should be explicit.
  • Request-specific state must not be static.
  • Mutable global state should be treated very carefully.
  • Static mocking should not be required for routine new application code.

During Pull Request review, pay particular attention to:

  • Repositories stored in static fields
  • Static service dependencies
  • Static setter injection
  • ApplicationContext-based service lookup
  • Request data stored globally
  • Unbounded static caches
  • Static methods performing database writes
  • Static methods calling external APIs
  • Large CommonUtils classes
  • Static business workflows disguised as utilities

Do not reject a method merely because it is static.

Instead ask:

Does this operation genuinely behave like a stateless utility, or does it have dependencies, side effects, lifecycle, or business responsibility that should belong to an object?

That question leads to much better production code than a blanket rule such as "never use static methods."