What Is the Database-Per-Service Pattern?

D·A·T·A·B·A·S·E·-·P·E·R·-·S·E·R·V·I·C·E

What Is the Database-Per-Service Pattern?

A complete, beginner-friendly guide to how modern microservices keep their data private, why it matters, and how companies like Netflix, Amazon, and Uber use it to scale to hundreds of millions of users.

Before · shared DB
MANY SERVICES, ONE DB Orders Inventory Payments ONE SHARED DATABASE any schema change can break every team
One database, many services — independence in name only.
After · DB per service
EACH SERVICE, OWN DB Orders orders_db Inventory inventory_db Payments payments_db API / EVENT BUS API API API schema change stays local; blast radius is one team
One database per service — true team & failure isolation.
01 · Foundations

Introduction & History

Before we look at any code or architecture diagram, let’s ground the idea in an everyday scene — because the pattern really is that intuitive.

Imagine a big family living in one giant house where everyone shares a single refrigerator. One person can’t find their leftovers because a sibling ate them by mistake. Someone rearranges the shelves, and suddenly nobody can find the ketchup. If the fridge breaks, the whole family goes hungry — not just one person.

Now imagine instead that every family member gets their own small fridge in their own room. They organise it however they like. If one fridge breaks, only that person is affected — everyone else still has food. If someone wants to change how they organise their fridge, they don’t need to ask permission from the whole family.

Simple analogy

The database-per-service pattern is exactly like giving every family member their own fridge instead of sharing one big fridge. In software, each “family member” is a microservice, and each “fridge” is that service’s own private database.

In plain words: the database-per-service pattern is a rule in microservices architecture that says every microservice must own and manage its own database, and no other service is allowed to touch that database directly. If another service needs data, it must ask politely — through an API — never by sneaking into the database.

1.1 · A short history

To understand why this pattern exists, it helps to know what came before it.

In the 1990s and 2000s, most companies built what we now call monolithic applications. A monolith is one large program that contains everything — the user interface, the business logic, and the data access code — all bundled together, usually talking to a single, shared database. This was simple to build at first: one codebase, one database, one deployment.

But as companies grew, monoliths grew with them. Amazon, Netflix, eBay, and Google all hit the same wall in the mid-2000s to early 2010s: their monoliths became so large and tangled that a small change in one part of the code could break something completely unrelated. Deployments took hours. Teams stepped on each other’s toes. The shared database became the biggest bottleneck of all — every team needed to coordinate schema changes, and a slow query from one team’s feature could slow down the entire company’s website.

This pain led to the rise of microservices architecture — breaking one giant application into many small, independent services, each responsible for one business capability (like “Orders,” “Payments,” or “Inventory”). Netflix was one of the earliest and most public pioneers of this shift, starting around 2009. Amazon had already been moving toward a service-oriented model even earlier, famously mandating that teams could only communicate through well-defined APIs.

But engineers quickly discovered a hidden trap: if you split your application into independent microservices but they all still talk to one shared database, you haven’t actually gained independence. The database becomes a silent thread that ties every “independent” service back together. Changing a column name for one service could break five other services that quietly depended on that same table.

The database-per-service pattern emerged directly from this lesson. It became a foundational principle documented by pattern communities like microservices.io (curated by Chris Richardson) and is now considered one of the “must-know” building blocks of proper microservices design.

Key idea

Microservices without database-per-service are not truly independent — they are a monolith wearing a costume. Real independence requires owning your own data.

02 · Motivation

The Problem & Motivation

Let’s build up the problem step by step, the way an architect would explain it to a new engineer on their first day.

2.1 · What a shared database looks like

Picture an e-commerce company with three teams: Orders, Inventory, and Payments. Each team builds its own microservice. But for convenience (and because it feels faster at first), all three services connect to the same big database, reading and writing tables like orders, products, and transactions.

The Shared-Database Anti-Pattern Order Service Java / Spring Boot Inventory Service Node.js Payment Service Python ONE SHARED DATABASE orders · products · transactions Every team's code is secretly entangled here.
Fig 1 — Three “independent” microservices, all silently coupled through one shared database.

2.2 · The cracks start to show

This shared setup looks fine on day one. But over months, real problems appear.

A · Hidden coupling

The Inventory team decides to rename a column from qty to quantity_available to make it clearer. They test their own service — it works. They deploy. Suddenly the Order service, which was quietly reading that same column, crashes in production. Nobody on the Inventory team even knew the Order service depended on it. That’s hidden coupling — services that look separate in the org chart but are welded together at the data layer.

B · No independent scaling

During a big sale, the Order service gets hit with 50x normal traffic. The team wants to scale their database to handle it. But since Payments and Inventory share that same database, scaling it up also means paying for capacity those other services don’t need, and any database-level change (like adding an index) risks slowing down someone else’s queries too.

C · Technology lock-in

The Inventory team realises their data is really a graph (products connect to categories connect to suppliers) and a graph database would suit them much better than a relational one. But they can’t switch — the whole company is standardised on one shared relational database, and switching would require rewriting every other team’s queries too.

D · One failure takes everyone down

If the shared database server has a disk failure or a spike in slow queries from one careless team, every service that depends on it goes down together — even services that had nothing to do with the mistake. This is called a single point of failure.

The real cost

A shared database quietly turns “independent microservices” back into a monolith. You get all the operational complexity of microservices (multiple codebases, multiple deployments, network calls) without getting the real benefit — true team and system independence.

2.3 · The solution

The database-per-service pattern fixes this by drawing a hard boundary: each service gets its own database, and only that service is allowed to read or write to it directly. Anyone else who needs that data must go through the service’s API.

