Why Does Each Microservice Usually Need Its Own Database?
A complete, beginner‑to‑production guide to the Database‑per‑Service pattern — why shared databases quietly destroy microservice architectures, how independent data ownership works, and how companies like Netflix and Amazon actually do it in production.
Introduction & History
Imagine a big family living in one giant house, sharing one refrigerator. Everyone — mom, dad, grandma, the kids — puts their food in the same fridge. One day, the older brother eats the milk that grandma needed for her tea. Another day, someone rearranges the shelves and nobody can find anything anymore. The fridge becomes a fight zone, not because anyone is a bad person, but because too many people depend on one shared thing that nobody fully controls.
This is almost exactly what happens inside software systems when many services share one database. This tutorial is about a simple but powerful idea in modern software architecture: each microservice should usually own its own database, and no other service should be allowed to touch it directly.
A Quick Walk Through History
To understand why this rule exists, it helps to see where software architecture came from.
In the 1990s and early 2000s, most business applications were built as a monolith — one single large program that handled everything: the website, the business logic, the payment processing, the inventory tracking, all glued together and connected to one big shared database. This worked fine when companies were small. A single team could understand the whole codebase, and one database was simple to manage.
As companies like Amazon, Netflix, and eBay grew massively in the 2000s, their monoliths grew too — into what engineers now jokingly call a “big ball of mud.” Thousands of features were tangled together. A tiny change to the shipping module could accidentally break the billing module, because both modules quietly read and wrote the same database tables. Deployments became terrifying. Teams stepped on each other’s toes constantly.
Amazon was one of the pioneers of a different approach. Around the mid‑2000s, Amazon restructured its engineering teams around the idea of small, independent “two‑pizza teams” (teams small enough to be fed by two pizzas), each owning a specific business capability end‑to‑end — including its own data. Netflix followed a similar path around 2009–2012 while migrating to the cloud after a major database corruption incident convinced them that tightly coupled systems were too fragile. These experiences, among others, gave rise to what the industry now calls microservices architecture, popularized broadly around 2014 when James Lewis and Martin Fowler wrote an influential article describing the pattern.
A core, non‑negotiable rule emerged from all of this real‑world pain: if you split your application into independent services, you must also split your data. Otherwise, you have only pretended to build microservices — underneath, you still have one giant, fragile monolith wearing a costume.
By the end of this tutorial, you will understand exactly why shared databases break microservice architectures, how the “Database‑per‑Service” pattern works internally, how services talk to each other without a shared database, what trade‑offs you accept, and how real companies implement this at massive scale.
Problem & Motivation
Before learning the solution, you must deeply feel the problem. Let’s build it up slowly.
What Is a Microservice? (Quick Refresher)
A microservice is a small, independently deployable piece of software that does one specific job for a business — for example, an “Order Service” that only handles placing and tracking orders, or a “Payment Service” that only handles charging customers. Each microservice runs as its own separate program, usually communicating with others over a network using APIs.
Think of a restaurant kitchen split into stations: one chef only grills meat, another only makes salads, another only bakes desserts. Each station is a “microservice” — specialized, independent, and focused on one job.
An e‑commerce app might be split into: User Service, Product Service, Cart Service, Order Service, and Payment Service — each a separate small program.
The Trap: Sharing One Database
When teams first move from a monolith to microservices, a very natural (and very dangerous) mistake happens: they split the code into separate services, but leave all of them pointing at the same shared database. On the surface this looks fine — it even feels easier, because you don’t have to figure out how services will exchange data. But underneath, this single decision quietly destroys almost every benefit microservices were supposed to bring.
Fig 1 — Four “independent” services silently glued together by one shared database.
Here is what goes wrong, one problem at a time.
1. Hidden Coupling Through Tables
Suppose the Order Service reads directly from a users table that “belongs” to the User Service. Now the User Service team cannot rename a column, split a table, or change a data type without secretly breaking the Order Service — even though the two teams never talk to each other or review each other’s code. This is called tight coupling, and it is exactly what microservices were meant to eliminate.
2. You Cannot Deploy Independently Anymore
One of the biggest promises of microservices is that each team can release its own service whenever it wants, without asking permission from other teams. A shared database breaks this promise. A single schema migration (like renaming a column) can require every team touching that table to update their code and deploy together — turning “independent” services into a synchronized monolith release, just with extra network hops and extra complexity.
3. No Real Ownership
When ten services can write to the same table, nobody truly owns the data’s correctness. If the order_status column has a bad value, is it the Order Service’s bug, or did the Shipping Service write it wrong? Debugging becomes a blame game instead of a clear investigation.
4. One Database, One Point of Failure
If the shared database is overloaded — say, because the Reporting Service runs a huge, slow query — every other service that depends on that database slows down or fails too, even services that have nothing to do with reporting. Microservices were supposed to isolate failures, but a shared database reintroduces a single point of failure right in the center of the system.
5. Technology Lock‑In
Different services often have very different data needs. A Product Catalog Service might benefit from a document database (flexible, tree‑like product attributes). A Payment Service might need a strict relational database (for strong transactional guarantees). A Search Service might want a specialized search index. If everyone is forced onto one shared relational database, you lose the freedom to pick the right storage technology for each job.
Splitting your code into services but keeping one shared database does not give you microservices. It gives you a “distributed monolith” — you get all the complexity of a distributed system (network calls, partial failures, deployment coordination) with none of the true independence that justifies that complexity.
A Small Story to Make This Concrete
Imagine a mid‑size online bookstore. Three teams exist: the Catalog team, the Orders team, and the Reviews team. All three read and write to one shared PostgreSQL database because, early on, it felt faster to just add a new table than to set up a whole new database.
Six months later, the Catalog team wants to change how book prices are stored — moving from a single price column to a more flexible price_history table, so they can track discounts over time. Sounds simple. But the Orders team’s checkout code reads price directly from the books table to calculate totals, and the Reviews team’s “Top Rated Books Under $20” widget also reads that same column. The Catalog team cannot make their change without first finding, notifying, and coordinating with two other teams — teams that may be in different time zones, with different sprint schedules.
What was meant to be a two‑day improvement turns into a six‑week cross‑team project, full of meetings, shared migration scripts, and a synchronized “big bang” deployment weekend where all three teams must release together. This is exactly the kind of friction that database‑per‑service is designed to eliminate — not by making the underlying business complexity disappear, but by making sure that complexity doesn’t accidentally spread across teams that shouldn’t need to know about each other’s internal decisions.
Core Concepts
A handful of key terms carry almost all the weight in this pattern. Each one below follows the same structure: what it is, why it exists, and where you’ll actually see it in production.
The Database‑per‑Service Pattern
WhatEach microservice gets its own private database (or private schema / private set of tables) that only that service is allowed to read from or write to directly. No other service is permitted to open a direct connection to it.
WhyIt exists to guarantee true independence — the same reason microservices exist in the first place. If a service’s internal data structures are truly private, the team owning that service can change them freely, scale them independently, choose the best database technology for the job, and deploy on their own schedule.
Where usedVirtually every serious microservices deployment at Netflix, Amazon, Uber, Spotify, and Airbnb follows this pattern for their core business services.
Each family now lives in its own house with its own fridge. If the neighbor wants your leftovers, they have to knock on your door and ask — they can’t just walk in and open your fridge.
The Order Service has its own orders_db. The Payment Service has its own payments_db. Neither database is visible to the other.
Bounded Context (from Domain‑Driven Design)
What it is: A “bounded context” is a clear boundary around a specific part of the business, within which certain words and concepts have one precise meaning. For example, the word “Customer” might mean something slightly different in the Billing context (a paying account) versus the Support context (a person asking for help).
Why it exists: Big businesses are naturally made of separate sub‑domains. Trying to model everything with one giant shared data model causes constant conflict, because different teams need different views of the same real‑world concept. Bounded contexts let each team define “Customer” in a way that fits their own needs.
Simple analogy: In a hospital, the word “Patient” means something different to the Billing department (an account with charges) than to the Emergency department (a person needing urgent care right now). Both are correct — just for different contexts.
Database‑per‑Service is basically the physical, technical enforcement of bounded contexts: each bounded context gets its own database so its model of the world stays clean and independent.
Data Ownership
What it is: The principle that exactly one service is the single source of truth for a particular piece of data. Any other service that needs that data must ask the owning service for it — never read or write it directly.
Why it exists: Without clear ownership, data quickly becomes inconsistent, because multiple writers can disagree about what the “correct” value is.
Loose Coupling & High Cohesion
What it is: Loose coupling means services depend on each other as little as possible. High cohesion means everything inside one service is closely related and belongs together. Database‑per‑Service pushes both: data related to orders stays inside the Order Service (high cohesion), and other services don’t reach in and grab it directly (loose coupling).
“If you can write to my database directly, you are not really a separate service — you’re just running in a separate process.”
One more way to internalize this: think of each service as a small, independent company that happens to sit inside a larger organization. Just like a real company keeps its own internal financial records private and only shares agreed‑upon reports (an invoice, a quarterly summary) with partner companies, a well‑designed microservice keeps its internal database private and only shares agreed‑upon data through its public API or its published events. Nobody outside the company gets to walk into their accounting department and edit the ledger directly — and no other microservice should be able to do that to your database either.
Architecture & Components
Let’s look at what a properly isolated microservices data architecture actually looks like, piece by piece.
Fig 2 — Each service owns a private data store; cross‑service communication happens only via APIs and events.
Key Components
| Component | Role |
|---|---|
| Private Database | Owned exclusively by one microservice; no external direct access. |
| Service API (REST/gRPC) | The only “door” through which other services can request data or trigger actions. |
| Message Broker | Carries asynchronous events between services (e.g. Kafka, RabbitMQ, AWS SQS/SNS). |
| API Gateway | Single entry point for external clients; routes requests to the correct service. |
| Service Registry | Keeps track of which service instances are alive and where they are (e.g. Consul, Eureka). |
Three Common Isolation Levels
“Its own database” doesn’t always mean a completely separate physical server. There are a few valid levels of isolation, from lightest to strongest:
| Level | Description | When Used |
|---|---|---|
| Private Tables | Same database server, but only that service’s code is allowed to touch its tables (enforced by convention / code review). | Small teams, early‑stage systems |
| Private Schema | Same database server, separate schema / namespace per service, enforced with database permissions. | Medium systems, cost‑conscious teams |
| Private Database Instance | A fully separate database server or managed cloud database per service. | Large‑scale production systems, strict isolation needs |
You don’t have to start with a separate physical database server for every microservice on day one. Many teams begin with the “private schema, enforced by permissions” level and graduate to fully separate database instances as the service grows.
Internal Working
So if services can’t just read each other’s databases, how do they actually get the data they need? There are three main techniques.
1. Synchronous API Calls
One service calls another service’s API directly and waits for a response — like calling a friend on the phone and waiting for them to answer.
// Order Service calling Payment Service synchronously (Java + REST client)
public class PaymentClient {
private final RestTemplate restTemplate;
private final String paymentServiceUrl = "http://payment-service/api/payments";
public PaymentClient(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
public PaymentResponse chargeCustomer(String orderId, BigDecimal amount) {
PaymentRequest request = new PaymentRequest(orderId, amount);
// Order Service never touches payments_db directly.
// It only knows about this API contract.
return restTemplate.postForObject(paymentServiceUrl, request, PaymentResponse.class);
}
}
Simple to understand, but this creates a temporary, “live” dependency: if the Payment Service is slow or down, the Order Service call can hang or fail too.
2. Asynchronous Events
Instead of calling directly, a service publishes an “event” (a small message describing something that happened) onto a message broker. Other services subscribe to events they care about and react on their own time — like posting a notice on a public bulletin board instead of calling every neighbor individually.
// Order Service publishes an event after saving to its own database
public class OrderService {
private final OrderRepository orderRepository; // talks only to orders_db
private final EventPublisher eventPublisher; // talks to Kafka
public void placeOrder(Order order) {
orderRepository.save(order); // 1. Save to OWN database
OrderPlacedEvent event = new OrderPlacedEvent(
order.getId(), order.getCustomerId(), order.getTotalAmount()
);
eventPublisher.publish("order.placed", event); // 2. Announce the fact
}
}
// Inventory Service listens and reacts independently
@KafkaListener(topics = "order.placed")
public void onOrderPlaced(OrderPlacedEvent event) {
inventoryRepository.reserveStock(event.getOrderId()); // writes to OWN database
}
This is the preferred style for most cross‑service data flows in production, because it doesn’t create a live, blocking dependency between services.
3. Data Replication via Events (CQRS‑style read models)
Sometimes a service needs to display data that “belongs” to another service very frequently, and calling an API every time would be too slow. In that case, the service keeps its own small, local, read‑only copy of just the fields it needs, and keeps that copy updated by listening to events. This copy is not the source of truth — it’s a fast local cache built from events.
Fig 3 — A service keeps a lightweight, eventually‑consistent local copy instead of calling the owning service every time.
Replicated data is always a copy, never the source of truth. The owning service is still the only one allowed to change the real record. If the copy is stale for a second or two, that’s an accepted trade‑off called eventual consistency, explained later.
Idempotency: A Small Concept That Saves You From Big Bugs
What it is: An operation is “idempotent” if doing it once and doing it five times produce the exact same result. In event‑driven systems, messages can occasionally be delivered more than once (because of retries, network hiccups, or broker restarts), so every event handler needs to be safe to run twice.
Why it exists: Without idempotency, a duplicate payment.succeeded event could accidentally mark an order as paid twice, or worse, trigger a second charge. Idempotency is the safety net that lets services tolerate the messiness of distributed message delivery.
Simple analogy: Pressing an elevator button five times doesn’t send the elevator five times — the first press is what matters, and the rest are safely ignored.
Practical example: Before processing an event, a service checks whether it has already handled that event’s unique ID (often stored in a small “processed_events” table inside its own database). If it has, the handler simply does nothing and returns — no duplicate side effects.
// Idempotent event handling using a processed-events table (Java)
@KafkaListener(topics = "payment.succeeded")
@Transactional
public void onPaymentSucceeded(PaymentSucceededEvent event) {
if (processedEventRepository.existsById(event.getEventId())) {
return; // already handled — safe no-op
}
Order order = orderRepository.findById(event.getOrderId());
order.setStatus(OrderStatus.CONFIRMED);
orderRepository.save(order);
processedEventRepository.save(new ProcessedEvent(event.getEventId()));
}
Recording every processed event ID inside the same local transaction makes re‑delivery harmless.
Data Flow & Lifecycle
Let’s trace a complete, realistic example: a customer places an order on an e‑commerce site. Watch how data flows across services that each keep their own database.
Fig 4 — One business action fans out into multiple isolated, independently‑owned writes.
Step‑by‑Step Breakdown
- Order created: The Order Service writes a new row into
orders_dbwith statusPENDING. - Event published: The Order Service publishes an
order.placedevent. This is a fact, not a request — it simply says “this happened.” - Payment Service reacts: It reads the event, attempts to charge the customer, and writes the result into its own
payments_db. It then publishes eitherpayment.succeededorpayment.failed. - Inventory Service reacts: Independently, it reserves stock in
inventory_dband publishesstock.reservedorstock.unavailable. - Order Service listens back: It subscribes to payment and stock events and updates its own order status accordingly —
CONFIRMED,CANCELLED, etc. Only the Order Service ever writes to theorderstable.
Notice something important: no service ever reaches into another service’s tables. Every update stays inside the boundary of the service that owns that data. The system stays consistent through communication, not through shared storage.
Advantages, Disadvantages & Trade‑offs
Every architectural choice trades one thing for another. Being honest about both sides is what turns database‑per‑service from a fashionable rule into an engineering discipline.
Advantages
- True independent deployment — teams ship changes without coordinating schema changes with other teams.
- Fault isolation — a database outage in one service doesn’t directly crash unrelated services.
- Technology freedom (polyglot persistence) — pick SQL, NoSQL, graph, or search databases per service’s needs.
- Independent scaling — scale the busiest service’s database without over‑provisioning everyone else’s.
- Clear ownership and accountability — bugs and data quality issues map cleanly to one team.
- Smaller, more understandable schemas — easier onboarding, safer migrations.
Disadvantages
- No more simple SQL JOINs across services — you must call APIs or maintain replicated read models instead.
- Eventual consistency — data can be briefly out of sync across services (no more single ACID transaction spanning everything).
- Operational overhead — many small databases to provision, back up, patch, and monitor instead of one.
- Distributed transactions are hard — you need patterns like Sagas (covered later) instead of a simple database transaction.
- Higher infrastructure cost at small scale — running 15 databases costs more than running 1, until the org is big enough to benefit.
- Data duplication — some fields get copied into multiple services’ local read models, requiring careful sync logic.
When NOT to Use Database‑per‑Service
This pattern is powerful but not free. It usually isn’t worth the overhead for:
- Small startups with 1–3 engineers, still validating their product idea.
- Simple internal tools with low traffic and one small team.
- Systems that genuinely benefit from strong, immediate cross‑entity consistency and rarely need independent scaling (some financial ledgers, for example, may deliberately stay monolithic on purpose).
Teams sometimes adopt microservices and database‑per‑service purely because “Netflix does it,” without having Netflix’s scale or organizational size. The pattern solves organizational and scaling problems — if you don’t have those problems yet, you may just be adding complexity for no benefit.
Performance & Scalability
One of the strongest real‑world reasons for database‑per‑service is independent scaling.
In a monolith with one shared database, if the Product Search feature suddenly gets 100× more traffic (say, during a big sale), you’re forced to scale the entire database — including tables for billing, HR, and internal admin tools that don’t need it. This is wasteful and expensive.
With database‑per‑service, you scale exactly the services under pressure. Amazon, for example, has described scaling individual services to handle order‑of‑magnitude spikes during events like Prime Day, without needing to touch unrelated systems.
Scaling Techniques Per Service
| Technique | What It Does | Good For |
|---|---|---|
| Vertical scaling | Bigger machine (more CPU / RAM) for the database. | Quick wins, simpler operations |
| Read replicas | Copies of the database that only serve reads, reducing load on the primary. | Read‑heavy services (catalog, search) |
| Sharding / Partitioning | Splitting data across multiple database instances by a key (e.g. customer region). | Very large write‑heavy services |
| Caching layer | In‑memory store (Redis) in front of the database for hot data. | Frequently‑read, rarely‑changed data |
| Connection pooling | Reuse database connections instead of opening new ones per request. | All services, always |
CAP Theorem — A Foundational Trade‑off
What it is: The CAP theorem states that a distributed data system can only fully guarantee two out of three properties at the same time: Consistency (everyone sees the same data at the same instant), Availability (every request gets a response, even during failures), and Partition tolerance (the system keeps working even if network communication between nodes breaks).
Why it matters here: Since network partitions are always possible in a distributed system, you’re really choosing between consistency and availability when a partition happens. Different microservices in the same system can make different choices — a Payment Service might favor strong consistency (better to be briefly unavailable than to charge someone twice), while a Product Recommendation Service might favor availability (better to show slightly stale recommendations than no page at all).
Imagine two friends updating the same shared shopping list app while their phones briefly lose signal. CAP theorem says: either both phones show possibly‑different lists (available but inconsistent), or one phone refuses to show anything until it reconnects (consistent but unavailable).
Amazon’s DynamoDB, used heavily inside AWS and by Amazon’s own services, was explicitly designed to favor availability and partition tolerance, accepting eventual consistency for many use cases.
The Economics of Independent Scaling
It’s worth spelling out why independent scaling matters so much in real budgets, not just in theory. Suppose a company runs one large shared database sized to handle its busiest workload — typically the checkout and search paths during a big sales event. Every other, much quieter workload (internal reporting, admin dashboards, a rarely‑used loyalty‑points feature) still pays for a slice of that oversized, expensive hardware around the clock, even at 3 a.m. on a random Tuesday.
With database‑per‑service, the Checkout Service’s database can be provisioned for peak load and even auto‑scaled up and down with traffic, while the Loyalty Service’s small, rarely‑touched database runs on modest, inexpensive hardware. Over a year, this difference compounds into real, measurable savings — and it also means an unexpected spike in one lightly‑used service can never accidentally starve resources away from checkout, since they no longer share the same underlying machine at all.
High Availability & Reliability
Availability in a database‑per‑service architecture is not free — it is engineered, at every hop, using replication, quorum protocols, and thoughtful failure handling.
Replication
What it is: Keeping multiple copies of the same database data on different machines, so if one machine fails, another has the same data ready to take over.
Why it exists: Hardware fails, data centers lose power, disks corrupt. Replication protects against losing data or losing availability when any single machine dies.
Simple analogy: Keeping a backup key to your house with a trusted neighbor — if you lose your key, you’re not locked out.
Common Replication Patterns
- Primary‑Replica (Leader‑Follower): One node accepts writes; others copy from it and serve reads.
- Multi‑Leader: Multiple nodes accept writes, useful across regions, but requires conflict resolution.
- Leaderless (Quorum‑based): Any node can accept a write; reads/writes require agreement from a quorum of nodes (used by Cassandra, DynamoDB).
Consensus Algorithms
What it is: Algorithms like Raft and Paxos let a group of database nodes agree on a single, consistent value or order of operations, even when some nodes fail or messages get delayed.
Where it’s used: etcd (used by Kubernetes), CockroachDB, and many modern distributed databases use Raft internally to keep replicas in agreement about the true state of the data.
Failure Recovery Patterns
| Pattern | Purpose |
|---|---|
| Circuit Breaker | Stops calling a failing downstream service repeatedly, giving it time to recover instead of piling on more load. |
| Retry with Backoff | Automatically retries a failed call, waiting progressively longer between attempts. |
| Bulkhead | Isolates resources (like thread pools) per dependency so one slow dependency can’t exhaust all resources. |
| Automated Failover | Automatically promotes a replica to primary if the current primary database dies. |
// Simple Circuit Breaker usage with Resilience4j (Java)
CircuitBreakerConfig config = CircuitBreakerConfig.custom()
.failureRateThreshold(50) // open circuit after 50% failures
.waitDurationInOpenState(Duration.ofSeconds(10))
.build();
CircuitBreaker breaker = CircuitBreaker.of("paymentService", config);
Supplier<PaymentResponse> decorated = CircuitBreaker
.decorateSupplier(breaker, () -> paymentClient.chargeCustomer(orderId, amount));
PaymentResponse response = Try.ofSupplier(decorated)
.recover(throwable -> PaymentResponse.fallbackFailure())
.get();
A tripped circuit protects your service from wasting resources on a doomed downstream call.
Because each service’s database is isolated, a failure in the Payment Service’s database does NOT automatically corrupt or lock up the Order Service’s database. Failures stay contained — this is one of the biggest reliability wins of the pattern.
Security
Database‑per‑service also creates a natural security boundary — and that’s a major, often under‑appreciated reason companies adopt it.
Reduced Blast Radius
If an attacker compromises one service, they only gain access to that service’s database — not the entire company’s data. In a shared‑database world, one SQL injection vulnerability in a minor internal tool could expose payment records, personal data, and everything else in the same database.
Principle of Least Privilege
Each service’s database credentials should only grant permissions that service actually needs (e.g. the Order Service’s database user cannot even technically query the Payments tables, because they don’t exist in its database at all).
Key Security Practices
- Encryption at rest — database files / disks encrypted so stolen hardware or backups are unreadable.
- Encryption in transit — TLS between services and their databases, and between services themselves.
- Secrets management — database credentials stored in a vault (e.g. HashiCorp Vault, AWS Secrets Manager), never hardcoded.
- Network segmentation — databases placed in private subnets, reachable only by their owning service.
- Audit logging — recording who accessed what data and when, important for compliance (GDPR, HIPAA, PCI‑DSS).
- Data minimization — replicated read models should only copy the fields actually needed, not entire sensitive records.
Regulations like PCI‑DSS (payment card data) often require strict isolation of sensitive data. A separate Payment Service database makes it far easier to prove and enforce that only authorized, audited code paths ever touch card data — compared to a shared database where dozens of services could theoretically query it.
Data Residency & Regional Isolation
Many countries require certain categories of personal data to stay physically stored within national borders (a rule often called data residency or data sovereignty). Because each service owns its own database, it becomes far easier to place, say, a European customer’s data in an EU‑region database while a US customer’s data stays in a US‑region database — without redesigning the entire system. A shared, monolithic database makes this kind of regional split extremely painful, often requiring a full re‑architecture.
Threat Modeling for Service Boundaries
When designing which service should own a piece of data, it helps to ask a security question alongside the business question: “If this service is compromised, what’s the worst that happens?” Sensitive data — passwords, payment details, government ID numbers — should live in the smallest number of services possible, each hardened with extra scrutiny (stricter code review, more frequent security audits, tighter network rules), rather than spread thinly across many services “just in case they need it later.”
Monitoring, Logging & Metrics
With many small databases instead of one big one, observability becomes more important — and slightly more complex.
Distributed Tracing
What it is: A way to follow a single user request as it hops across multiple services and databases, using a shared “trace ID” attached to every step.
Why it exists: When an order fails, you need to see the entire journey — Order Service → Payment Service → Inventory Service — to find out exactly where it broke, since no single database holds the full story anymore.
Where it’s used: Tools like Jaeger, Zipkin, and AWS X‑Ray; often paired with the OpenTelemetry standard.
Key Metrics to Track Per Service Database
| Metric | Why It Matters |
|---|---|
| Query latency (p50 / p95 / p99) | Detects slow queries before they cause timeouts elsewhere. |
| Connection pool usage | Exhausted pools cause cascading failures. |
| Replication lag | How far behind replicas are — critical for read consistency decisions. |
| Error rate | Sudden spikes often signal schema issues or bad deploys. |
| Disk usage / IOPS | Databases silently degrade as disks fill up or I/O saturates. |
Centralized Logging
Even though each service owns its own database, logs from all services should still flow into one centralized system (e.g. the ELK stack — Elasticsearch, Logstash, Kibana — or a cloud equivalent like AWS CloudWatch or Datadog) so engineers can search across services during an incident without SSHing into ten different machines.
Centralizing logs and metrics is completely different from centralizing the database. You want unified visibility, but still strictly separated, independently‑owned data storage.
Deployment & Cloud
Every major cloud provider offers building blocks specifically designed for the database‑per‑service model, so most teams no longer roll their own infrastructure from scratch.
Managed Databases
Most production teams today don’t run their own database servers by hand. They use managed cloud database services, where the cloud provider handles patching, backups, and failover:
- Amazon RDS / Aurora — managed relational databases (PostgreSQL, MySQL).
- Amazon DynamoDB — managed NoSQL key‑value / document store.
- Google Cloud Spanner — globally distributed relational database with strong consistency.
- MongoDB Atlas — managed document database.
- Azure Cosmos DB — managed multi‑model NoSQL database.
Containers & Orchestration
Microservices themselves are usually packaged as containers (Docker) and run on an orchestration platform like Kubernetes. Each service’s container connects out to its own managed database — the database itself is often kept outside the Kubernetes cluster for stability, though smaller setups do run databases inside Kubernetes using StatefulSets.
Infrastructure as Code
Provisioning dozens of small databases by hand doesn’t scale. Teams use tools like Terraform or AWS CloudFormation to define each service’s database declaratively, so it can be created, replicated, and torn down consistently and automatically.
CI/CD Per Service
Each microservice typically has its own independent CI/CD pipeline (build → test → deploy), including its own database migration step (using tools like Flyway or Liquibase for relational databases), which runs only against that service’s own database — never touching anyone else’s.
-- Example Flyway migration file: V2__add_discount_column.sql
-- Applies ONLY to orders_db, owned solely by the Order Service
ALTER TABLE orders ADD COLUMN discount_amount DECIMAL(10,2) DEFAULT 0.00;
Each service’s pipeline runs its own migrations against its own private database — no coordination with other teams required.
Databases, Caching & Load Balancing
The data layer is where the pattern becomes concrete. Each service is free to pick the storage technology that best matches its own access patterns.
Polyglot Persistence
What it is: Using different types of databases for different services, based on what each service’s data actually looks like and how it’s accessed.
| Service Type | Good Database Choice | Why |
|---|---|---|
| Order / Payment (transactional) | PostgreSQL, MySQL (relational) | Strong consistency, structured relationships, ACID transactions |
| Product Catalog | MongoDB (document) | Flexible, varying product attributes per category |
| Search | Elasticsearch | Fast full‑text search and ranking |
| Session / Cache | Redis (key‑value, in‑memory) | Extremely fast reads/writes, simple data |
| Social graph / recommendations | Neo4j (graph) | Efficient relationship traversal |
| Analytics / event history | Cassandra, ClickHouse (wide‑column / columnar) | Massive write throughput, time‑series friendly |
Caching Strategies
Each service can independently decide how to cache its own data, without affecting other services’ caching strategy — another benefit of isolation.
- Cache‑aside: Application checks cache first; on a miss, reads from the database and populates the cache.
- Write‑through: Every write goes to the cache and the database at the same time, keeping them in sync.
- Time‑based expiration (TTL): Cached data automatically expires after a set time, trading some freshness for speed.
// Simple cache-aside pattern in Java using Redis
public Product getProduct(String productId) {
String cached = redis.get("product:" + productId);
if (cached != null) {
return deserialize(cached); // fast path
}
Product product = productRepository.findById(productId); // slow path: own DB
redis.setex("product:" + productId, 300, serialize(product)); // cache 5 min
return product;
}
The cache is a private optimization of the service that owns the data — other services never see it.
Load Balancing
Load balancers distribute incoming requests across multiple instances of the same service, and separately, across read replicas of a database. Common approaches: round‑robin, least‑connections, and consistent hashing (useful for cache and sharded database routing, since it minimizes redistribution when nodes are added or removed).
APIs, Sagas & Cross‑Service Communication
This is the section that answers the question everyone eventually asks: “If services can’t share a database transaction, how do we keep a multi‑step business process consistent?”
The Saga Pattern
What it is: A saga is a sequence of local transactions, one per service, where each step publishes an event that triggers the next step. If a step fails, previously completed steps are undone using compensating actions (essentially, “undo” operations).
Why it exists: Traditional distributed transactions (like two‑phase commit) don’t scale well across many independent services and databases — they require every participant to be locked and available at the same time, which defeats the purpose of independent services.
Simple analogy: Booking a vacation package: 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 — you cancel them too. Each cancellation is a “compensating action.”
Fig 5 — When step 3 fails, the saga runs compensating actions backward through steps 2 and 1.
Two Ways to Coordinate a Saga
| Style | How It Works | Trade‑off |
|---|---|---|
| Choreography | Each service listens for events and decides what to do next on its own — no central controller. | Simple for small flows; hard to track for complex ones. |
| Orchestration | A central “saga orchestrator” service explicitly tells each service what step to perform next. | Easier to understand and monitor; orchestrator becomes an important component to design carefully. |
// Simplified Saga step using events (choreography style, Java)
@KafkaListener(topics = "payment.failed")
public void onPaymentFailed(PaymentFailedEvent event) {
// Compensating action: cancel the order this service owns
Order order = orderRepository.findById(event.getOrderId());
order.setStatus(OrderStatus.CANCELLED);
orderRepository.save(order); // writes ONLY to orders_db
eventPublisher.publish("order.cancelled", new OrderCancelledEvent(order.getId()));
}
Each participant in the saga only ever writes to its own database and communicates further steps through events.
CQRS (Command Query Responsibility Segregation)
What it is: Splitting the “write model” (how data is changed) from the “read model” (how data is queried), often using entirely different data structures optimized for each purpose.
Why it exists: Sometimes the way you need to write data (normalized, transactional) is very different from the way you need to read it (denormalized, fast, aggregated across services). CQRS lets each service build a specialized, fast read model built from events, without changing its core write model.
API Design Guidelines for Isolated Services
- Expose coarse‑grained, business‑meaningful endpoints (
POST /orders), not raw database operations. - Version your APIs (
/v1/orders) so internal database changes don’t break consumers. - Use API contracts / schemas (OpenAPI, Protobuf) as the real “interface” between services — the database schema stays private.
- Design events to carry enough context that consumers rarely need to call back for more detail.
Design Patterns & Anti‑patterns
Patterns you should reach for, and anti‑patterns you should learn to spot in design reviews before they ship.
Helpful Patterns
| Pattern | Purpose |
|---|---|
| Database‑per‑Service | Core pattern discussed throughout — one private database per service. |
| API Composition | A service (or API Gateway) calls multiple services’ APIs and combines the results for the client, instead of joining tables. |
| CQRS | Separate optimized read models built from events, avoiding cross‑service joins. |
| Saga | Manages multi‑service business transactions with compensating actions. |
| Event Sourcing | Stores every change as an immutable event, and the current state is derived by replaying events — useful for audit trails and rebuilding read models. |
| Outbox Pattern | Ensures a database write and an event publish happen reliably together, by writing the event to an “outbox” table in the same local transaction, then relaying it asynchronously. |
The Outbox Pattern (Worth a Closer Look)
Problem it solves: If you save an order to your database and then separately publish an event to Kafka, what happens if the database write succeeds but the event publish fails (network blip)? Other services never find out.
Solution: Write the order AND an “event record” into the same local database transaction (inside your own database — this is still allowed, since it’s your own data!). A separate background process reads new rows from this outbox table and reliably publishes them, retrying until successful.
Fig 6 — The Outbox pattern guarantees a write and an event announcement never get out of sync.
Anti‑Patterns to Avoid
Multiple services reading / writing the same tables directly. This is the single most common and most damaging anti‑pattern in microservices — it silently reintroduces monolith‑style coupling.
Services that are deployed separately but must always be deployed together because of hidden data or API coupling — you get all the operational pain of microservices with none of the independence benefits.
A single user request triggers dozens of synchronous cross‑service API calls, each one adding latency and a chance of failure — often a sign that service boundaries were drawn incorrectly.
One “central” service that ends up owning far too much data and logic, becoming a mini‑monolith inside the microservices system.
Best Practices & Common Mistakes
These are the habits that separate teams who ship database‑per‑service confidently from teams who quietly fight their own architecture every sprint.
Best Practices
- BoundariesDraw service boundaries around business capabilities (Order, Payment, Inventory), not technical layers (Database team, UI team) — this naturally aligns with bounded contexts.
- EnforceEnforce database isolation with real permissions, not just team discipline — misconfigured access is how shared‑database anti‑patterns creep back in.
- AsyncPrefer asynchronous events over synchronous calls where the business logic allows, to reduce cascading failures.
- OutboxUse the Outbox pattern for reliable event publishing tied to local database writes.
- VersionVersion your events and APIs so consumers aren’t broken by internal changes.
- MigrateAutomate schema migrations per service with tools like Flyway / Liquibase, run only against that service’s own database.
- EventualDesign for eventual consistency from day one — build UI and business processes that gracefully handle “processing” states rather than assuming instant consistency.
- Read modelsKeep replicated read models minimal — copy only the fields actually needed, refreshed via events.
Common Mistakes
- Splitting services before understanding the domain — leads to constantly‑changing, badly‑drawn boundaries and painful data migrations.
- Forgetting compensating actions in sagas — leaving the system in a half‑completed state when a step fails.
- Over‑fragmenting — creating too many tiny services with too many tiny databases, causing excessive network chatter and operational overhead.
- Ignoring monitoring until something breaks — distributed systems fail in more subtle ways than monoliths; you need distributed tracing from the start.
- Treating eventual consistency as a bug instead of designing for it explicitly.
Start with fewer, slightly larger services and well‑isolated databases. It’s much easier to split a well‑bounded service into two later than to untangle a shared database across ten services that grew organically.
Real‑World & Industry Examples
Everything above already runs, at enormous scale, inside companies whose products you use every day.
Netflix operates hundreds of microservices, each generally owning its own data store, chosen based on the service’s needs — including heavy use of Cassandra for high write‑throughput, highly available services, and various other databases for different purposes. Their move to this model followed a significant database corruption incident that pushed them toward the cloud and away from a single relational database serving many purposes.
Amazon’s internal engineering culture, often summarized by the “you build it, you run it” and “two‑pizza team” principles, is built around teams owning a service completely — code, deployment, and data. This organizational structure is often cited as a direct forerunner of formal microservices thinking, with each team’s service backed by its own datastore.
Uber’s engineering blog has described breaking their original monolith into thousands of microservices as the company scaled globally, with each service typically owning its own schema / database, using a mix of MySQL, Cassandra, and other purpose‑built storage systems depending on the service’s access patterns.
Spotify’s engineering culture, organized around autonomous “squads,” similarly emphasizes each squad owning its service and data end‑to‑end, choosing appropriate storage technology (including Cassandra and PostgreSQL) per service.
Across all these companies, the pattern is the same: small, autonomous teams, each fully owning a business capability and its data, communicating with the rest of the system only through well‑defined APIs and events — never through a shared database.
FAQ, Summary & Key Takeaways
Short, direct answers to the questions that come up most often in design reviews and interviews — followed by a compact summary and the ideas worth carrying into every future architecture decision.
Frequently Asked Questions
Does every microservice truly need a 100% separate physical database server?
Not necessarily at small scale. What matters most is logical isolation — no other service directly reads or writes your tables. This can start as a private schema on a shared server and graduate to a fully separate instance as the service grows.
How do you do a JOIN across two services’ data if they’re in separate databases?
You don’t do a database JOIN. Instead, you either call the owning service’s API and combine results in code (API composition), or maintain a small local read‑model copy of the fields you need, kept fresh through events.
What about ACID transactions across multiple services?
True multi‑service ACID transactions are avoided in this architecture. Instead, the Saga pattern breaks the operation into a sequence of local transactions with compensating actions if something fails, accepting eventual consistency in exchange for independence.
Isn’t this a lot more expensive and complex than one database?
Yes, in absolute terms. It’s a deliberate trade‑off: you accept more operational complexity and infrastructure cost in exchange for independent scaling, deployment, and fault isolation — valuable once your system and team are large enough to need it.
Can two services ever share a database safely?
It’s generally discouraged. If two services frequently need the exact same tightly‑coupled data with strong consistency, that’s often a signal they should actually be one service, not two.
How do you migrate an existing monolith with one database into database‑per‑service?
Gradually, not all at once. A common approach is the “Strangler Fig” pattern: build a new service around one bounded context, have it own a new database for that data, and slowly redirect traffic to it while keeping the old monolith running for everything else. Data is migrated table by table, often using temporary synchronization (like dual writes or change‑data‑capture) until the new service is fully trusted, at which point the old tables are retired.
Does this mean I should never write a SQL JOIN again?
Not at all — JOINs are still perfectly fine and encouraged within a single service’s own database, across its own tables. The rule only forbids JOINing across the boundary of two different services’ databases.
Summary
Splitting an application into microservices without splitting the underlying data creates a “distributed monolith” — a system with all the operational overhead of microservices and none of their real benefits. The Database‑per‑Service pattern solves this by giving each service exclusive, private ownership of its own data store, communicated with only through APIs and asynchronous events. This unlocks independent deployment, fault isolation, technology freedom, and independent scaling — at the cost of giving up simple cross‑service SQL joins and instant, system‑wide consistency, replaced instead by patterns like Sagas, CQRS, and the Outbox pattern.
Key Takeaways
- 01A shared database secretly re‑couples “independent” services — this is the single biggest microservices anti‑pattern.
- 02Database‑per‑Service means one service, one owner, one source of truth for each piece of data.
- 03Services communicate only through APIs (synchronous) and events (asynchronous) — never through direct database access.
- 04Cross‑service consistency is handled with the Saga pattern and compensating actions, not distributed transactions.
- 05Eventual consistency is an accepted, designed‑for trade‑off — not a bug.
- 06Polyglot persistence lets each service pick the best‑fit database technology for its own needs.
- 07The pattern pays off at organizational scale — small teams and simple apps may not need it yet.
- 08Real‑world giants (Netflix, Amazon, Uber, Spotify) all converge on this same core principle at scale.
“If you can write to my database directly, you are not really a separate service — you’re just running in a separate process.”