Microservices Database Patterns
Strategies for managing data in distributed microservices architectures.
Database Per Service Pattern
Core Principle
Each microservice owns and manages its own database, providing complete data autonomy.
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β API Gateway β
βββββββββββββββββββββββββββ¬ββββββββββββββββββββββββββββββββββββ
β
βββββββββββββββββββββββΌββββββββββββββββββββββ
β β β
βΌ βΌ βΌ
βββββββββββββ βββββββββββββ βββββββββββββ
β Order β β Product β β Customer β
β Service β β Service β β Service β
βββββββ¬ββββββ βββββββ¬ββββββ βββββββ¬ββββββ
β β β
βββββββΌββββββ βββββββΌββββββ βββββββΌββββββ
β Orders β β Products β β Customers β
β Database β β Database β β Database β
β (SQL) β β (MongoDB) β β (Postgres)β
βββββββββββββ βββββββββββββ βββββββββββββ
Benefits
- Loose Coupling: Services are independent
- Technology Freedom: Choose best database for each use case
- Independent Scaling: Scale databases separately
- Failure Isolation: One DB failure doesnβt affect others
- Schema Evolution: Change schema without coordination
Challenges
- Data consistency across services
- Cross-service queries
- Increased operational complexity
- Data duplication
Shared Database Anti-Pattern
Why to Avoid
βββββββββββββ βββββββββββββ βββββββββββββ
β Service A β β Service B β β Service C β
βββββββ¬ββββββ βββββββ¬ββββββ βββββββ¬ββββββ
β β β
ββββββββββββββββΌβββββββββββββββ
β
ββββββββΌβββββββ
β Shared β β Anti-pattern!
β Database β
βββββββββββββββ
Problems:
- Tight coupling
- Schema changes affect all services
- Single point of failure
- Cannot scale independently
- Technology lock-in
Data Consistency Patterns
Eventual Consistency
// Event-driven data synchronization
public class OrderCreatedHandler : IHandleMessages<OrderCreated>
{
public async Task Handle(OrderCreated @event)
{
// Customer service updates its read model
await _customerReadModel.AddOrder(new CustomerOrder
{
CustomerId = @event.CustomerId,
OrderId = @event.OrderId,
TotalAmount = @event.TotalAmount
});
}
}
Saga Pattern for Transactions
ββββββββββββ ββββββββββββ ββββββββββββ ββββββββββββ
β Create βββββΆβ Reserve βββββΆβ Process βββββΆβ Ship β
β Order β β Stock β β Payment β β Order β
ββββββββββββ ββββββββββββ ββββββββββββ ββββββββββββ
β β β β
βΌ βΌ βΌ βΌ
ββββββββββββ ββββββββββββ ββββββββββββ ββββββββββββ
β Cancel ββββββ Release ββββββ Refund ββββββ Cancel β
β Order β β Stock β β Payment β β Shipment β
ββββββββββββ ββββββββββββ ββββββββββββ ββββββββββββ
(Compensating transactions for rollback)
Implementation Example
public class CreateOrderSaga
{
private readonly IOrderRepository _orders;
private readonly IInventoryService _inventory;
private readonly IPaymentService _payment;
public async Task<Result> ExecuteAsync(CreateOrderCommand command)
{
// Step 1: Create Order
var order = await _orders.CreateAsync(command);
try
{
// Step 2: Reserve Inventory
var reservation = await _inventory.ReserveAsync(order.Items);
try
{
// Step 3: Process Payment
var payment = await _payment.ProcessAsync(order.Total);
// Success - complete order
await _orders.ConfirmAsync(order.Id);
return Result.Success(order);
}
catch
{
// Compensate: Release inventory
await _inventory.ReleaseAsync(reservation.Id);
throw;
}
}
catch
{
// Compensate: Cancel order
await _orders.CancelAsync(order.Id);
throw;
}
}
}
Cross-Service Queries
API Composition
public class OrderDetailsQueryHandler
{
public async Task<OrderDetailsDto> Handle(GetOrderDetails query)
{
// Fetch from multiple services in parallel
var orderTask = _orderService.GetOrderAsync(query.OrderId);
var customerTask = _customerService.GetCustomerAsync(query.CustomerId);
var productTasks = query.ProductIds
.Select(id => _productService.GetProductAsync(id));
await Task.WhenAll(
orderTask,
customerTask,
Task.WhenAll(productTasks)
);
// Compose the response
return new OrderDetailsDto
{
Order = await orderTask,
Customer = await customerTask,
Products = productTasks.Select(t => t.Result).ToList()
};
}
}
CQRS with Read Models
ββββββββββββββββ Events ββββββββββββββββββββββββ
β Order ServiceββββββββββββββββββΆβ Order Details β
ββββββββββββββββ β Read Model β
β (Denormalized view) β
ββββββββββββββββ Events β β
β Customer Svc ββββββββββββββββββΆβ Order + Customer + β
ββββββββββββββββ β Product data β
β in single table β
ββββββββββββββββ Events β β
β Product Svc ββββββββββββββββββΆβ β
ββββββββββββββββ ββββββββββββββββββββββββ
Database Technology Selection
Polyglot Persistence
| Service | Database | Reason |
|---|---|---|
| User Profiles | PostgreSQL | Complex relations, ACID |
| Product Catalog | MongoDB | Flexible schema, nested data |
| Shopping Cart | Redis | Fast access, TTL support |
| Order History | Cassandra | High write throughput, time-series |
| Search | Elasticsearch | Full-text search, analytics |
| Sessions | Redis | In-memory, fast expiration |
Selection Criteria
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Database Selection β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β Structured data + ACID βββΆ PostgreSQL/SQL Server β
β β
β Document/Flexible schema βββΆ MongoDB/CouchDB β
β β
β Key-Value/Caching βββΆ Redis/Memcached β
β β
β Time-series/Logs βββΆ InfluxDB/TimescaleDB β
β β
β Graph relationships βββΆ Neo4j/CosmosDB β
β β
β Full-text search βββΆ Elasticsearch/Solr β
β β
β Wide column/Scale βββΆ Cassandra/ScyllaDB β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Data Migration Strategies
Strangler Fig Pattern
Phase 1: Legacy database still primary
βββββββββββββββ
β Monolith βββββΆ Legacy DB (read/write)
βββββββββββββββ
Phase 2: Dual writes
βββββββββββββββββββΆ Legacy DB (write)
β Monolith βββββΆ New DB (write)
βββββββββββββββ
Phase 3: New database primary
βββββββββββββββββββΆ New DB (read/write)
β Microserviceβ
βββββββββββββββββββΆ Legacy DB (deprecated)
Change Data Capture (CDC)
// Using Debezium connector concept
public class OrderCdcProcessor
{
public async Task ProcessChange(ChangeEvent change)
{
switch (change.Operation)
{
case "INSERT":
await PublishEvent(new OrderCreated(change.After));
break;
case "UPDATE":
await PublishEvent(new OrderUpdated(change.Before, change.After));
break;
case "DELETE":
await PublishEvent(new OrderDeleted(change.Before));
break;
}
}
}
Best Practices
1. Define Clear Data Ownership
- Each service owns its domain data
- No direct database access between services
- Use APIs for data sharing
2. Design for Eventual Consistency
- Accept that data wonβt always be immediately consistent
- Implement idempotent operations
- Handle duplicate events
3. Implement Proper Boundaries
// Good: Service exposes API
public interface IOrderService
{
Task<Order> GetOrderAsync(string orderId);
}
// Bad: Direct database access
public class ReportService
{
// Don't do this!
private readonly OrderDbContext _orderDb;
}
4. Plan for Failure
- Implement compensating transactions
- Use outbox pattern for reliable messaging
- Handle partial failures gracefully
Sources
Arhitectura/microservices dbs.gif