(And How to Tell If You’re Just Feeling Lucky, Punk)


My last article about microservices cargo-culting was writen based on a personal story, but apparently it hit a nerve. And gave a nice pass to more than a few people, using it as ammunition to defend their decade-old Python 2.7 monoliths.

“See?” they said, “even this guy admits microservices are overrated.”

Hold up.

That’s not what I said. Let me clarify.

The Missing Context

My last article wasn’t anti-microservices. It was anti-cargo-cult.

That piece was inspired by an architecture meeting where someone proposed splitting a perfectly functional remote patient monitoring application — maybe 50,000 lines of clean, well-tested code — into a constellation of microservices, each with its own database, connected by event buses and message queues. When I asked what problem we were solving, the response was: “It’s the modern way to build systems.”

That’s not engineering. That’s architecture theater.

But here’s the nuance everyone missed: sometimes the monolith is the problem. Not theoretically. Actually.

There’s a world of difference between:

Architecture Theater: Splitting a healthy system because “that’s what modern companies do”

Strategic Extraction: Breaking up a system because the monolith itself has become an unsustainable liability

Today, we’re talking about the second case. The one where keeping the monolith together isn’t engineering pragmatism — it’s organizational cowardice wrapped in false prudence.

The Real Horror Story

Picture this scenario:

Python 2.7. Django 1.8. Started in 2011 as a simple document management system. By 2025, it had metastasized into something that would make Lovecraft weep:

  • 487,000 lines of Python
  • 156 database tables
  • 2,847 API endpoints
  • Zero. Test. Coverage.
  • Running on a version of Python that had been EOL for two years

The original architects? If they das self awareenss, they should hide in witness protection.

The remaining team? Stockholm syndrome at its finest. “Sure, it’s messy, but it works.”

It worked the way a Jenga tower works after six tequila shots. Technically upright. Practically, one sneeze away from catastrophic collapse.

Every. Single. Deploy. Was. Terror.

The Framework Death Spiral

Here’s what nobody wants to admit: framework end-of-life isn’t just an annoyance. It’s a ticking time bomb with a sense of humor.

When Python 2.7 hit EOL in January 2020, teams running it faced an existential choice:

Option 1: Upgrade to Python 3

Sounds simple, right? Just bump the version number. Ship it.

Plot twist: Django 1.8 doesn’t support Python 3.8+. So you need Django 2.2 minimum. But Django 2.2 has breaking changes in URL routing, middleware, and the ORM. Which means you’re not just upgrading Python. You’re rewriting chunks of your application.

Oh, and 15 of your dependencies don’t support Python 3 yet. Or they do, but not the versions you’re using. Time to update those too. Which breaks other things. Which requires updating more things. Which breaks different things.

Congratulations, you’re now playing dependency whack-a-mole. The prize? More whack-a-mole.

Option 2: Stay on Python 2.7

And accept that:

  • Security vulnerabilities will never be patched
  • Cloud providers are dropping support (AWS is literally laughing at you)
  • You can’t hire anyone (show me the engineer excited to work in Python 2.7 in 2024)
  • Your infrastructure team has a voodoo doll with your face on it
  • Every security audit reads like a horror movie script

Option 3: Extract and rewrite

We’ll come back to this. Spoiler: it’s the least terrible option.

The Big Ball of Mud

Framework EOL is bad enough. But the real nightmare is when your monolith has evolved into what architects politely call “organically grown” and engineers honestly call “a crime scene.”

Let me show you the evolution of horror:

Where It Started (2011: The Age of Innocence)

# 2011: Simple document upload service 
class DocumentView(View): 
    def post(self, request): 
        document = Document.objects.create( 
            file=request.FILES['file'], 
            user=request.user 
        ) 
        return JsonResponse({'id': document.id})

Clean. Simple. Single responsibility. A thing of beauty.

Look at it. Really look at it. Remember what hope feels like.

Where It Ended (2025: Abandon All Hope)

