Removing Dead Code

25 min read

Clean Code and Readability — review Java code for obsolete methods, commented-out logic, and retired integrations that no longer belong in production.

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:

JAVA
@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:

JAVA
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:

JAVA
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:

JAVA
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:

JAVA
private final LegacyNotificationClient legacyNotificationClient;

If the field is injected but never used, it may indicate an incomplete cleanup.

Unused Repository Methods

Example:

JAVA
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:

PROPERTIES
legacy.payment.retry-count=5

If 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:

JAVA
calculateRefund()

may find three implementations:

JAVA
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:

JAVA
@Component
public class CleanupJob {
    @Scheduled(cron = "0 0 2 * * *")
    public void removeExpiredSessions() {
        // Cleanup
    }
}

No Java method may directly call:

JAVA
removeExpiredSessions()

but the method is not dead.

Spring invokes it through the scheduling infrastructure.

Similarly:

JAVA
@EventListener
public void handleOrderCreated(OrderCreatedEvent event) {
    ...
}

may not have an explicit caller.

Therefore dead-code removal requires both:

  1. Static inspection
  2. 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, and legacyMethod remaining 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.

JAVA
@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:

JAVA
// 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:

JAVA
sendUsingLegacyProvider()
shouldUseLegacyProvider()
convertLegacyMessage()

If the migration is complete, they should be deleted.

Legacy Dependency Still Injected

The service still depends on:

JAVA
LegacySmsClient

even though normal production behavior no longer requires it.

This may cause Spring to instantiate unnecessary infrastructure.

Misleading Public Method

This method is particularly dangerous:

JAVA
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 LegacySmsClient is 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?

LegacySmsClient is still injected here even though the active flow uses only NewSmsClient. Please verify whether anything still depends on it and remove the bean/dependency if not.

Is sendTestLegacyNotification still 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

JAVA
@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:

JAVA
@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:

PROPERTIES
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:

JAVA
NewSmsClient

This 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:

JAVA
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

AspectBad CodeImproved Code
ReadabilityActive and retired logic mixed togetherOnly supported behavior remains
MaintainabilityDevelopers must understand legacy codeSmaller and focused service
TestabilityTests may mock unused dependenciesOnly current dependencies need testing
ReliabilityOld path can accidentally be reusedUnsupported behavior removed
SecurityLegacy credentials/dependencies may remainUnneeded integration surface reduced
ConfigurationObsolete settings may surviveConfiguration matches current architecture
PR ReviewReviewer must separate active from inactive codeIntent 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:

JAVA
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:

JAVA
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:

JAVA
@Scheduled

This 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:

JAVA
// 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:

JAVA
PaymentService
PaymentServiceOld
PaymentServiceV2
PaymentServiceLatest

make 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:

JAVA
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:

JAVA
@Component
@Service
@Repository
@Configuration

even without direct construction.

Scheduled Methods

Example:

JAVA
@Scheduled(fixedDelay = 60000)
public void refreshCache() {
    ...
}

No normal caller is expected.

Event Listeners

Example:

JAVA
@EventListener
public void handlePaymentCompleted(PaymentCompletedEvent event) {
    ...
}

The Spring event system invokes the method.

JPA Lifecycle Methods

Methods annotated with:

JAVA
@PrePersist
@PostLoad
@PreUpdate

may 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:

JAVA
@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:

JAVA
@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:

JAVA
@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:

JAVA
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:

JAVA
// public void processLegacyPayment(...) {
//     ...
// }

Git already preserves this history.

Keeping "Backup" Implementations

Avoid classes such as:

JAVA
PaymentServiceBackup
OldPaymentService
PaymentServiceV1

inside 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

  1. *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.*
  1. *This private method has no references. Before removing it, can we confirm it is not called through reflection or framework configuration?*
  1. *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?*
  1. *Is this feature flag still required for rollback? If rollout is complete, I would prefer deleting both the flag and the obsolete branch.*
  1. *This public method appears unused internally. Please verify external consumers before deleting it.*
  1. *After removing this code, the related Maven dependency appears unused as well. Can we remove it from the build file?*
  1. *Please check whether the corresponding configuration property and deployment secret can also be deleted.*
  1. *This scheduled method has no direct callers, but @Scheduled means it is still active. We should not treat it as dead based only on reference search.*
  1. *The legacy tests now validate behavior we no longer support. If the feature is permanently removed, those tests should be deleted with the implementation.*
  1. *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.

JAVA
@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:

JAVA
// legacyCrmClient.sync(customer);

If the old CRM is unsupported, delete it.

2. Apparently Unused Legacy Method

This method:

JAVA
syncWithLegacyCrm()

may appear unused during a quick inspection.

However, it is referenced by:

JAVA
.forEach(this::syncWithLegacyCrm);

inside the scheduler.

Therefore it is not dead.

3. Legacy Scheduler Is Still Active

The critical issue is:

JAVA
@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:

JAVA
false

unconditionally:

JAVA
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:

JAVA
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:

JAVA
legacyCrmClient

the 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:

JAVA
@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:

  • LegacyCrmClient
  • LegacyCustomerRequest
  • 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:

JAVA
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:

JAVA
@Scheduled

may have zero direct Java callers but still be active.

The same applies to:

JAVA
@EventListener
@Bean

and 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
  • OldService
  • ServiceV1
  • ServiceBackup
  • 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.