Back to Scenarios & Design Questions
Q31Question 31 of 67

Shared database versus database-per-service

Key Concepts
  • A shared database couples every service through one schema and one failure point
  • Database-per-service gives each service a private store accessed only through its API or events
  • Shared database preserves ACID transactions and joins but forces lockstep deployments
  • Database-per-service enables independent deployment, fault isolation, and polyglot persistence
  • The cost of database-per-service is losing cross-service joins and distributed transactions
  • Consistency is handled with Sagas, events, and the transactional outbox pattern
  • The anti-pattern is splitting code into microservices while sharing one database

Definition

A shared database couples everyone through one schema and one failure point; database-per-service gives each service a private store it fully owns and others reach only through its API.
  • 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.

JAVA
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);
    }
}
TEXT
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.

TEXT
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
JAVA
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.

JAVA
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);
    }
}
YAML
# 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
TEXT
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.

JAVA
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());
    }
}
TEXT
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.

JAVA
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);
        }
    }
}
TEXT
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.

DIAGRAM
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.

TEXT
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

DIAGRAM
  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.

---

Share
Use ← → arrow keys to navigate