That single rule — “no one touches another service’s data directly” — is what allows microservices to actually be deployed, scaled, and evolved independently, which is the entire reason microservices architecture exists in the first place.

03 · Fundamentals

Core Concepts

Before going deeper, let’s build a solid vocabulary. Each term below includes what it is, why it exists, where it’s used, an analogy, and an example.

Microservice Bounded Context Data Ownership Encapsulation Loose Coupling Polyglot Persistence

3.1 · Microservice

What it is: A small, independently deployable software service that handles one specific business capability.

Why it exists: To let teams build, deploy, and scale pieces of a system independently instead of one giant, tangled application.

Where it’s used: Large-scale systems at companies like Netflix, Amazon, Uber, and Spotify.

Analogy

A restaurant kitchen split into stations — a grill station, a salad station, a dessert station. Each station has its own chef, tools, and ingredients, and focuses on one job.

Example: An OrderService that only handles creating, updating, and cancelling orders.

3.2 · Bounded Context

What it is: A concept from Domain-Driven Design (DDD) describing a clear boundary within which a particular business model and its terms have one consistent meaning.

Why it exists: The word “Customer” might mean something different to the Billing team (someone with a payment method) than to the Support team (someone with an open ticket). A bounded context prevents this confusion by drawing a line around each meaning.

Where it’s used: As the guide for deciding where one microservice ends and another begins.

Analogy

The word “pitch” means something different to a football coach, a musician, and a salesperson. Each profession is its own “bounded context” for that word.

Example: The Order service’s idea of a “Product” (SKU, price, order-line quantity) is different from the Catalog service’s idea of a “Product” (images, descriptions, reviews).

3.3 · Data Ownership

What it is: The principle that exactly one service is responsible for a given piece of data — creating it, updating it, and being the “source of truth” for it.

Why it exists: Without clear ownership, multiple services might write conflicting updates to the same data, causing corruption or confusion about which value is correct.

Analogy

In a shared class notebook where everyone can erase and rewrite any page, notes get contradictory fast. If instead each student owns their own notebook and only shares summaries, everyone stays in sync without conflicts.

Example: Only the Inventory service is allowed to update stock counts. Every other service must ask the Inventory service’s API for the current stock — never write to the inventory table directly.

3.4 · Encapsulation (applied to data)

What it is: Hiding internal details of a service (including its database schema) behind a public interface (its API).

Why it exists: It lets a team completely redesign their internal database — even switch database technology — without breaking anyone else, as long as the public API contract stays the same.

Analogy

You don’t need to know how a car’s engine works to drive it. You just use the steering wheel and pedals — the “interface.” The manufacturer can redesign the engine completely, and you’d never notice, as long as the pedals still work the same way.

3.5 · Loose Coupling

What it is: A design quality where components depend on each other as little as possible, communicating only through well-defined interfaces.

Why it exists: Loosely coupled systems can change one part without breaking others — critical for large teams shipping code independently.

Example: The Order service calls GET /inventory/{productId}/stock instead of running SELECT quantity FROM inventory_db.stock directly.

3.6 · Polyglot Persistence

What it is: Using different types of databases for different services, based on what each service actually needs.

Why it exists: Not all data fits the same shape. Orders fit neatly into rows and columns (relational). Product recommendations fit better into a graph. Shopping cart sessions fit well into a fast key-value store.

Production example

Netflix famously uses Cassandra (a wide-column NoSQL store) for viewing history because it needs to handle huge write volumes across the globe, while using MySQL for more transactional, relationally-structured data elsewhere in the company.

ServiceData shapeGood database choice
Order ServiceStructured, transactionalPostgreSQL / MySQL
Product CatalogFlexible, document-likeMongoDB
Session / CartSimple key-value, fastRedis
RecommendationsHighly connected relationshipsNeo4j (Graph DB)
Search / DiscoveryFull-text searchElasticsearch
Analytics / EventsMassive write throughput, time-seriesCassandra / ClickHouse
04 · Structure

Architecture & Components

Let’s now look at what a properly designed database-per-service system looks like, piece by piece.

Database-Per-Service Architecture Order Service orders_db PostgreSQL private — no one else connects Inventory Service inventory_db MongoDB private — no one else connects Payment Service payments_db MySQL private — no one else connects REST · gRPC · Events API Gateway single entry point for clients Client Apps — Web / Mobile
Fig 2 — Each service owns a private database and a different technology. Services never share data directly — only through APIs or events.

4.1 · Component 1 — the private database

Each service gets its own database instance, schema, or even a separate database server entirely — the level of isolation depends on the maturity and scale of the organisation.

Isolation levelDescriptionWhen it’s enough
Separate schemaSame physical database server, but each service gets its own schema/namespace with restricted credentials.Smaller teams, early-stage systems, lower operational overhead.
Separate database instanceEach service connects to a logically distinct database (can be on the same server).Medium-sized systems needing clearer boundaries.
Separate database server / clusterComplete physical isolation, often even a different database technology.Large-scale systems (Netflix-, Amazon-scale) needing full independent scaling and failure isolation.

4.2 · Component 2 — the service’s public API

This is the only “front door” into a service’s data. It could be a REST API, a gRPC endpoint, or a GraphQL schema. Whatever the technology, the rule is the same: the API is the contract, and the database is a private implementation detail.

4.3 · Component 3 — access control at the database level

In good implementations, this rule isn’t just a “gentleman’s agreement” — it’s enforced technically. Each service is given database credentials that only that service’s application knows. Firewall rules or network policies (in Kubernetes, this is often done with Network Policies) can even block other services from being able to reach a database’s network port at all.

4.4 · Component 4 — inter-service communication layer

