How to Modernize a Legacy Laravel App Without a Rewrite
You have a Laravel application that runs the business. It was built five to eight years ago and it still works, mostly. But every change takes longer than it should, nobody wants to touch the billing code, and the framework is several major versions behind.
The first instinct is often to throw it away and start again. Sometimes that is the right call. Most of the time it is not. A rewrite freezes new features for months, and it has to rebuild years of small business rules that nobody wrote down.
This guide is for owners and CTOs who need to decide what to do with an aging Laravel codebase. It covers how to tell a refactor from a rewrite, the order to do the work in, and how to keep the risk and cost under control while the business keeps running.
Signs your Laravel app needs restructuring
Old code is not a problem in itself. These symptoms are:
Small changes break unrelated features. A fix in checkout breaks a report. This usually means logic is duplicated or tightly coupled.
Controllers with hundreds of lines. Validation, business rules, emails and database queries are mixed in one method.
No automated tests, or tests nobody runs. Every deploy depends on someone clicking through the app by hand.
The framework or PHP version no longer gets security fixes. Newer packages refuse to install, and each month behind makes the upgrade bigger.
One developer understands it. If that person leaves, the business is exposed.
Pages get slower as data grows. This is often a sign of N+1 queries and missing indexes.
If several of these sound familiar, the application needs work. That does not mean it needs to be replaced.
Refactor or rewrite: how to decide
A rewrite makes sense when the data model is wrong at its core, when the product has changed so much that most features are no longer used, or when the application is not really Laravel at all, for example a thin framework layer on top of years of raw PHP scripts.
Refactoring makes sense when the data model is mostly sound, the business still depends on most of the features, and the main problems are structure, tests and versions.
| Question | Points to refactor | Points to rewrite |
|---|---|---|
| Is the database schema mostly right? | Yes | No, core tables need redesign |
| Are most features still used? | Yes | Less than half |
| Can the business pause new features for months? | No | Yes |
| Is the code recognizably Laravel? | Yes | Barely |
| Are there undocumented business rules? | Many | Few, and they are known |
Sometimes the answer is a new build. For Marafiq, a training platform in Riyadh, the client replaced an off-the-shelf learning system (Rocket LMS) with a custom Laravel and React platform, and the data was migrated across. That decision came from looking at the system and the data, not from habit. For an application your own team built in Laravel, incremental modernization is usually the cheaper and safer path.
Step 1: Audit before you touch anything
Start with a short, written audit. It should answer:
Which Laravel and PHP versions are running, and which packages block an upgrade?
Which parts of the app carry the most money or risk, such as payments, invoicing and permissions?
Where are the slowest pages and heaviest queries?
What tests exist, and do they pass?
How is the app deployed, and can a release be rolled back?
The output is a ranked list of problems and a plan. Without it, refactoring turns into polishing whatever the developer happens to open first.
Step 2: Put a safety net in place with tests
You cannot restructure code safely if you cannot tell when you broke it. So before refactoring, write tests around the behavior that matters most.
On legacy code, start with feature tests, not unit tests. A feature test calls a route or runs a job and checks the result: the order is created, the email is queued, the total is right. These tests describe what the business needs, and they keep passing while you move code around underneath.
Focus on the critical paths first: sign-up and login, checkout and payments, the reports the finance team uses, and permissions. You do not need full coverage to start. You need coverage on the code you are about to change.
We write these tests with Pest. Marafiq shipped with roughly 940 automated tests. A legacy app will not start anywhere near that, but every refactor should leave more tests behind than it found.
Step 3: Fix deployment before fixing code
If deploying means copying files over FTP or running commands by hand on the server, fix that first. Set up a pipeline that runs the tests and deploys the same way every time, with a way to roll back.
This is not a detour. Refactoring means many small releases. If each release is manual and risky, the team will batch changes into large deploys, and that is where incidents come from.
Step 4: Upgrade one version at a time
Each major Laravel release has an official upgrade guide. The safe pattern is:
Check the PHP requirement of the next Laravel version, and move to a PHP version that both your current and your next Laravel version support.
Move one major Laravel version at a time. Do not jump four versions in one branch.
Replace abandoned packages before the upgrade that would break them.
Run the full test suite and a manual smoke test on staging at each step.
Deploy each version to production before starting the next.
Two things save time here. First, tools such as Rector, or a paid service like Laravel Shift, automate much of the mechanical change. Their output still needs a human review, and the tests from step 2 make that review fast. Second, you do not have to adopt every new convention to stay current. Laravel 11 introduced a slimmer application structure, but its upgrade guide says older applications can keep their existing structure. Upgrade first, restructure later.
Step 5: Restructure the code in small, shippable slices
Once the safety net is there, start moving code into the places Laravel expects it. We wrote about why Laravel's conventions matter. Refactoring is mostly about bringing an old app back to those conventions.
Validation moves from controllers into Form Requests.
Authorization moves into Policies, so permission checks live in one place.
Business logic moves into small action or service classes that can be tested on their own.
Slow work such as emails, PDFs and API calls moves into queued jobs.
Repeated queries move into model scopes, and N+1 problems get fixed with eager loading.
Each slice should be small enough to review in one sitting and ship in one release. If a pull request touches fifty files, it is too big.
Step 6: Use the strangler pattern for the worst parts
Some modules are too tangled to refactor in place. For those, use the strangler pattern. Build the new version alongside the old one, send one user group or a small share of traffic to it, and grow it until the old code has nothing left to do. Then delete the old code.
In a Laravel app this can be as simple as a new set of routes and controllers behind a feature flag. Laravel Pennant, the first-party feature flag package, can switch the new module on for internal users first, then for a percentage of customers. If something goes wrong, you turn the flag off and everyone is back on the old screens.
Two rules keep this safe. Both versions must read and write the same data, so users can move back and forth without losing anything. And each flag needs an end date, because a flag that is never removed becomes one more branch to maintain.
This is also how you move a Blade-heavy admin to a modern front end. You can introduce Inertia and React, or Filament for back-office screens, one section at a time instead of rebuilding the whole interface.
How to control risk and cost
Keep shipping features. Plan modernization as a share of each sprint, not as a separate project that stops everything else.
Measure before and after. Page load times, error rates and deploy frequency tell you if the work is paying off.
Rehearse data migrations. Run them on a recent copy of production before running them for real, and keep a tested backup.
Write down what you learn. Every undocumented business rule you find should end up in a test or a short note.
Agree on what "done" means. A supported Laravel version, tests on the critical paths and an automated pipeline is a realistic first milestone.
What it costs and how long it takes
It depends on the size of the codebase and how far behind it is, so we will not give a number without seeing the code. The order of the work is predictable, though. A short audit comes first. Then come tests and the deployment pipeline. After that, the framework upgrades happen, and restructuring runs alongside normal feature work.
The audit is the part to pay for first. It turns an open-ended worry into a fixed plan and an estimate. Our posts on fixed price vs. time and materials and what happens after launch explain how we price and maintain this kind of work.
The short version
Do not rewrite by default. Most legacy Laravel apps can be modernized in place.
Audit first, then add tests around the critical paths, then automate deploys.
Upgrade one Laravel version at a time, and restructure after you are current.
Restructure in small slices, and use the strangler pattern behind feature flags for the worst modules.
Keep shipping features while you do it.
Have a Laravel application that has become hard to change? Tell us what it does and which version it runs, and we will give you an honest view on refactor or rebuild. Book a consultation or ask for a quote.

