Legacy — it’s not just old code. It’s a working system that brings money to the business but slows down development. In mobile development, legacy can be written in Objective-C, use outdated libraries or architectural patterns. According to a CAST Software (2024) report, the average age of a line of code in enterprise projects exceeds 14 years. The strategy for working with legacy determines whether it becomes a bottleneck or remains a manageable asset.
Key Takeaways
Legacy — code or a system that continues to run in production but no longer meets modern quality standards. Legacy can be written in an outdated language (e.g., Objective-C instead of Swift), use unsupported libraries, or architectural patterns long considered anti-patterns.
The key characteristic of legacy is the absence of tests. By Michael Feathers (2004)’s definition, legacy code is code without tests. If you can’t safely change behavior, the system is in legacy status regardless of age. Fresh code without unit tests is legacy from day one.
Legacy is not necessarily bad. A well-designed system in Java 8 can be more reliable and understandable than chaotic Kotlin code with coroutines. Code age is not an indicator of quality — what matters is how easily the system can be changed and extended.
Every successful system becomes legacy over time. This is a natural process: technologies evolve faster than code can be rewritten. An app written 5 years ago in Swift 2 is legacy today, even though it was modern at the time of creation.
The business value of legacy is often underestimated. The system works reliably, processes transactions, stores data — rewriting carries risks. According to Standish Group (2024), 35% of full rewrite projects end in failure. It’s economically justified not to get rid of legacy, but to learn to work with it.
The best strategies are gradual migration, encapsulating old code behind new interfaces, and automated testing. Legacy only becomes a problem when it stops being changeable at a predictable cost.
Lack of automated tests — the main indicator. If after changing a single line a developer cannot run tests and confirm nothing broke — you’re dealing with legacy. An additional sign: the deployment process takes hours and requires manual steps.
Documentation doesn’t match the code — another marker. Architectural diagrams are outdated, comments describe behavior that has already changed. Time-to-ramp-up for a new developer exceeds a month — a sign of high complexity and low maintainability.
Additional signs: monolithic architecture without clear boundaries, manual testing as the primary verification method, long CI pipeline (over 30 minutes), use of libraries without current versions, and inability to update dependencies without breaking related modules.
The “fragile code” phenomenon — a change in one place breaks three others. This results from tight coupling, where modules know too much about each other. The higher the coupling, the faster the system transitions into the legacy category.
Reduced speed — the main risk. Adding a simple feature takes hours of studying code and days of testing. According to Stripe (2024), developers spend 33% of their time overcoming technical debt, which is directly related to the presence of legacy modules in the project.
Expertise leakage — original code authors leave the company, and documentation is incomplete. New developers are afraid to touch unfamiliar modules, leading to the “frozen code” effect: the module doesn’t evolve but continues to work. The bus factor of such systems is critically low.
Security — outdated libraries contain known vulnerabilities. Using OpenSSL 1.0.2 or outdated versions of Jackson in Java projects is a direct path to security incidents that can cost the business reputation and customers.
Team demotivation — working with legacy without a strategy for improvement reduces developer satisfaction. The team stops being proud of the product, staff turnover increases, which further slows down system development.
Characterization tests — the first step before any change to legacy code. Run the code on known input data and record the expected output. These tests capture current behavior as a specification. Golden master testing is a variant where output is compared against a reference file.
Seam analysis — finding points where coupling can be broken without changing behavior. Michael Feathers identifies several types of seams: preprocessor seam, object seam, link seam. Object seam is the most common: replacing a real object with a test stub through an interface.
Sprout method and Sprout class — techniques for adding new code alongside old code rather than inside it. Instead of modifying an existing method, create a new method with the desired logic and call it from the old one. This minimizes the risk of breaking working code.
class LegacyPaymentProcessor {
def process(payment) {
// 200 lines of legacy code that should not be touched
logPayment(payment) // sprout method
}
def logPayment(payment) {
// new code added alongside legacy
}
}
Strangler Fig pattern — the recommended approach for legacy migration. A new module is created in parallel, traffic is gradually switched from old to new. The old module “dies” naturally when it stops receiving requests. The pattern minimizes risks and allows rollback if problems arise.
Branch by Abstraction — a technique where an abstraction is created over the old and new implementations. Client code switches to the abstraction, and the old implementation is gradually replaced. Example: replacing the networking layer from AFNetworking to Alamofire through a unified NetworkService protocol.
Phased migration — breaking the transition into small steps: encapsulate the old module → write tests → create a new module → run in parallel → remove the old module. Each step ends with a stable system state, allowing deployment at any moment.
Frequently Asked Questions
Complete rewriting is the riskiest option. Only 25% of projects doing a Big Rewrite succeed on time. It’s better to apply the Strangler Fig pattern: replace modules gradually without stopping the product. Each iteration brings business value, and risks are distributed over time.
Start with characterization tests: run the module on known data, record the result. Golden master testing is a simple way to capture behavior. Add tests every time you touch a line of code. In 6 months, you’ll have a framework that protects against regressions.
If the system is stable, doesn’t require frequent changes, and doesn’t affect the development speed of other modules — leave it. “If it ain’t broken, don’t fix it” is a reasonable approach for isolated legacy modules with low change frequency. Only touch code when business changes are needed.
Use semantic versioning and update in steps: patch → minor → major. Write compatibility tests for each library. Dependabot or Renovate automate creating PRs for updates. If a library is deprecated, plan its replacement through an abstraction.
Technical debt is a metaphor for estimating the cost of deferred improvements. Legacy is a specific system or code that has already become outdated. Tech debt can accumulate in a month, but legacy takes time. Not all tech debt becomes legacy, but all legacy contains tech debt.
Summary
We will develop a mobile application turnkey
IT Sectr creates iOS and Android applications for startups and businesses since 2017. We will advise you and propose the best solution.
Read also