# 2025: The same view 
# (Narrator: It was not the same view) 
class DocumentView(View): 
    def post(self, request): 
        # Validate user permissions (3 different auth systems because reasons) 
        if not self.check_legacy_permissions(request.user): 
            if not self.check_rbac_permissions(request.user): 
                if not self.check_new_permissions(request.user): 
                    return JsonResponse({'error': 'unauthorized'}, status=403) 
         
        # Handle document upload 
        document = Document.objects.create( 
            file=request.FILES['file'], 
            user=request.user, 
            organization=request.user.organization 
        ) 
         
        # Update user statistics (why is this here? nobody knows) 
        request.user.document_count += 1 
        request.user.save() 
         
        # Trigger billing calculation (DEAR GOD WHY) 
        if request.user.organization.billing_plan == 'premium': 
            calculate_storage_fees(request.user.organization) 
         
        # Send notification (to 4 different systems because we couldn't decide) 
        send_email_notification(request.user) 
        send_websocket_notification(request.user) 
        send_mobile_push(request.user) 
        post_to_activity_feed(document) 
         
        # Update search index (at least this makes sense) 
        index_document(document) 
         
        # Log analytics event (to the analytics system we'll replace next quarter) 
        track_event('document_uploaded', user=request.user) 
         
        # Check storage quota (AFTER we already saved the document) 
        if get_storage_used(request.user) > get_storage_limit(request.user): 
            # Wait, we already saved it. Now what? 
            # TODO: Fix this 
            # TODO from 2015: Still not fixed 
            pass 
         
        return JsonResponse({'id': document.id})

This is not an exaggeration. This is lighter than reality.

One view touching: authentication, authorization, document storage, user statistics, billing, notifications, search indexing, analytics, and quota management.

SOLID principles? Never heard of her.

Single Responsibility Principle? Sounds fake.

The Cascading Nightmare

Want to change how notifications work? Cool, better understand:

  • The document upload flow
  • The user statistics system
  • The billing logic (because it triggers notifications)
  • The activity feed (because notifications affect it)
  • The mobile app (because push notifications)
  • Quantum mechanics (it might help at this point)

Want to fix a billing bug? Hope you don’t break document uploads.

Want to upgrade Django? May God have mercy on your soul.

The Deployment Terror

In a healthy system, deployments are boring. You push code. Tests pass. It deploys. You get coffee. Life is good.

In this system, deployments were orchestrated events requiring:

  • A 34-page runbook (yes, really)
  • A 2-hour maintenance window
  • Three engineers on standby
  • A rollback plan
  • A stress ball
  • A prayer to any deity who’ll listen
  • Your manager’s blessing
  • Possibly a small sacrifice

Every deployment was a full-system rollback risk. You couldn’t deploy a fix to the billing service without potentially breaking document uploads. Because they’re not separate services. They’re tangled together like headphone cables in your pocket.

The deploy schedule? Tuesdays at 2 AM. Production traffic was lowest then.

The team’s response to bugs in production? “Let’s wait until next Tuesday.”

Feature velocity? What’s that? Is it edible?

The Talent Trap

The hiring conversation:

Recruiter: “We’re looking for senior Python engineers.”

Candidate: “Great! What stack?”

Recruiter: “Python 2.7, Django 1.8.”

Candidate: “…is this a prank?”

Recruiter: “I wish I was pranking you.”

Candidate: has already hung up

Can’t hire. Existing team openly job hunting. Knowledge locked in two engineers’ heads. They’re burning out faster than a Galaxy Note 7.

“We should just rewrite it in something modern,” they said. “Python 3. FastAPI. Microservices. Something engineers actually want to work on. Something that won’t make us cry.”

And you know what? They were absolutely right.

The Irony of Context

Here’s the beautiful irony: that remote patient monitoring system I mentioned earlier — the clean, well-architected one that someone wanted to needlessly explode into microservices? It was running Python 3.12, FastAPI, had 94% test coverage, clear domain boundaries, and deployed multiple times per day without incident.

That system didn’t need microservices. It needed to be left the hell alone.

The 487K-line Django 1.8 nightmare? That one desperately needed to be broken up. Not because microservices are inherently better, but because the monolith had become genuinely unsustainable. Like, “this building is condemned” unsustainable.

Same architectural pattern (microservices). Completely opposite appropriateness.

That’s the difference between cargo-cult architecture and strategic thinking. Context matters. Who knew?

The Critical Question

So when do you actually need to kill the monolith?

Not because it’s trendy. Not because some consultant with a Medium blog said so. But because the monolith itself has become the problem.

Here’s the honest assessment:

❌ Wrong Reasons to Split

  • “Microservices are best practice”
  • “We need to scale” (without proving current bottlenecks)
  • “It’ll make us look modern”
  • “Google does it this way” (you are not Google)
  • Resume-driven development
  • “That 50K-line well-tested app should definitely be 10 services with Kafka”
  • Your manager read a blog post on the plane

✅ Right Reasons to Split

The monolith is legitimately unsustainable:

1. Framework Death Spiral

  • Running EOL versions with no security patches
  • Upgrade path is structurally impossible without rewrite
  • Can’t hire because nobody knows the ancient stack (or wants to)
  • Cloud providers are actively deprecating support
  • Your security team sends you passive-aggressive Slack messages