Since services can’t read each other’s databases, they need another way to exchange information — this is where APIs and event-driven messaging (like Kafka or RabbitMQ) come in. We’ll cover this in depth in the Data Patterns section.

05 · Mechanics

Internal Working

Let’s trace exactly what happens, step by step, when one service needs data that “belongs” to another service.

5.1 · Scenario — placing an order

A customer wants to buy 2 units of a product. The Order service needs to know: does Inventory have enough stock? And it needs Payment to charge the customer. Neither of those pieces of data live in the Order service’s own database.

Placing an Order — Cross-Service Workflow Client Order Service Inventory Service inventory_db Payment Service payments_db 1. POST /orders 2. checkStock() 3. stock OK 4. chargeCard() 5. paid 6. order confirmed orders_db No direct database access anywhere — only API calls.
Fig 3 — Order Service orchestrates the workflow by calling other services’ APIs instead of reading their databases.

5.2 · Step-by-step breakdown

  1. Client request

    The customer’s app sends POST /orders to the Order service with the product ID and quantity.

  2. Ask Inventory

    The Order service calls the Inventory service’s API — for example GET /inventory/{productId} — instead of querying inventory_db directly. It has no idea (and doesn’t need to know) whether Inventory uses MongoDB, PostgreSQL, or anything else.

  3. Inventory responds

    The Inventory service checks its own private database and returns a simple JSON answer: {"available": true, "stock": 48}.

  4. Ask Payment

    The Order service calls the Payment service’s API to charge the customer’s card.

  5. Payment responds

    Payment processes the charge against its own private database and returns a confirmation.

  6. Order is saved

    Only now does the Order service write a new row into its own orders_db, marking the order as confirmed.

Why this matters

Notice that the Order service never opened a direct connection to Inventory’s or Payment’s database. Every cross-service interaction happened through a published, versioned API — the only “legal” doorway into another service’s world.

5.3 · A minimal Java example

Here’s a simplified Spring Boot snippet showing how the Order service calls the Inventory service through its API, using a REST client, rather than a repository pointed at Inventory’s database.

OrderService.java
@Service
public class OrderService {

    private final RestTemplate restTemplate;
    private final OrderRepository orderRepository; // talks ONLY to orders_db

    public OrderService(RestTemplate restTemplate, OrderRepository orderRepository) {
        this.restTemplate = restTemplate;
        this.orderRepository = orderRepository;
    }

    public Order placeOrder(String productId, int quantity, String customerId) {

        // Step 1: Ask Inventory service via its API - NOT its database
        String inventoryUrl = "http://inventory-service/api/inventory/" + productId;
        StockResponse stock = restTemplate.getForObject(inventoryUrl, StockResponse.class);

        if (stock == null || stock.getAvailableQuantity() < quantity) {
            throw new InsufficientStockException(productId);
        }

        // Step 2: Ask Payment service via its API
        String paymentUrl = "http://payment-service/api/payments/charge";
        PaymentRequest paymentRequest = new PaymentRequest(customerId, quantity * stock.getUnitPrice());
        PaymentResponse payment = restTemplate.postForObject(paymentUrl, paymentRequest, PaymentResponse.class);

        if (payment == null || !payment.isSuccessful()) {
            throw new PaymentFailedException(customerId);
        }

        // Step 3: Only now write to OUR OWN database
        Order order = new Order(productId, quantity, customerId, payment.getTransactionId());
        return orderRepository.save(order);
    }
}

Notice the pattern: restTemplate is used to call other services, but orderRepository — the only class allowed to touch a real database — only ever talks to the Order service’s own tables.

06 · Lifecycle

Data Flow & Lifecycle

In a database-per-service system, data doesn’t just flow through synchronous API calls — it often flows through events as well. Let’s look at both models.

6.1 · Model 1 — synchronous (request / response)

This is what we saw above — one service directly calls another and waits for an answer. It’s simple to reason about, but it creates a runtime dependency: if Inventory is down, the Order service’s request fails too.

6.2 · Model 2 — asynchronous (event-driven)

Instead of Order directly calling Payment and Inventory and waiting, the Order service can publish an event — like OrderPlaced — onto a message broker (like Apache Kafka or RabbitMQ). Inventory and Payment services subscribe to that event and react independently, each updating their own database and possibly emitting their own follow-up events.

Event-Driven Data Flow Order Service Event Broker Kafka / RabbitMQ Inventory Service Payment Service Notification Service publish OrderPlaced subscribe inventory_db payments_db notify_db Each subscriber updates its own database independently.
Fig 4 — Event-driven data flow: services react to events instead of being directly called.

6.3 · The full lifecycle of a piece of data

  1. Creation

    Data is born inside the owning service (e.g., a new order row is created only by the Order service).

  2. Local persistence

    The owning service writes it to its own private database using its own schema design.

  3. Publication (optional but common)

    The owning service may publish an event describing what changed (e.g., OrderPlaced, OrderCancelled).

  4. Consumption

    Interested services subscribe to that event and build their own local copies or derived views of the data they need (this is sometimes called a “read model” or “materialised view”).

  5. Query via API

    Any service (or client) that needs fresh data on demand calls the owning service’s API — never the database.

  6. Update / deletion

    Only the owning service can update or delete a record. It then republishes events so subscribers can update their local copies.

Important concept — eventual consistency

Because updates propagate through events rather than shared transactions, other services’ local copies of the data might be a few milliseconds — or in rare cases, seconds — out of date. This is called eventual consistency: the system guarantees that, given enough time and no new updates, all copies will eventually agree. This is a fundamental trade-off of the database-per-service pattern, and we’ll dig into it deeply in the next section.

07 · Trade-offs

Advantages, Disadvantages & Trade-offs

