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.
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.
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.
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.
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.
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.
| Service | Data shape | Good database choice |
|---|---|---|
| Order Service | Structured, transactional | PostgreSQL / MySQL |
| Product Catalog | Flexible, document-like | MongoDB |
| Session / Cart | Simple key-value, fast | Redis |
| Recommendations | Highly connected relationships | Neo4j (Graph DB) |
| Search / Discovery | Full-text search | Elasticsearch |
| Analytics / Events | Massive write throughput, time-series | Cassandra / ClickHouse |
Architecture & Components
Let’s now look at what a properly designed database-per-service system looks like, piece by piece.
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 level | Description | When it’s enough |
|---|---|---|
| Separate schema | Same physical database server, but each service gets its own schema/namespace with restricted credentials. | Smaller teams, early-stage systems, lower operational overhead. |
| Separate database instance | Each service connects to a logically distinct database (can be on the same server). | Medium-sized systems needing clearer boundaries. |
| Separate database server / cluster | Complete 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.
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.
5.2 · Step-by-step breakdown
Client request
The customer’s app sends
POST /ordersto the Order service with the product ID and quantity.Ask Inventory
The Order service calls the Inventory service’s API — for example
GET /inventory/{productId}— instead of queryinginventory_dbdirectly. It has no idea (and doesn’t need to know) whether Inventory uses MongoDB, PostgreSQL, or anything else.Inventory responds
The Inventory service checks its own private database and returns a simple JSON answer:
{"available": true, "stock": 48}.Ask Payment
The Order service calls the Payment service’s API to charge the customer’s card.
Payment responds
Payment processes the charge against its own private database and returns a confirmation.
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.
@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.
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.
6.3 · The full lifecycle of a piece of data
Creation
Data is born inside the owning service (e.g., a new order row is created only by the Order service).
Local persistence
The owning service writes it to its own private database using its own schema design.
Publication (optional but common)
The owning service may publish an event describing what changed (e.g.,
OrderPlaced,OrderCancelled).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”).
Query via API
Any service (or client) that needs fresh data on demand calls the owning service’s API — never the database.
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.
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. |
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.
@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:
| Strategy | How it works | Trade-off |
|---|---|---|
| Range-based | Split 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-based | A hash function maps each key to a shard, spreading load evenly. | Even distribution, but range queries (e.g., “all orders this week”) become harder. |
| Geographic | Split 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:
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.
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.
@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.
| Term | Meaning |
|---|---|
| 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.
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.
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.
management:
endpoint:
health:
show-details: always
health:
db:
enabled: true # Automatically checks database connectivity
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:
| Cloud | Managed relational | Managed NoSQL |
|---|---|---|
| AWS | RDS, Aurora | DynamoDB, DocumentDB |
| Google Cloud | Cloud SQL, AlloyDB | Firestore, Bigtable |
| Azure | Azure SQL Database | Cosmos 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.
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.
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
| Strategy | How it works | Good for |
|---|---|---|
| Cache-aside | App checks cache first; on a miss, reads from DB and populates the cache. | General-purpose read-heavy data. |
| Write-through | Every write goes to the cache and the database at the same time. | Data that must always be fresh in cache. |
| Write-behind | Writes 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.
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
| Style | Best for | Notes |
|---|---|---|
| REST (HTTP/JSON) | Public APIs, simple internal calls | Human-readable, widely supported, but heavier than binary protocols. |
| gRPC | High-performance internal service-to-service calls | Uses Protocol Buffers (binary), much faster, strongly typed contracts. |
| GraphQL | Client-facing composition of multiple services | Lets 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.
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.
Best Practices & Common Mistakes
A distilled checklist you can print out and stick above the whiteboard.
16.1 · Best practices
Design boundaries using DDD first, then databases second
Get your bounded contexts right before deciding how many databases you need.
Enforce isolation technically, not just by convention
Use separate credentials and network policies so it’s physically impossible to “cheat.”
Publish domain events for anything another service might care about
This keeps synchronous coupling low.
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.
Automate schema migrations per service
Use Flyway or Liquibase, tied to that service’s own CI/CD pipeline.
Monitor replication lag & eventual consistency windows
So you know how “eventual” your eventual consistency really is in production.
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
| Mistake | Why it hurts |
|---|---|
| Splitting services before understanding the domain | Results in constant, painful cross-service joins because the boundaries are in the wrong place. |
| Forgetting that “eventual consistency” needs product and UX buy-in | Customers 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 time | Ignores event-driven local read models where they would be more appropriate and cheaper. |
| Underestimating operational cost | Of running, patching, and monitoring dozens of separate databases. |
| Not planning for distributed transactions | Until a production incident forces the issue — Sagas should be designed up-front for any multi-service business workflow involving money or inventory. |
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.
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
- Each microservice must own its data exclusively — no other service may access its database directly.
- All cross-service data access happens through APIs or asynchronous events, never raw database queries.
- This pattern is what makes microservices genuinely independent — without it, you have a “distributed monolith.”
- Polyglot persistence lets each team choose the best database technology for their specific data shape.
- Cross-service consistency requires new patterns: Sagas for transactions, CQRS and events for reads.
- Eventual consistency is a deliberate, designed-for trade-off — not a bug to be “fixed away.”
- Enforce database isolation technically (credentials, network policies), not just as a team agreement.
- 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.