2. The Big Ball of Mud

  • No clear boundaries between business domains
  • Circular dependencies everywhere (it’s circles all the way down)
  • God classes touching everything
  • Zero test coverage because it’s untestable by design
  • To change X, you must understand Y, Z, and the cosmic background radiation
  • The documentation is a Post-it note that says “good luck”

3. Deployment Paralysis

  • Can’t deploy safely
  • Every change risks full-system failure
  • Deploy windows measured in hours, anxiety measured in years
  • Feature velocity approaching zero
  • Team spends more time coordinating deploys than shipping features
  • You’re considering a return to waterfall (dark times)

4. Organizational Decay

  • Team threatening to quit
  • Can’t hire replacements
  • Knowledge concentrated in 1–2 people
  • Those people have “update resume” scheduled in their calendars
  • Technical debt has become organizational debt
  • Your therapist knows about this codebase

The Three Options (Choose Wisely)

When you’re staring at a legacy nightmare, you have three choices:

Option 1: In-Place Modernization

Upgrade framework versions. Refactor while maintaining structure. Add tests gradually. Keep the monolith, make it better.

Timeline: 12–18 months minimum (18–24 months in reality)

Risk: High. Every change can break something unpredictable. Probably will.

Cost: Team burnout. Slow feature velocity. Hidden complexity bombs. Your sanity.

This works when:

  • Codebase has decent structure already
  • Good test coverage exists (or can be added without time travel)
  • Framework upgrade path is clear and documented
  • You have deep expertise in both old and new versions
  • Business can tolerate 18 months of minimal new features
  • You believe in miracles

This fails when:

  • Code is fundamentally coupled (spoiler: it is)
  • No tests and can’t add them (because untestable)
  • Framework changes are breaking and extensive
  • Team lacks expertise in the ancient stack
  • Nobody remembers why that function exists but removing it breaks everything

Option 2: Strategic Extraction

Identify bounded contexts. Extract and rewrite one service at a time. Strangle the monolith gradually. Watch it die slowly. Feel nothing.

Timeline: 18–24 months for complete migration

Risk: Medium. Operational complexity, but failures are isolated.

Cost: Running parallel systems. Operational overhead. Learning curve. Coffee budget triples.

This works when:

  • Clear business domains exist (auth, billing, documents, etc.)
  • Can run parallel systems temporarily
  • Team can handle distributed systems (or learn quickly)
  • Business can tolerate gradual migration
  • Different components genuinely have different scaling needs
  • You have patience and strong liquor

This fails when:

  • Domains are hopelessly entangled (it’s all one domain called “chaos”)
  • Team lacks distributed systems knowledge
  • Business demands immediate feature velocity
  • Operational capacity is already maxed
  • Debugging across service boundaries makes you weep

Option 3: Do Nothing

Keep running EOL versions. Hope nothing breaks. Pray you don’t get hacked. Watch your best engineers leave. Update your LinkedIn profile.

Timeline: Unknown (until catastrophic failure)

Risk: Yes.

Cost: Security breaches. Data loss. Talent exodus. Technical bankruptcy. That thing that keeps you up at 3 AM.

This works when: You’re feeling lucky, punk.

The Honest Calculus

Let’s do actual math. Real scenario (hypothetically real):

System: 487K lines Python 2.7, Django 1.8, zero tests, coupled everything, held together by prayers and duct tape

In-Place Upgrade Cost:

Month 1-6:   Framework upgrade (Python 3.9, Django 3.2) 
             Cry daily 
              
Month 7-10:  Fix breaking changes across entire codebase 
             Cry more 
              
Month 11-14: Add test coverage to critical paths 
             Discover critical paths you didn't know existed 
              
Month 15+:   Debug production issues from changes 
             Fix bugs introduced by framework differences 
             Refactor worst coupled code 
             Question life choices 
             Update resume

Total: 15+ months minimum (22 months in reality)
Team capacity: 100% consumed by migration
New features shipped: Near zero
Team morale: Also near zero
Risk of catastrophic failure: High throughout
Chance of success: Unclear, hazy, consult your local fortune teller

Strategic Extraction Cost:

Month 1-2:   Identify bounded contexts 
             Map dependencies 
             Draw lots of diagrams 
             Design service boundaries

Month 3-6:   Extract Service 1: Authentication
            New FastAPI service
            Full test coverage
            Parallel run with gradual traffic shift
            Sleep slightly betterMonth 7-10:  Extract Service 2: Billing
            Modern stack
            Actually tested
            Independent deployment
            Team morale improvingMonth 11-14: Extract Service 3: Document Management
            Core business logic
            Clean implementation
            People stop cryingMonth 15-18: Extract remaining services
            Data migration
            Decommission monolith
            Celebrate
            Delete the old code
            Celebrate againTotal: 18 months (actually 18-20 months)