Every architectural choice has two sides. Here they are laid out honestly.

Advantages

  • True independence: Teams can change their schema without coordinating with other teams.
  • Independent scaling: Scale only the database that actually needs it.
  • Fault isolation: A database outage in one service doesn’t crash unrelated services.
  • Polyglot persistence: Pick the best database technology per service’s needs.
  • Faster, safer deployments: Schema migrations are local to one team, reducing “big bang” release risk.
  • Clear ownership: Everyone knows exactly who is responsible for a given piece of data.

Disadvantages

  • No easy joins across services: You can’t write one SQL query that joins Orders and Inventory tables anymore.
  • Distributed transactions are hard: Classic ACID transactions across two databases aren’t naturally possible — you need patterns like Saga (covered later).
  • Eventual consistency complexity: Developers must design for the fact that data can be briefly out of sync.
  • Operational overhead: More databases to provision, monitor, back up, and secure.
  • Data duplication: Services often keep local copies of data they don’t own, which uses more storage and requires sync logic.
  • Higher learning curve: Teams need to understand distributed systems concepts, not just SQL.

7.1 · The CAP theorem connection

The CAP theorem, formulated by computer scientist Eric Brewer in 2000, states that a distributed data system can only fully guarantee two out of these three properties at the same time during a network failure:

  • Consistency (C): every read receives the most recent write.
  • Availability (A): every request receives a (non-error) response, even if it’s not the latest data.
  • Partition tolerance (P): the system keeps working even if network communication between nodes breaks down.

Since real-world networks do occasionally fail, partition tolerance isn’t really optional — so in practice, systems must choose between prioritising Consistency (CP) or Availability (AP) during a network split. Database-per-service architectures generally lean toward AP for cross-service data (favouring availability, accepting eventual consistency), while each individual service’s own database can still be fully CP internally for its own local transactions.

Analogy for eventual consistency

Imagine mailing a letter to a friend telling them you moved to a new address. The moment you drop the letter in the mailbox, your friend doesn’t instantly know — there’s a delay while the letter travels. Eventually, though, they’ll have the updated information. Distributed data works the same way: updates take a little time to “travel” between services.

7.2 · When to use vs. when to avoid

Use database-per-service when…Be cautious when…
You have (or are building toward) true microservices with independent teams.You have a small team and a small application — the overhead may not be worth it yet.
Services need to scale very differently from each other.Your data is tightly relational and heavily joined across “services” (may indicate wrong service boundaries).
Different data types genuinely benefit from different database technologies.Your team lacks experience with distributed systems and eventual consistency.
Fault isolation between business domains is a business requirement.You need strict, real-time cross-entity consistency for regulatory or safety reasons without extra engineering investment.
08 · Scale

Performance & Scalability

One of the biggest wins of database-per-service is the ability to scale each piece of your system according to its actual load — not a one-size-fits-all approach.

8.1 · Independent vertical & horizontal scaling

Imagine a flash-sale event. The Order and Inventory services get hammered with 100x normal traffic, but the internal “Employee HR” service sees no change at all. With database-per-service, you can add read replicas, increase compute, or shard specifically for Orders and Inventory — while leaving the HR database completely untouched, saving cost and avoiding unnecessary risk.

8.2 · Read replicas

What it is: Copies of a database that handle read-only queries, keeping the primary database free to handle writes.

Why it exists: Most systems read data far more often than they write it (often 80–90% reads). Spreading reads across replicas reduces load on the primary.

Analogy

A library with one master copy of a popular book locked in the reference section, but ten photocopies available for people to browse. The librarian (primary database) only needs to update the master copy; the photocopies (replicas) are refreshed periodically.

8.3 · Sharding (horizontal partitioning)

What it is: Splitting a single service’s database into multiple smaller databases (“shards”), each holding a subset of the data — commonly split by a key like customer region or customer ID range.

Why it exists: Even a service’s own database can eventually get too large or too busy for one machine. Sharding lets one logical database scale beyond a single server’s limits.

Production example

Uber’s data platform historically sharded its trip and location data by geography and time, since queries were naturally scoped to a city or region, allowing near-linear horizontal scaling as they expanded to new markets.

8.4 · Concurrency control

What it is: The set of techniques a database uses to make sure that when multiple requests try to read and write the same piece of data at the same time, the result stays correct instead of corrupted.

Why it exists: Imagine two customers trying to buy the very last unit of a product at the exact same millisecond. Without concurrency control, both requests could read “1 in stock,” both proceed to “buy,” and the Inventory service would end up with -1 stock — a data-corruption bug.

Analogy

Think of the last seat on a train. If two booking clerks both look at the seat map at the same instant and both mark it as sold to different people, you get double-booking. A good booking system makes sure only one clerk can “lock” that seat at a time.

There are two broad strategies:

  • Pessimistic locking: the database locks a row the moment it’s read for update, forcing other transactions to wait. Safe, but can hurt throughput under heavy contention.
  • Optimistic locking: no lock is taken upfront. Instead, each row carries a version number. When updating, the database checks that the version hasn’t changed since it was read; if it has, the update is rejected and the application retries. This performs better when conflicts are rare.
Java — optimistic locking with JPA
@Entity
public class InventoryItem {
    @Id
    private String productId;

    private int quantity;

    @Version   // JPA automatically checks and increments this on every update
    private long version;
}

Because each service’s database is private and independently scoped, concurrency-control decisions (which strategy, which isolation level) become a purely local choice for that service’s team — another quiet benefit of the database-per-service pattern.

8.5 · Consensus algorithms (under the hood)

What it is: An algorithm that lets multiple machines agree on a single, consistent value or ordering of events, even when some machines might fail or messages might be delayed.

