Should You Upgrade, Refactor or Rewrite a Legacy PHP Application?
The choice is not keep PHP versus rewrite in Go. It is leave it, upgrade it, refactor it, extract one workload, or — rarely — rewrite. Most teams skip the first four.
The binary that shows up in Slack is usually fake: stay on the current PHP stack forever, or rewrite the product. Real options are more boring, which is why they get skipped. You can leave a stable system alone. You can upgrade the runtime and framework. You can refactor inside PHP. You can extract one workload. You can rewrite. Only the last one makes a conference talk.
Selective PHP to Go migration is option four, not option five with extra branding. Incremental PHP to Go migration describes the mechanics once extraction is justified. This article is the choice before those mechanics. If you skip it, you will pay legacy PHP modernization cost for a strategy you never chose.
Five legitimate options
1. Leave it alone. The system is ugly and it makes money. Change is risk. If pain is low and the runtime is still patchable, “do nothing this year” is a strategy. Engineering culture hates this option because it looks like neglect. Product companies ship on neglected systems all the time. The question is whether the neglect is currently costing incidents, hiring, or a blocked upgrade — not whether a new hire finds the code tasteful.
2. Upgrade. PHP version, framework major, obsolete extensions. The product behaviour stays. This is underrated because it is not a new architecture. It is also the option that security and hiring actually care about: an unsupported runtime is a compliance and recruiting problem even when the app is fast.
3. Refactor / modernise in place. Boundaries, tests, killing the god class, splitting a module that is not a service. Still PHP. Still one deployable if that is what you have. This is the work that makes later extraction possible. Doing it as a rewrite in another language is how you pay twice.
4. Selectively extract workloads. One worker, one fan-out path, one CPU-heavy pipeline. PHP remains the product. The strangler pattern for incremental migration is the operational pattern, not a mandate to strangle everything. Extraction without a boundary is just a distributed monolith with extra latency.
5. Full rewrite. New system, long dual-run, product freeze risk, hiring a second team. Exceptional. Usually a disguised admission that nobody wants to read the current code. If you cannot describe the first production slice and the rollback, you are not ready to rewrite. You are ready to be unavailable for a year.
These are not ranks of virtue. They are responses to different evidence. Using option 5 because option 2 felt unglamorous is a common way to burn a roadmap.
Decision dimensions
Business-rule density. If the rules are the company, a rewrite will rediscover them painfully. Prefer upgrade and tests. A billing engine with twenty years of exceptions is not “legacy to delete.” It is the product. Characterise it before you replace it.
Automated tests. No tests: do not rewrite. You do not have a specification. Buy tests and characterisation first. A rewrite without tests is two systems whose disagreement you will discover in production.
Current performance. If you have not found out what is actually slow first, you do not have a runtime argument. Missing indexes survive a language change. N+1 queries survive a language change. A connection pool that is too small survives a language change until it becomes a different incident.
Framework and runtime age. Unpatched PHP is a security and hiring problem even if the app is fast. Upgrade can be mandatory while rewrite remains optional. Do not bundle them. “We have to rewrite because we are on PHP 7.4” is usually false; you have to leave PHP 7.4.
Release frequency. If you cannot ship weekly, a rewrite will ship never. Fix deploy, then argue architecture. A team that releases quarterly cannot operate two systems. They can barely operate one.
Architecture coupling. If everything imports everything, extraction has nowhere to land. Refactor until a boundary exists. The boundary is a contract and a data owner, not a new repo with the same use statements copied in.
Hiring and maintenance. PHP talent for this codebase versus Go talent for a new one. “We cannot hire PHP” is sometimes true and sometimes an excuse not to pay seniors. A rewrite into a language the current team does not know is a hiring plan disguised as architecture. Write the hiring plan if that is what it is.
Infrastructure cost. Language changes rarely dominate cloud bills. Data and chatty calls do. If the CFO’s argument is “PHP is expensive to run,” ask for the line items. Worker memory can be a real number. Framework taste is not.
Concurrency profile. The actual case for Go: many in-flight operations, long-lived processes, connection density. Not “Go is faster.” If the profiler shows the database, Go will wait faster.
Operational pain. If deploys are terrifying, rewrite will be a longer terrifying period. Stabilise. Fear of deploy is a process and pipeline problem until proven otherwise.
Opportunity cost. Six months of rewrite is six months of product you did not build. Finance understands this even when engineering wants a clean repo. Put the product bets next to the rewrite calendar and see which one the company actually wants.
Product roadmap. A rewrite during a year of aggressive product change is how you get two incomplete systems. If the next two quarters are full of features that must ship, options 2 and 3 are how you keep the option of 4 later. Option 5 is how you freeze the wrong thing.
Decision table
| If this is true | Prefer | Avoid |
|---|---|---|
| Pain is low, runtime still supported | Leave it | Rewrite |
| Blocked on PHP/framework version | Upgrade | New language |
| Changes keep breaking distant modules | Refactor in PHP | More services |
| Profiler shows I/O and queries | Database and app design | Go as a spell |
| Profiler shows fan-out / long-lived workers | Extract that workload | Rewriting the HTTP app |
| No tests, no staging | Characterisation and pipeline | Any rewrite |
| You cannot ship the current app weekly | Deploy and CI | Greenfield |
| Dual-running two products for a year is unacceptable | Incremental options 2–4 | Option 5 |
| One named workload with rollback | Extract | “Big bang Saturday” |
| The team wants a clean repo more than a metric | Pause and name the metric | Starting anyway |
Read the table left to right. If several rows apply, sequence them. Do not average them into “microservices in Go.” Upgrade can be this quarter; extraction can be next; rewrite can remain off the table.
How to tell upgrade from refactor from extract
Upgrade changes the floor: PHP 8.3, Symfony LTS, a Composer ecosystem that still gets security releases. Users should not notice, except that deploys get less frightening and candidates stop bouncing off the version number.
Refactor changes the interior: a billing module that no longer imports shipping, tests around the money path, a queue that is retry-safe. Users should not notice either, except that the next billing feature takes a week instead of a month.
Extract changes the runtime for one slice: the worker is now a Go binary with a clear contract; PHP still owns checkout. Users might notice if you did it because the backlog never drained. They should not notice a new failure mode because you forgot rollback.
Rewrite changes the company calendar. Everyone notices. That is the point, and the problem.
If you cannot say which of the four you are currently doing, you are probably doing a rewrite while calling it a refactor. That naming error is expensive. It lets the scope grow without a decision.
How teams skip to rewrite anyway
Rewrite feels like progress: new repo, new CI, new hope. Upgrade feels like housework. The housework is what keeps the lights on. A rewrite that copies the old query patterns into Go copies the old p99 as well.
Other skip patterns:
- The conference-driven rewrite. A talk was convincing. The production traces were never opened.
- The hiring-driven rewrite. It is easier to post a Go job than to pay for senior PHP. You now have a hiring plan and an unstaffed PHP system.
- The cleanliness rewrite. The code is embarrassing. Embarrassment is not a production metric.
- The big-bang Saturday. There is no Saturday long enough for a product of any size. The strangler pattern for incremental migration exists because Saturday does not.
If the table above is mixed, do not average it into a microservices-and-Go programme. Sequence: baseline metrics, upgrade what is blocking patches, refactor the one boundary you need, extract only with evidence.
A note on “leave it alone”
Leaving it alone is not the same as ignoring CVEs. It means you are not opening an architecture programme. You still patch, you still take dependency updates that are cheap, you still refuse to add more coupling. “Leave it” with an unpatched runtime is neglect. “Leave it” with a supported PHP 8.3, boring deploys, and a product that is not blocked on the stack is discipline.
Write that down. Otherwise the next new hire will restart the rewrite conversation from zero.
When the choice has to be made against the actual coupling in your system rather than a generic matrix, that is the Migration Readiness Audit: €1,200, written, allowed to say upgrade-only. Implementation at €402/day stays optional. The selective PHP to Go migration offer is built around that order on purpose.