Stability Over Removal: Strategies for Legacy Code Coexistence in Large Projects
In long-running, high-load systems, deleting legacy code carries risks of regressions and instability. Instead of full-scale refactoring, it's smarter to let old and new code coexist using feature toggles and façades. This approach enables continuous development without downtime, keeping proven components as fallbacks.
Risks of Aggressive Code Removal
Even removing dead code can trigger failures due to hidden dependencies—reflection, dynamic loading, RPC calls. Legacy system tests rarely cover edge cases, and production data often activates rare execution paths. Loss of institutional knowledge worsens the issue: departing teams leave behind critical code for niche scenarios like migrations or compliance.
In enterprise environments, vendor-specific custom modules become vulnerable—changes can break vendor support.
The "Let Well Alone" Principle in Practice
A classic example is NASA’s Deep Space 1 probe: 1970s-era code remained untouched, wrapped in adapters. Stable components stay unchanged if they work reliably. This mature strategy suits systems older than five years.
Coexistence Architecture: The Strangler Fig in Reverse
Don’t destroy legacy—build the new system in parallel and gradually shift traffic. Two strategies:
| Strategy | Actions | When to Apply |
|----------|---------|----------------|
| Removal | Rewrite on a new stack | Simple logic, 100% test coverage, no dependencies |
| Coexistence | New code runs alongside, gradual routing | Critical functionality, weak tests, high risk |
Feature toggles ensure safety:
- Develop the new implementation.
- Wrap it in a flag check.
- Run it on 1% of traffic.
- Scale up if successful.
- Roll back via flag if issues arise.
Legacy code stays in the repo for 2–3 years for reliability analytics.
Implementation in Spring Boot
Wrap legacy code in an interface without modifying the original.
// Legacy service (untouched)
@Service
public class LegacyDiscountService {
public double calculateDiscount(Order order) {
// Logic from 5 years ago
return order.getTotal() * 0.1;
}
}
// New service
@Service
public class NewDiscountService {
public double calculateDiscount(Order order) {
return applyComplexRules(order);
}
}
Façade with toggle:
@Component
public class DiscountServiceFacade {
private final LegacyDiscountService legacyService;
private final NewDiscountService newService;
@Value("${feature.discount.new.enabled:false}")
private boolean useNewService;
public DiscountServiceFacade(LegacyDiscountService legacyService,
NewDiscountService newService) {
this.legacyService = legacyService;
this.newService = newService;
}
public double calculateDiscount(Order order) {
if (useNewService) {
return newService.calculateDiscount(order);
}
return legacyService.calculateDiscount(order);
}
}
Isolation via profiles:
@Configuration
public class DiscountConfiguration {
@Bean
@ConditionalOnProperty(name = "feature.discount.new", havingValue = "false", matchIfMissing = true)
public DiscountService legacyDiscountService() {
return new LegacyDiscountService();
}
@Bean
@ConditionalOnProperty(name = "feature.discount.new", havingValue = "true")
public DiscountService newDiscountService() {
return new NewDiscountService();
}
}
Tests exclude legacy:
@Test
@DisabledIf("!${feature.discount.new.enabled}")
void testNewDiscountLogic() {
// Only new logic
}
Traffic Switching Workflow
API Gateway routes based on flags: monolith as fallback, microservice scales incrementally. Feature Toggle Service manages real-time percentages. Legacy remains in production for 2–3 years.
When Removal Is Unavoidable
- Security: CVEs in outdated libraries.
- Compliance: GDPR violations.
- EOL tech: Java versions without patches.
- No expertise: No specialists available.
Even then—use gradual rollout with rollback capability.
Key Takeaways
- System stability trumps code aesthetics: uptime matters more than deleted legacy lines.
- Hidden dependencies require monitoring actual production calls before deletion.
- Feature toggles reduce migration risks.
- Technical debt pays off if maintenance costs less than refactoring.
- Institutional memory in code: don’t delete without 100% confidence.
— Editorial Team
No comments yet.