Module 5: Framework and Language Migration

Strangler Fig Pattern — Old/New Coexistence

Strangler Fig Pattern — Old/New Coexistence

Capsule description

In the previous capsules you migrated endpoints from Flask to FastAPI sequentially. But in a real project with production traffic, you can't pause the service while you migrate. You need old and new to coexist: some endpoints serve from Flask, others from FastAPI, and the traffic moves gradually until Flask can be removed.

This pattern is called Strangler Fig, inspired by tropical plants that grow around an existing tree until they eventually replace it. Your FastAPI app grows around the Flask app until Flask is no longer needed.

In this capsule you're going to implement real coexistence: a proxy that routes requests to Flask or FastAPI depending on the state of the migration. It's the safest pattern for production migrations.


The Concept

How Strangler Fig works

Phase 1: Everything goes to Flask
┌─────────┐     ┌──────────┐
│  Proxy   │────▶│  Flask   │  (100% Flask)
└─────────┘     └──────────┘

Phase 2: Some endpoints migrated
┌─────────┐     ┌──────────┐
│  Proxy   │──┬─▶│  Flask   │  (60% Flask)
└─────────┘  │  └──────────┘
              │  ┌──────────┐
              └─▶│ FastAPI  │  (40% FastAPI)
                 └──────────┘

Phase 3: Complete migration
                 ┌──────────┐
                 │ FastAPI  │  (100% FastAPI)
                 └──────────┘

Implementation with a simple proxy

# proxy.py — Migration router
from fastapi import FastAPI, Request
import httpx

app = FastAPI(title="Migration Proxy")

# Configuration: which endpoints go to which backend
FASTAPI_ROUTES = {
    "GET /health",
    "GET /users",
    "GET /users/{id}",
    # Add endpoints as they're migrated
}

FLASK_URL = "http://localhost:5000"
FASTAPI_URL = "http://localhost:8000"

@app.api_route("/{path:path}", methods=["GET", "POST", "PUT", "DELETE"])
async def proxy(request: Request, path: str):
    route_key = f"{request.method} /{path}"
    
    # Determine backend
    target = FASTAPI_URL if route_key in FASTAPI_ROUTES else FLASK_URL
    
    # Forward request
    async with httpx.AsyncClient() as client:
        response = await client.request(
            method=request.method,
            url=f"{target}/{path}",
            headers=dict(request.headers),
            content=await request.body(),
        )
    
    return Response(
        content=response.content,
        status_code=response.status_code,
        headers=dict(response.headers),
    )

Step-by-Step Implementation

Step 1: Coexistence setup

# Terminal 1: Flask (port 5000)
flask run --port 5000

# Terminal 2: FastAPI (port 8000)
uvicorn fastapi_app:app --port 8000

# Terminal 3: Proxy (port 3000 — the one clients see)
uvicorn proxy:app --port 3000

Step 2: Migrate an endpoint and register it

# After migrating GET /health to FastAPI:
FASTAPI_ROUTES = {
    "GET /health",  # ← Add here
}
# The proxy now sends GET /health to FastAPI
# Everything else keeps going to Flask

Step 3: Verify and move to the next

# Equivalence test via the proxy:
> "Send GET /health to the proxy (port 3000) and verify
   that the response comes from FastAPI"

# If it works, migrate the next endpoint
# If it fails, revert: remove it from FASTAPI_ROUTES

Simplified Approach (No Proxy)

For internal or smaller-scale projects, you can use a simpler approach: mount both apps in the same process.

# combined_app.py — Flask and FastAPI together
from fastapi import FastAPI
from flask import Flask
from a2wsgi import WSGIMiddleware

# FastAPI as the main app
fastapi_app = FastAPI()

# Flask as a fallback for non-migrated endpoints
flask_app = Flask(__name__)

# Mount Flask inside FastAPI
fastapi_app.mount("/legacy", WSGIMiddleware(flask_app))

# Migrated endpoints go directly in FastAPI
@fastapi_app.get("/health")
def health():
    return {"status": "healthy", "framework": "fastapi"}

# Non-migrated endpoints stay in Flask (under /legacy)
@flask_app.route("/orders", methods=["POST"])
def create_order():
    # ... original Flask logic
    pass

Cutover Strategies

When to remove Flask

Checklist for the final cutover:

  • 100% of endpoints migrated to FastAPI
  • 100% of equivalence tests pass
  • The proxy sends 0 requests to Flask (monitoring)
  • There's no code that depends on Flask-specific features
  • Team alignment: everyone knows Flask is being removed

The cutover

# Step 1: Verify that all the traffic goes to FastAPI
> "Verify that FASTAPI_ROUTES contains all the endpoints
   and that no endpoint is left served by Flask."

# Step 2: Remove Flask
> "Remove flask_app.py, the proxy, and the Flask
   dependencies from requirements.txt. FastAPI is the only app."

# Step 3: Final tests
> "Run the whole test suite. Confirm green."

Connection with the Project

In the Module Project (capsule 06), you implement strangler fig if the project has endpoints you can't migrate all at once. For the scale of the project (4-6 endpoints), it may be optional — but for your professional work it's essential.


