Or: The Business Person’s Fantasy About Legacy Code

You know what’s hilarious? Watching a business person discover their 15-year-old monolith can’t “just be upgraded to the cloud.”

It’s like watching someone realize their 1995 Fiat Punto can’t be “upgraded” to a Tesla by swapping the engine. Sure, they’re both cars. Sure, they both have four wheels. But somewhere between “it has wheels” and “it drives itself,” there’s a fundamental mismatch in expectations.

The Conversation That Never Changes

Business: “Can’t we just upgrade the stack?”

Me: “Which part? The Python 2.7? The Django 1.8? The MySQL 4.1? The EC2 instance that’s literally running Amazon Linux 1?”

Business: “All of it. You know, modernize it.”

Me: “So… a rewrite?”

Business: “No, no. Just an upgrade. Keep everything working exactly the same, but make it modern.”

Me: *stares in backend engineer*

Why They Think This Way

Here’s the thing — they’re not stupid. They’re operating with a mental model that works perfectly well for *their* world:

  • You upgrade your phone every year. Same apps, newer phone.
  • You upgrade Office 365. Same Word, better features.
  • You upgrade your car lease. Same driving, newer model.

So why can’t you just upgrade Python 2.7 to Python 3.12 and call it a day?

Because, dear business person, software isn’t a phone. It’s more like a house built by 47 different contractors over 15 years, where every contractor used a different blueprint, half the load-bearing walls are actually cardboard, and somewhere in the basement there’s a celery task from 2009 that nobody understands but if you touch it the entire order processing system explodes.

The Upgrade Fantasy vs. Reality

What they think happens:

# Just change this line in requirements.txt 
Django==1.8.0. →. Django==5.0.0 
 
# Run this 
pip install  - upgrade -r requirements.txt 
git push 
 
# 🎉 Done! We're modern now!

What actually happens:

# Day 1: Dependencies conflict 
ERROR: django-haystack 2.4.0 requires Django<1.9, but you have Django 5.0.0 
 
# Day 3: Half the tests fail 
AttributeError: module 'django.conf.urls' has no attribute 'url' 
 
# Day 7: The import system changed 
ImportError: cannot import name 'smart_text' from 'django.utils.encoding' 
 
# Day 14: We discover critical features rely on Django 1.8 bugs 
# Turns out our authentication system depends on a quirk in how  
# Django 1.8 handled session cookies that was "fixed" in 1.11 
 
# Day 21: Everything compiles but nothing works 
print "hello" # This was valid Python 2 
SyntaxError: invalid syntax 
 
# Day 30: We've rebuilt the authentication system from scratch 
 
# Day 60: We admit this is a rewrite 
 
# Day 90: Business asks "why is this taking so long?"

The Real Blind Spot

The blind spot isn’t that business people don’t understand technology. It’s that they don’t see the difference between incremental improvement and architectural change.

They see the product. We see the 200,000 lines of code held together by hope, `try/except: pass`, and one senior engineer who’s threatening to quit.

They see features. We see this monstrosity:

# Actual code I've seen in production 
def process_order(order_id): 
    try: 
        order = Order.objects.get(id=order_id) 
    except: 
        pass 
     
    # 500 more lines of code that assume order exists 
    return order.total.  
     
    # Narrator: order was None 
    # They see “it works fine.”  
    # We see the connection pool that maxes out every Tuesday at 3 PM  
    #   because someone in 2012 thought synchronous requests to the  
    #   payment gateway were “good enough.”

Why Rewrites Are Scary

I get it. Rewrites are terrifying from a business perspective:

  • They take forever
  • They deliver no new features
  • They’re expensive
  • They often fail spectacularly
  • Netscape did a rewrite and died (this will be brought up, guaranteed)

But you know what’s more terrifying? Running your entire business on infrastructure where this is actual production code:

# settings.py 
SECRET_KEY = 'abc123' # TODO: Change this before production 
DEBUG = True 
ALLOWED_HOSTS = ['*'] 
 
# This has been in production for 8 years

The Python 2 to 3 Parable

Let me tell you about the “simple upgrade” that broke the internet.

Python 3 was released in 2008. Python 2 was officially EOL’d in 2020. That’s 12 years for the entire ecosystem to migrate. And you know what? Tons of companies still haven’t done it.

Why? Because it wasn’t an upgrade. It was a rewrite disguised as an upgrade.

# Python 2 - worked fine for 10 years 
print "Hello, world!" 
data = raw_input("Enter something: ") 
result = 5 / 2. # Returns 2 
unicode_text = u"Hello" 
 
# Python 3 - "just upgrade" 
print("Hello, world!") # Parentheses now required 
data = input("Enter something: ") # raw_input is gone 
result = 5 / 2. # Now returns 2.5, use // for integer division 
unicode_text = "Hello" # All strings are Unicode now

Multiply these changes across 200,000 lines of code, 50 dependencies, and 15 engineers who have left the company, and you start to understand why “just upgrading” is a fantasy.

The Compromise Nobody Wants to Hear

Here’s what actually works:

Option 1: The Strangler Pattern

# Route requests to new system gradually 
def handle_request(request): 
    if feature_flag.is_enabled('new_system', request.user): 
        return new_system.handle(request) 
    else: 
        return legacy_system.handle(request)

This takes 2–3x longer than you think and requires discipline nobody has.

Option 2: The Big Bang Rewrite

Freeze features, rewrite everything, pray to the deployment gods. This fails 60% of the time but when it works, it really works.

Option 3: The “We’ll Just Keep Patching It” Approach

# production_code.py  
# Last modified: 2024 
# Don't touch this file 
# Seriously 
# I mean it 
# The guy who wrote this quit 5 years ago 
# We've tried to refactor it twice 
# Both times the site went down 
# Just leave it alone

This is what most companies do. They keep patching until one day the whole thing collapses or they get acquired and it becomes someone else’s problem.

Option 4: The Honest Conversation

“This will take 18 months and cost €2M. Or we can keep limping along for another year and then it’ll take 24 months and cost €4M. Your call.”

What I Wish Business People Understood

Your legacy stack isn’t a car that needs an oil change. It’s not even “broken” — it’s just “pivoting” between states of working and catastrophic failure at 3 AM on a Saturday when you’re three beers deep at a wedding.

When we say “we need to modernize,” we’re not being dramatic. We’re not trying to play with shiny new FastAPI toys instead of boring reliable Flask. We’re telling you that your foundation is crumbling and one day soon you’re going to ask us to add real-time WebSocket support and we’re going to have to tell you that we can’t because the first floor is made of `pickle`, global state, and Django views that are 3000 lines long.

The Punchline

The real joke? By the time business finally approves the rewrite, the stack you wanted to upgrade to is already becoming legacy.

That “modern” FastAPI microservices architecture you greenlit in 2023? In 2026 some engineer will be explaining to you why it needs to be “upgraded” to whatever serverless edge runtime framework comes next.

# 2015: "We need to upgrade to Python 3!" 
# 2020: "We need to migrate to async/await!" 
# 2025: "We need to move to serverless!" 
# 2030: "We need to rewrite in Rust!" 
# 2035: "We need to upgrade to QuantumScript!"

It’s legacy code all the way down.