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 serviceBad service
payment-service: charge, refund, payment statusutility-service: payments + emails + PDF invoices + coupons
product-service: catalogue and stockdatabase-service: a CRUD wrapper over all tables
Split by business capabilitySplit 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 thereFields it needs
Catalogue (product-service)Something we sellname, description, price, category, images, stock
Ordering (order-service)A line on an orderproductId, productName, unitPrice at purchase, quantity
Shipping (future delivery-service)A parcelproductId, 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.