Database Patterns
Data management is one of the most challenging aspects of microservices. This chapter covers patterns for managing data across distributed services.Learning Objectives:
- Implement database-per-service pattern
- Handle data consistency across services
- Design shared data strategies
- Execute zero-downtime database migrations
- Choose the right database for each service
Database Per Service Pattern
Why Separate Databases?
Copy
┌─────────────────────────────────────────────────────────────────────────────┐
│ SHARED DATABASE (Anti-Pattern) │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Service A │ │ Service B │ │ Service C │ │
│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │
│ │ │ │ │
│ └─────────────────┼─────────────────┘ │
│ ▼ │
│ ┌───────────────────┐ │
│ │ Shared Database │ │
│ │ ┌─────────────┐ │ │
│ │ │ users │ │ │
│ │ │ orders │ │ │
│ │ │ products │ │ │
│ │ │ payments │ │ │
│ │ └─────────────┘ │ │
│ └───────────────────┘ │
│ │
│ ⚠️ Problems: │
│ • Tight coupling through schema │
│ • Can't deploy independently │
│ • Schema changes affect all services │
│ • Single point of failure │
│ • Can't scale databases independently │
│ • Technology lock-in │
│ │
│ ═══════════════════════════════════════════════════════════════════════════│
│ │
│ DATABASE PER SERVICE (Recommended) │
│ ───────────────────────────────────────────────────────────────────────── │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ User Svc │ │ Order Svc │ │ Product Svc │ │
│ │ ┌────────┐ │ │ ┌────────┐ │ │ ┌────────┐ │ │
│ │ │ API │ │ │ │ API │ │ │ │ API │ │ │
│ │ └───┬────┘ │ │ └───┬────┘ │ │ └───┬────┘ │ │
│ │ │ │ │ │ │ │ │ │ │
│ │ ┌───▼────┐ │ │ ┌───▼────┐ │ │ ┌───▼────┐ │ │
│ │ │MongoDB │ │ │ │Postgres│ │ │ │ Redis │ │ │
│ │ │(users) │ │ │ │(orders)│ │ │ │(cache) │ │ │
│ │ └────────┘ │ │ └────────┘ │ │ └────────┘ │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │
│ ✅ Benefits: │
│ • Loose coupling │
│ • Independent deployment │
│ • Right database for the job │
│ • Independent scaling │
│ • Fault isolation │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Implementation Patterns
Copy
// services/user-service/database.js
// Each service manages its own database connection
const mongoose = require('mongoose');
class UserDatabase {
constructor() {
this.connection = null;
}
async connect() {
// User service uses MongoDB
this.connection = await mongoose.connect(process.env.USER_DB_URL, {
useNewUrlParser: true,
useUnifiedTopology: true,
maxPoolSize: 10,
serverSelectionTimeoutMS: 5000,
socketTimeoutMS: 45000
});
mongoose.connection.on('error', (err) => {
console.error('MongoDB connection error:', err);
});
mongoose.connection.on('disconnected', () => {
console.log('MongoDB disconnected, attempting reconnection...');
this.connect();
});
console.log('User service connected to MongoDB');
}
async disconnect() {
await mongoose.connection.close();
}
}
// User schema - owned by user service only
const userSchema = new mongoose.Schema({
id: { type: String, required: true, unique: true },
email: { type: String, required: true, unique: true },
name: { type: String, required: true },
hashedPassword: { type: String, required: true },
profile: {
avatar: String,
bio: String,
preferences: mongoose.Schema.Types.Mixed
},
status: {
type: String,
enum: ['active', 'inactive', 'suspended'],
default: 'active'
},
createdAt: { type: Date, default: Date.now },
updatedAt: { type: Date, default: Date.now }
});
const User = mongoose.model('User', userSchema);
module.exports = { UserDatabase, User };
Copy
// services/order-service/database.js
// Order service uses PostgreSQL for ACID transactions
const { Pool } = require('pg');
const { Sequelize } = require('sequelize');
class OrderDatabase {
constructor() {
this.sequelize = null;
}
async connect() {
this.sequelize = new Sequelize(process.env.ORDER_DB_URL, {
dialect: 'postgres',
pool: {
max: 20,
min: 5,
acquire: 30000,
idle: 10000
},
logging: process.env.NODE_ENV === 'development' ? console.log : false
});
await this.sequelize.authenticate();
console.log('Order service connected to PostgreSQL');
}
async runMigrations() {
await this.sequelize.sync({ alter: process.env.NODE_ENV !== 'production' });
}
}
// Order model - owned by order service only
const { DataTypes } = require('sequelize');
module.exports = (sequelize) => {
const Order = sequelize.define('Order', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
userId: {
type: DataTypes.STRING,
allowNull: false,
// Note: No foreign key to users table - that's in a different database!
index: true
},
status: {
type: DataTypes.ENUM('pending', 'confirmed', 'processing', 'shipped', 'delivered', 'cancelled'),
defaultValue: 'pending'
},
items: {
type: DataTypes.JSONB,
allowNull: false
},
totalAmount: {
type: DataTypes.DECIMAL(10, 2),
allowNull: false
},
shippingAddress: {
type: DataTypes.JSONB
},
metadata: {
type: DataTypes.JSONB,
defaultValue: {}
}
}, {
timestamps: true,
indexes: [
{ fields: ['userId', 'createdAt'] },
{ fields: ['status'] }
]
});
return Order;
};
Handling Data Ownership
Copy
// Order service needs user data but doesn't own it
// Pattern: Store only the user ID, fetch details when needed
// services/order-service/services/order-service.js
class OrderService {
constructor(userClient, productClient) {
this.userClient = userClient; // HTTP client to user service
this.productClient = productClient; // HTTP client to product service
}
async createOrder(userId, items) {
// Validate user exists (call user service)
const user = await this.userClient.getUser(userId);
if (!user) {
throw new NotFoundError('User not found');
}
// Validate products and get prices (call product service)
const products = await this.productClient.getProducts(
items.map(i => i.productId)
);
// Calculate total
const orderItems = items.map(item => {
const product = products.find(p => p.id === item.productId);
if (!product) {
throw new NotFoundError(`Product ${item.productId} not found`);
}
return {
productId: product.id,
name: product.name, // Denormalize for historical record
price: product.price, // Price at time of order
quantity: item.quantity,
subtotal: product.price * item.quantity
};
});
const totalAmount = orderItems.reduce((sum, item) => sum + item.subtotal, 0);
// Create order (only store references, not full data)
const order = await Order.create({
userId, // Just the ID, not full user data
items: orderItems,
totalAmount,
status: 'pending'
});
// Publish event for other services
await this.eventBus.publish('order.created', {
orderId: order.id,
userId,
items: orderItems,
totalAmount
});
return order;
}
async getOrderWithDetails(orderId) {
const order = await Order.findByPk(orderId);
if (!order) return null;
// Enrich with current user data (for display purposes)
const user = await this.userClient.getUser(order.userId);
return {
...order.toJSON(),
user: user ? {
id: user.id,
name: user.name,
email: user.email
} : null
};
}
}
Data Consistency Strategies
Eventual Consistency
Copy
// Most microservices use eventual consistency
// Example: User updates email, order service eventually learns about it
// user-service/handlers/user-updated.js
class UserEventHandler {
async handleUserUpdated(event) {
const { userId, changes } = event;
// Publish event
await eventBus.publish('user.updated', {
userId,
email: changes.email,
name: changes.name,
updatedAt: new Date().toISOString()
});
}
}
// order-service/handlers/user-events.js
class UserEventConsumer {
constructor() {
// Local cache/read model of user data needed by order service
this.userCache = new Map();
}
async handleUserUpdated(event) {
const { userId, email, name } = event;
// Update local cache
this.userCache.set(userId, {
id: userId,
email,
name,
lastUpdated: new Date()
});
// Optionally update denormalized data in orders
// (depends on business requirements)
}
}
Saga Pattern for Distributed Transactions
Copy
// Example: Order creation saga with compensation
class OrderSaga {
constructor(orderService, inventoryClient, paymentClient, eventBus) {
this.orderService = orderService;
this.inventoryClient = inventoryClient;
this.paymentClient = paymentClient;
this.eventBus = eventBus;
}
async execute(orderData) {
const saga = new SagaExecution();
let order = null;
let reservation = null;
let payment = null;
try {
// Step 1: Create order
saga.addStep({
name: 'createOrder',
execute: async () => {
order = await this.orderService.createOrder(orderData);
return order;
},
compensate: async () => {
if (order) {
await this.orderService.cancelOrder(order.id);
}
}
});
// Step 2: Reserve inventory
saga.addStep({
name: 'reserveInventory',
execute: async () => {
reservation = await this.inventoryClient.reserve({
orderId: order.id,
items: order.items
});
return reservation;
},
compensate: async () => {
if (reservation) {
await this.inventoryClient.release(reservation.id);
}
}
});
// Step 3: Process payment
saga.addStep({
name: 'processPayment',
execute: async () => {
payment = await this.paymentClient.charge({
orderId: order.id,
userId: order.userId,
amount: order.totalAmount
});
return payment;
},
compensate: async () => {
if (payment) {
await this.paymentClient.refund(payment.id);
}
}
});
// Step 4: Confirm order
saga.addStep({
name: 'confirmOrder',
execute: async () => {
await this.orderService.confirmOrder(order.id);
await this.inventoryClient.commit(reservation.id);
return order;
},
compensate: async () => {
// Nothing to compensate - previous steps will handle it
}
});
// Execute saga
await saga.run();
// Publish success event
await this.eventBus.publish('order.completed', {
orderId: order.id,
userId: order.userId
});
return order;
} catch (error) {
// Saga will automatically run compensations
console.error('Order saga failed:', error);
await this.eventBus.publish('order.failed', {
orderId: order?.id,
userId: orderData.userId,
reason: error.message
});
throw error;
}
}
}
// Saga execution engine
class SagaExecution {
constructor() {
this.steps = [];
this.completedSteps = [];
}
addStep(step) {
this.steps.push(step);
}
async run() {
for (const step of this.steps) {
try {
console.log(`Executing step: ${step.name}`);
const result = await step.execute();
this.completedSteps.push(step);
} catch (error) {
console.error(`Step ${step.name} failed:`, error);
await this.compensate();
throw error;
}
}
}
async compensate() {
// Execute compensations in reverse order
for (const step of this.completedSteps.reverse()) {
try {
console.log(`Compensating step: ${step.name}`);
await step.compensate();
} catch (error) {
console.error(`Compensation for ${step.name} failed:`, error);
// Log for manual intervention
}
}
}
}
Outbox Pattern
Ensure events are published reliably:Copy
// database/models/outbox.js
const OutboxMessage = sequelize.define('OutboxMessage', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
aggregateType: {
type: DataTypes.STRING,
allowNull: false
},
aggregateId: {
type: DataTypes.STRING,
allowNull: false
},
eventType: {
type: DataTypes.STRING,
allowNull: false
},
payload: {
type: DataTypes.JSONB,
allowNull: false
},
published: {
type: DataTypes.BOOLEAN,
defaultValue: false
},
publishedAt: {
type: DataTypes.DATE
},
retryCount: {
type: DataTypes.INTEGER,
defaultValue: 0
}
}, {
indexes: [
{ fields: ['published', 'createdAt'] }
]
});
// services/order-service.js
class OrderService {
async createOrder(orderData, transaction) {
// Create order and outbox message in same transaction
const t = transaction || await sequelize.transaction();
try {
const order = await Order.create(orderData, { transaction: t });
// Write to outbox in same transaction
await OutboxMessage.create({
aggregateType: 'Order',
aggregateId: order.id,
eventType: 'order.created',
payload: {
orderId: order.id,
userId: order.userId,
items: order.items,
totalAmount: order.totalAmount,
createdAt: order.createdAt
}
}, { transaction: t });
await t.commit();
return order;
} catch (error) {
await t.rollback();
throw error;
}
}
}
// workers/outbox-publisher.js
class OutboxPublisher {
constructor(eventBus) {
this.eventBus = eventBus;
this.batchSize = 100;
this.pollInterval = 1000; // 1 second
}
async start() {
console.log('Outbox publisher started');
while (true) {
try {
await this.publishPendingMessages();
} catch (error) {
console.error('Outbox publisher error:', error);
}
await new Promise(r => setTimeout(r, this.pollInterval));
}
}
async publishPendingMessages() {
const messages = await OutboxMessage.findAll({
where: {
published: false,
retryCount: { [Op.lt]: 5 }
},
order: [['createdAt', 'ASC']],
limit: this.batchSize
});
for (const message of messages) {
try {
await this.eventBus.publish(message.eventType, {
...message.payload,
_metadata: {
messageId: message.id,
aggregateType: message.aggregateType,
aggregateId: message.aggregateId,
timestamp: message.createdAt
}
});
await message.update({
published: true,
publishedAt: new Date()
});
} catch (error) {
console.error(`Failed to publish message ${message.id}:`, error);
await message.increment('retryCount');
}
}
}
}
Data Replication Strategies
Change Data Capture (CDC)
Copy
// Using Debezium for CDC (conceptual Node.js consumer)
const { Kafka } = require('kafkajs');
class CDCConsumer {
constructor(kafkaConfig) {
this.kafka = new Kafka(kafkaConfig);
this.consumer = this.kafka.consumer({ groupId: 'cdc-consumer' });
}
async start() {
await this.consumer.connect();
// Subscribe to CDC topics (Debezium format)
await this.consumer.subscribe({
topics: [
'dbserver1.inventory.products', // CDC topic for products table
'dbserver1.users.users' // CDC topic for users table
]
});
await this.consumer.run({
eachMessage: async ({ topic, partition, message }) => {
const change = JSON.parse(message.value.toString());
await this.handleChange(topic, change);
}
});
}
async handleChange(topic, change) {
// Debezium message format
const { before, after, op, source } = change.payload;
/*
op values:
'c' = create
'u' = update
'd' = delete
'r' = read (snapshot)
*/
switch (op) {
case 'c':
await this.handleCreate(topic, after);
break;
case 'u':
await this.handleUpdate(topic, before, after);
break;
case 'd':
await this.handleDelete(topic, before);
break;
}
}
async handleCreate(topic, data) {
if (topic.includes('products')) {
// Update local product cache/read model
await this.productCache.set(data.id, {
id: data.id,
name: data.name,
price: data.price / 100, // Convert cents to dollars
stock: data.stock
});
}
}
async handleUpdate(topic, before, after) {
if (topic.includes('products')) {
// Update local cache
await this.productCache.set(after.id, {
id: after.id,
name: after.name,
price: after.price / 100,
stock: after.stock
});
// Check for significant changes
if (before.price !== after.price) {
await this.eventBus.publish('product.price_changed', {
productId: after.id,
oldPrice: before.price / 100,
newPrice: after.price / 100
});
}
}
}
async handleDelete(topic, before) {
if (topic.includes('products')) {
await this.productCache.delete(before.id);
}
}
}
CQRS (Command Query Responsibility Segregation)
Copy
// Separate read and write models
// Write side - commands
class OrderCommandService {
constructor(orderRepository, eventStore) {
this.orderRepository = orderRepository;
this.eventStore = eventStore;
}
async createOrder(command) {
// Validate command
this.validateCreateOrder(command);
// Create order aggregate
const order = new OrderAggregate();
order.create(command);
// Save to event store
await this.eventStore.save(order.id, order.uncommittedEvents);
// Publish events
for (const event of order.uncommittedEvents) {
await this.eventBus.publish(event.type, event);
}
return order.id;
}
async cancelOrder(orderId, reason) {
// Load aggregate from event store
const events = await this.eventStore.getEvents(orderId);
const order = OrderAggregate.fromEvents(events);
// Apply command
order.cancel(reason);
// Save new events
await this.eventStore.save(orderId, order.uncommittedEvents);
// Publish events
for (const event of order.uncommittedEvents) {
await this.eventBus.publish(event.type, event);
}
}
}
// Read side - queries (optimized for reads)
class OrderQueryService {
constructor(readDatabase) {
this.db = readDatabase; // Optimized read database (could be Elasticsearch, Redis, etc.)
}
async getOrderById(orderId) {
return this.db.orders.findOne({ id: orderId });
}
async getOrdersByUser(userId, options = {}) {
const { page = 1, limit = 20, status } = options;
const query = { userId };
if (status) query.status = status;
return this.db.orders.find(query)
.sort({ createdAt: -1 })
.skip((page - 1) * limit)
.limit(limit);
}
async getOrderStats(userId) {
// Aggregate query on read model
return this.db.orders.aggregate([
{ $match: { userId } },
{
$group: {
_id: '$status',
count: { $sum: 1 },
totalAmount: { $sum: '$totalAmount' }
}
}
]);
}
}
// Projection - updates read model from events
class OrderProjection {
constructor(readDatabase) {
this.db = readDatabase;
}
async handleEvent(event) {
switch (event.type) {
case 'order.created':
await this.onOrderCreated(event);
break;
case 'order.confirmed':
await this.onOrderConfirmed(event);
break;
case 'order.shipped':
await this.onOrderShipped(event);
break;
case 'order.cancelled':
await this.onOrderCancelled(event);
break;
}
}
async onOrderCreated(event) {
await this.db.orders.insertOne({
id: event.orderId,
userId: event.userId,
items: event.items,
totalAmount: event.totalAmount,
status: 'pending',
createdAt: event.timestamp,
updatedAt: event.timestamp
});
}
async onOrderShipped(event) {
await this.db.orders.updateOne(
{ id: event.orderId },
{
$set: {
status: 'shipped',
shippedAt: event.timestamp,
trackingNumber: event.trackingNumber,
updatedAt: event.timestamp
}
}
);
}
}
Zero-Downtime Database Migrations
Expand-Contract Pattern
Copy
// migrations/expand-contract/add-email-verified.js
/*
* Goal: Add email_verified column to users table
*
* Phase 1: EXPAND (add column, nullable)
* Phase 2: MIGRATE (backfill data)
* Phase 3: CONTRACT (make non-nullable, remove old logic)
*/
// Phase 1: Expand - Add nullable column
const phase1_expand = {
async up(queryInterface, Sequelize) {
await queryInterface.addColumn('users', 'email_verified', {
type: Sequelize.BOOLEAN,
allowNull: true, // Nullable initially
defaultValue: null
});
},
async down(queryInterface) {
await queryInterface.removeColumn('users', 'email_verified');
}
};
// Phase 2: Update application to write to new column
// Deploy code that writes to both old and new columns
class UserService {
async updateEmailVerification(userId, verified) {
// Write to new column
await User.update(
{ email_verified: verified },
{ where: { id: userId } }
);
// Also update any legacy fields if they exist
// ...
}
}
// Phase 3: Backfill - Migrate existing data
const phase3_backfill = {
async up(queryInterface) {
// Backfill in batches to avoid locking
let offset = 0;
const batchSize = 1000;
while (true) {
const [results] = await queryInterface.sequelize.query(`
UPDATE users
SET email_verified = CASE
WHEN verified_at IS NOT NULL THEN true
ELSE false
END
WHERE email_verified IS NULL
LIMIT ${batchSize}
`);
if (results.affectedRows === 0) break;
console.log(`Backfilled ${offset + results.affectedRows} rows`);
offset += batchSize;
// Small delay to reduce database load
await new Promise(r => setTimeout(r, 100));
}
}
};
// Phase 4: Contract - Make non-nullable after backfill complete
const phase4_contract = {
async up(queryInterface, Sequelize) {
// Verify all rows have values
const [nullCount] = await queryInterface.sequelize.query(`
SELECT COUNT(*) as count FROM users WHERE email_verified IS NULL
`);
if (nullCount[0].count > 0) {
throw new Error('Cannot contract: null values still exist');
}
// Make column non-nullable
await queryInterface.changeColumn('users', 'email_verified', {
type: Sequelize.BOOLEAN,
allowNull: false,
defaultValue: false
});
}
};
Renaming a Column
Copy
// Renaming user.name to user.full_name (zero-downtime)
// Step 1: Add new column
const step1 = {
async up(queryInterface, Sequelize) {
await queryInterface.addColumn('users', 'full_name', {
type: Sequelize.STRING,
allowNull: true
});
}
};
// Step 2: Deploy code that writes to BOTH columns
class UserService {
async updateName(userId, name) {
await User.update(
{
name: name, // Old column
full_name: name // New column
},
{ where: { id: userId } }
);
}
}
// Step 3: Backfill new column
const step3 = {
async up(queryInterface) {
await queryInterface.sequelize.query(`
UPDATE users SET full_name = name WHERE full_name IS NULL
`);
}
};
// Step 4: Deploy code that reads from new column
class UserService {
async getUser(userId) {
const user = await User.findByPk(userId);
return {
...user.toJSON(),
// Use new column, fallback to old
fullName: user.full_name || user.name
};
}
}
// Step 5: Deploy code that ONLY writes to new column
class UserService {
async updateName(userId, name) {
await User.update(
{ full_name: name },
{ where: { id: userId } }
);
}
}
// Step 6: Drop old column (after verifying all reads use new column)
const step6 = {
async up(queryInterface) {
await queryInterface.removeColumn('users', 'name');
}
};
Polyglot Persistence
Choosing the Right Database
Copy
┌─────────────────────────────────────────────────────────────────────────────┐
│ POLYGLOT PERSISTENCE │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ Service │ Database │ Why │
│ ─────────────────┼─────────────────┼───────────────────────────────────── │
│ User Service │ PostgreSQL │ ACID transactions, complex queries │
│ Product Catalog │ MongoDB │ Flexible schema, document structure │
│ Shopping Cart │ Redis │ Fast, ephemeral, TTL support │
│ Search Service │ Elasticsearch │ Full-text search, faceting │
│ Order Service │ PostgreSQL │ Transactions, consistency required │
│ Analytics │ ClickHouse │ Columnar, fast aggregations │
│ Session Store │ Redis │ Fast reads, TTL, distributed │
│ Notifications │ MongoDB │ Flexible schema, time-series │
│ Audit Logs │ Cassandra │ Write-heavy, time-series, scalable │
│ Graph Relations │ Neo4j │ Relationship queries, recommendations │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Copy
// Multi-database service example
// services/product-service/databases/index.js
class ProductDatabases {
constructor() {
this.mongo = null; // Primary storage
this.elastic = null; // Search index
this.redis = null; // Cache
}
async connect() {
// MongoDB for product data
this.mongo = await mongoose.connect(process.env.MONGO_URL);
// Elasticsearch for search
this.elastic = new Client({
node: process.env.ELASTICSEARCH_URL
});
// Redis for caching
this.redis = createClient({
url: process.env.REDIS_URL
});
await this.redis.connect();
}
}
// Product service using multiple databases
class ProductService {
constructor(databases) {
this.db = databases;
}
async createProduct(productData) {
// Write to MongoDB (source of truth)
const product = await Product.create(productData);
// Index in Elasticsearch for search
await this.db.elastic.index({
index: 'products',
id: product.id,
body: {
name: product.name,
description: product.description,
category: product.category,
price: product.price,
tags: product.tags
}
});
// Invalidate cache
await this.db.redis.del(`product:${product.id}`);
return product;
}
async getProduct(productId) {
// Try cache first
const cached = await this.db.redis.get(`product:${productId}`);
if (cached) {
return JSON.parse(cached);
}
// Fetch from MongoDB
const product = await Product.findById(productId);
if (!product) return null;
// Cache for 5 minutes
await this.db.redis.setEx(
`product:${productId}`,
300,
JSON.stringify(product)
);
return product;
}
async searchProducts(query, filters = {}) {
// Use Elasticsearch for search
const result = await this.db.elastic.search({
index: 'products',
body: {
query: {
bool: {
must: [
{
multi_match: {
query,
fields: ['name^3', 'description', 'tags']
}
}
],
filter: this.buildFilters(filters)
}
},
aggs: {
categories: { terms: { field: 'category.keyword' } },
price_ranges: {
range: {
field: 'price',
ranges: [
{ to: 25 },
{ from: 25, to: 50 },
{ from: 50, to: 100 },
{ from: 100 }
]
}
}
}
}
});
return {
hits: result.hits.hits.map(h => ({ id: h._id, ...h._source })),
total: result.hits.total.value,
facets: result.aggregations
};
}
}
Interview Questions
Q1: Why database-per-service? What are the challenges?
Q1: Why database-per-service? What are the challenges?
Answer:Benefits:
- Loose coupling between services
- Independent scaling
- Right database for each use case
- Isolated failures
- Independent deployments
- Data consistency (no ACID across services)
- Query complexity (no JOINs across databases)
- Duplicate data management
- Operational overhead
- Saga pattern for transactions
- Event sourcing for consistency
- CQRS for complex queries
- API composition for cross-service data
Q2: How do you handle transactions across services?
Q2: How do you handle transactions across services?
Answer:Avoid 2PC (two-phase commit) - doesn’t scale, blocks resources.Use Saga pattern:Key practices:
- Choreography: Services react to events, publish compensating events on failure
- Orchestration: Central coordinator manages the flow
Copy
Create Order → Reserve Inventory → Charge Payment → Confirm
↓ failure ↓ failure ↓ failure
Cancel Order ← Release Inventory ← Refund Payment
- Design idempotent operations
- Store saga state for recovery
- Implement timeouts
- Log everything for debugging
Q3: What is the Outbox pattern?
Q3: What is the Outbox pattern?
Answer:The Outbox pattern ensures reliable event publishing by writing events to an outbox table in the same transaction as the business data.Flow:
- Begin transaction
- Update business data
- Insert event to outbox table
- Commit transaction
- Background worker publishes events from outbox
- Mark events as published
- Atomic: Data and event saved together
- Reliable: Won’t lose events if message broker is down
- Idempotent: Can replay if needed
Q4: How do you perform zero-downtime database migrations?
Q4: How do you perform zero-downtime database migrations?
Answer:Expand-Contract Pattern:
- Expand: Add new column (nullable)
- Deploy: Code writes to both old and new
- Migrate: Backfill existing data
- Switch: Code reads from new column
- Contract: Make non-nullable, remove old
- Rename columns
- Change column types
- Drop columns
- Add NOT NULL without default
- Backward compatible migrations
- Small batches for data migration
- Test rollback procedures
Q5: When would you use CQRS?
Q5: When would you use CQRS?
Answer:CQRS = Command Query Responsibility SegregationSeparate read (Query) and write (Command) models.Use when:
- Read and write patterns differ significantly
- Complex read queries (aggregations, joins)
- Need to scale reads independently
- Event sourcing is used
- Optimized read models (denormalized)
- Better read performance
- Simpler write logic
- Simple CRUD operations
- Strong consistency required
- Small team (adds complexity)
Chapter Summary
Key Takeaways:
- Each service should own its database (no shared schemas)
- Use Saga pattern for distributed transactions
- Outbox pattern ensures reliable event publishing
- CQRS separates read and write concerns
- Zero-downtime migrations use expand-contract pattern
- Choose the right database for each use case (polyglot persistence)