Skip to content

Legacy code refactoring: restoring maintainability

Your application works. It has been running in production for years, serving real users. But working with the codebase has become harder: changes take longer than they should, bugs appear in unexpected places, and new developers struggle to see how things fit together. That is legacy code, and fixing it does not require a complete rewrite.

By Daniele Barbaro, Tech Lead & Software Engineer. Last reviewed .

Approach
Incremental, no big-bang rewrite
Steps
Small and reversible
Your team
I work alongside your developers
Formats
Focused days or monthly

What a refactoring looks like in practice

I don't believe in big-bang rewrites. They are risky, expensive and often unnecessary. I practice incremental refactoring: making the codebase better step by step while the system keeps running and delivering value.

Each step leaves the system in a working state, and your team keeps shipping features while I improve the foundation they are building on.

app/Http/Controllers/ContestController.php+6 -10
@@ -9,17 +9,13 @@ class ContestController extends Controller {removed: public function register(Request $request, Contest $contest)added: public function register(RegisterToContestRequest $request, RegisterToContestAction $action) {removed: $request->validate([/* 15 rules */]); if ($contest->registrations()->count() >= $contest->max_participants) { return back()->withErrors(['contest' => 'Full']); } $user = User::firstOrCreate(['email' => $request->email], [...]); $registration = $contest->registrations()->create([...]); Mail::to($user)->send(new RegistrationConfirmed($registration));added: $action->execute( $request->contest(), new RegistrationData(...$request->registrationData()), $request->user(), ); return redirect()->route('thanks'); } }
Same route, same response. Validation moves to a form request, the queries and the email to an action. From my own Laravel code, trimmed.

What makes code "legacy"

Legacy code is not just old code. Code becomes legacy when it resists change, when the cost of a modification exceeds what seems reasonable for the task at hand. This happens for many reasons.

Accumulated technical decisions
Every codebase reflects the context in which it was built. Framework versions change, best practices evolve and business requirements shift. Decisions that made sense five years ago may now create friction, because the architecture was designed for a different scale, a different team or a different understanding of the problem.
Missing or inadequate tests
Without tests, changes become risky. Developers work more slowly, afraid of breaking something they cannot see. That fear leads to workarounds rather than proper fixes, and the codebase gradually accumulates layers of defensive code that obscure the original intent.
Knowledge loss
Team members leave, documentation grows stale and tribal knowledge disappears. Parts of the system become black boxes that everyone is afraid to touch. The code works, but nobody fully understands why or how.
Organic growth without design
Successful applications grow. Features are added under deadline pressure, edge cases are handled with quick fixes and integrations are bolted on wherever they fit. Over time, the architecture that emerged stops serving the application's actual needs.

How I work, step by step

Four habits shape every refactoring engagement.

Understanding before changing
Every engagement begins with a careful study of the existing system. I read code, trace execution paths and map dependencies. I identify what the code is actually doing versus what it was intended to do. That understanding guides every later decision.
Establishing safety nets
Before making significant changes, I need to know if I have broken something. This often means adding tests to code that lacks them: characterization tests that capture current behavior, integration tests that verify critical paths, and unit tests that document intended behavior as I refactor.
Small, reversible steps
Large changes are hard to review, hard to test and hard to roll back. I work in small increments, each one leaving the system in a working state. If something goes wrong, I know exactly what changed and can undo it easily.
Continuous delivery throughout
Refactoring should not mean months of work before any value is delivered. I structure the work so improvements reach production regularly. Your team continues to ship features while I improve the foundation.

What refactoring involves

The specific work depends on what your codebase needs, but common patterns include:

Extracting responsibilities
Large classes and methods that do too much are split into focused, single-purpose components. This makes code easier to understand, test and modify.
Clarifying boundaries
When business logic is scattered across controllers, models and views, I consolidate it into explicit domain services or actions. Clear boundaries make the system easier to reason about.
Improving data access patterns
N+1 queries, missing indexes and inefficient data loading are identified and fixed. I optimize Eloquent or Doctrine usage for your actual access patterns.
Adding test coverage
I build a test suite that gives your team confidence to make changes. It includes high-level tests that verify behavior and focused unit tests that document expectations.
Updating dependencies
Old framework versions and outdated packages are carefully upgraded. The work is done incrementally, with thorough testing at each step.

Who this is for

Legacy refactoring makes sense for teams in specific situations.

Your application is still valuable
The business runs on this code and users depend on it. Walking away is not an option, and a complete rewrite would be too risky or expensive. You need to improve what you have.
Development velocity has declined
Features that should take days now take weeks. Your team spends more time working around problems than solving them, and the codebase hinders your ability to respond to business needs.
You are planning for growth
You are about to hire more developers, enter new markets or build significant new functionality. The current codebase will not support that growth without investment in its foundations.
Technical debt is compounding
Every shortcut adds to the debt, and you are paying interest on years of accumulated compromises. It is time to start paying it down systematically.
Onboarding new developers is painful
Every new team member takes months to become productive. They ask questions nobody can answer and learn by trial and error which parts of the codebase to avoid. That slows growth and increases risk.

My rates are published, with no form in between: see the rates page.

Common questions about legacy refactoring

What makes a codebase legacy?
Do you rewrite the application from scratch?
How do you upgrade a Laravel or Symfony application across major versions?
What does refactoring work usually involve?
How is legacy refactoring billed?

Getting started

me@plincode.tech

In TurinToolbox Coworking (opens in a new tab)
via Agostino da Montefeltro 2
Torino, TO 10134

The door is always open for a chat over coffee.