skills/software-architecture-design/references/migration-modernization-guide.md

540 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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<Response> {
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<Order> {
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 | 1001000 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.11% increase | >1% increase |
| P95 latency | <10% increase | 1050% increase | >50% increase |
| Data mismatches | <0.01% | 0.010.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: 03 → Improve the monolith (modularize, add tests, fix CI)
Score: 46 → 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