Team capacity: 60% migration, 40% new features (in new services)
New features: Shipping in modern stack starting Month 6
Risk: Isolated to individual services
Team morale: Recovering
Career prospects: Significantly improved

The Critical Difference:

With in-place upgrade, you’re stuck in the old system for 15+ months. Every bug. Every weird coupling. Every terrible decision from 2011 that made sense “at the time.” You’re carrying it all forward like Sisyphus but with worse code.

With strategic extraction, by Month 6 you’re shipping new features in the modern stack. By Month 12, half your system is clean, tested, and maintainable. By Month 18, the nightmare is over. You can sleep again. You remember what joy feels like.

When Extraction Is The Right Call

Here’s the decision matrix (use it wisely):

+--+-------------------------------------------------+---------------------------+----------------------+--+ 
|  |                     Factor                      |     In-Place Upgrade      | Strategic Extraction    | 
+--+-------------------------------------------------+---------------------------+----------------------+--+ 
|  | Framework is EOL + no viable upgrade path       | ❌ Fails                   | ✅ Works               |  
|  | Codebase violates SOLID principles structurally | ❌ Fails                   | ✅ Works               |  
|  | Clear domain boundaries exist                   | ✅ Either works            | ✅ Preferred           |  
|  | Zero test coverage, untestable code             | ❌ Extremely risky         | ✅ Safer               |  
|  | Team has distributed systems experience         | ⚠️ Not needed             |  ✅ Required            |   
|  | Can't hire for legacy tech                      | ❌ Stuck                   | ✅ Solves problem      |  
|  | Business needs feature velocity                 | ✅ Preferred (if possible) | ⚠️ Slower initially    |  
+--+-------------------------------------------------+---------------------------+----------------------+--+

Extract and rewrite when:

1. The Framework Is Actually Dead

  • Not “old” — DEAD dead
  • No security patches (ever)
  • No hiring pool (nobody under 40 knows it)
  • Infrastructure providers dropping support
  • No community support
  • Stack Overflow questions go unanswered
  • You’re the only one left

2. The Coupling Is Structural

  • Not just bad code — fundamentally, architecturally entangled
  • Database schema is a maze designed by a sadist
  • Shared mutable state everywhere
  • “It works because of this weird side effect nobody understands”
  • Cannot add tests because untestable by design
  • The last person who understood it left in 2016

3. Clear Bounded Contexts Exist

  • Can draw lines around business domains
  • Authentication is separable from documents
  • Billing is separable from storage
  • Notifications are separable from core logic
  • Conway’s Law suggests this structure anyway
  • Even your PM can see the boundaries

4. Team Has (or Can Build) Capacity

  • Can handle running two systems temporarily
  • Has or can learn distributed systems patterns
  • Won’t quit from operational burden (immediately)
  • Management supports the investment
  • Coffee budget is adequate

How to Extract Without Disaster

If you’re going to do this, do it right:

Rule 1: Start Small and Low-Risk

Don’t extract the hairiest, most critical service first. That’s not strategy. That’s ego. And possibly masochism.

Start with:

  • High value to the team (morale boost when it works)
  • Low coupling to other systems (minimal coordination)
  • Well-understood domain (clear requirements)
  • Non-critical path (failure is survivable, not career-ending)

Authentication is often a good first service. Clear boundaries. Well-understood. If it fails, you can fall back to the monolith. Nobody dies. You keep your job.

Rule 2: Strangler Pattern Is Non-Negotiable

Never big-bang cutover. Ever. Not even once. Not even if you’re “pretty sure it’ll work.”

Phase 1: Build new service (monolith still serving traffic) 
Phase 2: Run both in parallel (verify correctness obsessively) 
Phase 3: Route 1% of traffic to new service (hold your breath) 
Phase 4: Gradually increase (5%, 10%, 25%, 50%, 100%) 
         (breathe normally again) 
Phase 5: Keep monolith code for 30 days (safety net) 
Phase 6: Delete monolith code (ceremonially, with joy)

At any point, you can roll back. The monolith is still there. Still working (barely). Still terrible. But there. You’re not all-in until you’re certain.

Rule 3: Port First, Improve Later

The new service doesn’t need to be perfect. It doesn’t need to showcase your knowledge of design patterns. It needs to be functionally equivalent. That’s it.

First: Port the logic faithfully

