Automating code migrations with speed and accuracy - Part I
Strategies for automating code migrations in large codebases
Migrating old code to new systems isn't just about keeping up with industry trends — it’s about making sure your software stays efficient, scalable, and secure. Code migrations also yield second order benefits, such as cost efficiency and improved developer sentiment.
Code migrations are inevitable as teams strive to keep up-to-date with security updates and modern frameworks, all while replacing legacy technologies. These migrations can range from simple API deprecations to comprehensive system rewrites. Below are some common types of code migrations and the challenges involved in handling them:
Upgrading frameworks and libraries: The Log4j vulnerability is a great example where developers had to quickly upgrade library versions to patch security vulnerabilities and perform extensive testing to verify logging behavior & performance. Similarly, front-end library upgrades such as React, Angular, or Vue often involve more than just updating library versions as they span from deprecating features to making significant refactoring across components, services, and modules to leverage new architecture patterns.
A recent study of 19K+ Java GitHub projects revealed that between 8% and 25% of software projects undergo library migrations, especially in areas such as logging, JSON processing, testing, and web services.
API changes and Feature deprecation: Google and Apple often update their development frameworks, such as migrating an app from old Android support libraries to AndroidX. Such migrations involve changing imports, updating build files, and modifying code across multiple modules. Similarly, moving from AWS SDK v1->v2 requires developers to adapt to new API signatures, resource management patterns, configuration changes, policy updates, and revalidate security & compliance.
Language migrations: In the finance industry, migrating from COBOL to Java is a large-scale, cross-departmental project requiring careful mapping business logic to Java’s object-oriented paradigm and verifying accuracy across millions of transaction records. With Python 2 reaching its end of life, developers have been transitioning to Python 3, which requires complex syntax changes, updating libraries, modifying data types, and ensuring compatibility with third-party packages.
Build system and Configuration changes: Migrating from Maven to Gradle or vice versa is more complex than switching tools. It often requires rewriting build scripts, reconfiguring dependencies, and restructuring project layouts to fit the new build-system’s conventions. Similarly, migrating from Buck to Bazel is a complex process involving changes to build files, dependency management, custom rule adaptations, and configuration adjustments.
Cross-language/Platform migration: Splitting a monolithic service architecture into microservices requires significant code refactoring. This migration involves architectural changes, such as breaking dependencies, defining service boundaries, and establishing inter-service communication via REST or gRPC. Similarly, migrating from a single database (MySQL) to a multi-database approach with specialized databases for different tasks (e.g., Redis for caching, Cassandra for high-throughput data) is a far more complex migration, impacting data models and performance optimizations.
Service Convergence: Over time, tech debt often arises as teams race independently to ship functionality and end up with duplication of functionality across microservices. Service convergence involves consolidating such services, reducing redundancy, and refactoring code. This type of debt can accumulate after corporate acquisitions, as duplicated functionality across microservices becomes increasingly challenging to maintain and optimize.
Automation Potential
Below, we provide a radar chart showing the complexity and automation potential of various migration types. The chart uses a 1 to 4 scale to represent the relative complexity of each type (1 = Low, 4 = Very High) across factors like code rewrites, build system updates, configuration changes, and architectural modifications. The Automation Potential axis reflects how much of each migration type can typically be automated, on a scale from 0 to 1 (e.g., 0.8 for 80%).
Insights from the Chart:
Upgrading Frameworks/Libraries: Moderate code rewrites and configuration changes, with a high automation potential of around 80%, due to tools that can automate repetitive upgrade tasks.
API Changes/Feature Deprecation: Moderate complexity with very high automation potential of up to 90%, as language-agnostic tools can easily handle syntax updates and manage deprecations.
Language Migrations: High complexity in code rewrites and architectural changes, with automation potential up to 70% due to advanced tools that handle multiple languages and configurations simultaneously.
Build System Changes: High complexity in build and configuration updates, with automation potential at around 70% as migration tools manage configuration rewrites across various formats (e.g., Gradle, JSON, YAML, & Bazel).
Cross-Language/Platform Migration: The most complex migration type across all domains, especially in architectural restructuring, but automation potential of up to 60% with cross-language tools that handle code and configuration across diverse environments.
Service Convergence: Moderate code rewrites and configuration changes, but high architectural complexity as redundant services and their functionalities are merged, and dependencies are unified. Automation potential is lower (up to 40%) because consolidating functionality and dependencies across services may require manual analysis.
As a testament to the effectiveness of migration tools, Amazon used its tool, Amazon Q, to migrate over half of its Java systems from Java 8 to newer versions, saving an estimated 4,500 developer years and $260 million. This case demonstrates how effective automation can be in reducing time and cost while maintaining code quality [source: Andy Jassy’s Twitter shown below].
The Role of Static Analysis in Migration Tools
Automated migration tools, built on foundational static analysis techniques, can reduce migration times by up to 80% compared to manual rewrites, significantly easing the workload on developers. By automating repetitive and error-prone tasks, these tools enable developers to handle large-scale changes with greater accuracy, consistency, and speed, allowing them to focus more on feature development. These tools excel at identifying outdated APIs, refactoring code, and updating configurations in large codebases in large codebases, making them invaluable for complex migrations.
Tools like OpenRewrite, Error Prone, and jscodeshift are widely used for migrations, enabling transformations across codebases. OpenRewrite excels in updating Java codebases, such as upgrading dependency versions and enforcing coding standards. Error Prone, developed by Google, identifies and prevents common bugs in Java, particularly useful for enhancing code quality during migrations. jscodeshift, popular in the JavaScript ecosystem, allows rapid refactoring across React and Node.js projects by automating syntax transformations.
Despite these advantages, these tools can sometimes produce false positives, requiring careful validation to ensure accuracy.
The Potential and Limitations of LLMs
LLMs (Large Language Models) bring unique strengths to code migration tasks, especially for generating contextually relevant code snippets, handling syntax updates, and translating between languages or frameworks. LLMs excel at repetitive refactoring within limited contexts, making them ideal for tasks like standardizing coding patterns across modules or assisting with multi-language support in codebases.
LLMs have shown promise in supporting API migrations, such as assisting with upgrades in the pandas library from v0.x to v1.x in Python by suggesting updated API calls and helping to refactor code accordingly. They have also been helpful in converting callback-based JavaScript code to async/await syntax and in providing conversion suggestions during Python 2 to Python 3 translations, including syntax adjustments and compatibility hints with tools like Hugging Face’s transformers library.
While these capabilities make LLMs powerful tools for targeted migrations, they have limitations when handling large, complex codebases with millions of lines of code and extensive cross-module dependencies. Current LLMs have token limits that restrict the amount of data they can handle at once, making it challenging to analyze cross-file dependencies, configuration files, or interconnected modules. Additionally, LLMs can exhibit idempotency issues, producing the same output repeatedly no matter how many times you execute it, which can limit their effectiveness in migrations. For these reasons, LLMs could be best suited as complementary tools, assisting with simpler tasks and generating code or test snippets, rather than handling entire migrations independently.
Combining Static Analysis and LLMs for Effective Migrations
At Gitar, we combine foundational static analysis techniques with LLMs to provide a robust approach to code migrations by leveraging each tool’s strengths to offset the other's limitations. Together, they provide the scalability needed to efficiently manage large codebases and the precision to reduce hallucinations and false positives. This powerful combination also accelerates code migrations and enhances accuracy through rigorous validation and testing.
Real-World Results at Uber
At Uber, we led and automated many large-scale migrations, including upgrades to their in-house Experimentation API, modernization of annotation processors, migrating away from Thrift to gRPC across tens of millions of lines of code, and refactoring Uber’s millions of lines of Go codebase to use explicit context for traceability [polyglot-piranha, piranha, go-context-refactor]. Additionally, we addressed technical debt through an extensive feature flag cleanup using our tool
For the Experimentation API migration, annotation processor migration, and feature flag cleanup alone, our tool generated an impressive 4,881 pull requests within just six months at Uber, averaging around 800 PRs per month. This automation resulted in annual savings of tens of millions of dollars, significantly accelerating development, and improving code quality across the organization.
If your organization faces challenges with migrations, we'd love to hear from you and would be keen on automating these tasks for you.
Connect with us on our Slack channel to learn more about Gitar.