Why it exists: A single database server is a single point of failure. To replicate data safely across multiple machines without them disagreeing about “what really happened,” distributed databases need a mathematically proven way to reach agreement.

Analogy

A group of friends deciding where to eat dinner over a spotty phone connection, where some messages might arrive late or get lost. A consensus algorithm is like an agreed-upon voting procedure that guarantees the group still lands on one final decision everyone accepts, even with some dropped calls.

Two of the most well-known consensus algorithms are:

  • Paxos: one of the earliest formally proven consensus algorithms, published by Leslie Lamport, but notoriously difficult to understand and implement correctly.
  • Raft: designed later specifically to be easier to understand and implement than Paxos, while providing the same guarantees. Raft elects a single “leader” node that coordinates writes and replicates them to “follower” nodes.

As an application developer building services in a database-per-service architecture, you rarely implement Raft or Paxos yourself — but it’s important to know that they’re working quietly underneath managed database clusters (like etcd, CockroachDB, or multi-node MongoDB replica sets) to keep replicas in agreement and to elect a new primary automatically if the current one fails.

8.6 · Partitioning strategies in depth

Beyond simple sharding, there are a few common ways to split data:

StrategyHow it worksTrade-off
Range-basedSplit by a range of values (e.g., customer IDs 1–1,000,000 on shard A).Simple, but can create “hot shards” if traffic isn’t evenly spread across ranges.
Hash-basedA hash function maps each key to a shard, spreading load evenly.Even distribution, but range queries (e.g., “all orders this week”) become harder.
GeographicSplit by region (e.g., EU customers, US customers).Great for data residency and latency, but requires careful handling of cross-region operations.

8.7 · Connection pooling

What it is: Reusing a fixed set of already-open database connections instead of opening a brand-new connection for every request.

Why it exists: Opening a database connection is expensive (a TCP handshake plus authentication). Under high load, reusing pooled connections dramatically improves throughput.

Spring Boot — HikariCP pool configuration
spring:
  datasource:
    url: jdbc:postgresql://orders-db-host:5432/orders_db
    username: orders_service_user
    password: ${ORDERS_DB_PASSWORD}
    hikari:
      maximum-pool-size: 20
      minimum-idle: 5
      connection-timeout: 30000
      idle-timeout: 600000

8.8 · Caching layer

Adding a cache (like Redis) in front of a frequently-read but rarely-changed dataset (e.g., product catalog details) reduces database load significantly. We’ll expand on this in the Data Patterns section.

80–90%
typical read-vs-write ratio in web apps
10–100x
possible throughput gain from proper caching
1 service
= 1 independently scalable unit
0 shared
write paths across service boundaries
09 · Resilience

High Availability & Reliability

Owning your own database means owning your own uptime story too. Here are the techniques that keep it up.

9.1 · Replication

What it is: Keeping multiple synchronised copies of a database across different machines (and ideally different data centers).

Why it exists: If the primary database server fails — hardware crash, disk corruption, data-center outage — a replica can be promoted to take over, minimising downtime.

Analogy

A backup goalkeeper who watches every practice and knows the exact game plan. If the main goalkeeper gets injured, the backup steps in immediately without the team missing a beat.

9.2 · Failover

What it is: The automatic (or manual) process of switching from a failed primary database to a healthy replica.

Where it’s used: Managed cloud databases like Amazon RDS Multi-AZ, Azure SQL failover groups, and Google Cloud SQL high-availability configurations all provide automated failover.

9.3 · Circuit breakers

What it is: A safety mechanism that “trips” and stops sending requests to a service that’s failing repeatedly, giving it time to recover instead of piling on more load.

Why it exists: In a database-per-service world, the Order service depends on the Inventory service’s API. If Inventory’s database is struggling and every request times out, continuing to hammer it makes recovery harder — and wastes the Order service’s own resources waiting.

Java — Resilience4j circuit breaker
@CircuitBreaker(name = "inventoryService", fallbackMethod = "fallbackStock")
public StockResponse checkStock(String productId) {
    return restTemplate.getForObject(
        "http://inventory-service/api/inventory/" + productId,
        StockResponse.class
    );
}

public StockResponse fallbackStock(String productId, Throwable t) {
    // Return a safe default instead of crashing the whole order flow
    return new StockResponse(productId, false, 0);
}

9.4 · Backup & disaster recovery

Since each service now owns its own database, each team is responsible for its own backup strategy — but this is also an advantage: a corrupted backup or failed restore in one service doesn’t threaten every other team’s data.

TermMeaning
RPO (Recovery Point Objective)The maximum acceptable amount of data loss, measured in time (e.g., “we can afford to lose up to 5 minutes of data”).
RTO (Recovery Time Objective)The maximum acceptable time to restore service after a failure (e.g., “we must be back online within 15 minutes”).

Common mistake

Teams sometimes forget that “more databases” means “more things that need backup policies, encryption keys, and monitoring.” Skipping this for a “small” service is a common cause of painful, avoidable data-loss incidents.

10 · Protection

Security

Owning your own data isn’t only about deployment speed — it also gives you sharper security boundaries.

10.1 · Principle of least privilege

What it is: Giving each service (and each person) only the exact permissions they need — nothing more.

Why it exists: If a service’s credentials are ever leaked or the service is compromised, the “blast radius” is limited to just that one service’s data — not the entire company’s database.

Analogy

A hotel gives you a keycard that only opens your room, not every room in the building. If you lose your keycard, only your room is at risk.

10.2 · Network-level isolation

In modern cloud-native deployments, databases live inside private subnets or VPCs, and only the owning service’s application pods are allowed network access — enforced through security groups (AWS), firewall rules (GCP), or Kubernetes Network Policies.

