“We’ll Clean This Up Later” Is How Legacy Systems Are Born

Technical debt in SaaS rarely comes from bad engineers or sloppy work. It usually starts with reasonable shortcuts taken under pressure and a quiet promise to clean things up later. This article is about how those good intentions slowly harden into legacy systems—and how to recognize the moment before that happens.

“We’ll Clean This Up Later” Is How Legacy Systems Are Born

Nobody wakes up and says, “Let’s build a legacy system today.”

Legacy systems aren’t created by lazy teams or bad engineers.
They’re created by optimistic ones.

Smart people.
Moving fast.
Making reasonable tradeoffs.

And saying one very dangerous sentence:

“We’ll clean this up later.”


Nobody Means for This to Happen

Early on, that sentence feels responsible.

You’re not pretending the code is great.
You’re not ignoring the issue.
You’re being honest about priorities.

There’s a feature to ship.
A customer waiting.
A deadline that doesn’t care about your refactor.

So you take the shortcut.
You leave the TODO.
You move on.

That’s not negligence.
That’s optimism.

And optimism is how most SaaS systems get into trouble.


Why “Later” Feels Like a Real Plan

In the moment, “we’ll clean this up later” feels rational.

You believe:

  • Later will be calmer
  • Later will have fewer unknowns
  • Later will justify the time
  • Later will be easier than now

It almost never is.

Later comes with:

  • More users
  • More features
  • More dependencies
  • More fear of breaking things

The cleanup didn’t get smaller.
It got entangled.


The Difference Between Debt You Choose and Debt That Chooses You

Not all technical debt is bad.

Sometimes you intentionally cut a corner:

  • You know what you’re skipping
  • You know why
  • You know who owns fixing it

That’s a tradeoff.

The problem is accidental debt.

That’s the kind where:

  • Nobody owns it
  • Nobody schedules it
  • Everyone assumes someone else will deal with it

Strong opinion here:

If no one owns the cleanup, it’s not a tradeoff.
It’s a bet that future-you will have more time and less pressure.

Future-you almost never does.


How Temporary Code Becomes Permanent Infrastructure

This is where things quietly go wrong.

The “temporary” code works, so:

  • New code builds on it
  • Tests assume it exists
  • Edge cases get layered on top

At some point, removing it feels dangerous.

Not because it’s good.
But because everything now depends on it.

That’s how scaffolding becomes load-bearing.

And once that happens, you’re not “cleaning up” anymore.
You’re performing surgery on a live system.


Example: Auth Logic That Started as a Shortcut

This one shows up everywhere.

The naive version

Early auth is simple.

One middleware.
A couple of checks.
Maybe some inline logic for edge cases.

You tell yourself:

“We’ll extract this into something cleaner once auth settles down.”

Reasonable. At the time.

The moment it broke

Then auth did settle down.

Just not how you expected.

You added:

  • SSO
  • Magic links
  • Account switching
  • Different rules for different users

All of that logic piled into the same “temporary” area.

What developers noticed

  • Everyone was afraid to touch auth
  • Bugs only affected certain users
  • Code comments started sounding like warnings

“This is fragile.”
“Don’t refactor this unless you have to.”
“This hurts later.”

Later had arrived.

What actually fixed it

Not a rewrite.

You pulled the logic apart.

  • Authentication flows became explicit
  • Validation was separated from enforcement
  • Assumptions were surfaced instead of hidden

You deleted a surprising amount of code.

Velocity improved immediately.

Not because it was prettier.
Because it was honest.


“Later” Gets More Expensive Every Sprint

This is the part people underestimate.

Cleanup doesn’t get linearly harder.

It compounds.

Every sprint adds:

  • More callers
  • More states
  • More assumptions baked into behavior

The cleanup you skipped when the system was small now requires:

  • Migrations
  • Backward compatibility
  • Coordinating multiple teams

Same cleanup.
Much higher cost.


Example: Background Jobs That Were “Good Enough for Now”

Another classic.

The naive version

You start with:

  • One job type
  • One queue
  • Minimal retry logic

Jobs run.
They finish.
Life is good.

The moment it broke

Then workflows get more complex.

Jobs can:

  • Retry
  • Branch
  • Trigger other jobs
  • Resume after failure

The original implementation assumed a straight line.

Reality didn’t.

What developers noticed

  • Duplicate side effects
  • Jobs stuck in weird states
  • Manual cleanup scripts becoming normal

When engineers start writing cleanup scripts “just in case,” something is wrong.

What actually fixed it

You made state explicit.

Jobs had clear lifecycle stages.
Handlers became idempotent.
Orchestration was separated from execution.

It wasn’t clever.

It just stopped pretending the system was simpler than it was.


Why Teams Keep Deferring Cleanup Even When It Hurts

At this point, everyone knows it’s a problem.

So why does it keep happening?

Because cleanup work:

  • Doesn’t demo well
  • Competes with features
  • Feels like admitting past mistakes

There’s social pressure to keep shipping.

Strong take:

Most teams don’t avoid cleanup because they’re lazy.
They avoid it because it’s hard to justify out loud.


Example: Multi-Tenant Logic That Started “Just for One Customer”

This one is sneaky.

The naive version

You land your first enterprise customer.

They need:

  • Different limits
  • Slightly different behavior

You add a few conditionals.

“It’s just this one customer.”

The moment it broke

Then a second enterprise customer shows up.

Their needs don’t match the first.

Now the core logic has opinions about tenants.

What developers noticed

  • Tenant-specific bugs
  • Exploding test combinations
  • Engineers scared of shared code paths

The system no longer had a clear center.

What actually fixed it

You stopped pretending all tenants were the same.

Tenant profiles were introduced.
Variability moved to boundaries.
The core regained a single mental model.

The system got simpler by admitting it was more complex.


How to Decide What You Can’t Defer Anymore

Not everything needs to be cleaned up.

But some things absolutely do.

Here’s a practical rule:

If it’s on the critical path, stop deferring it.

Also:

  • Code people avoid touching
  • Code every new feature depends on
  • Code that keeps spawning “temporary” fixes

Those are not future problems.
They’re present ones with delayed consequences.


What “Good Enough” Actually Looks Like

This is not about perfection.

Good enough means:

  • Someone owns it
  • The assumptions are visible
  • The limits are understood

You don’t need beauty.
You need stability and clarity.


Optimism Is Good. Unchecked Optimism Is Expensive.

Optimism ships products.

It gets you to market.
It wins customers.
It keeps teams moving.

But unchecked optimism builds legacy systems.

So the next time someone says:

“We’ll clean this up later”

Ask three questions:

  • Who owns that?
  • When does it happen?
  • What breaks if we don’t?

That pause has saved more SaaS systems than any framework ever will.

Scroll to Top