Definition
- Database-per-service – each service privately owns its data; other services use its API or events, never its database.
- SQL (PostgreSQL, Oracle) – for strongly-consistent, transactional data where ACID guarantees and relationships matter.
- NoSQL (MongoDB, Cassandra) – for high-volume, flexible-schema, horizontally-scaled data where availability matters more than joins.
- Redis – for caching, counters, sessions, and locks; sits in front of the real source of truth with a TTL.
- Polyglot persistence – each service picks the engine that fits its workload rather than forcing one database on everyone.
- Trade-off – cross-service joins and distributed transactions are lost; consistency is handled with Sagas and events.
Think of National Bank of India as many departments. The core ledger that holds Rajesh's and Priya's actual balances needs strict, double-entry, never-wrong bookkeeping: that is a relational database. The audit department that stores billions of log events, where the shape varies and nobody updates old entries, is happy with a NoSQL store. And the teller's quick note of Rajesh's balance so the next screen loads instantly is a sticky note you throw away after thirty seconds: that is Redis. Each department keeps its own records; no department reaches into another's cabinet.
Understanding the Concepts
What is the database-per-service pattern?
Each microservice owns its data exclusively. Other services may read or change that data only by calling the owning service's API or by consuming its events, never by connecting to its database. This gives independent deployability, loose coupling, fault isolation, and the freedom to pick the best engine per service.
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 AccountController {
private final AccountRepository repository;
public AccountController(AccountRepository repository) {
this.repository = repository;
}
// Other services call this API to get balance
// They never connect to the account database directly
@GetMapping("/api/accounts/{accountId}/balance")
public BigDecimal getBalance(
@PathVariable String accountId) {
return repository.findBalanceByAccountId(accountId);
}
}
Database-per-service rule:
Account Service -> owns accounts_db (PostgreSQL)
Statement Service -> owns statements_db (Cassandra)
Notification Service -> owns notifications_db (Redis)
Statement Service needs Rajesh's account name?
WRONG: SELECT name FROM accounts_db.accounts (direct DB access)
RIGHT: GET /api/accounts/SAV2001 (API call)
When should you choose SQL?
Use SQL when you need strong consistency, ACID transactions, and complex relationships. The core ledger, accounts, and transfers where a lost or duplicated rupee is unacceptable demand PostgreSQL with real transactions.
package com.bank.transfer;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
@Service
public class TransferService {
private final AccountRepository accountRepo;
public TransferService(AccountRepository accountRepo) {
this.accountRepo = accountRepo;
}
@Transactional // ACID: both succeed or both roll back
public void transfer(String from, String to, BigDecimal amount) {
BigDecimal fromBalance = accountRepo.getBalance(from);
if (fromBalance.compareTo(amount) < 0) {
throw new InsufficientFundsException(
"Account " + from + " has insufficient funds");
}
accountRepo.debit(from, amount);
accountRepo.credit(to, amount);
// If credit fails, debit rolls back automatically
}
}
SQL strengths:
ACID transactions: debit + credit in one atomic commit
Foreign keys: accounts.customer_id -> customers.id
Complex queries: JOIN accounts ON transfers WHERE date BETWEEN...
Strong consistency: Rajesh always sees the correct balance
When should you choose NoSQL?
Use NoSQL when scale, write throughput, flexible schema, or geographic distribution matters more than cross-entity transactions. Cassandra suits massive write-heavy, time-series data like transaction history.
package com.bank.history;
import org.springframework.data.cassandra.core.mapping.Column;
import org.springframework.data.cassandra.core.mapping.PrimaryKey;
import org.springframework.data.cassandra.core.mapping.Table;
import java.math.BigDecimal;
import java.time.Instant;
@Table("transaction_history")
public class TransactionRecord {
@PrimaryKey
private TransactionKey key; // account_id + timestamp
@Column("amount")
private BigDecimal amount;
@Column("type")
private String type; // DEBIT, CREDIT
@Column("description")
private String description;
// Cassandra: append-only, never updated, queried by
// account + time range, horizontally scalable
}
NoSQL strengths:
Write throughput: 100K+ writes/sec across cluster
Horizontal scale: add nodes to increase capacity linearly
Flexible schema: new fields added without migration
Tunable consistency: eventual for reads, quorum for writes
When should you choose Redis?
Use Redis for anything that must be extremely fast and is either a cache or inherently ephemeral. Redis is not the source of truth; it sits in front of the real database with a TTL.
package com.bank.cache;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.time.Duration;
@Service
public class BalanceCacheService {
private final StringRedisTemplate redis;
private final AccountRepository accountRepo;
public BalanceCacheService(StringRedisTemplate redis,
AccountRepository accountRepo) {
this.redis = redis;
this.accountRepo = accountRepo;
}
// Cache-aside pattern
public BigDecimal getBalance(String accountId) {
String cached = redis.opsForValue().get("bal:" + accountId);
if (cached != null) {
return new BigDecimal(cached); // Cache hit
}
// Cache miss: read from PostgreSQL
BigDecimal balance = accountRepo.findBalance(accountId);
redis.opsForValue().set("bal:" + accountId,
balance.toPlainString(), Duration.ofSeconds(30));
return balance;
}
public void invalidate(String accountId) {
redis.delete("bal:" + accountId);
}
}
Redis use cases:
Cached balances: TTL 30s, cache-aside
Rate-limit counters: atomic INCR with EXPIRE
Session data: Spring Session with Redis
Distributed locks: Redisson or RedLock
NOT source of truth: always backed by a real database
How do the three compare?
| Factor | SQL (PostgreSQL) | NoSQL (Cassandra) | Redis |
|---|---|---|---|
| Consistency | Strong (ACID) | Tunable (eventual) | None (cache) |
| Scale | Vertical + replicas | Horizontal (linear) | In-memory (fast) |
| Query power | Joins, aggregates | Key-based lookups | Key-value only |
| Data shape | Structured, relational | Flexible, denormalized | Ephemeral |
| Use case | Accounts, transfers | History, audit logs | Cache, counters |
| Banking example | Rajesh's balance | Transaction history | Cached balance |
What is polyglot persistence?
Polyglot persistence means each service uses the engine that best fits its workload. This is only possible with database-per-service, because once each service owns its store, you are free to choose the best engine.
Polyglot persistence at National Bank of India:
account-service -> PostgreSQL (ACID for money)
transaction-history -> Cassandra (billions of append-only records)
customer-profile -> MongoDB (flexible nested documents)
notification-service -> Redis (ephemeral, fast)
search-service -> Elasticsearch (full-text search)
What are the trade-offs of database-per-service?
You lose cross-service joins and distributed transactions. A business operation spanning two services becomes a Saga with compensating actions and eventual consistency.
package com.bank.saga;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
@Service
public class TransferSaga {
private final AccountServiceClient accountService;
private final NotificationServiceClient notificationService;
public TransferSaga(AccountServiceClient accountService,
NotificationServiceClient notificationService) {
this.accountService = accountService;
this.notificationService = notificationService;
}
public void execute(String from, String to, BigDecimal amount) {
try {
// Step 1: debit source (account-service owns this)
accountService.debit(from, amount);
// Step 2: credit destination (account-service owns this)
accountService.credit(to, amount);
// Step 3: notify (notification-service owns this)
notificationService.sendTransferNotification(from, to, amount);
} catch (Exception e) {
// Compensate: reverse the debit
accountService.credit(from, amount);
throw new TransferFailedException("Transfer rolled back", e);
}
}
}
Trade-offs of database-per-service:
Lost: Cross-service JOINs, distributed ACID transactions
Gained: Independent deployment, fault isolation, polyglot persistence
Handle: Sagas, events, eventual consistency, outbox pattern
What are common mistakes when choosing databases?
The most common mistake is using NoSQL to avoid writing SQL. Another is putting money into an eventually-consistent store.
WRONG decisions:
Using MongoDB for account balances -> eventual consistency loses money
Using PostgreSQL for billions of logs -> does not scale horizontally
Using Redis as source of truth -> data lost on restart without persistence
Using one DB for all services -> tight coupling, no fault isolation
CORRECT decisions:
PostgreSQL for money (strong consistency, ACID)
Cassandra for history (write-heavy, append-only, scalable)
Redis for caching (fast, ephemeral, TTL)
MongoDB for profiles (flexible schema, nested documents)
Visual Overview
┌──────────────────────────────────────────────┐
│ Polyglot Persistence │
│ │
│ ┌─────────────┐ ┌──────────────────────┐ │
│ │ Account Svc │───>│ PostgreSQL (ACID) │ │
│ │ (transfers) │ │ accounts, balances │ │
│ └─────────────┘ └──────────────────────┘ │
│ │
│ ┌─────────────┐ ┌──────────────────────┐ │
│ │ History Svc │───>│ Cassandra (scale) │ │
│ │ (audit logs) │ │ transaction_history │ │
│ └─────────────┘ └──────────────────────┘ │
│ │
│ ┌─────────────┐ ┌──────────────────────┐ │
│ │ Profile Svc │───>│ MongoDB (flexible) │ │
│ │ (customers) │ │ customer_profiles │ │
│ └─────────────┘ └──────────────────────┘ │
│ │
│ ┌─────────────┐ ┌──────────────────────┐ │
│ │ Cache Layer │───>│ Redis (speed) │ │
│ │ (balances) │ │ TTL 30s, cache-aside │ │
│ └─────────────┘ └──────────────────────┘ │
└──────────────────────────────────────────────┘
Rule: each service owns its database privately.
Other services use APIs or events, never direct DB access.
Key Takeaway
Designing databases for microservices starts with one non-negotiable principle: database-per-service. Each service privately owns its data and schema, and no other service ever connects to that database directly. This gives independent deployability, loose coupling, fault isolation, and the freedom to choose the best engine for each service's workload.
Choosing the engine is driven by the access pattern. At National Bank of India, the transfer between Rajesh and Priya must debit one and credit the other atomically, so the account service sits on PostgreSQL with real ACID transactions. Every transfer also produces history records, written constantly, queried by account and time, never updated, so the history service sits on Cassandra, which scales horizontally. And because the mobile app shows a balance on every screen, Redis sits in front with a thirty-second TTL using cache-aside, so most reads are served from memory.
The honest trade-off is that data is now distributed, so cross-service joins and distributed transactions disappear. Consistency is handled with Sagas, events, and eventual consistency. The senior rule is to never put money into an eventually-consistent store, and to choose NoSQL because the access pattern genuinely fits, not to avoid writing SQL.
Remember one sentence: give every service its own private database, then choose that database by the workload: relational for correctness, NoSQL for scale, Redis for speed.
---