10.3 · Secrets management

Database credentials should never be hardcoded. Tools like HashiCorp Vault, AWS Secrets Manager, or Kubernetes Secrets (with encryption at rest) inject credentials securely at runtime.

10.4 · Encryption

  • Encryption at rest: data stored on disk is encrypted, so a stolen hard drive doesn’t expose raw data.
  • Encryption in transit: data traveling between a service and its database (and between services) uses TLS, so it can’t be read if intercepted on the network.

10.5 · Auditing & compliance

With clear data ownership, compliance becomes easier: for GDPR “right to be forgotten” requests, you know exactly which service owns a customer’s personal data and needs to delete it, instead of hunting across a giant shared schema.

Bonus benefit

Database-per-service naturally supports data-residency requirements too — for example, storing European customer data only in EU-based database instances for a specific service, without affecting how other services are deployed.

11 · Observability

Monitoring, Logging & Metrics

With many small databases instead of one big one, visibility becomes even more important — you can’t just “look at the one database” anymore.

11.1 · Key metrics to track per database

  • Query latency (p50, p95, p99): how long queries take, especially the slowest ones that affect real users.
  • Connection pool saturation: whether a service is running out of available database connections.
  • Replication lag: how far behind a read replica is from the primary.
  • Disk usage & IOPS: storage capacity and read/write operations per second.
  • Error rate: failed queries, deadlocks, timeouts.

11.2 · Distributed tracing

What it is: A technique for following a single request as it travels across multiple services (and their databases), tagging each step with a shared “trace ID.”

Why it exists: When the Order → Inventory → Payment flow is slow, distributed tracing tells you exactly which service (and which database call) was the bottleneck — something impossible to see by looking at just one service’s logs.

Analogy

A package-tracking number that follows your parcel through every warehouse and truck. You can see exactly where it is and how long it spent at each stop — instead of just knowing it eventually arrived.

Popular tools: OpenTelemetry (the modern, vendor-neutral standard), Jaeger, Zipkin, and commercial APM tools like Datadog and New Relic.

11.3 · Centralised logging

Even though each service owns its database, logs from all services should still flow into one centralised system (like the ELK stack — Elasticsearch, Logstash, Kibana — or Grafana Loki) so engineers can search across the whole system in one place during an incident.

11.4 · Health checks

Each service should expose a health endpoint that also verifies it can reach its own database — not just that the service process is alive.

Spring Boot Actuator health check
management:
  endpoint:
    health:
      show-details: always
  health:
    db:
      enabled: true   # Automatically checks database connectivity
12 · Operations

Deployment & Cloud

More databases doesn’t have to mean more chaos — if you automate the platform layer well.

12.1 · Infrastructure as Code

With many databases to manage, manually clicking through a cloud console doesn’t scale. Tools like Terraform or Pulumi let teams define each service’s database — including size, backups, and access rules — as version-controlled code.

12.2 · Containers & orchestration

Services (and often their databases, especially in non-production environments) run in containers managed by Kubernetes. Each service’s database credentials and connection details are injected as Kubernetes Secrets and ConfigMaps.

12.3 · Managed database services

Most companies today avoid manually running their own database servers and instead use managed offerings:

CloudManaged relationalManaged NoSQL
AWSRDS, AuroraDynamoDB, DocumentDB
Google CloudCloud SQL, AlloyDBFirestore, Bigtable
AzureAzure SQL DatabaseCosmos DB

12.4 · Schema migrations

Since each service owns its schema independently, database migration tools like Flyway or Liquibase are typically embedded directly into each service’s own deployment pipeline — meaning a schema change ships and rolls back together with the service’s own code, not as a company-wide event.

12.5 · CI/CD per service

Each microservice — and by extension, its database migrations — gets its own independent CI/CD pipeline. This means the Order team can deploy ten times a day without needing sign-off from the Payment or Inventory teams.

Production example

Amazon has described deploying changes to production on average every few seconds across its many independent service teams — a pace that would be impossible if every team shared one database and needed to coordinate every schema change.

13 · Data Management

Databases, Caching & Related Patterns

Database-per-service creates a new challenge: how do you handle operations that used to be “easy” with one shared database, like joins and transactions across entities? Several well-established patterns solve this.

13.1 · Pattern — Saga (distributed transactions)

What it is: A sequence of local transactions, where each service completes its own transaction and then publishes an event to trigger the next step. If a step fails, previous steps are undone using compensating transactions.

Why it exists: Traditional ACID transactions can’t span multiple independent databases. Sagas provide a way to maintain data consistency across services without a shared database or a distributed lock.

Analogy

Booking a trip: you reserve a flight, then a hotel, then a rental car. If the rental car booking fails, you don’t just leave the flight and hotel booked randomly — you cancel them too. Each step has a corresponding “undo” action.

Saga Pattern — Distributed Transaction with Compensation 1. Reserve Stock 2. Charge Payment 3. Confirm Order FAILS card declined Compensating — Release Reserved Stock undo step 1 automatically Each service's local transaction has a matching "undo" action.
Fig 5 — A Saga: when step 2 fails, a compensating transaction undoes step 1 instead of relying on a shared database rollback.

There are two common ways to coordinate a Saga:

  • Choreography: each service listens for events and decides what to do next on its own — no central coordinator. Simple for a few steps, but can get hard to follow as complexity grows.
  • Orchestration: a dedicated orchestrator service explicitly tells each participant what to do and in what order, and handles compensations centrally. Easier to understand and debug for complex workflows.

13.2 · Pattern — API Composition

