Refactoring Legacy Code with AI: Real-World Results
AI tools promise to modernize aging codebases. How does that actually play out with real legacy .NET and Java projects? Results from the field.
Jean-Pierre Broeders
Freelance DevOps Engineer
Refactoring Legacy Code with AI: Real-World Results
Everyone knows the scenario. An eight-year-old codebase, originally built on .NET Framework 4.6, with synchronous database calls, manual dependency management, and a Global.asax carrying more responsibilities than a startup CTO. It needs to move to .NET 8. Manual refactoring takes months. Can AI speed that up?
Setting expectations
No AI tool is going to transform a complete legacy application into modern, clean code in one pass. That just doesn't happen. What does work is targeted, file-by-file refactoring where the AI recognizes patterns and converts them.
The key is breaking the work into chunks. Not "refactor this entire application" but "convert this synchronous repository to async/await" or "replace this manual mapping block with an AutoMapper profile." Small, well-scoped tasks.
Sync to async: where AI shines
This might be the most rewarding refactoring scenario. Converting synchronous database calls to async is largely mechanical work — exactly the kind of pattern recognition AI handles well.
Take a typical legacy repository method:
public List<Order> GetOrdersByCustomer(int customerId)
{
using (var context = new OrderContext())
{
return context.Orders
.Where(o => o.CustomerId == customerId)
.Include(o => o.OrderLines)
.ToList();
}
}
The AI-generated async variant:
public async Task<List<Order>> GetOrdersByCustomerAsync(int customerId)
{
await using var context = new OrderContext();
return await context.Orders
.Where(o => o.CustomerId == customerId)
.Include(o => o.OrderLines)
.ToListAsync();
}
Clean. The using statement becomes await using, ToList() becomes ToListAsync(), and the method signature is correct. About 90% of these conversions produce directly usable results.
But here's the catch — the AI doesn't automatically update all calling code. Every place that calls GetOrdersByCustomer now needs to go async too. That cascade effect requires human judgment. Sometimes there's a synchronous event handler in the chain that can't simply become async.
Configuration migration: mixed results
Converting web.config XML to appsettings.json sounds straightforward. For standard connection strings and app settings, it works fine:
<!-- Old -->
<connectionStrings>
<add name="DefaultConnection"
connectionString="Server=prod-sql;Database=Orders;Integrated Security=true;" />
</connectionStrings>
Becomes neatly:
{
"ConnectionStrings": {
"DefaultConnection": "Server=prod-sql;Database=Orders;Integrated Security=true;"
}
}
Once custom configuration sections with their own IConfigurationSectionHandler implementations enter the picture, things fall apart. The AI generates code that compiles but deviates functionally. Custom XML structures get flattened into key-value pairs, losing the original hierarchy.
Where it really breaks: business logic
Legacy applications are riddled with implicit business rules. A method called CalculateDiscount that also checks inventory, makes a logging call, and under certain conditions sends an email. The AI sees the code, not the intent.
During a refactoring of an order processing module, the AI produced nicely split, SOLID-looking code. Beautifully structured. Except it missed a subtle null check that the original code handled as a side effect of a LINQ query. In production, that would cause a NullReferenceException for orders without a shipping address — a scenario that only occurs for about 3% of orders.
Those kinds of bugs don't surface in code review. They surface in production, on a Friday afternoon.
A practical approach that works
After multiple legacy migrations, a pattern emerges that consistently delivers good results:
| Phase | AI-suitable | Manual |
|---|---|---|
| Sync → async conversion | ✅ 85-90% accurate | Check cascade effects |
| Config migration (standard) | ✅ Almost always correct | Custom sections |
| Dependency injection setup | ✅ Good for standard patterns | Evaluate lifetime scoping |
| Business logic refactoring | ⚠️ Starting point only | Always full review |
| Database migration scripts | ❌ Too risky | Fully manual |
The golden rule: use AI for the boring, mechanical parts. Sync-to-async, modernizing using statements, namespace reorganization. Leave the business logic alone until a human has reviewed it.
Test coverage as a safety net
Before any AI-driven refactoring, test coverage needs to be in place. Sounds obvious, but legacy projects usually lack it. The advice: write characterization tests first. Not to prove the code is correct, but to capture current behavior — bugs included.
[Fact]
public void GetOrdersByCustomer_ReturnsEmptyList_WhenNoOrders()
{
// This tests CURRENT behavior, not desired behavior
var result = _repository.GetOrdersByCustomer(99999);
Assert.Empty(result);
// If the refactored version throws an exception here,
// you know something changed
}
With those tests in place, the AI can refactor freely, and the test suite catches regressions. Without them, it's Russian roulette.
Time savings in numbers
A recent migration of a .NET Framework 4.7 application to .NET 8 (roughly 120 files, 15,000 lines of code) broke down like this:
- AI-assisted refactoring: 40% of files, averaging 70% faster than manual
- Manual refactoring: 35% of files (business logic, complex edge cases)
- Hybrid: 25% where AI produced a first version that needed significant reworking
Total time saved compared to fully manual: roughly 30-35%. Significant, but no silver bullet. It shifts work from typing to reviewing. And reviewing AI-generated code requires just as much expertise as writing it yourself — maybe more, because the mistakes are subtler.
No neat wrap-up
Legacy refactoring with AI isn't magic and it isn't hype. It's a tool. Good for mechanical conversions, unreliable for business logic, and always dependent on solid tests. The biggest win isn't the code the AI writes, but the time it frees up to think about the parts that actually matter.
