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