Transitioning from monolith to microservices requires careful planning.
When to Migrate#
Consider microservices when:
✓ Team scaling challenges
✓ Independent deployment needed
✓ Different scaling requirements per component
✓ Technology diversity requirements
Keep the monolith when:
✗ Small team (< 10 developers)
✗ Simple domain
✗ Unclear boundaries
✗ Premature optimization
Strangler Fig Pattern#
Phase 1: Identify boundaries
┌─────────────────────────────┐
│ Monolith │
│ ┌─────┐ ┌─────┐ ┌─────┐ │
│ │Users│ │Orders│ │Inv. │ │
│ └─────┘ └─────┘ └─────┘ │
└─────────────────────────────┘
Phase 2: Extract service
┌─────────────────────────────┐
│ Monolith │
│ ┌─────┐ ┌─────┐ │
│ │Users│ │Orders│ │
│ └─────┘ └─────┘ │
└─────────────────────────────┘
│
▼
┌─────────┐
│Inventory│ (New Service)
│ Service │
└─────────┘
Phase 3: Route traffic
┌──────────────┐
│ API Gateway │
└──────┬───────┘
│
┌────┴────┐
▼ ▼
┌────┐ ┌─────────┐
│Mono│ │Inventory│
│lith│ │ Service │
└────┘ └─────────┘
Identifying Service Boundaries#
1// Look for bounded contexts
2// Users & Authentication → User Service
3// Orders & Payments → Order Service
4// Products & Inventory → Inventory Service
5
6// Signs of good boundaries:
7// - Independent data ownership
8// - Clear API contracts
9// - Minimal cross-boundary transactions
10// - Team ownership alignmentDatabase Decomposition#
1// Phase 1: Shared database (anti-pattern, temporary)
2// Both monolith and new service access same DB
3
4// Phase 2: Database per service with sync
5class OrderService {
6 // Own database
7 private orderDb: OrderDatabase;
8
9 // Sync product data via events
10 @Subscribe('product.updated')
11 async handleProductUpdate(event: ProductUpdatedEvent) {
12 await this.orderDb.products.upsert({
13 id: event.productId,
14 name: event.name,
15 price: event.price,
16 });
17 }
18}
19
20// Phase 3: API calls for cross-service data
21class OrderService {
22 async createOrder(data: CreateOrderInput) {
23 // Call product service for current data
24 const product = await this.productClient.getProduct(data.productId);
25
26 return this.orderDb.orders.create({
27 ...data,
28 productSnapshot: {
29 id: product.id,
30 name: product.name,
31 price: product.price,
32 },
33 });
34 }
35}API Gateway for Migration#
1// Route old and new paths
2const routes = {
3 // Already migrated - route to new service
4 '/api/inventory': 'http://inventory-service:3001',
5
6 // Still in monolith - route to legacy
7 '/api/users': 'http://monolith:8080',
8 '/api/orders': 'http://monolith:8080',
9};
10
11// Feature flags for gradual rollout
12app.use('/api/inventory', async (req, res, next) => {
13 const useNewService = await featureFlags.isEnabled('new-inventory-service', {
14 userId: req.user?.id,
15 });
16
17 if (useNewService) {
18 return proxy(req, res, next, 'http://inventory-service:3001');
19 }
20
21 return proxy(req, res, next, 'http://monolith:8080/inventory');
22});Event-Driven Communication#
1// Publish events from monolith
2class MonolithOrderController {
3 async createOrder(data: OrderInput) {
4 const order = await this.orderRepository.create(data);
5
6 // Publish event for other services
7 await this.eventBus.publish('order.created', {
8 orderId: order.id,
9 userId: order.userId,
10 items: order.items,
11 total: order.total,
12 });
13
14 return order;
15 }
16}
17
18// New service subscribes
19class NotificationService {
20 @Subscribe('order.created')
21 async handleOrderCreated(event: OrderCreatedEvent) {
22 await this.sendOrderConfirmation(event.userId, event.orderId);
23 }
24}Data Migration Strategy#
1// Dual-write during migration
2async function updateInventory(productId: string, quantity: number) {
3 // Write to old database
4 await legacyDb.query(
5 'UPDATE inventory SET quantity = $1 WHERE product_id = $2',
6 [quantity, productId]
7 );
8
9 // Write to new service
10 await inventoryService.updateQuantity(productId, quantity);
11
12 // Eventually: remove legacy write
13}
14
15// Verification
16async function verifyDataConsistency() {
17 const legacyProducts = await legacyDb.query('SELECT * FROM products');
18 const newProducts = await productService.getAll();
19
20 const mismatches = findMismatches(legacyProducts, newProducts);
21 if (mismatches.length > 0) {
22 logger.warn('Data inconsistencies found', { mismatches });
23 }
24}Service Communication#
1// Synchronous (HTTP/gRPC) - for queries
2const user = await userService.getUser(userId);
3
4// Asynchronous (Events) - for commands
5await eventBus.publish('user.registered', { userId, email });
6
7// Saga for distributed transactions
8async function createOrderSaga(orderData: OrderInput) {
9 const sagaId = generateId();
10
11 try {
12 // Reserve inventory
13 await inventoryService.reserve(sagaId, orderData.items);
14
15 // Process payment
16 await paymentService.charge(sagaId, orderData.payment);
17
18 // Create order
19 await orderService.create(sagaId, orderData);
20
21 } catch (error) {
22 // Compensate
23 await inventoryService.release(sagaId);
24 await paymentService.refund(sagaId);
25 throw error;
26 }
27}Common Pitfalls#
❌ Big bang migration
✓ Incremental extraction
❌ Distributed monolith (tight coupling)
✓ Loose coupling via events/APIs
❌ Shared database
✓ Database per service
❌ Synchronous chains
✓ Async communication where possible
❌ No observability
✓ Distributed tracing, logging, metrics
Start small, migrate incrementally, and maintain backward compatibility.