StackPractices
intermediate By Mathias Paulenko

Database per Service Pattern

Give each microservice its own private database to ensure loose coupling, independent deployment, and technology heterogeneity across the application portfolio.

Overview

Under the Database per Service Pattern each microservice owns a private database that no other service may touch. A team can pick Postgres, Mongo, or whatever its workload calls for, and ship without waiting on anyone. With all reads and writes funneled through one API, each domain has exactly one place holding the truth.

Inside a monolith, every module sits on one shared database — tight coupling by default: a schema change needs sign-off across teams, one module’s heavy query slows everyone else down, and growing means growing the whole database. With Database per Service, each team owns its schema, can optimize independently, and deploys without fear of breaking other services.

What you pay is complexity: cross-service queries go through API composition or event sync, and anything that writes across databases needs a Saga or accepts eventual consistency.

flowchart diagram: subgraph own[

When to Use

Use the Database per Service Pattern when:

  • Building microservices where teams need independent deployment velocity
  • Different services have fundamentally different data access patterns (OLTP vs analytics)
  • Services need different database technologies (graph, document, relational)
  • You want to prevent accidental coupling through shared database schemas

When to Avoid

  • The application is a monolith or small enough that a single database suffices
  • The overhead of managing multiple databases exceeds the independence benefit
  • Complex cross-service queries are frequent and API composition is too slow
  • Strong consistency across services is required and eventual consistency is unacceptable

Solution

Python (Per-Service Databases with Event Sync)

from dataclasses import dataclass
from datetime import datetime
from typing import List, Optional
import sqlite3
import json

# ============================================================================
# ORDER SERVICE DATABASE
# ============================================================================

class OrderDatabase:
    """Private database for the Order Service"""
    def __init__(self, db_path: str = "order_service.db"):
        self.conn = sqlite3.connect(db_path)
        self._init_schema()

    def _init_schema(self):
        self.conn.execute("""
            CREATE TABLE IF NOT EXISTS orders (
                order_id TEXT PRIMARY KEY,
                customer_id TEXT NOT NULL,
                total_amount REAL NOT NULL,
                status TEXT DEFAULT 'pending',
                created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
            )
        """)
        self.conn.execute("""
            CREATE TABLE IF NOT EXISTS order_events (
                event_id INTEGER PRIMARY KEY AUTOINCREMENT,
                order_id TEXT NOT NULL,
                event_type TEXT NOT NULL,
                payload TEXT NOT NULL,
                created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
            )
        """)
        self.conn.commit()

    def create_order(self, order_id: str, customer_id: str, total: float) -> dict:
        self.conn.execute(
            "INSERT INTO orders (order_id, customer_id, total_amount) VALUES (?, ?, ?)",
            (order_id, customer_id, total)
        )
        self._emit_event("ORDER_CREATED", {
            "order_id": order_id,
            "customer_id": customer_id,
            "total": total
        })
        self.conn.commit()
        return {"order_id": order_id, "status": "pending"}

    def _emit_event(self, event_type: str, payload: dict):
        """Publish event to local outbox for downstream consumers"""
        self.conn.execute(
            "INSERT INTO order_events (order_id, event_type, payload) VALUES (?, ?, ?)",
            (payload["order_id"], event_type, json.dumps(payload))
        )

    def get_order(self, order_id: str) -> Optional[dict]:
        cursor = self.conn.execute(
            "SELECT * FROM orders WHERE order_id = ?", (order_id,)
        )
        row = cursor.fetchone()
        if row:
            return {
                "order_id": row[0], "customer_id": row[1],
                "total_amount": row[2], "status": row[3]
            }
        return None


# ============================================================================
# CUSTOMER SERVICE DATABASE
# ============================================================================

class CustomerDatabase:
    """Private database for the Customer Service"""
    def __init__(self, db_path: str = "customer_service.db"):
        self.conn = sqlite3.connect(db_path)
        self._init_schema()

    def _init_schema(self):
        self.conn.execute("""
            CREATE TABLE IF NOT EXISTS customers (
                customer_id TEXT PRIMARY KEY,
                name TEXT NOT NULL,
                email TEXT NOT NULL,
                loyalty_points INTEGER DEFAULT 0
            )
        """)
        self.conn.execute("""
            CREATE TABLE IF NOT EXISTS customer_events (
                event_id INTEGER PRIMARY KEY AUTOINCREMENT,
                customer_id TEXT NOT NULL,
                event_type TEXT NOT NULL,
                payload TEXT NOT NULL,
                created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
            )
        """)
        self.conn.commit()

    def create_customer(self, customer_id: str, name: str, email: str):
        self.conn.execute(
            "INSERT INTO customers (customer_id, name, email) VALUES (?, ?, ?)",
            (customer_id, name, email)
        )
        self.conn.commit()

    def add_loyalty_points(self, customer_id: str, points: int):
        self.conn.execute(
            "UPDATE customers SET loyalty_points = loyalty_points + ? WHERE customer_id = ?",
            (points, customer_id)
        )
        self.conn.commit()

    def get_customer(self, customer_id: str) -> Optional[dict]:
        cursor = self.conn.execute(
            "SELECT * FROM customers WHERE customer_id = ?", (customer_id,)
        )
        row = cursor.fetchone()
        if row:
            return {"customer_id": row[0], "name": row[1], "email": row[2], "loyalty_points": row[3]}
        return None


# ============================================================================
# EVENT BUS (simulating message broker for cross-service sync)
# ============================================================================

class EventBus:
    """Simplified event bus for inter-service communication"""
    def __init__(self):
        self.subscribers = {}

    def subscribe(self, event_type: str, handler):
        self.subscribers.setdefault(event_type, []).append(handler)

    def publish(self, event_type: str, payload: dict):
        for handler in self.subscribers.get(event_type, []):
            handler(payload)


# ============================================================================
# SERVICE LAYER
# ============================================================================

class OrderService:
    """Encapsulates order business logic and private database"""
    def __init__(self, database: OrderDatabase, event_bus: EventBus):
        self.db = database
        self.events = event_bus

    def place_order(self, order_id: str, customer_id: str, items: List[dict]) -> dict:
        total = sum(item["price"] * item["quantity"] for item in items)
        result = self.db.create_order(order_id, customer_id, total)
        return result

    def get_order(self, order_id: str) -> Optional[dict]:
        return self.db.get_order(order_id)


class CustomerService:
    """Encapsulates customer business logic and private database"""
    def __init__(self, database: CustomerDatabase, event_bus: EventBus):
        self.db = database
        self.events = event_bus
        self._subscribe_to_events()

    def _subscribe_to_events(self):
        self.events.subscribe("ORDER_CREATED", self._on_order_created)

    def _on_order_created(self, payload: dict):
        """React to orders by updating loyalty points"""
        points = int(payload["total"] * 0.1)  # 10% of order value
        self.db.add_loyalty_points(payload["customer_id"], points)
        print(f"Added {points} loyalty points to customer {payload['customer_id']}")

    def register_customer(self, customer_id: str, name: str, email: str):
        self.db.create_customer(customer_id, name, email)

    def get_customer(self, customer_id: str) -> Optional[dict]:
        return self.db.get_customer(customer_id)


# ============================================================================
# USAGE: Services communicate via events, not shared database
# ============================================================================

bus = EventBus()
order_service = OrderService(OrderDatabase(), bus)
customer_service = CustomerService(CustomerDatabase(), bus)

# Register a customer
customer_service.register_customer("C-001", "Alice Johnson", "alice@example.com")

# Place an order (triggers loyalty point update via event)
order_service.place_order("ORD-001", "C-001", [
    {"sku": "A1", "price": 50.0, "quantity": 2}
])

# Verify data in respective databases
print("Order:", order_service.get_order("ORD-001"))
print("Customer:", customer_service.get_customer("C-001"))

Java (Spring Boot with Separate DataSources)

import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.JpaRepository;
import javax.persistence.*;
import javax.sql.DataSource;

// Order Service Configuration
@Configuration
public class OrderDatabaseConfig {
    @Bean
    @ConfigurationProperties("app.datasource.order")
    public DataSourceProperties orderDataSourceProperties() {
        return new DataSourceProperties();
    }

    @Bean
    public DataSource orderDataSource() {
        return orderDataSourceProperties()
            .initializeDataSourceBuilder()
            .build();
    }
}

// Order Entity (in Order Service database)
@Entity
@Table(name = "orders")
class Order {
    @Id
    private String orderId;
    private String customerId;
    private double totalAmount;
    private String status = "pending";

    // getters, setters
}

interface OrderRepository extends JpaRepository<Order, String> {}

// Customer Service (separate service, separate database)
@Entity
@Table(name = "customers")
class Customer {
    @Id
    private String customerId;
    private String name;
    private String email;
    private int loyaltyPoints = 0;

    public void addLoyaltyPoints(int points) {
        this.loyaltyPoints += points;
    }

    // getters, setters
}

interface CustomerRepository extends JpaRepository<Customer, String> {}

// Event-driven sync
record OrderCreatedEvent(String orderId, String customerId, double total) {}

@Service
class CustomerEventHandler {
    private final CustomerRepository customerRepo;

    public CustomerEventHandler(CustomerRepository customerRepo) {
        this.customerRepo = customerRepo;
    }

    @EventListener
    @Transactional
    public void onOrderCreated(OrderCreatedEvent event) {
        Customer customer = customerRepo.findById(event.customerId())
            .orElseThrow();
        int points = (int) (event.total() * 0.1);
        customer.addLoyaltyPoints(points);
        customerRepo.save(customer);
    }
}

JavaScript (Node.js with Separate MongoDB Collections)

const { MongoClient } = require('mongodb');

class DatabasePerService {
  constructor(uri) {
    this.client = new MongoClient(uri);
  }

  async connect() {
    await this.client.connect();
    // Each service gets its own database
    this.orderDb = this.client.db('order_service');
    this.customerDb = this.client.db('customer_service');
    this.inventoryDb = this.client.db('inventory_service');
  }

  // Order Service methods
  async createOrder(orderId, customerId, items) {
    const total = items.reduce((sum, item) => sum + item.price * item.qty, 0);
    const orders = this.orderDb.collection('orders');
    await orders.insertOne({
      orderId, customerId, total, status: 'pending',
      createdAt: new Date()
    });
    return { orderId, status: 'pending' };
  }

  async getOrder(orderId) {
    return this.orderDb.collection('orders').findOne({ orderId });
  }

  // Customer Service methods
  async createCustomer(customerId, name, email) {
    await this.customerDb.collection('customers').insertOne({
      customerId, name, email, loyaltyPoints: 0
    });
  }

  async addLoyaltyPoints(customerId, points) {
    await this.customerDb.collection('customers').updateOne(
      { customerId },
      { $inc: { loyaltyPoints: points } }
    );
  }

  // Inventory Service methods
  async reserveInventory(orderId, sku, qty) {
    const inventory = this.inventoryDb.collection('inventory');
    const result = await inventory.updateOne(
      { sku, available: { $gte: qty } },
      { $inc: { available: -qty, reserved: qty } }
    );
    return result.modifiedCount > 0;
  }
}

// Event bus for cross-service communication
class EventBus {
  constructor() {
    this.handlers = new Map();
  }

  on(event, handler) {
    if (!this.handlers.has(event)) this.handlers.set(event, []);
    this.handlers.get(event).push(handler);
  }

  emit(event, payload) {
    (this.handlers.get(event) || []).forEach(h => h(payload));
  }
}

// Usage
async function demo() {
  const db = new DatabasePerService('mongodb://localhost:27017');
  await db.connect();

  const bus = new EventBus();

  // Subscribe customer service to order events
  bus.on('ORDER_CREATED', async (payload) => {
    const points = Math.floor(payload.total * 0.1);
    await db.addLoyaltyPoints(payload.customerId, points);
    console.log(`Added ${points} points to ${payload.customerId}`);
  });

  // Create customer
  await db.createCustomer('C-001', 'Alice', 'alice@example.com');

  // Create order + emit event
  const order = await db.createOrder('ORD-001', 'C-001', [
    { sku: 'A1', price: 50, qty: 2 }
  ]);
  bus.emit('ORDER_CREATED', { orderId: 'ORD-001', customerId: 'C-001', total: 100 });

  console.log('Order:', await db.getOrder('ORD-001'));
}

demo().catch(console.error);

Explanation

Database per Service enforces boundaries through physical isolation:

  1. Private schema: Each service owns its tables/collections and can change them independently
  2. API-only access: Other services interact through HTTP/gRPC/Events, not SQL queries
  3. Technology choice: One service uses PostgreSQL, another uses MongoDB, another uses Redis — whatever fits best
  4. Event synchronization: Data that needs to be shared is published as events that other services consume into their own databases

Variants

VariantIsolation LevelUse Case
Separate database serversFull physical isolationMaximum independence, different technologies
Separate schemasLogical isolation within one serverSame tech, reduced operational overhead
Separate collections/tablesMinimal isolationMigration phase from monolith
Schema per tenant + serviceMulti-tenant microservicesSaaS applications with per-tenant data

What Works

  • Never expose your database directly. Always access other services through their APIs or events.
  • Use an outbox pattern for events. Publish events atomically with database transactions.
  • Adopt eventual consistency. Cross-service data will be temporarily inconsistent; design for it.
  • Implement sagas for multi-service transactions. If something breaks mid-transaction, each finished step runs a compensating action to undo it.
  • Keep service databases small. Once one grows past what the team can reason about, that’s usually the sign the service is doing too much — split it.

Common Mistakes

  • Direct database access between services. This creates the same coupling the pattern is designed to prevent.
  • Sharing a database “temporarily.” Temporary shared databases become permanent and defeat the purpose.
  • Not handling eventual consistency. Users see stale data because the UI assumes immediate consistency.
  • Overly chatty APIs. Making 10 API calls to compose a page is a sign of poor service boundaries.
  • Ignoring data duplication. The same fact copied into two services is fine — normalize inside a service, tolerate duplication across them.

Real-World Examples

Amazon

Amazon’s architecture famously uses database per service. The order service, customer service, and inventory service each have their own data stores, synchronized via events — so each team moves at its own pace.

Netflix

Netflix mixes it up: Cassandra for some services, Elasticsearch for others, S3 for the rest — each team picks whatever matches their access patterns and scale.

Uber

Uber migrated from a monolithic Postgres database to microservices with separate databases. The trip service, payment service, and driver service each have dedicated data stores, with change data capture (CDC) streaming events between them.

Frequently Asked Questions

How do I query across multiple services?

Two options. Compose API calls on the fly when you need fresh data, or maintain a read model updated by events when the query runs hot — that second path is what CQRS and materialized views are for.

What about reporting and analytics?

Extract data from services into a data warehouse or lake via ETL/CDC. Services publish change events that are consumed by analytics pipelines.

Does every service need a different database technology?

No. Most teams pick one or two database engines and stick with them — running five different stacks is an operations nightmare. Heterogeneity is a side effect; ownership is the rule.

How do I handle foreign key relationships across services?

They can't — a foreign key can't reach into another service's database. What services store is each other's IDs, and consistency becomes an application-level concern handled by sagas or events.