Definition
- Shared database – one physical database that many services read from and write to directly; the schema becomes a public contract coupling every service.
- Database-per-service – each service owns a private database; others access data only through the service's API or events.
- Shared DB strengths – ACID transactions across entities, easy joins, one source of truth, simple operations.
- Shared DB weaknesses – tight coupling, no independent deployment, no fault isolation, no polyglot persistence.
- Per-service strengths – loose coupling, independent deployment, fault isolation, polyglot persistence.
- Per-service costs – no cross-service joins, no distributed transactions, more operational surface.
Imagine National Bank of India keeps one giant master ledger book that every department writes into at the same time. If the accounts team changes a column heading, the loans team's entries break; if two departments grab the book at once there is contention; and if the book is dropped, the whole bank stops. That is a shared database. Now imagine each department keeps its own private ledger locked in its own cabinet, and when the loans team needs Rajesh's balance they phone the accounts team and ask. That is database-per-service: slower to ask around, but no department can break another's records.
Understanding the Concepts
What is a shared database?
With a shared database, all services point at the same physical database and the same tables. The immediate appeal is real: ACID transactions spanning entities, SQL joins across domains, one source of truth with no synchronization, and one database to operate.
package com.bank.shared;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
@Service
public class SharedDbTransferService {
private final AccountRepository accountRepo;
private final StatementRepository statementRepo;
public SharedDbTransferService(AccountRepository accountRepo,
StatementRepository statementRepo) {
this.accountRepo = accountRepo;
this.statementRepo = statementRepo;
}
@Transactional // One transaction across both tables
public void transfer(String from, String to, BigDecimal amount) {
accountRepo.debit(from, amount);
accountRepo.credit(to, amount);
// Same DB: can write statement record in same transaction
statementRepo.recordTransfer(from, to, amount);
}
}
Shared database advantages:
One ACID transaction: debit + credit + statement record
Easy JOIN: SELECT a.name, s.amount FROM accounts a JOIN statements s...
One source of truth: no synchronization needed
One DB to operate: backup, monitor, tune
What are the problems with a shared database?
The problems appear as the system and organization grow. The schema becomes a hidden shared contract, there is runtime coupling, and you are forced into one engine.
Shared database problems:
1. Schema coupling:
Account table used by: transfer-service, statement-service, fraud-service
Rename account_number -> acc_num? All three services break!
Result: lockstep deployments, cross-team coordination
2. Runtime coupling:
Statement service runs heavy month-end report query
-> locks account rows
-> transfer service blocked for 30 seconds
-> Rajesh's transfer times out
3. Single engine:
Account service needs PostgreSQL (ACID)
History service needs Cassandra (write scale)
Both forced onto PostgreSQL with shared DB
package com.bank.shared;
// Problem: changing this entity breaks all services that use the table
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import jakarta.persistence.Column;
import java.math.BigDecimal;
@Entity
@Table(name = "accounts")
public class Account {
// If you rename this column, transfer-service,
// statement-service, and fraud-service all break
@Column(name = "account_number")
private String accountNumber;
@Column(name = "balance")
private BigDecimal balance;
// Getters and setters
}
What is database-per-service?
With database-per-service, each service owns its data exclusively. The owning service is the only one that connects, and others access data through a well-defined API or published events.
package com.bank.account;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import java.math.BigDecimal;
@RestController
public class AccountApi {
private final AccountRepository repository;
public AccountApi(AccountRepository repository) {
this.repository = repository;
}
// The ONLY way other services access account data
@GetMapping("/api/accounts/{id}/balance")
public BigDecimal getBalance(@PathVariable String id) {
return repository.findBalanceById(id);
}
}
# account-service application.yml - own database
spring:
datasource:
url: jdbc:postgresql://accounts-db:5432/accounts
username: account_svc
# statement-service application.yml - separate database
spring:
datasource:
url: jdbc:postgresql://statements-db:5432/statements
username: statement_svc
Database-per-service benefits:
Loose coupling: schema is private, change freely
Independent deploy: migrate one service without coordinating
Fault isolation: statement-db slow? transfer-service unaffected
Polyglot persistence: accounts on PostgreSQL, history on Cassandra
How do you handle cross-service data needs?
Without a shared database, a service that needs another's data either calls its API or keeps a local read model updated by events.
package com.bank.statement;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.stereotype.Service;
@Service
public class AccountEventConsumer {
private final LocalAccountReadModel readModel;
public AccountEventConsumer(LocalAccountReadModel readModel) {
this.readModel = readModel;
}
@KafkaListener(topics = "account-events")
public void onAccountChanged(AccountChangedEvent event) {
// Statement service keeps its own read copy of account names
readModel.updateAccountName(
event.getAccountId(), event.getAccountName());
}
}
Cross-service data access patterns:
1. API call: GET /api/accounts/SAV2001 (synchronous, real-time)
2. Event + local copy: consume AccountChangedEvent, store locally
3. API composition: aggregate data from multiple APIs at gateway
Trade-off: eventual consistency in the local copy
(brief delay after transfer before event is applied)
How do you handle distributed transactions?
A business operation spanning two services becomes a Saga with compensating actions.
package com.bank.saga;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
@Service
public class TransferSaga {
private final AccountServiceClient accountClient;
public TransferSaga(AccountServiceClient accountClient) {
this.accountClient = accountClient;
}
public void execute(String from, String to, BigDecimal amount) {
// Step 1: debit
accountClient.debit(from, amount);
try {
// Step 2: credit
accountClient.credit(to, amount);
} catch (Exception e) {
// Compensate: reverse the debit
accountClient.credit(from, amount);
throw new TransferFailedException(
"Credit failed, debit reversed", e);
}
}
}
Saga pattern:
Step 1: debit SAV2001 -> success
Step 2: credit SAV2002 -> failure!
Compensate: credit SAV2001 (reverse the debit)
Result: no money lost, eventual consistency
How do the two approaches compare?
| Aspect | Shared Database | Database-per-Service |
|---|---|---|
| Coupling | Tight (schema is public) | Loose (schema is private) |
| Deployment | Lockstep | Independent |
| Transactions | ACID across entities | Sagas + eventual consistency |
| Joins | SQL joins work | API calls or local copies |
| Fault isolation | None (one DB failure hits all) | Yes (isolated databases) |
| Polyglot | No (one engine) | Yes (best engine per service) |
| Operations | One DB to manage | Many DBs to manage |
| Best for | Small teams, unclear boundaries | Many teams, clear boundaries |
What is the worst anti-pattern?
The worst of both worlds is splitting the code into microservices but leaving them all on one shared database. You get distributed-system complexity with none of the isolation benefits.
Anti-pattern: microservices + shared database
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Transfer Svc │ │ Statement Svc│ │ Fraud Svc │
└──────┬───────┘ └──────┬───────┘ └──────┬───────┘
│ │ │
└────────────────┼────────────────┘
│
┌─────▼─────┐
│ Shared DB │ <- Single point of failure
└───────────┘ + tight coupling
+ distributed complexity
= worst of both worlds
When should you choose each approach?
Start with a shared database or modular monolith when the domain is small or unclear. Adopt database-per-service when teams and services multiply and independent deployment and fault isolation become worth the added complexity.
Decision framework:
Small team, few services, unclear boundaries?
-> Shared database (or modular monolith)
-> Simpler, faster to build
Many teams, clear boundaries, need independent deploy?
-> Database-per-service
-> Accept Saga complexity for isolation benefits
Migrating from shared to per-service?
-> Incremental: extract one service at a time
-> Use Strangler Fig pattern
-> Keep shared DB for remaining monolith
Visual Overview
Shared Database:
┌──────────┐ ┌──────────┐ ┌──────────┐
│Service A │ │Service B │ │Service C │
└─────┬────┘ └─────┬────┘ └─────┬────┘
│ │ │
└─────────────┼─────────────┘
│
┌─────▼─────┐
│ One DB │ Schema = public contract
│ (coupled) │ One failure = all fail
└───────────┘
Database-per-Service:
┌──────────┐ ┌──────────┐ ┌──────────┐
│Service A │ │Service B │ │Service C │
└─────┬────┘ └─────┬────┘ └─────┬────┘
│ │ │
┌────▼────┐ ┌────▼────┐ ┌────▼────┐
│ DB A │ │ DB B │ │ DB C │
│PostgreSQL│ │Cassandra│ │ Redis │
└─────────┘ └─────────┘ └─────────┘
Schema=private Each scales Polyglot
independently independently persistence
Key Takeaway
The difference between a shared database and database-per-service is fundamentally about coupling. With a shared database, many services read and write the same physical database, so the schema silently becomes a public contract binding every service together. That buys simplicity: ACID transactions across entities, SQL joins, one source of truth, and one database to operate. For a small system or modular monolith it is often the right choice.
But as services and teams multiply, the shared database becomes the single biggest coupler. At National Bank of India, the transfer and statement services both hit the same accounts table. When the statement team wanted a schema change it threatened the transfer service, and month-end report queries locked account rows and slowed live transfers for Rajesh. We moved to database-per-service: the account service owns the balances and publishes AccountChangedEvents, and the statement service keeps its own local read model. After that, each team could evolve its schema freely and a statement-database slowdown could never touch live money movement.
The honest cost is losing cross-service joins and distributed transactions, handled with Sagas, events, and the outbox pattern. The anti-pattern to avoid is splitting code into microservices while sharing one database, which gives all the distributed complexity with none of the isolation.
Remember one sentence: a shared database couples every service through one schema and one failure point, while database-per-service gives each service a private store it fully owns, trading joins and transactions for the loose coupling and fault isolation that make microservices worthwhile.
---