Saturday, 29 August 2026

Why “Just Rewrite It” Is Usually Bad Modernization Advice

Why “Just Rewrite It” Is Usually Bad Modernization Advice

There is a sentence developers say very easily when they are looking at an old codebase:

“We should just rewrite it.”

I understand the temptation.

You open a project and find an old PHP version, a framework that is no longer supported, classes with thousands of lines, duplicated business logic, direct SQL everywhere, globals, static state, and dependencies nobody wants to touch.

Starting again sounds cleaner.

Sometimes it is the right decision.

But in my experience, “just rewrite it” is often advice that underestimates what the existing system actually contains.

Old code contains more than code

A legacy application may look ugly technically and still carry years of business knowledge.

Imagine a system that has been running for ten years.

During that time:

  • customers reported edge cases,

  • accountants changed reporting rules,

  • payment providers behaved unexpectedly,

  • regulators introduced new requirements,

  • staff developed unusual workflows,

  • developers fixed hundreds of production bugs.

Not all of that knowledge made it into documentation.

A lot of it ended up here:

if this customer type...
unless this account was created before...
except when this payment provider...
but only after this approval...

Those conditions may look terrible when you read them.

Some probably are.

But some represent real business rules that nobody remembers anymore.

When you rewrite the application, you are not just rewriting PHP.

You are trying to rediscover the business.

The new system starts behind

This is one of the biggest rewrite problems.

Suppose the existing application already has:

Authentication
Customers
Orders
Billing
Reporting
Notifications
Administration
Integrations

The rewrite starts with none of them.

So while the old system is running the business, the new team begins rebuilding features that already exist.

Meanwhile, the business does not stop.

A new payment integration is needed.

Management requests another report.

A security issue needs fixing.

A customer discovers an edge case.

Now the old system continues changing while the new system is trying to catch it.

The finish line moves.

That is how a six-month rewrite quietly becomes an eighteen-month project.

Feature parity is harder than it sounds

Teams often create a checklist:

Login ✓
Orders ✓
Payments ✓
Reports ✓

Then everyone feels close to completion.

But “Orders works” can hide hundreds of behaviors.

What happens when an order is partially cancelled?

What happens when the customer's account was migrated from an older version?

What happens when an external API times out after accepting the request?

What happens to historical reports?

What permissions does a regional administrator have?

The old application may already know the answers because production forced someone to solve them years ago.

The rewrite team may discover them again one bug at a time.

Rewrites create two systems to maintain

During a large rewrite, you temporarily create a strange architecture:

Legacy Application
        +
New Application

Both matter.

Both require developers.

Both may need infrastructure.

Both need security fixes.

Both may contain versions of the same business logic.

And developers eventually start asking:

Where should I implement this new feature?

Sometimes the answer becomes:

both.

That is not modernization.

It is duplicated risk.

I prefer asking a smaller question

Instead of starting with:

“How do we replace this system?”

I think teams should first ask:

“What part of this system is actually hurting us?”

Maybe it is Billing.

Maybe the reporting subsystem is impossible to change.

Maybe authentication is insecure.

Maybe an old framework prevents the PHP version from being upgraded.

Maybe deployment takes two hours.

Maybe nothing is fundamentally wrong except one badly coupled module.

Those are very different problems.

They should not automatically receive the same solution.

Modernization does not have to mean replacement

There are several ways an old application can evolve.

You can upgrade dependencies.

You can introduce tests around risky areas.

You can replace infrastructure adapters.

You can establish module boundaries.

You can extract one capability.

You can put a modern API in front of legacy logic.

You can move some workloads to workers.

You can replace one subsystem and leave five others alone.

Conceptually:

Legacy Application
├── Customers
├── Orders
├── Billing
├── Reporting
└── Notifications

might become:

Legacy Application
├── Customers
├── Orders
├── Billing
└── Notifications

        |
        v

Modern Reporting Module

Nothing dramatic happened.

Customers still log in.

Orders still work.

The business keeps operating.

Only the part that needed change moved.

I think that is often a healthier modernization strategy.

Sometimes the old code should stay

This is another thing developers do not say enough.

Imagine a legacy component that:

  • has worked reliably for eight years,

  • rarely changes,

  • has no known serious security problem,

  • is well understood,

  • costs almost nothing to maintain.

Why rewrite it?

Because the code style looks old?

Because another framework is more fashionable?

Modernization should reduce risk or cost.

Otherwise we may simply be exchanging known problems for new ones.

A good modernization assessment should sometimes conclude:

Leave this part alone.

Data makes rewrites even harder

Code is usually easier to move than data.

A mature application might have millions of records and years of history.

Then questions appear:

Which system owns new records during migration?

Do we synchronize databases?

How do we verify migrated data?

Can we roll back?

What happens if the new system writes data that the old one cannot understand?

This is where supposedly clean rewrites become complicated very quickly.

The database remembers the history of the business even when the developers do not.

This thinking influenced Evolve Bridge

One reason I am building EvolvePHP around incremental modernization is that I don't want adoption to require replacing an entire application.

The direction is closer to:

Audit
  ↓
Understand the system
  ↓
Identify boundaries
  ↓
Bridge
  ↓
Modernize one capability
  ↓
Validate
  ↓
Repeat only if useful

Evolve Bridge is intended to let an existing Laravel, Symfony, legacy PHP, or custom system coexist with newer EvolvePHP capabilities.

Not convert the entire application magically.

Not hide difficult migration decisions.

Just create controlled boundaries where change can happen gradually.

Rewrites still have their place

There are cases where starting again makes sense.

The existing system may be tiny.

Its behavior may be well documented.

The technology may be completely incompatible with future requirements.

The business may be willing to freeze the old product while the new one is built.

Or maintaining the current architecture may genuinely cost more than replacement.

In those cases, rewrite.

But make that decision because the evidence supports it.

Not because new code feels better than old code.

Modernization should preserve what still works

The goal should not be to produce the cleanest possible repository.

The goal should be to improve the system without unnecessarily putting the business at risk.

Sometimes that means replacing a lot.

Sometimes it means replacing almost nothing.

Most of the time, I suspect the best answer sits somewhere between those extremes.

So when I hear:

“Just rewrite it.”

my next question is usually:

Which part, exactly—and what problem are we solving by rewriting it?

That question often leads to a much better modernization plan.

No comments:

Post a Comment