Back to Blog
MicroservicesArchitectureMigrationBackend

Migrating from Monolith to Microservices

Plan and execute a monolith to microservices migration. Learn strategies, patterns, and common pitfalls.

B
Bootspring Team
Engineering
February 27, 2026
4 min read

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 alignment

Database 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.

Share this article

Help spread the word about Bootspring