What it is: When a client needs data that spans multiple services (like “show me an order with its product details and shipping status”), a composer — often an API Gateway or a dedicated “composer service” — calls each relevant service’s API and merges the results in memory.

Analogy

Ordering a combo meal where the rice comes from one station, the curry from another, and the bread from a third — but the server on the counter assembles them onto one tray before handing it to you.

13.3 · Pattern — CQRS (Command Query Responsibility Segregation)

What it is: Separating the model used to write data (commands) from the model used to read data (queries) — often backed by entirely different data stores optimised for each purpose.

Why it exists: API composition can get slow and complicated for read-heavy, multi-service views. CQRS solves this by maintaining a pre-built, denormalised “read model” that’s updated via events, so complex reads become a single fast query instead of many API calls stitched together.

13.4 · Pattern — Event Sourcing

What it is: Instead of storing only the current state of data, you store every change as a sequence of immutable events. The current state is derived by replaying those events.

Analogy

A bank doesn’t just store your current balance — it keeps a ledger of every deposit and withdrawal. Your balance is simply the result of adding up the entire history. This gives a complete audit trail and lets you “replay” history to any point in time.

13.5 · Caching strategies

StrategyHow it worksGood for
Cache-asideApp checks cache first; on a miss, reads from DB and populates the cache.General-purpose read-heavy data.
Write-throughEvery write goes to the cache and the database at the same time.Data that must always be fresh in cache.
Write-behindWrites go to the cache first, and are asynchronously flushed to the database later.Very high write-throughput scenarios (with some risk of loss).

13.6 · Load balancing

Load balancers (like NGINX, AWS ELB, or a service mesh like Istio) distribute incoming traffic across multiple instances of a service — and by extension, spread database load evenly too, especially when combined with read replicas.

14 · Communication

APIs & Microservices

If the API is now the only doorway into a service’s data, it’s worth choosing the doorway carefully.

14.1 · REST vs gRPC vs GraphQL for inter-service calls

StyleBest forNotes
REST (HTTP/JSON)Public APIs, simple internal callsHuman-readable, widely supported, but heavier than binary protocols.
gRPCHigh-performance internal service-to-service callsUses Protocol Buffers (binary), much faster, strongly typed contracts.
GraphQLClient-facing composition of multiple servicesLets clients ask for exactly the fields they need across services.

14.2 · API Gateway

What it is: A single entry point that sits in front of all microservices, routing client requests to the right service, and often handling authentication, rate limiting, and composition.

Analogy

A hotel’s front desk. Guests don’t wander the building looking for housekeeping or room service directly — they ask the front desk, which routes their request to the right department.

14.3 · Service discovery

Since each service (and its database) can be scaled independently, IP addresses of running instances change constantly. Tools like Consul, Eureka, or Kubernetes’ built-in DNS-based service discovery let services find each other by name instead of a hardcoded address.

14.4 · Contract-first API design

Because a service’s database is now hidden, its API contract becomes its most important public promise. Teams often define this contract first — using OpenAPI (Swagger) for REST or .proto files for gRPC — before writing implementation code, so consuming teams can start integrating early.

15 · Patterns

Related Design Patterns & Anti-patterns

Every real production architecture is a mix of related patterns — and a careful avoidance of a few anti-patterns.

15.1 · Related patterns

  • Shared database (anti-pattern, the “before” picture): multiple services reading/writing the same database directly. This is essentially the problem database-per-service was invented to solve.
  • Database view pattern: a limited, sometimes-used compromise where a read-only database view exposes a restricted slice of one service’s data to another — generally discouraged for true microservices because it still creates schema coupling.
  • Shared library / shared kernel (DDD): sharing a small, carefully-versioned code library (like common data validation logic) between services — different from sharing a database, and used sparingly.
  • Backend for Frontend (BFF): a specialised API composition layer built specifically for one type of client (mobile vs. web), often aggregating multiple database-per-service backends into a tailored response.

15.2 · Anti-patterns to avoid

Anti-pattern 1 — the “secret backdoor”

A developer, under deadline pressure, adds direct database credentials for Inventory’s database into the Order service “just this once” to fix an urgent bug. This quietly reintroduces the shared-database problem and is one of the most common ways database-per-service architectures decay over time.

Anti-pattern 2 — the distributed monolith

Services are physically separated with separate databases, but they’re so tightly coupled through synchronous calls (Order must call Inventory must call Payment, all waiting on each other) that a single slow dependency cascades and takes down the whole system anyway — the isolation exists on paper but not in practice.

Anti-pattern 3 — god service

One service ends up owning far too much data and logic — effectively becoming a mini-monolith inside the microservices architecture — because service boundaries weren’t drawn using proper Domain-Driven Design analysis.

Anti-pattern 4 — chatty services

To assemble one screen of data, a service makes dozens of small synchronous calls to five other services, creating huge latency and fragility. This usually signals a need for CQRS-style read models or better service boundaries.

16 · Practice

Best Practices & Common Mistakes

A distilled checklist you can print out and stick above the whiteboard.

16.1 · Best practices

  1. Design boundaries using DDD first, then databases second

    Get your bounded contexts right before deciding how many databases you need.

  2. Enforce isolation technically, not just by convention

    Use separate credentials and network policies so it’s physically impossible to “cheat.”

  3. Publish domain events for anything another service might care about

    This keeps synchronous coupling low.

  4. Version your APIs

    Since the database is hidden, the API is now your real contract — treat breaking changes with the seriousness of a breaking database migration.

  5. Automate schema migrations per service

    Use Flyway or Liquibase, tied to that service’s own CI/CD pipeline.

  6. Monitor replication lag & eventual consistency windows

    So you know how “eventual” your eventual consistency really is in production.

  7. Start with fewer, coarser services

    If you’re early-stage, split further only when clear boundaries and team ownership emerge — don’t over-fragment prematurely.

