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:
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:
PaymentUtils
OrderUtils
DatabaseUtils
EmailUtils
ApplicationUtilsand place important application behavior inside static methods.
For example:
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:
Math.max(...)
Collections.emptyList(...)
Objects.requireNonNull(...)
UUID.randomUUID()Application code can also have appropriate static helpers.
For example:
public final class MoneyUtils {
private MoneyUtils() {
}
public static boolean isPositive(BigDecimal amount) {
return amount != null && amount.signum() > 0;
}
}This is very different from:
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:
PaymentUtils.processPayment(request);the consumer is tied directly to PaymentUtils.
Replacing the implementation later requires modifying the caller.
With an injected dependency:
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:
PaymentGatewaycan receive a mock.
A service calling:
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:
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:
CommonUtilsmay 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
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
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:
PaymentUtilswith another implementation using dependency injection.
Instance-based design allows:
PaymentProcessor paymentProcessor;with implementations such as:
StripePaymentProcessor
RazorpayPaymentProcessor
MockPaymentProcessorThis 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.
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:
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:
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:
register(CustomerRequest request)but the actual operation depends on:
- CustomerRepository
- JavaMailSender
The caller cannot see these dependencies.
Initialization Risk
If:
setCustomerRepository(...)or:
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:
CustomerUtilsReplacing 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
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:
setCustomerRepository(...)are a major design smell in Spring applications.
They effectively create global mutable dependency configuration.
Finding 3: Hidden Side Effects
The name:
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:
CustomerRegistrationServicerather than in a general utility class.
9. Reviewer Comment Example
CustomerUtilskeeps 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
@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.
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:
CustomerRepository
CustomerNotificationServicethrough 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:
CustomerRegistrationServiceusing mocks.
Each test has its own dependencies.
Business Responsibility Has a Proper Owner
Customer registration is represented by:
CustomerRegistrationServiceinstead of hiding inside:
CustomerUtilsNotification 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
| Area | Excessive Static Design | Improved Design |
|---|---|---|
| Dependencies | Hidden in static fields | Explicit constructor dependencies |
| State | Global mutable state | Bean instance state |
| Testing | Requires static setup | Easy mock injection |
| Test isolation | Risk of cross-test contamination | Independent test instances |
| Spring integration | Bypasses DI | Works naturally with Spring |
| Substitution | Hard-coded class calls | Replaceable collaborators |
| Side effects | Hidden inside utility method | Owned by service |
| Readability | Utility naming hides workflow | Service naming communicates intent |
| Concurrency | Shared state risk | Safer dependency ownership |
13. Real Project Scenario
Consider a banking microservice.
A project begins with:
BankingUtilsThe class initially contains:
formatAccountNumber()
calculateInterest()These are relatively harmless utility functions.
Later developers add:
validateCustomer()
fetchAccount()
transferMoney()
sendOtp()
logTransaction()
callFraudService()
saveTransaction()All methods become static because the class already exists.
Eventually the transfer flow looks like:
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:
AccountRepository
FraudClient
NotificationClient
TransactionRepositoryThe 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:
MoneyTransferService
FraudChecker
AccountRepository
NotificationService
TransactionAuditServiceand 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:
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:
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:
CustomerUtils.findCustomer(id);when the implementation calls a repository.
Static HTTP Clients
Avoid:
PaymentUtils.callGateway(request);if it performs network operations.
Static Dependency Injection Hacks
Examples:
static ApplicationContext context;
static Repository repository;These usually indicate misuse of the DI framework.
Static Service Locator
Avoid patterns such as:
BeanProvider.getBean(PaymentService.class);inside normal business logic.
Dependencies should be explicit.
Global Configuration
Avoid:
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:
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:
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:
private static final Logger LOGGER =
LoggerFactory.getLogger(PaymentService.class);Factory Methods
Static factory methods can be useful.
Example:
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:
@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:
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:
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:
public static String API_SECRET;or:
private static String accessToken;if values are mutable or unnecessarily exposed.
Cross-Request Leakage
Never store user-specific information in static fields:
currentUser
currentToken
currentAccountThese 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:
@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:
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:
PaymentUtils.processPayment()may internally depend on:
PaymentGateway
PaymentRepositoryMake those dependencies explicit.
Step 3: Create an Instance Service
Create:
PaymentServicewith constructor dependencies.
Step 4: Move the Static Implementation
Move behavior without changing business rules.
Step 5: Update One Caller
Replace:
PaymentUtils.processPayment(request);with:
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:
PaymentRequest.of(...)can be clear and safe.
Make Utility Classes Non-Instantiable
Use:
private UtilityClass() {
}when the class intentionally contains only static operations.
Keep Utilities Cohesive
Prefer:
DateTimeUtils
MoneyUtilsover:
CommonUtilswhen 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:
ApplicationContextProvider.getBean(...);for ordinary application dependencies.
God Utility Classes
Avoid:
CommonUtils
ApplicationUtils
GeneralUtilscontaining unrelated static methods.
Static Request Context
Avoid storing request information in normal static fields.
Static Repository Wrappers
Avoid:
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
ApplicationContextbeing 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
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?
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.
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.
Could we avoid static setter injection here? Bean initialization order becomes implicit and a missing initialization can result in runtime null failures.
This cache is a static mutable HashMap shared across request threads. Please review thread safety, eviction, and memory-growth behavior.
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.
We are using static mocking in several tests for this new code. That may indicate this dependency should be injectable instead.
This CommonUtils class now contains validation, HTTP calls, JSON mapping, and database helpers. Could we move these responsibilities into focused components?
Please avoid storing the current user in a static field. The value would be shared across concurrent requests.
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
currentCustomerIdsafe? - 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:
paymentRepository
paymentGatewayClientglobally.
Dependencies should be explicit instance dependencies.
Issue 2: currentCustomerId Is Unsafe
This field:
private static String currentCustomerId;is shared by all requests.
Consider two requests:
Request A -> customerId = CUST-100
Request B -> customerId = CUST-200Request 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
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:
process(...)someone must call:
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
@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:
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
CommonUtilsclasses - 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."