# Don't try to fix everything at once 
# Just make it work in the new service 
class AuthService: 
    def authenticate(self, username, password): 
        # Yes, this logic is weird 
        # Yes, we'll fix it later 
        # For now, match monolith behavior EXACTLY 
        # Even the weird parts 
        # Especially the weird parts 
        return legacy_auth_logic(username, password)

Then: Prove it’s stable in production

Finally: Improve it incrementally

# NOW we can refactor safely 
class AuthService: 
    def authenticate(self, username, password): 
        # Modern, clean implementation 
        # With tests 
        # No weird legacy behavior 
        # Sleep soundly at night 
        return new_clean_auth_logic(username, password)

The goal is to reduce migration risk, not fix all technical debt simultaneously. You’re not a superhero. You’re an engineer with finite time and a desire to see your family again.

Rule 4: Measure Everything

Before extraction:

Error rate: 0.1% 
p99 latency: 250ms 
Deployment frequency: Weekly 
Mean time to recovery: 4 hours 
Team velocity: 15 story points/sprint 
Coffee consumption: High 
Stress levels: Higher

After extraction:

Error rate: Still 0.1%? Better? WORSE?! 
p99 latency: Better or worse? MEASURE IT 
Deployment frequency: Still weekly or improved? 
MTTR: Faster or slower? 
Team velocity: Maintained or collapsed? 
Coffee consumption: Hopefully lower 
Stress levels: Hopefully much lower

If you can’t measure impact, you can’t justify the migration. And you definitely can’t justify the next extraction.

The Part Everyone Ignores

Here’s what nobody wants to admit:

Sometimes the monolith problem is actually an organizational problem.

If your “monolith problem” is actually:

  • No code reviews for 10 years
  • No engineering standards or guidelines
  • Everybody cowboy coding their own approach
  • Zero documentation
  • No architectural oversight
  • Hero culture (one person knows how everything works)
  • That person is you and you’re tired

Then splitting into microservices just gives you distributed spaghetti instead of monolithic spaghetti.

You haven’t fixed the problem. You’ve just made it exponentially harder to debug. Congratulations, you played yourself.

Before extracting services, ask:

  • Do we have engineering standards?
  • Do we do code reviews?
  • Do we write tests?
  • Do we document architectural decisions?
  • Do we have on-call rotation and runbooks?
  • Can someone other than Dave deploy?

If the answer is no, fix that first. Or at least in parallel.

Otherwise you’re building 10 terrible services instead of one terrible monolith. That’s not progress. That’s just distributed chaos.

The Real Decision

My last article wasn’t anti-microservices. It was anti-thoughtless-architecture.

This article isn’t pro-microservices. It’s pro-honest-assessment.

The real question isn’t “monolith or microservices?”

The real question is: “What problem am I actually solving?”

If your monolith:

  • Is on a modern, supported framework
  • Has decent structure and test coverage
  • Deploys safely and frequently
  • Allows teams to ship features
  • Isn’t driving engineers away
  • Doesn’t appear in your nightmares

Keep it. Don’t fix what isn’t broken. That remote patient monitoring system? Perfect candidate to leave as-is. Let it live its best life.

If your monolith:

  • Runs on EOL software with no upgrade path
  • Is structurally coupled in ways that can’t be refactored
  • Makes deployments terrifying
  • Prevents feature velocity
  • Is driving your best engineers away
  • Has become an organizational liability
  • Features prominently in your therapy sessions

Strategic extraction might be your least-bad option.

Not because microservices are superior architecture.

Because you’re choosing controlled demolition over waiting for the condemned building to collapse on your head.

That’s a valid reason.

Maybe the only valid reason.

The Bottom Line

Sometimes the monolith is fine. Sometimes it’s genuinely the problem.

The difference is whether you’re running TOWARD a better architecture or running AWAY from fixing your actual issues.

If your monolith is on Python 2.7, has 487K lines of untested spaghetti code, violates every SOLID principle known to computer science, and nobody under 40 knows how it works…

Then yeah, extract and rewrite might be your best path forward.

But be honest about why.

You’re not choosing microservices because they’re inherently better. You’re choosing extraction because the monolith has become genuinely unsustainable. Because the building is condemned. Because staying is riskier than leaving.

You’re not being trendy. You’re being strategic.

And strategic architecture — architecture driven by actual constraints rather than fashionable patterns — is always the right answer.

Even when the answer is “controlled demolition.”


The choice is yours:

Upgrade the monolith. Extract services. Or do nothing and hope.

Just don’t pretend you’re making an architectural decision when you’re really just feeling lucky.

Punk.


What’s your take? Have you successfully migrated away from a legacy monolith? Or watched an extraction project become a distributed disaster? Drop your war stories in the comments. Misery loves company.