Single Responsibility and Bounded Contexts
Single Responsibility
Each service should have one reason to change: one business capability. If a change to "how we calculate delivery fees" forces you to edit three services, the boundaries are wrong.
| Good service | Bad service |
|---|---|
payment-service: charge, refund, payment status | utility-service: payments + emails + PDF invoices + coupons |
product-service: catalogue and stock | database-service: a CRUD wrapper over all tables |
| Split by business capability | Split by technical layer (controller-service, dao-service) |
Bounded Contexts (Domain-Driven Design)
A bounded context is a boundary inside which a word has one clear meaning. The same real-world thing can look different in each context, and that is fine:
| Context (service) | What "Product" means there | Fields it needs |
|---|---|---|
| Catalogue (product-service) | Something we sell | name, description, price, category, images, stock |
| Ordering (order-service) | A line on an order | productId, productName, unitPrice at purchase, quantity |
| Shipping (future delivery-service) | A parcel | productId, weight, dimensions, fragile? |
Tip: Do not build one giant shared Product class used by every service. Each service keeps its own small model of the parts it needs.
Example: Each Context Has Its Own Model
Example
Catalogue and Ordering both talk about products, but each keeps its own record with only the fields it needs. Ordering copies the price at purchase time:
import java.math.BigDecimal;
import java.util.*;
public class Main {
public static void main(String[] args) {
// Catalogue context: the full product
CatalogProduct p = new CatalogProduct(1L, "boAt Rockerz 450", "Wireless headphones",
new BigDecimal("1499.00"), 120, "Electronics");
// Ordering context: only what an order line needs
OrderLine line = OrderLine.from(p, 2);
System.out.println("Catalogue: " + p);
System.out.println("Order line: " + line);
System.out.println("Line total: Rs " + line.total());
// Price changes in the catalogue later...
CatalogProduct newPrice = new CatalogProduct(p.id(), p.name(), p.description(),
new BigDecimal("1299.00"), p.stock(), p.category());
System.out.println("New catalogue price: Rs " + newPrice.price());
System.out.println("Order still shows: Rs " + line.unitPrice());
}
}
record CatalogProduct(Long id, String name, String description, BigDecimal price,
int stock, String category) {}
record OrderLine(Long productId, String productName, BigDecimal unitPrice, int quantity) {
static OrderLine from(CatalogProduct p, int qty) {
return new OrderLine(p.id(), p.name(), p.price(), qty);
}
BigDecimal total() { return unitPrice.multiply(BigDecimal.valueOf(quantity)); }
}Output
Catalogue: CatalogProduct[id=1, name=boAt Rockerz 450, description=Wireless headphones, price=1499.00, stock=120, category=Electronics] Order line: OrderLine[productId=1, productName=boAt Rockerz 450, unitPrice=1499.00, quantity=2] Line total: Rs 2998.00 New catalogue price: Rs 1299.00 Order still shows: Rs 1499.00
Practice
Exercise: A team proposes these ShopEase services: controller-service, business-logic-service, dao-service. What is wrong, and what would you propose instead?
Show answer
It splits by technical layer, so EVERY feature change touches all three
services and they must always be deployed together (a distributed monolith).
Split by business capability instead:
product-service (catalogue, stock)
order-service (orders, order items)
user-service (users, cart)
payment-service (payments, refunds)
Each one has its own controller, service and repository layers inside it.