1. Introduction
Dead code is code that is no longer used, no longer reachable, no longer required, or no longer contributes to the current behavior of the application.
In real Java projects, dead code commonly appears after:
- Feature changes
- Refactoring
- API migrations
- Database redesign
- Replacing old business rules
- Removing integrations
- Introducing new service implementations
- Renaming or replacing methods
- Moving functionality between modules
- Disabling legacy workflows
- Removing feature flags
Dead code may include:
- Unused methods
- Unused classes
- Unused private fields
- Unused local variables
- Unreachable branches
- Commented-out code
- Obsolete business logic
- Old repository methods
- Unused DTO fields
- Deprecated integrations that are no longer called
- Configuration properties that no longer affect runtime behavior
Consider:
@Service
public class PaymentService {
public PaymentResponse processPayment(PaymentRequest request) {
return processUsingNewGateway(request);
}
private PaymentResponse processUsingOldGateway(PaymentRequest request) {
// Legacy implementation
return new PaymentResponse();
}
private PaymentResponse processUsingNewGateway(PaymentRequest request) {
return new PaymentResponse();
}
}If processUsingOldGateway has no callers and the old payment integration has permanently been retired, keeping the method adds no value.
It creates questions for future developers:
- Is this method still supported?
- Should new code use it?
- Is it kept for rollback?
- Does some reflection-based code call it?
- Is the old gateway still required?
Removing dead code keeps the codebase aligned with the application that actually exists today.
The purpose is not to aggressively delete everything that appears unused.
The purpose is to identify obsolete code safely, verify that it has no required runtime or business purpose, and remove it so developers are not forced to maintain code that should no longer exist.
2. What This Topic Means
In Java development and code review, removing dead code means identifying code that no longer participates meaningfully in the application and deleting it after verifying that removal is safe.
Dead code can be divided into several categories.
Unused Code
Example:
private BigDecimal calculateLegacyDiscount(Order order) {
return order.getTotalAmount().multiply(new BigDecimal("0.05"));
}If no code calls this method and the legacy discount is no longer supported, the method is dead.
Unreachable Code
Example:
public void processOrder(Order order) {
if (order == null) {
throw new IllegalArgumentException("Order is required");
}
return;
System.out.println("Processing order");
}The statement after return cannot execute.
Java detects some forms of unreachable code at compile time.
Obsolete Business Logic
A method can still compile and even have callers while being conceptually obsolete.
For example, a migration may leave an old validation path connected through a feature flag that is permanently disabled.
Commented-Out Code
Example:
public void processCustomer(Customer customer) {
validateCustomer(customer);
saveCustomer(customer);
// sendLegacyWelcomeEmail(customer);
// legacyAuditService.write(customer);
// updateOldCrm(customer);
}Version control already preserves old implementations.
Commented-out executable code usually creates noise rather than useful history.
Unused Imports, Variables, and Fields
Example:
private final LegacyNotificationClient legacyNotificationClient;If the field is injected but never used, it may indicate an incomplete cleanup.
Unused Repository Methods
Example:
List<Order> findByLegacyCustomerReference(String legacyReference);If the old identifier is no longer part of the application, retaining the repository method creates misleading API surface.
Dead Configuration
Example:
legacy.payment.retry-count=5If no component reads the property, operations teams may believe changing it affects the system when it does not.
Removing dead code therefore involves more than satisfying compiler warnings.
It means keeping implementation, configuration, tests, documentation, and runtime behavior synchronized.
3. Why It Matters in Real Projects
Readability
Dead code forces developers to distinguish between active and inactive logic.
Consider a service containing:
- Current implementation
- Legacy implementation
- Commented-out previous implementation
- Old helper methods
- Temporary migration code
A reviewer must determine which parts actually matter before understanding the current behavior.
Removing obsolete code reduces that cognitive load.
Maintainability
Dead code still has a maintenance cost.
Developers may:
- Update it unnecessarily
- Include it in refactoring
- Add tests for it
- Fix static-analysis warnings inside it
- Modify it during framework upgrades
- Investigate it during production incidents
Code that should not exist consumes engineering time.
Debugging
Dead paths can mislead developers during incident investigation.
A developer searching for:
calculateRefund()may find three implementations:
calculateRefund()
calculateOldRefund()
calculateRefundV2()If only one is used, the unused methods make the debugging process slower.
Reliability
Obsolete code can accidentally become active again.
For example, a future developer may discover an old method and reuse it without knowing that its business assumptions are outdated.
Security
Dead code can contain:
- Old authentication logic
- Obsolete authorization rules
- Legacy encryption code
- Deprecated API clients
- Old secrets handling
- Unsafe deserialization paths
Even when currently unused, retaining insecure legacy components may increase future risk if they are accidentally reconnected.
Performance
Most dead methods do not affect runtime performance because they are never executed.
However, apparently unused components may still be instantiated by Spring, register listeners, schedule jobs, create connection pools, or initialize expensive clients.
Therefore runtime impact depends on the type of dead code.
Team Development
A clean codebase gives developers confidence that existing code has a purpose.
When large amounts of dead code remain, developers begin to distrust the repository and hesitate to remove anything.
That slows future development.
4. Core Concept
The key principle is:
If code no longer represents required behavior, remove it after proving that it is safe to remove.
The important part is proving safety.
A method that appears unused in an IDE may still be invoked through:
- Reflection
- Spring dependency injection
- Spring event listeners
- Scheduled jobs
- JPA entity lifecycle callbacks
- Jackson serialization
- Framework annotations
- JSP or template expressions
- Configuration
- Test fixtures
- External modules
- Plugin architectures
For example:
@Component
public class CleanupJob {
@Scheduled(cron = "0 0 2 * * *")
public void removeExpiredSessions() {
// Cleanup
}
}No Java method may directly call:
removeExpiredSessions()but the method is not dead.
Spring invokes it through the scheduling infrastructure.
Similarly:
@EventListener
public void handleOrderCreated(OrderCreatedEvent event) {
...
}may not have an explicit caller.
Therefore dead-code removal requires both:
- Static inspection
- Framework and runtime awareness
Version Control Is the History
Developers often keep old code because:
We may need it later.
This is usually unnecessary.
Git already preserves:
- Deleted methods
- Previous implementations
- Old configuration
- Earlier business logic
Keeping obsolete code inside production sources turns historical information into current maintenance burden.
5. Important Rules
- Never keep obsolete executable code only for historical reference.
- Use version control instead of commented-out code.
- Verify framework-driven usage before deleting apparently unused methods.
- Search for references across all modules, not only the current class.
- Check configuration-driven usage.
- Check reflection-based usage.
- Check Spring annotations before treating methods as unused.
- Remove obsolete tests together with obsolete production code.
- Remove unused configuration properties when their feature is removed.
- Remove unused dependencies if the deleted code was their only consumer.
- Remove old imports after deleting code.
- Remove related constants and helper methods that become unused.
- Avoid names such as
oldMethod,methodV2, andlegacyMethodremaining indefinitely. - Treat temporary migration code as temporary.
- Attach cleanup work to feature-removal or migration tasks.
- Run automated tests after removal.
- Run static analysis after removal.
- Review logs and runtime metrics if uncertain whether a path is active.
- Do not remove public APIs only because the local repository has no caller.
- Check external consumers before deleting public methods or endpoints.
6. Bad Code Example
Consider a Spring Boot customer-notification service that previously used an old SMS provider.
The application has already migrated to a new provider.
@Service
public class NotificationService {
private final NewSmsClient newSmsClient;
private final LegacySmsClient legacySmsClient;
private final CustomerRepository customerRepository;
public NotificationService(NewSmsClient newSmsClient, LegacySmsClient legacySmsClient, CustomerRepository customerRepository) {
this.newSmsClient = newSmsClient;
this.legacySmsClient = legacySmsClient;
this.customerRepository = customerRepository;
}
public void sendOrderConfirmation(Long customerId, String message) {
Customer customer = customerRepository.findById(customerId)
.orElseThrow(() -> new IllegalArgumentException("Customer not found"));
newSmsClient.send(customer.getMobileNumber(), message);
// legacySmsClient.send(customer.getMobileNumber(), message);
}
private void sendUsingLegacyProvider(Customer customer, String message) {
legacySmsClient.send(customer.getMobileNumber(), message);
}
private boolean shouldUseLegacyProvider(Customer customer) {
return customer.getCreatedAt().isBefore(LocalDate.of(2022, 1, 1));
}
private String convertLegacyMessage(String message) {
return "[LEGACY] " + message;
}
public void sendTestLegacyNotification(Long customerId) {
Customer customer = customerRepository.findById(customerId)
.orElseThrow(() -> new IllegalArgumentException("Customer not found"));
sendUsingLegacyProvider(customer, "Test");
}
}Assume the migration is complete and the old SMS provider contract has been terminated.
7. Problems in the Bad Code
Commented-Out Production Code
This line remains:
// legacySmsClient.send(customer.getMobileNumber(), message);It no longer contributes to behavior.
Git already preserves the historical implementation.
Unused Legacy Helper Methods
These methods exist only for the retired provider:
sendUsingLegacyProvider()
shouldUseLegacyProvider()
convertLegacyMessage()If the migration is complete, they should be deleted.
Legacy Dependency Still Injected
The service still depends on:
LegacySmsClienteven though normal production behavior no longer requires it.
This may cause Spring to instantiate unnecessary infrastructure.
Misleading Public Method
This method is particularly dangerous:
sendTestLegacyNotification()It still makes the old integration reachable.
If exposed through another service or controller, developers may accidentally continue using an unsupported provider.
Incorrect Maintenance Signal
The class suggests that both SMS providers remain supported.
Future developers may assume the legacy path is intentional.
Potential Operational Cost
If LegacySmsClient initializes:
- HTTP connection pools
- Authentication tokens
- SDK clients
the dead integration may still consume resources.
Potential Security Risk
The legacy integration may require:
- API credentials
- Old TLS configuration
- Deprecated SDKs
Keeping unused integration code may unnecessarily extend the lifetime of those dependencies and secrets.
Incomplete Feature Removal
The migration is technically incomplete because application code still contains the retired provider.
8. Code Review Findings
A senior reviewer should identify:
- The migration appears complete, but the old implementation remains.
- Commented-out executable code should be deleted.
- The legacy client is still injected.
- Several private methods exist only for the retired integration.
- The public legacy test method may still make the obsolete provider callable.
- The reviewer should verify whether any controller, test, scheduler, or external module calls that public method.
- The reviewer should check whether
LegacySmsClientis still a Spring bean. - Configuration properties and secrets associated with the old provider should also be reviewed.
- Maven or Gradle dependencies used exclusively by the legacy provider may now be removable.
- Tests that validate old-provider behavior should be deleted if the business no longer supports it.
- Documentation and operational runbooks should be updated if they still mention the provider.
The important review point is that removal should be complete, not limited to deleting one commented line.
9. Reviewer Comment Example
The legacy SMS migration appears complete. Can we remove the commented call and the unused legacy helper methods instead of keeping historical code in the service?
LegacySmsClientis still injected here even though the active flow uses onlyNewSmsClient. Please verify whether anything still depends on it and remove the bean/dependency if not.
Is
sendTestLegacyNotificationstill used by any controller, scheduler, test utility, or external module? If the provider has been retired, keeping this public path could accidentally reactivate unsupported behavior.
Please also check whether the legacy provider configuration and secrets can be removed as part of this cleanup.
If we need the previous implementation later, Git history already preserves it. I would prefer deleting the commented-out code.
10. Improved Code
@Service
public class NotificationService {
private final NewSmsClient newSmsClient;
private final CustomerRepository customerRepository;
public NotificationService(NewSmsClient newSmsClient, CustomerRepository customerRepository) {
this.newSmsClient = newSmsClient;
this.customerRepository = customerRepository;
}
public void sendOrderConfirmation(Long customerId, String message) {
Customer customer = customerRepository.findById(customerId)
.orElseThrow(() -> new IllegalArgumentException("Customer not found"));
newSmsClient.send(customer.getMobileNumber(), message);
}
}If the old provider had a dedicated Spring configuration:
@Configuration
public class LegacySmsConfiguration {
@Bean
public LegacySmsClient legacySmsClient() {
return new LegacySmsClient();
}
}that configuration should also be removed if no longer required.
Associated properties such as:
legacy-sms.base-url=https://legacy.example.com
legacy-sms.api-key=${LEGACY_SMS_API_KEY}should also be removed from application configuration and deployment secrets.
11. Improved Code Explanation
Legacy Dependency Removed
The service now depends only on:
NewSmsClientThis accurately represents the supported architecture.
Commented Code Removed
Historical implementation is no longer stored in production source.
Git preserves it if needed.
Obsolete Helper Methods Removed
The class no longer contains unused legacy decisions and message transformations.
Public Legacy Entry Point Removed
Unsupported behavior can no longer be accidentally called through:
sendTestLegacyNotification()Constructor Simplified
The service has fewer dependencies.
That improves:
- Readability
- Test setup
- Dependency clarity
Configuration Cleanup
Removing dead code should include related infrastructure.
If legacy configuration remains, operations teams may still believe the provider is active.
Security Surface Reduced
Unused credentials and dependencies can potentially be removed from deployment.
12. Bad Code vs Improved Code
| Aspect | Bad Code | Improved Code |
|---|---|---|
| Readability | Active and retired logic mixed together | Only supported behavior remains |
| Maintainability | Developers must understand legacy code | Smaller and focused service |
| Testability | Tests may mock unused dependencies | Only current dependencies need testing |
| Reliability | Old path can accidentally be reused | Unsupported behavior removed |
| Security | Legacy credentials/dependencies may remain | Unneeded integration surface reduced |
| Configuration | Obsolete settings may survive | Configuration matches current architecture |
| PR Review | Reviewer must separate active from inactive code | Intent is immediately clear |
13. Real Project Scenario
Consider a banking application migrating from a legacy fraud-scoring engine to a new real-time fraud platform.
During migration, the service temporarily supports both systems:
if (featureFlags.isNewFraudEngineEnabled()) {
return newFraudClient.evaluate(transaction);
}
return legacyFraudClient.evaluate(transaction);After several months:
- 100% of traffic uses the new engine.
- The legacy vendor contract is terminated.
- Production credentials are revoked.
- The feature flag is permanently enabled.
However, developers leave:
LegacyFraudClient- Legacy DTOs
- Legacy configuration
- Legacy mapping code
- Legacy tests
- Legacy feature flag
inside the repository.
Six months later, a developer sees the feature flag and assumes the old provider is still a supported fallback.
During an unrelated incident, the flag is changed.
The application attempts to call a retired endpoint.
This can lead to:
- Failed transactions
- Timeouts
- Incorrect operational assumptions
- Emergency debugging
Once a migration is permanently complete, the cleanup should remove the obsolete branch and its supporting code.
The final flow should simply be:
public FraudDecision evaluate(Transaction transaction) {
return newFraudClient.evaluate(transaction);
}Dead-code removal completes the migration.
14. Production Impact
Dead code often appears harmless because it is supposedly not executed.
However, it can still create real production consequences.
Accidental Reuse
A future developer may call an obsolete method because it appears valid.
Misleading Incident Investigation
During an outage, support engineers may investigate inactive code paths and lose valuable time.
Unnecessary Spring Bean Initialization
Unused Spring components may still be created at startup.
They may initialize:
- HTTP clients
- Database connections
- SDKs
- Thread pools
- Metrics
- Scheduled tasks
Obsolete Scheduled Jobs
A method may appear disconnected from normal business code but still run because of:
@ScheduledThis is why framework awareness is essential.
Old Configuration Creates Operational Confusion
Operations teams may modify dead properties expecting behavior changes.
Legacy Security Exposure
Unused integrations may keep:
- Secrets
- Old libraries
- Unsupported protocols
inside the deployment.
Increased Upgrade Effort
Framework upgrades may require developers to fix code that no longer serves any business purpose.
Deleting obsolete code reduces future migration work.
15. Common Developer Mistakes
Commenting Code Instead of Deleting It
Example:
// if (oldCondition) {
// processLegacyOrder(order);
// }Version control makes this unnecessary.
Keeping Code "Just in Case"
Developers often retain obsolete code because it may theoretically be needed later.
If the feature is genuinely removed, this creates indefinite maintenance cost.
Trusting IDE Reference Counts Blindly
Framework-driven methods may look unused but still be active.
Deleting Public APIs Without Checking Consumers
A public method may be used by another module or library.
Ignoring Configuration
Developers remove Java classes but forget:
- Properties
- Environment variables
- Kubernetes secrets
- Helm values
- Docker configuration
Ignoring Tests
Tests for removed features remain and continue increasing maintenance effort.
Ignoring Dependencies
The old Maven or Gradle dependency remains even though nothing uses it.
Leaving Feature Flags Forever
Temporary rollout flags become permanent code complexity.
Keeping Old and New Implementations
Classes such as:
PaymentService
PaymentServiceOld
PaymentServiceV2
PaymentServiceLatestmake ownership unclear.
Leaving Obsolete TODO Blocks
Long-lived TODO comments can become dead design documentation.
16. Edge Cases
Reflection
A method can be invoked dynamically.
Example:
Method method = handler.getClass().getMethod(methodName);
method.invoke(handler);Static reference search may not find such usage.
Spring Bean Discovery
A class may be instantiated because of:
@Component
@Service
@Repository
@Configurationeven without direct construction.
Scheduled Methods
Example:
@Scheduled(fixedDelay = 60000)
public void refreshCache() {
...
}No normal caller is expected.
Event Listeners
Example:
@EventListener
public void handlePaymentCompleted(PaymentCompletedEvent event) {
...
}The Spring event system invokes the method.
JPA Lifecycle Methods
Methods annotated with:
@PrePersist
@PostLoad
@PreUpdatemay appear unused in static analysis.
Serialization
Getters, setters, constructors, or fields may be used by Jackson.
Framework Configuration
A class may be referenced using a fully qualified name in configuration rather than Java source.
External API Consumers
Removing an endpoint because there are no internal callers may break:
- Mobile applications
- Partner systems
- Other microservices
- Batch jobs
Feature Flags
A disabled path may be intentionally retained for temporary rollback.
The reviewer should verify whether the rollback period has expired.
Database Migration Support
Temporary compatibility code may still be required while older data remains.
Deleting it too early can break historical records.
17. Performance Considerations
Dead-code removal is mainly a maintainability improvement.
Unused private methods do not normally consume meaningful runtime resources simply by existing.
However, some forms of dead code can affect performance.
Unused Spring Beans
A Spring bean can still be instantiated at startup.
If it creates expensive resources, startup time and memory usage may increase.
Unused Schedulers
A forgotten scheduler can continue executing database queries.
Example:
@Scheduled(cron = "0 */5 * * * *")
public void synchronizeLegacyCustomers() {
legacyRepository.findAll();
}If the legacy synchronization is no longer required, this is not merely dead code—it is wasted runtime work.
Unused Event Listeners
Old listeners may continue processing events even though their results are discarded.
Unnecessary Dependencies
Large libraries can affect:
- Build time
- Artifact size
- Dependency scanning
although impact varies by project.
JVM Optimization
Do not keep dead Java code because of concern about method-call performance.
Removing unused methods generally has no negative runtime effect.
The performance review should focus on whether the supposedly dead component still performs runtime work indirectly.
18. Security Considerations
Dead code can increase security risk when it preserves obsolete attack surfaces.
Old Authentication Paths
A deprecated authentication method may use weaker validation.
If accidentally re-enabled, it can bypass current security controls.
Obsolete Dependencies
Legacy code may require libraries with known vulnerabilities.
Removing the code may allow the dependency to be removed.
Secrets
Retired integrations often leave:
- API keys
- Client secrets
- Certificates
- Service-account credentials
in configuration.
These should be removed when the integration is permanently deleted.
Old Endpoints
An endpoint believed to be unused may still be externally reachable.
Example:
@PostMapping("/legacy-admin/reset")If it has weak or outdated authorization, retaining it creates unnecessary risk.
Old Logging Code
Legacy code may log sensitive information that current standards prohibit.
Important Review Rule
Before deleting security-related code, confirm that it is genuinely obsolete.
Do not remove:
- Authorization checks
- Input validation
- Audit logging
simply because their importance is not immediately obvious.
19. Testing Considerations
Dead-code removal should reduce tests rather than create unnecessary new tests.
However, tests are essential for proving that active behavior remains unchanged.
Regression Tests
Before deleting legacy implementation, ensure current behavior is covered.
Example:
@Test
void shouldSendOrderConfirmationUsingCurrentSmsProvider() {
Customer customer = new Customer();
customer.setId(1L);
customer.setMobileNumber("9999999999");
when(customerRepository.findById(1L)).thenReturn(Optional.of(customer));
notificationService.sendOrderConfirmation(1L, "Order confirmed");
verify(newSmsClient).send("9999999999", "Order confirmed");
}Remove Obsolete Tests
If the legacy provider is removed, tests such as:
shouldSendUsingLegacyProvider()should also be deleted.
Integration Tests
If configuration or Spring beans are removed, run application-context tests to ensure dependency injection still succeeds.
API Regression
If removing endpoints or public methods, verify external consumer impact first.
Startup Verification
After removing configuration classes and dependencies, confirm the application starts successfully.
Static Analysis
Tools such as:
- IntelliJ inspections
- SonarQube
- SpotBugs
- compiler warnings
can help find additional unused code after cleanup.
They should support engineering judgment rather than replace it.
20. Refactoring Guidelines
Step 1: Identify Candidate Dead Code
Use:
- IDE reference search
- Static analysis
- Code search
- Coverage information
- Runtime metrics
- Logs
Step 2: Understand Why It Exists
Check Git history and issue tracking.
Determine whether the code is:
- Permanently obsolete
- Temporarily disabled
- Rollback support
- Framework invoked
- Externally consumed
Step 3: Find All References
Search across:
- Current module
- Other modules
- Tests
- Configuration
- Templates
- Scripts
- Deployment files
Step 4: Check Framework Annotations
Look for:
@Scheduled@EventListener@Bean@PostConstruct@PrePersist@JsonCreator
and similar framework hooks.
Step 5: Remove the Smallest Complete Feature Slice
Do not delete only the obvious method.
Also inspect:
- Helper methods
- DTOs
- Constants
- Properties
- Dependencies
- Tests
- Secrets
Step 6: Compile
Compilation often reveals hidden dependencies.
Step 7: Run Tests
Run:
- Unit tests
- Integration tests
- Relevant end-to-end tests
Step 8: Run Static Analysis
Look for additional code made unused by the removal.
Step 9: Review Deployment Configuration
Remove dead runtime configuration only when safe.
Step 10: Keep the Commit Focused
A dead-code cleanup PR should preferably avoid unrelated behavior changes.
That makes review safer.
21. Best Practices
- Delete obsolete code instead of commenting it out.
- Use Git history for historical reference.
- Remove dead code as part of completed migrations.
- Keep feature-flag lifecycle explicit.
- Remove temporary compatibility code after its support window ends.
- Use IDE and static-analysis tools to identify candidates.
- Verify framework-driven usage before deletion.
- Check public API consumers.
- Remove unused dependencies.
- Remove unused configuration.
- Remove obsolete tests.
- Remove stale documentation associated with retired features.
- Keep cleanup PRs focused.
- Prefer small, reviewable deletions.
- Use production metrics when uncertain whether a feature is still active.
- Track deprecation deadlines.
- Treat dead code as technical debt, not harmless clutter.
22. Practices to Avoid
Commenting Out Large Blocks
Avoid:
// public void processLegacyPayment(...) {
// ...
// }Git already preserves this history.
Keeping "Backup" Implementations
Avoid classes such as:
PaymentServiceBackup
OldPaymentService
PaymentServiceV1inside active production source unless they still have a defined responsibility.
Blind Deletion Based on IDE Warnings
Framework callbacks may have no direct Java references.
Leaving Unused Dependencies
Deleting Java code but retaining obsolete libraries leaves incomplete cleanup.
Leaving Dead Configuration
Old properties create misleading operational behavior.
Deleting Without Tests
A method may have less obvious callers or side effects.
Removing Public APIs Without Deprecation Planning
Internal dead code and externally consumed APIs are different problems.
Huge Cleanup PRs
Deleting thousands of unrelated lines across many domains makes review risky.
Mixing Feature Changes With Cleanup
A reviewer should be able to distinguish behavior changes from pure removal.
23. Code Review Checklist
- Is this method, field, class, or configuration still used?
- Does this code represent an active business requirement?
- Is any code commented out instead of deleted?
- Does version control already preserve the historical implementation?
- Is this method invoked by a framework annotation?
- Could reflection invoke this code?
- Is this public API used outside the current module?
- Is this endpoint used by another service or external client?
- Is this feature flag still required?
- Has the rollback period expired?
- Is an old integration still represented in the dependency graph?
- Are unused Spring beans still created?
- Are obsolete scheduled jobs still running?
- Are old event listeners still active?
- Are legacy configuration properties still present?
- Are obsolete environment variables or secrets still deployed?
- Are tests maintaining behavior that is no longer supported?
- Did deleting this code make additional helpers unused?
- Can any Maven or Gradle dependency now be removed?
- Does the cleanup preserve current business behavior?
- Has the application been compiled after removal?
- Have relevant unit and integration tests been run?
- Is the PR focused enough to review safely?
- Does the remaining code clearly represent the supported architecture?
24. Common Pull Request Review Comments
- *This block has been commented out since the migration. If the old behavior is no longer supported, please delete it and rely on Git history instead.*
- *This private method has no references. Before removing it, can we confirm it is not called through reflection or framework configuration?*
- *The legacy provider is no longer used by the active workflow, but the Spring bean and configuration still remain. Can we remove the complete integration slice?*
- *Is this feature flag still required for rollback? If rollout is complete, I would prefer deleting both the flag and the obsolete branch.*
- *This public method appears unused internally. Please verify external consumers before deleting it.*
- *After removing this code, the related Maven dependency appears unused as well. Can we remove it from the build file?*
- *Please check whether the corresponding configuration property and deployment secret can also be deleted.*
- *This scheduled method has no direct callers, but
@Scheduledmeans it is still active. We should not treat it as dead based only on reference search.*
- *The legacy tests now validate behavior we no longer support. If the feature is permanently removed, those tests should be deleted with the implementation.*
- *Can we keep this cleanup separate from the functional changes? A focused deletion-only PR will be easier to verify.*
25. Code Review Exercise
Review the following Spring Boot service.
Identify:
- Problems
- Code smells
- Risks
- Improvements
Do not read the solution until completing your review.
@Service
public class CustomerSyncService {
private final CustomerRepository customerRepository;
private final NewCrmClient newCrmClient;
private final LegacyCrmClient legacyCrmClient;
public CustomerSyncService(CustomerRepository customerRepository, NewCrmClient newCrmClient, LegacyCrmClient legacyCrmClient) {
this.customerRepository = customerRepository;
this.newCrmClient = newCrmClient;
this.legacyCrmClient = legacyCrmClient;
}
public void syncCustomer(Long customerId) {
Customer customer = customerRepository.findById(customerId)
.orElseThrow(() -> new IllegalArgumentException("Customer not found"));
newCrmClient.sync(customer);
// legacyCrmClient.sync(customer);
}
private void syncWithLegacyCrm(Customer customer) {
legacyCrmClient.sync(customer);
}
private LegacyCustomerRequest createLegacyRequest(Customer customer) {
LegacyCustomerRequest request = new LegacyCustomerRequest();
request.setCustomerId(customer.getId());
request.setName(customer.getName());
return request;
}
@Scheduled(cron = "0 0 1 * * *")
public void runLegacyCustomerSync() {
customerRepository.findAll()
.forEach(this::syncWithLegacyCrm);
}
public boolean isLegacySyncEnabled() {
return false;
}
}Review the code carefully.
Questions to consider:
- Which code is genuinely dead?
- Which code only appears dead?
- Does the scheduler change your conclusion?
- Is the old CRM really inactive?
- What should be removed if the migration is complete?
- What production risk exists in deleting code blindly?
- What configuration and dependency cleanup may also be required?
26. Exercise Solution
Problems Identified
1. Commented-Out Code
This line should not remain:
// legacyCrmClient.sync(customer);If the old CRM is unsupported, delete it.
2. Apparently Unused Legacy Method
This method:
syncWithLegacyCrm()may appear unused during a quick inspection.
However, it is referenced by:
.forEach(this::syncWithLegacyCrm);inside the scheduler.
Therefore it is not dead.
3. Legacy Scheduler Is Still Active
The critical issue is:
@Scheduled(cron = "0 0 1 * * *")
public void runLegacyCustomerSync()The system still calls the old CRM every day at 1 AM.
Even if no normal application flow uses LegacyCrmClient, the integration is still active.
4. Misleading Method
This method returns:
falseunconditionally:
public boolean isLegacySyncEnabled() {
return false;
}But the legacy scheduler still runs.
The method gives a false impression that legacy synchronization is disabled.
5. Potentially Dead Mapping Method
This method:
createLegacyRequest()has no usage in the provided class.
It may be dead, but repository-wide and framework usage should still be checked.
6. Legacy Dependency Is Still Required
Because the scheduled job uses:
legacyCrmClientthe dependency cannot be removed until the scheduler is removed.
7. Production Risk
Deleting LegacyCrmClient without noticing the scheduler could:
- Break application startup
- Break scheduled execution
- Cause missing synchronization
- Trigger runtime failures
Correct Cleanup If Migration Is Confirmed Complete
If the business confirms that legacy synchronization is permanently retired, the improved class becomes:
@Service
public class CustomerSyncService {
private final CustomerRepository customerRepository;
private final NewCrmClient newCrmClient;
public CustomerSyncService(CustomerRepository customerRepository, NewCrmClient newCrmClient) {
this.customerRepository = customerRepository;
this.newCrmClient = newCrmClient;
}
public void syncCustomer(Long customerId) {
Customer customer = customerRepository.findById(customerId)
.orElseThrow(() -> new IllegalArgumentException("Customer not found"));
newCrmClient.sync(customer);
}
}Additional Cleanup
The developer should also inspect and potentially remove:
LegacyCrmClientLegacyCustomerRequest- Legacy CRM configuration
- Legacy API credentials
- Legacy CRM dependencies
- Legacy synchronization tests
- Scheduler-specific properties
- Monitoring dashboards related only to the retired job
- Runbook documentation for the old CRM
Why Each Change Is Useful
The resulting service now reflects only supported behavior.
There is no misleading "disabled" legacy logic.
No obsolete scheduler remains.
The dependency graph is simpler.
Operations teams no longer need legacy CRM configuration.
Future developers are less likely to mistakenly reactivate unsupported functionality.
27. Interview Perspective
Dead-code questions in interviews often test whether a developer understands the difference between:
- Apparently unused code
- Actually unreachable code
- Framework-driven code
- Externally consumed APIs
A basic candidate may say:
If IntelliJ says zero usages, delete it.
A stronger candidate asks:
- Is it called through Spring?
- Is it scheduled?
- Is it an event listener?
- Is it used by reflection?
- Is it part of serialization?
- Is it called by another module?
- Is it an external API?
Java Interview
The interviewer may ask about unreachable code.
Example:
return;
System.out.println("Hello");The candidate should understand that Java detects certain unreachable statements at compile time.
Spring Boot Interview
A method annotated with:
@Scheduledmay have zero direct Java callers but still be active.
The same applies to:
@EventListener
@Beanand similar framework mechanisms.
Senior Developer Interview
A senior candidate may be asked:
How would you safely remove 20,000 lines of legacy code?
A strong answer should include:
- Dependency analysis
- Production usage verification
- Logging and metrics
- Feature-flag verification
- Test coverage
- Incremental removal
- Configuration cleanup
- Dependency cleanup
- Focused PRs
Code Review Interview
The candidate may be shown commented-out code and asked what review comment they would leave.
A strong answer should explain that version control is the correct historical record.
28. Interview Questions and Answers
Basic Question
Question: What is dead code?
Answer:
Dead code is code that no longer contributes to required application behavior. It can include unused methods, unreachable branches, commented-out implementations, obsolete business logic, retired integrations, unused configuration, and old dependencies.
Intermediate Question
Question: Why should commented-out code usually be removed?
Answer:
Because version control already preserves previous implementations. Commented-out code increases visual noise, creates uncertainty about whether the code is still relevant, and can become stale as the active implementation evolves.
Advanced Question
Question: Can a Java method with zero direct references still be active?
Answer:
Yes. Frameworks may invoke methods indirectly. Examples include Spring @Scheduled methods, @EventListener handlers, JPA lifecycle callbacks, reflection-based plugins, Jackson constructors or accessors, and configuration-driven class loading. Static reference count alone is not enough to prove dead code.
Scenario-Based Question
Question: A feature flag has been enabled for the new implementation for six months and the old branch has received no production traffic. What should you do?
Answer:
First confirm that rollback to the old path is no longer part of the operational plan. Then remove the old branch, feature flag, associated configuration, tests, dependencies, metrics, and documentation as one controlled cleanup. Long-lived flags create permanent conditional complexity if they are not removed.
Code-Review Question
Question: You see a private method with no IDE usages in a Spring service. Should you request deletion?
Answer:
I would first check whether it is called through annotations, method references, reflection, configuration, or framework lifecycle behavior. If repository-wide analysis confirms there is no runtime or business use, then I would recommend removing it.
Real-Project Question
Question: How would you safely remove a legacy external integration?
Answer:
I would verify that production traffic no longer depends on it, confirm the rollback period has ended, locate every caller including scheduled jobs and event listeners, remove the integration code, configuration, credentials, tests, and dedicated dependencies, then run unit, integration, startup, and relevant end-to-end tests. I would also verify deployment configuration and monitoring so the removal is complete.
29. Quick Rule to Remember
If the system no longer needs the behavior, delete the code after proving nothing still depends on it.
30. Final Takeaway
What the Developer Should Remember
Dead code is not free.
Even when it does not execute, developers still:
- Read it
- Review it
- Search through it
- Refactor around it
- Upgrade it
- Question whether it is required
A clean Java codebase should represent the current supported behavior of the system.
Developers should:
- Delete obsolete code.
- Use Git for history.
- Remove completed migration paths.
- Clean up temporary feature flags.
- Remove unused dependencies.
- Remove stale configuration.
- Remove obsolete tests.
- Check framework-driven execution before deleting code.
- Verify external consumers before removing public APIs.
- Keep cleanup changes focused and testable.
What the Reviewer Should Check
During Pull Request review, ask:
- Is this code still required?
- Is this only being kept for historical reasons?
- Is it genuinely unused or framework-invoked?
- Does a scheduled job still call it?
- Does an event listener use it?
- Could reflection or configuration reference it?
- Is a public API externally consumed?
- Are old dependencies still present?
- Are legacy properties and secrets still deployed?
- Are obsolete tests maintaining removed behavior?
- Has the migration really finished?
- Can the cleanup remove an entire obsolete feature slice rather than one method?
What Should Be Avoided in Production Code
Avoid leaving:
- Commented-out methods
OldServiceServiceV1ServiceBackup- Permanently disabled branches
- Expired feature flags
- Retired integrations
- Unused configuration
- Obsolete dependencies
- Legacy secrets
- Unsupported endpoints
Do not delete code blindly because an IDE reports zero usages.
Do not keep dead code indefinitely because it may theoretically be useful later.
The safest production approach is:
Confirm that the behavior is obsolete, identify every dependency and indirect execution path, remove the complete unused feature slice, and let version control preserve the history.