16.2 · Common mistakes

MistakeWhy it hurts
Splitting services before understanding the domainResults in constant, painful cross-service joins because the boundaries are in the wrong place.
Forgetting that “eventual consistency” needs product and UX buy-inCustomers may briefly see stale data, and that needs to be an accepted, designed-for behavior, not a surprise bug.
Treating every read as needing to call another service’s API in real timeIgnores event-driven local read models where they would be more appropriate and cheaper.
Underestimating operational costOf running, patching, and monitoring dozens of separate databases.
Not planning for distributed transactionsUntil a production incident forces the issue — Sagas should be designed up-front for any multi-service business workflow involving money or inventory.
17 · Industry

Real-World & Industry Examples

These aren’t textbook diagrams — they’re the actual patterns running behind services you use every day.

Netflix

Netflix is one of the most cited examples of large-scale microservices adoption, with hundreds of independently deployable services, each generally owning its own datastore, often choosing Cassandra for services needing massive write throughput across globally distributed data centers, while other services use different databases suited to their specific access patterns.

Amazon

Amazon’s internal architecture is built around the principle that teams communicate exclusively through service interfaces — famously formalised in an internal mandate attributed to Jeff Bezos in the early 2000s — which naturally extends to teams owning their own data stores rather than sharing a central database.

Uber

Uber operates thousands of microservices supporting its ride-hailing, delivery, and freight businesses, with different services using different datastores (including MySQL, Cassandra, and its own custom-built storage systems) matched to each service’s specific latency and throughput needs.

Spotify

Spotify’s engineering teams are organised around autonomous “squads,” each owning specific services end-to-end, including their own data-storage decisions — a structure that reflects Conway’s Law: system architecture tends to mirror an organisation’s communication structure.

Conway’s Law connection

Melvin Conway observed in 1967 that organisations design systems that mirror their own communication structure. Database-per-service works especially well when paired with small, autonomous teams — the technical boundary (database) and the organisational boundary (team) reinforce each other.

18 · Wrap-up

FAQ, Summary & Key Takeaways

Common questions engineers ask when adopting this pattern — and the takeaways worth pinning to your team wiki.

18.1 · Frequently asked questions

Does every microservice really need its own physical database server?

Not necessarily. Especially for smaller systems, a shared database server with strictly separated schemas and access-controlled credentials per service can be a reasonable first step. The core rule that matters most is logical isolation — no service reads or writes another service’s tables directly — not necessarily physical server separation.

How do you handle reporting or analytics that need data from many services?

Most organisations build a separate analytics pipeline — often using change data capture (CDC) or event streams to feed a data warehouse (like Snowflake or BigQuery) — rather than querying live operational databases directly for reports.

Is database-per-service only for microservices?

It’s most closely associated with microservices, but the underlying principle — clear data ownership and encapsulation — is valuable in any modular system design, including well-structured modular monoliths preparing for a future split.

What happens if two services both need to be the “source of truth” for overlapping data?

This usually signals that the domain boundaries need revisiting. In healthy designs, exactly one service owns each piece of data; other services may cache a local read-only copy, but they never treat their copy as authoritative.

How do you migrate from a shared database to database-per-service without downtime?

Most teams follow an incremental approach known as the “Strangler Fig” pattern: first, they identify the tables genuinely owned by one service and give that service exclusive write access, while other services are gradually migrated to call its new API instead of querying those tables directly. Once all direct access is removed, the tables are physically moved into a separate database. This is done table-group by table-group, over weeks or months, rather than as one risky “big bang” cutover.

Does this pattern cost more money?

Often yes, in raw infrastructure terms — more database instances mean more baseline cost. But it frequently saves money in the long run by avoiding downtime, preventing one team’s mistake from affecting everyone, and letting you right-size (and not over-provision) each service’s resources individually.

18.2 · Summary

The database-per-service pattern is a core architectural principle stating that each microservice owns a private database, and all access to that data must flow through the service’s own API. It emerged directly from painful lessons learned when companies split monoliths into microservices but left a shared database in place, silently recreating the same tight coupling they were trying to escape.

This pattern enables true independence — independent scaling, independent technology choices, independent deployments, and fault isolation — at the cost of new complexity: no easy cross-service joins, the need for distributed-transaction patterns like Sagas, and designing for eventual consistency. Supporting patterns like API composition, CQRS, and event-driven architecture exist specifically to manage this added complexity gracefully.

18.3 · Key takeaways

  1. Each microservice must own its data exclusively — no other service may access its database directly.
  2. All cross-service data access happens through APIs or asynchronous events, never raw database queries.
  3. This pattern is what makes microservices genuinely independent — without it, you have a “distributed monolith.”
  4. Polyglot persistence lets each team choose the best database technology for their specific data shape.
  5. Cross-service consistency requires new patterns: Sagas for transactions, CQRS and events for reads.
  6. Eventual consistency is a deliberate, designed-for trade-off — not a bug to be “fixed away.”
  7. Enforce database isolation technically (credentials, network policies), not just as a team agreement.
  8. Real companies — Netflix, Amazon, Uber, Spotify — rely on this pattern to operate at massive scale.

The one idea to remember

Database-per-service isn’t about having “more” databases — it’s about having owned ones. Ownership is what turns a set of programs into an actual microservices architecture.

Microservices without database-per-service are not microservices at all — they are a monolith wearing a very convincing costume.
#database-per-service #microservices #system-design #software-architecture #bounded-context #polyglot-persistence #saga-pattern #cqrs #event-driven #eventual-consistency