Troubleshooting

Problem 1: The proxy adds latency

Solution: Normal. In development it's acceptable. In production, use a reverse proxy like nginx or an API gateway.

Problem 2: Headers get lost in the proxy

Solution: Forward all the headers:

headers=dict(request.headers)

Problem 3: I don't know when to do the cutover

Solution: When monitoring shows 0 requests to Flask for 24+ hours and all the tests pass.


Exercises

Exercise 1: Design a strangler fig plan (Easy)

You have 8 endpoints. Design the migration order and in which phase you add each one to FASTAPI_ROUTES.

See solution
Phase 1: GET /health → FASTAPI_ROUTES
Phase 2: GET /users, GET /products → FASTAPI_ROUTES
Phase 3: POST /users, PUT /users → FASTAPI_ROUTES
Phase 4: POST /orders (most complex) → FASTAPI_ROUTES
Phase 5: DELETE /users, DELETE /orders → FASTAPI_ROUTES
Phase 6: Cutover → remove Flask

Exercise 2: Implement conditional routing (Medium)

Modify the proxy to use feature flags instead of a static list. If MIGRATE_USERS=true, the users endpoints go to FastAPI.

See solution
import os

FEATURE_FLAGS = {
    "users": os.getenv("MIGRATE_USERS", "false") == "true",
    "orders": os.getenv("MIGRATE_ORDERS", "false") == "true",
}

def get_backend(path: str) -> str:
    if path.startswith("/users") and FEATURE_FLAGS["users"]:
        return FASTAPI_URL
    if path.startswith("/orders") and FEATURE_FLAGS["orders"]:
        return FASTAPI_URL
    return FLASK_URL

Feature flags let you enable/disable the migration without a deploy.


Common Errors in Strangler Fig

Error 1: Migrating the most complex endpoint first

Symptom: The first migrated endpoint blocks the migration for weeks because it has tangled dependencies.

Why it happens: The instinct is to "attack the big problem first". In migrations, the optimal order is the simplest first — /health, /version, endpoints without dependencies. This validates the proxy setup and builds confidence before touching business logic.

How to fix: List all the endpoints. Order by complexity (no dependencies → with DB → with external services → with state). Migrate in that order.

Error 2: Not monitoring which backend serves each request

Symptom: You think you migrated /users but the proxy keeps sending 30% of the traffic to Flask because of a misconfigured rule.

Why it happens: Without metrics, FASTAPI_ROUTES can have a bug and you never find out until the cutover. And at the cutover it's already too late.

How to fix: Add logging to the proxy with the backend used per request. Then dashboard it: requests_to_flask and requests_to_fastapi per endpoint. Before the cutover, the counts to Flask must be zero for days, not minutes.

Error 3: Using the proxy as a permanent solution

Symptom: The migration "finished" 3 months ago but the proxy keeps running "just in case". Extra latency and operational debt.

Why it happens: The cutover requires a decision and a bit of courage. Keeping the proxy feels "safe" but accumulates complexity.

How to fix: The cutover is part of the plan, not optional. Explicitly define the deadline ("two weeks with 0 requests to Flask = cutover") and stick to it. Capsule 05 gives you the equivalence criteria to make the decision with data.

Error 4: Sharing state between Flask and FastAPI without thinking

Symptom: Inconsistent sessions, race conditions, data that appears in one app but not the other.

Why it happens: If Flask and FastAPI share a DB but use different session managers, ORMs, or caches, they can have inconsistent views of the same data.

How to fix: Decide explicitly: they share a DB with the same config, they share a cache, or each has its own. Document that decision. Equivalence tests (capsule 05) must cover state-sharing.

Error 5: Not having a rollback plan

Symptom: You migrated /orders, something fails in production, and you don't know how to go back to Flask in 5 minutes.

Why it happens: The proxy makes the migration easy — but the rollback must be easy too. Without a process, in a crisis everyone improvises.

How to fix: Document the rollback as a one-line change: FASTAPI_ROUTES = FASTAPI_ROUTES - {"GET /orders"}. Practice the rollback before migrating the endpoint to production. If it takes more than 5 minutes, the plan is incomplete.


Summary

  • Strangler Fig lets old and new coexist during the migration
  • Proxy-based: a router decides which backend serves each request
  • Simplified: mount Flask inside FastAPI with WSGIMiddleware
  • Cutover when 100% of the traffic goes to the new app and all the tests pass
  • It's the safest pattern for production migrations
  • Order matters: the simplest first, the most complex last
  • Active monitoring + a rollback plan are non-negotiable

Next capsule: Migration Testing — tests that verify equivalence between Flask and FastAPI.


Additional Resources

  1. Strangler Fig Pattern - Martin Fowler - The original definition
  2. ASGI/WSGI Middleware - To mount WSGI apps in ASGI
  3. Feature Flags Best Practices - Feature flags for migrations
  4. nginx Reverse Proxy - For a proxy in production
  5. API Gateway Pattern - The most robust pattern for routing
  6. Blue-Green Deployments - Complement to the strangler fig

Module 5, Capsule 04 — Refactoring & Legacy Code with Claude Code Guide