Database per Service
The Rule
Each service owns its data. Only product-service reads and writes the products table. If order-service needs product data, it asks product-service through its API (or keeps its own copy from events). It never connects to product_db.
order-service product-service
| |
| GET /api/products/1 ------> |
| <------ {"id":1,"price":1499} |
| |
order_db product_db
(orders, order_items) (products, categories)
order-service ---X---> product_db NOT allowedWhy It Matters
- Independent changes: product-service can rename a column or move to MongoDB without breaking anyone.
- Independent scaling: the product database can get read replicas; the order database can be tuned for writes.
- Clear ownership: one team is responsible for the data quality of each table.
- Right tool for the job: catalogue in MySQL, search in Elasticsearch, carts in Redis.
| Option | Description | Verdict |
|---|---|---|
| Database per service | Separate DB server or instance for each service | Best isolation |
| Schema per service | One DB server, separate schema/user per service | Fine for starting, cheaper |
| Shared tables | Services read each other's tables | Avoid: creates a distributed monolith |
Example: No Shared Tables
Example
Two in-process "services", each with a private store. Order service can only reach product data through the public API method, never the map itself:
import java.math.BigDecimal;
import java.util.*;
public class Main {
public static void main(String[] args) {
ProductService products = new ProductService();
OrderService orders = new OrderService(products);
System.out.println(orders.place(1L, 2));
System.out.println(orders.place(2L, 5));
System.out.println(orders.place(1L, 200));
System.out.println("Stock of product 1 now: " + products.get(1L).stock());
}
}
record ProductDto(Long id, String name, BigDecimal price, int stock) {}
class ProductService {
// product_db: private, nobody else can touch it
private final Map<Long, ProductDto> productDb = new TreeMap<>(Map.of(
1L, new ProductDto(1L, "boAt Rockerz 450", new BigDecimal("1499.00"), 120),
2L, new ProductDto(2L, "Prestige Kettle", new BigDecimal("899.00"), 3)));
ProductDto get(Long id) { return productDb.get(id); }
boolean reserve(Long id, int qty) {
ProductDto p = productDb.get(id);
if (p == null || p.stock() < qty) return false;
productDb.put(id, new ProductDto(p.id(), p.name(), p.price(), p.stock() - qty));
return true;
}
}
class OrderService {
private final List<String> orderDb = new ArrayList<>(); // order_db
private final ProductService productApi; // only the API, not the map
OrderService(ProductService productApi) { this.productApi = productApi; }
String place(Long productId, int qty) {
ProductDto p = productApi.get(productId);
if (!productApi.reserve(productId, qty)) return "REJECTED: not enough stock of " + p.name();
String order = "ORDER-" + (1001 + orderDb.size()) + " " + qty + " x " + p.name()
+ " @ Rs " + p.price();
orderDb.add(order);
return "CREATED: " + order;
}
}Output
CREATED: ORDER-1001 2 x boAt Rockerz 450 @ Rs 1499.00 REJECTED: not enough stock of Prestige Kettle REJECTED: not enough stock of boAt Rockerz 450 Stock of product 1 now: 118
Note: No join across services means "show order history with product images" is done by the order service storing what it needs (name, price) or by calling product-service. The Hibernate & JPA material covers joins inside one service.
Practice
Exercise: The reporting team wants a daily sales report that joins orders, products and users. Each lives in a different database. Name two ways to build it without breaking "database per service".
Show answer
1. API composition: a report job calls order-service, product-service and
user-service APIs and combines the results in code.
2. Events into a reporting store: services publish events (OrderPlaced,
ProductUpdated, UserRegistered); a reporting service consumes them into its
own read-only database (or a data warehouse such as BigQuery) built for
reports.
Never let the reporting job connect directly to every service's database.