Home/Learn/Microservices/Database per Service Pattern

Database per Service Pattern

Intermediate
Data Management

Each microservice owns its data store (SQL, NoSQL, graph) chosen for its workload; inter-service data access happens only through APIs, never direct DB queries.

Overview

Database per Service is a fundamental microservices data pattern: each service has its own private data store that no other service can access directly. This enforces loose coupling — services can evolve their schemas, migrate to different database technologies, and scale independently without coordinating with other teams. The pattern eliminates the "shared database" anti-pattern (the most common cause of microservices coupling) but introduces new challenges: **joins must become API calls**, **referential integrity must be enforced at the application layer**, and **distributed data consistency requires eventual consistency patterns** like the Saga and Outbox patterns.

Enforcing Isolation — One Schema, One Service

At minimum, each service should own its own schema (even within a shared DBMS) so that no other service can issue SQL across schema boundaries. Ideally each service has its own DBMS instance. Kubernetes helps: each service's database is a separate StatefulSet with its own PVC, and network policies block cross-service DB port access.

Docker / K8s — enforce one database per service
# docker-compose.yml (dev) — each service has its own DB instance
services:
  order-service:
    image: order-service:latest
    environment:
      SPRING_DATASOURCE_URL: jdbc:postgresql://order-db:5432/orders

  order-db:
    image: postgres:16
    volumes: [order-data:/var/lib/postgresql/data]

  inventory-service:
    image: inventory-service:latest
    environment:
      SPRING_DATASOURCE_URL: jdbc:mongodb://inventory-mongo:27017/inventory

  inventory-mongo:
    image: mongo:7

# Kubernetes NetworkPolicy — deny direct DB access from other services
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: { name: order-db-policy }
spec:
  podSelector: { matchLabels: { app: order-db } }
  ingress:
    - from: [{ podSelector: { matchLabels: { app: order-service } } }]
      ports: [{ port: 5432 }]

Cross-Service Data Access — API calls and Materialised Views

When Service B needs data owned by Service A, it calls Service A's API (synchronous) or subscribes to Service A's events and maintains a local read-replica (asynchronous). The local copy is a **materialised view** — denormalised, kept eventually consistent by event consumption. This is faster than API calls on hot read paths and survives Service A being down.

Microservices — API call vs local materialised view
// Option 1: Synchronous API call (simple, coupled)
@Service
class OrderService {
    private final InventoryClient inventoryClient; // Feign / WebClient

    public Order createOrder(CreateOrderRequest req) {
        // Check stock — direct API call to inventory service
        StockResponse stock = inventoryClient.checkStock(req.getSku());
        if (stock.available() < req.getQuantity())
            throw new InsufficientStockException();
        // ...
    }
}

// Option 2: Local materialised view via event consumption (better for reads)
@Entity
class ProductSnapshot {   // owned by order-service DB
    String sku;
    String name;
    BigDecimal price;     // kept in sync by consuming ProductPriceUpdatedEvent
}

@KafkaListener(topics = "product-events")
void onProductEvent(ProductPriceUpdatedEvent event) {
    productSnapshotRepo.findBySku(event.sku())
        .ifPresent(p -> { p.setPrice(event.newPrice()); productSnapshotRepo.save(p); });
}

Challenges: Distributed Joins and Referential Integrity

Relational concepts that are trivial in a shared DB become distributed systems problems. Cross-service JOINs must be done at the application layer (N+1 risk) or with materialised views. Referential integrity (FK constraints across services) is replaced by application-level validation + compensating transactions. The Outbox pattern ensures events are published atomically with DB writes.

Microservices — application-level join and Outbox pattern
// Cross-service "join" — application layer fan-out
public OrderDetailsDTO getOrderDetails(Long orderId) {
    Order order = orderRepo.findById(orderId).orElseThrow();

    // Parallel calls to avoid sequential N+1
    CompletableFuture<CustomerDTO> customerFuture =
        CompletableFuture.supplyAsync(() -> customerClient.get(order.getCustomerId()));
    CompletableFuture<List<ProductDTO>> productsFuture =
        CompletableFuture.supplyAsync(() -> productClient.getBatch(order.getSkus()));

    CompletableFuture.allOf(customerFuture, productsFuture).join();

    return new OrderDetailsDTO(order, customerFuture.join(), productsFuture.join());
}

// Outbox pattern — atomic event publish with DB write
// (solve dual-write problem between DB commit and message publish)
@Transactional
public Order createOrder(CreateOrderRequest req) {
    Order order = orderRepo.save(new Order(req));
    outboxRepo.save(new OutboxEvent("order.created", toJson(order)));
    // Outbox poller (separate thread) reads + publishes to Kafka
    return order;
}

Key Points to Remember

  • 1Each microservice owns its private data store — no other service can query it directly
  • 2Different services can use different DB technologies chosen for their workload (polyglot persistence)
  • 3Cross-service reads: call the API (synchronous) or maintain a local materialised view (async)
  • 4Referential integrity becomes application-level validation + compensating transactions
  • 5The Outbox pattern solves dual-write: persist event to DB in the same transaction as the state change
  • 6Shared database is the most common microservices anti-pattern — it tightly couples schemas and teams

Interview Questions

Sign in to ask Aria
1

Why does the database-per-service pattern eliminate the need for cross-service JOINs?

EasyAmazon
2

How would you implement a cross-service query that previously was a SQL JOIN?

MediumNetflix
3

What is the dual-write problem and how does the Outbox pattern solve it?

HardConfluent
4

What is polyglot persistence and when would you use it?

MediumThoughtWorks
5

How do you enforce referential integrity between entities owned by different services?

HardUber

Ask Aria about Database per Service Pattern

Your personal AI tutor — ask anything about this concept

Revision Status

Personal Notes

Sign in to save personal notes for this topic.

Discussion

Sign in to join the discussion.

Loading discussion…