# Migration & Modernization Guide Step-by-step patterns for migrating from monoliths to microservices, decomposing databases, and incrementally modernizing legacy systems. Use when planning major refactors, strangler fig migrations, or database decomposition strategies. ## Contents - [Strangler Fig Pattern](#strangler-fig-pattern) - [Database Decomposition](#database-decomposition) - [Feature Flags for Migration](#feature-flags-for-migration) - [Risk Assessment Framework](#risk-assessment-framework) - [Parallel Running and Shadow Traffic](#parallel-running-and-shadow-traffic) - [Migration Path: Monolith to Microservices](#migration-path-monolith-to-microservices) - [Anti-Patterns](#anti-patterns) - [Decision Framework](#decision-framework) - [Cross-References](#cross-references) --- ## Strangler Fig Pattern ### Core Concept Incrementally replace pieces of a legacy system by routing traffic to new implementations while the old system remains operational. Named after strangler fig trees that grow around a host tree. ```text Phase 1: Identify Phase 2: Intercept Phase 3: Replace ┌────────────────┐ ┌────────────────┐ ┌────────────────┐ │ Monolith │ │ Facade/Proxy │ │ Facade/Proxy │ │ ┌──────────┐ │ │ │ │ │ │ │ │ │ Feature A │ │ │ ┌───┴───┐ │ │ ┌───┴───┐ │ │ │ Feature B │ │ │ │ │ │ │ │ │ │ │ │ Feature C │ │ │ Old A New B │ │ New A New B │ │ └──────────┘ │ │ Old C │ │ New C │ └────────────────┘ └────────────────┘ └────────────────┘ ``` ### Step-by-Step Implementation **Step 1: Add a routing facade** ```typescript // API Gateway or reverse proxy routes requests // Start with 100% traffic to monolith // nginx.conf or API gateway config // location /api/orders { // proxy_pass http://monolith:3000; # All traffic to monolith initially // } ``` **Step 2: Identify extraction candidates** Rank modules by these criteria: | Criterion | Weight | Description | |-----------|--------|-------------| | Business value of independent deployment | High | Revenue impact, release frequency needs | | Coupling to other modules | High | Fewer dependencies = easier extraction | | Team ownership clarity | Medium | Clear owner = better extraction outcome | | Data isolation feasibility | High | Shared tables make extraction hard | | Change frequency | Medium | Frequently changed code benefits most | | Performance isolation needs | Medium | One module's load affecting another | **Step 3: Build the new service** ```typescript // New service implements the SAME interface as the monolith feature // This is critical — callers should not know about the migration // New orders-service (standalone) app.post('/api/orders', async (req, res) => { // New implementation with same API contract const order = await createOrder(req.body); res.json(order); }); ``` **Step 4: Gradual traffic shift** ```yaml # Canary routing: shift traffic incrementally # Week 1: 5% to new service # Week 2: 25% to new service # Week 3: 50% to new service # Week 4: 100% to new service (if metrics are green) # Example: Istio VirtualService apiVersion: networking.istio.io/v1beta1 kind: VirtualService metadata: name: orders spec: hosts: - orders.example.com http: - route: - destination: host: orders-new weight: 25 - destination: host: monolith weight: 75 ``` **Step 5: Decommission the old code** After 100% traffic runs on the new service with stable metrics for at least 2 weeks: - [ ] Remove old code path from monolith - [ ] Remove old database tables (after data migration verification) - [ ] Update documentation and runbooks - [ ] Remove feature flags related to migration --- ## Database Decomposition ### Shared Database Problem ```text [FAIL] Multiple services sharing one database: ┌─────────┐ ┌─────────┐ ┌─────────┐ │Service A│ │Service B│ │Service C│ └────┬────┘ └────┬────┘ └────┬────┘ │ │ │ └────────────┼────────────┘ │ ┌───────▼───────┐ │ Shared DB │ │ (everything) │ └───────────────┘ Problems: - Schema changes break multiple services - No independent deployment - Performance contention - Unclear data ownership ``` ### Decomposition Strategies **Strategy 1: Database View Layer** Intermediate step. Create views that simulate per-service schemas. ```sql -- Service A sees only its data through a view CREATE VIEW service_a_orders AS SELECT id, customer_id, total, status, created_at FROM orders WHERE department = 'retail'; -- Service B sees different columns CREATE VIEW service_b_orders AS SELECT id, warehouse_id, shipping_status, tracking_number FROM orders WHERE shipping_status IS NOT NULL; ``` **Strategy 2: Schema-Per-Service (Same Instance)** Each service gets its own schema within the same database instance. ```sql -- Separate schemas, same PostgreSQL instance CREATE SCHEMA orders_service; CREATE SCHEMA inventory_service; CREATE SCHEMA payments_service; -- Each service's migrations target its own schema -- Cross-schema access is explicitly forbidden (enforce via permissions) REVOKE ALL ON SCHEMA orders_service FROM inventory_user; ``` **Strategy 3: Database-Per-Service (Full Separation)** Each service has its own database instance. ```text ┌─────────┐ ┌─────────┐ ┌─────────┐ │Service A│ │Service B│ │Service C│ └────┬────┘ └────┬────┘ └────┬────┘ │ │ │ ┌────▼────┐ ┌────▼────┐ ┌────▼────┐ │ DB A │ │ DB B │ │ DB C │ └─────────┘ └─────────┘ └─────────┘ ``` ### Data Synchronization During Decomposition | Approach | Description | When | |----------|-------------|------| | Dual writes | Write to both old and new DB | Short transition periods only | | CDC (Change Data Capture) | Stream changes from old to new | Gradual migration, minimal code changes | | ETL batch sync | Periodic bulk sync | Non-real-time data, analytics | | API calls | New service fetches data via API | Loosely coupled, eventually consistent | **CDC Example with Debezium:** ```json { "name": "orders-connector", "config": { "connector.class": "io.debezium.connector.postgresql.PostgresConnector", "database.hostname": "monolith-db", "database.port": "5432", "database.dbname": "monolith", "table.include.list": "public.orders,public.order_items", "topic.prefix": "migration", "slot.name": "orders_migration" } } ``` ### Join Elimination Strategies When decomposing a shared database, you lose cross-table joins. Solutions: | Pattern | Use When | Trade-off | |---------|----------|-----------| | API composition | Low-frequency queries, few services | Latency from multiple API calls | | Data denormalization | Read-heavy, stale data OK | Storage duplication, sync complexity | | Event-driven sync | Real-time needs, owned data changes | Eventual consistency | | Materialized views in read DB | Complex reporting queries | Additional infrastructure | --- ## Feature Flags for Migration ### Migration-Specific Flag Patterns ```typescript // Flag: route traffic to new vs old implementation const flags = { 'orders-service-v2': { type: 'percentage', value: 25, // 25% of traffic to new service metadata: { migration: 'orders-extraction', startDate: '2026-01-15', targetCompletion: '2026-03-01', rollbackPlan: 'Set to 0%, revert proxy config', }, }, }; // Usage in routing layer async function handleOrderRequest(req: Request): Promise { if (featureFlags.isEnabled('orders-service-v2', { userId: req.userId })) { return newOrdersService.handle(req); } return monolith.handle(req); } ``` ### Flag Lifecycle for Migrations ```text Phase 1: Create flag (default: OFF) Phase 2: Enable for internal users (testing) Phase 3: Enable for 5% of traffic (canary) Phase 4: Ramp to 25%, 50%, 75%, 100% Phase 5: Remove flag + old code path (cleanup) IMPORTANT: Set a cleanup deadline. Stale flags are tech debt. ``` ### Comparison Read Pattern ```typescript // During migration: read from BOTH, compare results, return old async function getOrder(orderId: string): Promise { const oldResult = await monolith.getOrder(orderId); if (featureFlags.isEnabled('orders-comparison-reads')) { try { const newResult = await newService.getOrder(orderId); if (!deepEqual(oldResult, newResult)) { logger.warn('Migration mismatch', { orderId, diff: generateDiff(oldResult, newResult), }); metrics.increment('migration.comparison.mismatch'); } else { metrics.increment('migration.comparison.match'); } } catch (error) { metrics.increment('migration.comparison.error'); } } return oldResult; // Always return the trusted source during migration } ``` --- ## Risk Assessment Framework ### Migration Risk Matrix | Risk Factor | Low Risk | Medium Risk | High Risk | |-------------|----------|-------------|-----------| | Data coupling | No shared tables | Shared lookup tables | Shared mutable tables with joins | | Traffic volume | <100 req/s | 100–1000 req/s | >1000 req/s | | Consistency requirements | Eventual OK | Read-your-writes needed | Strong ACID required | | Team experience | Done migrations before | Some distributed systems experience | First microservice extraction | | Rollback complexity | Stateless, easy revert | Some state to reconcile | Data divergence makes rollback hard | | Business criticality | Internal tool | Customer-facing, non-revenue | Payment, checkout, auth | ### Risk Mitigation Checklist - [ ] **Runbook** written for rollback (with specific steps, not "revert changes") - [ ] **Monitoring** dashboards ready before migration starts - [ ] **Alerts** configured for error rate increase, latency spikes - [ ] **Data reconciliation** script ready to compare old vs new - [ ] **Communication plan** for stakeholders (downtime windows, expected behavior changes) - [ ] **Rollback tested** in staging environment - [ ] **Feature flag** wired for instant traffic revert - [ ] **Incremental rollout** plan with go/no-go criteria at each stage ### Go/No-Go Criteria Per Phase | Metric | Green (Go) | Yellow (Pause) | Red (Rollback) | |--------|------------|----------------|-----------------| | Error rate | <0.1% increase | 0.1–1% increase | >1% increase | | P95 latency | <10% increase | 10–50% increase | >50% increase | | Data mismatches | <0.01% | 0.01–0.1% | >0.1% | | Business metric (conversion, etc.) | No change | <2% drop | >2% drop | --- ## Parallel Running and Shadow Traffic ### Shadow Traffic Pattern Route a copy of production traffic to the new service without affecting users. ```text ┌────────┐ ┌──────────────┐ ┌────────────┐ │ Client │────▶│ Proxy/LB │────▶│ Monolith │──▶ Response to client └────────┘ └──────┬───────┘ └────────────┘ │ (async copy) └──────────────▶┌─────────────┐ │ New Service │──▶ Response discarded └─────────────┘ (logged + compared) ``` ### Implementation ```typescript // Shadow traffic middleware async function shadowTraffic(req: Request, res: Response, next: NextFunction) { // 1. Process the real request normally next(); // 2. Asynchronously send a copy to the new service (fire-and-forget) if (featureFlags.isEnabled('shadow-traffic-orders')) { setImmediate(async () => { try { const shadowStart = Date.now(); const shadowResponse = await newService.mirror(req); const shadowLatency = Date.now() - shadowStart; metrics.histogram('shadow.latency', shadowLatency); // Compare responses (logged, not returned to client) if (res.locals.responseBody) { const match = deepEqual(res.locals.responseBody, shadowResponse); metrics.increment(match ? 'shadow.match' : 'shadow.mismatch'); } } catch (error) { metrics.increment('shadow.error'); // Failures in shadow traffic NEVER affect the real response } }); } } ``` ### Parallel Running Checklist - [ ] Shadow traffic does NOT mutate production data - [ ] Shadow requests are clearly marked (header: `X-Shadow: true`) - [ ] New service has separate database/storage from production - [ ] Comparison results are aggregated into dashboards - [ ] Shadow traffic can be turned off instantly via feature flag - [ ] Load from shadow traffic is accounted for in capacity planning --- ## Migration Path: Monolith to Microservices ### Recommended Progression ```text Stage 1: Modular Monolith └─ Enforce module boundaries within the monolith └─ Define clear APIs between modules └─ Separate schemas per module (same DB) └─ Duration: 2-6 months Stage 2: Extract First Service └─ Pick the module with least coupling └─ Use strangler fig + shadow traffic └─ Establish service infrastructure (CI/CD, monitoring, service mesh) └─ Duration: 1-3 months Stage 3: Extract Core Services └─ Extract 2-4 more services in parallel (different teams) └─ Introduce async messaging for cross-service communication └─ Decompose database per service └─ Duration: 3-9 months Stage 4: Steady State └─ New features built as services by default └─ Remaining monolith handles only legacy features └─ Monolith shrinks over time (or stays as a module) ``` ### Case Study Pattern: E-Commerce Migration | Phase | Extract | Why First | Risk Level | |-------|---------|-----------|------------| | 1 | Notifications | No writes to core data, read-only | Low | | 2 | Search/Catalog | Heavy reads, independent index | Low-Medium | | 3 | Inventory | Clear bounded context, event-driven | Medium | | 4 | Orders | Core domain, complex state machine | High | | 5 | Payments | Regulatory, compliance needs isolation | High | | 6 | User/Auth | Shared dependency, extract last | Very High | **Key principle:** Extract the easiest services first to build team confidence and infrastructure. Save the hardest (most coupled, most critical) for last. --- ## Anti-Patterns ### 1. Big Bang Rewrite ```text [FAIL] "Let's rewrite the entire monolith as microservices over 6 months" → Delayed value delivery, high risk, second-system effect, team burnout [PASS] Incremental extraction with strangler fig. Ship value every 2-4 weeks. Each extraction is independently valuable and rollback-safe. ``` ### 2. Premature Decomposition ```text [FAIL] Splitting into 20 microservices before understanding domain boundaries → Distributed monolith, wrong service boundaries, expensive to fix [PASS] Start with a modular monolith. Let boundaries emerge from real usage. Extract services only when you have clear, stable bounded contexts. ``` ### 3. Shared Database Migration ```text [FAIL] Extracting services but keeping the shared database → Services are coupled at the data layer, no independent deployment [PASS] Database decomposition is part of the migration plan, not an afterthought. Use the view layer or schema-per-service as intermediate steps. ``` ### 4. No Rollback Plan ```text [FAIL] "We'll figure out rollback if something goes wrong" → Panic during incidents, data loss, extended outages [PASS] Write the rollback runbook BEFORE starting migration. Test rollback in staging. Include data reconciliation steps. ``` ### 5. Ignoring Data Migration ```text [FAIL] Extracting the service but leaving historical data in the monolith → Split brain, queries return partial results, reporting breaks [PASS] Plan data migration as a first-class concern. Include: - Historical data transfer - Data format transformation - Validation and reconciliation - Cutover strategy ``` ### 6. Migrating Without Observability ```text [FAIL] Extracting services without distributed tracing or unified logging → Impossible to debug issues across service boundaries [PASS] Set up observability BEFORE the first extraction: - Distributed tracing (OpenTelemetry) - Centralized logging with correlation IDs - Service-level dashboards and alerts ``` --- ## Decision Framework ### Should You Migrate at All? ```text 1. Are deployment bottlenecks hurting business velocity? [Yes → +2, No → 0] 2. Do different parts of the system need different scaling? [Yes → +2, No → 0] 3. Are multiple teams blocked by monolith coupling? [Yes → +2, No → 0] 4. Is the monolith technically healthy (tests, CI, docs)? [Yes → +1, No → -1] 5. Does the team have distributed systems experience? [Yes → +1, No → -2] 6. Is there executive buy-in for a multi-quarter effort? [Yes → +1, No → -2] Score: 0–3 → Improve the monolith (modularize, add tests, fix CI) Score: 4–6 → Start with modular monolith, plan first extraction Score: 7+ → Begin migration with strangler fig approach ``` ### Extraction Order Prioritization Score each module (1-5 per criterion), extract in descending total order: | Module | Coupling (inverse) | Change Frequency | Business Value | Team Readiness | Total | |--------|-------------------|-----------------|----------------|----------------|-------| | Module A | ? | ? | ? | ? | ? | --- ## Cross-References - [modern-patterns.md](modern-patterns.md) — Architecture patterns overview (microservices, modular monolith) - [data-architecture-patterns.md](data-architecture-patterns.md) — CQRS, event sourcing, saga patterns for distributed data - [scalability-reliability-guide.md](scalability-reliability-guide.md) — Scaling strategies post-migration - [api-gateway-service-mesh.md](api-gateway-service-mesh.md) — Service mesh and gateway patterns for microservices - [../software-backend/SKILL.md](../software-backend/SKILL.md) — Service-level implementation patterns - [../../ops-devops-platform/SKILL.md](../../ops-devops-platform/SKILL.md) — CI/CD and deployment for microservices