Clear Boundaries
Bounded contexts provide natural service boundaries for microservices. No guessing where to split!
Imagine you’re building a school. You don’t organize it by “all the chairs in one room, all the desks in another.” Instead, you create:
Each area has everything it needs to do its job. That’s Domain-Driven Design - organizing software around what the business does, not how the tech works!
Traditional Approach (Technical):
Database LayerService LayerAPI LayerProblem: Business logic scattered across technical layers!
DDD Approach (Business):
Order Management ContextPayment Processing ContextInventory Management ContextCustomer Management ContextBenefit: Each context is self-contained with clear business responsibility!
A bounded context is a explicit boundary where a particular domain model applies.
Key Insight: The word “Customer” means different things in different contexts! That’s OK and even desirable in DDD.
A shared language between developers and domain experts.
Bad (Technical Language):
Developer: "We need to persist the entity to the repository using the ORM mapper."Business: "What? I just want to save the order!"Good (Ubiquitous Language):
Developer: "When a customer places an order, we reserve inventory."Business: "Yes! And if payment fails, we release the reservation."Developer: "Got it. Let me model that..."In Code:
# BAD - Technical languageclass DataManager: def persist(self, entity): self.db.insert(entity)
def retrieve(self, id): return self.db.select(id)
# GOOD - Ubiquitous languageclass OrderService: def place_order(self, customer: Customer, items: list[Item]): """Customer places an order""" order = Order(customer, items) self._reserve_inventory(order.items) return order
def cancel_order(self, order: Order): """Customer cancels their order""" self._release_inventory_reservation(order.items) order.status = OrderStatus.CANCELLED// BAD - Technical languagepublic class DataManager { public void persist(Entity entity) { db.insert(entity); }
public Entity retrieve(Long id) { return db.select(id); }}
// GOOD - Ubiquitous languagepublic class OrderService { /** * Customer places an order */ public Order placeOrder(Customer customer, List<Item> items) { Order order = new Order(customer, items); reserveInventory(order.getItems()); return order; }
/** * Customer cancels their order */ public void cancelOrder(Order order) { releaseInventoryReservation(order.getItems()); order.setStatus(OrderStatus.CANCELLED); }}An aggregate is a cluster of objects treated as a single unit for data changes.
Rules:
Implementation:
from dataclasses import dataclass, fieldfrom typing import Listfrom decimal import Decimal
@dataclassclass OrderLine: """Value object - part of aggregate""" product_id: str quantity: int price: Decimal
def subtotal(self) -> Decimal: return self.price * self.quantity
@dataclassclass Order: """Aggregate Root""" id: str customer_id: str # Reference by ID only! lines: List[OrderLine] = field(default_factory=list) status: str = "draft"
# All modifications go through aggregate root def add_item(self, product_id: str, quantity: int, price: Decimal): """Business logic in aggregate""" if self.status != "draft": raise ValueError("Cannot modify confirmed order")
# Check if product already in order for line in self.lines: if line.product_id == product_id: # Update existing line line.quantity += quantity return
# Add new line self.lines.append(OrderLine(product_id, quantity, price))
def remove_item(self, product_id: str): """Business logic in aggregate""" if self.status != "draft": raise ValueError("Cannot modify confirmed order")
self.lines = [l for l in self.lines if l.product_id != product_id]
def calculate_total(self) -> Decimal: """Aggregate maintains its invariants""" return sum(line.subtotal() for line in self.lines)
def confirm(self): """State transition""" if not self.lines: raise ValueError("Cannot confirm empty order") if self.status != "draft": raise ValueError("Order already confirmed")
self.status = "confirmed"
# Usageorder = Order(id="123", customer_id="cust-456")order.add_item("prod-1", quantity=2, price=Decimal("19.99"))order.add_item("prod-2", quantity=1, price=Decimal("49.99"))total = order.calculate_total() # $89.97order.confirm()
# ❌ DON'T modify order lines directly# order.lines[0].quantity = 100 # Bypasses business logic!
# ✅ DO go through aggregate rootorder.add_item("prod-1", quantity=1, price=Decimal("19.99"))// Value object - part of aggregatepublic record OrderLine( String productId, int quantity, BigDecimal price) { public BigDecimal subtotal() { return price.multiply(BigDecimal.valueOf(quantity)); }}
// Aggregate Rootpublic class Order { private final String id; private final String customerId; // Reference by ID only! private final List<OrderLine> lines; private OrderStatus status;
public Order(String id, String customerId) { this.id = id; this.customerId = customerId; this.lines = new ArrayList<>(); this.status = OrderStatus.DRAFT; }
// All modifications go through aggregate root public void addItem(String productId, int quantity, BigDecimal price) { if (status != OrderStatus.DRAFT) { throw new IllegalStateException("Cannot modify confirmed order"); }
// Check if product already in order for (OrderLine line : lines) { if (line.productId().equals(productId)) { // Would need to update existing line // (records are immutable, so we'd replace) lines.remove(line); lines.add(new OrderLine( productId, line.quantity() + quantity, price )); return; } }
// Add new line lines.add(new OrderLine(productId, quantity, price)); }
public void removeItem(String productId) { if (status != OrderStatus.DRAFT) { throw new IllegalStateException("Cannot modify confirmed order"); }
lines.removeIf(line -> line.productId().equals(productId)); }
public BigDecimal calculateTotal() { return lines.stream() .map(OrderLine::subtotal) .reduce(BigDecimal.ZERO, BigDecimal::add); }
public void confirm() { if (lines.isEmpty()) { throw new IllegalStateException("Cannot confirm empty order"); } if (status != OrderStatus.DRAFT) { throw new IllegalStateException("Order already confirmed"); }
status = OrderStatus.CONFIRMED; }
// Don't expose mutable lines! public List<OrderLine> getLines() { return Collections.unmodifiableList(lines); }}
// UsageOrder order = new Order("123", "cust-456");order.addItem("prod-1", 2, new BigDecimal("19.99"));order.addItem("prod-2", 1, new BigDecimal("49.99"));BigDecimal total = order.calculateTotal(); // $89.97order.confirm();Capture significant business events that domain experts care about.
from dataclasses import dataclassfrom datetime import datetime
@dataclassclass OrderPlacedEvent: """Domain event - something that happened""" order_id: str customer_id: str total: Decimal timestamp: datetime
@dataclassclass OrderCancelledEvent: order_id: str reason: str timestamp: datetime
@dataclassclass PaymentReceivedEvent: order_id: str amount: Decimal payment_method: str timestamp: datetime
# In aggregateclass Order: def __init__(self): self._events = []
def place_order(self): # Business logic self.status = "placed"
# Record event self._events.append( OrderPlacedEvent( order_id=self.id, customer_id=self.customer_id, total=self.calculate_total(), timestamp=datetime.now() ) )
def get_events(self): """Get uncommitted events""" events = self._events self._events = [] return eventsWhy Events?
Each bounded context becomes a microservice!
How bounded contexts relate to each other:
Context Mapping Patterns:
Customer-Supplier
Conformist
Published Language
Anticorruption Layer
# Anticorruption Layer Exampleclass LegacyOrderAdapter: """Translates between new domain model and legacy system"""
def __init__(self, legacy_system): self._legacy = legacy_system
def get_order(self, order_id: str) -> Order: """Convert legacy order to our domain model""" legacy_order = self._legacy.fetch_order(order_id)
# Translate legacy data to our model return Order( id=legacy_order["ORDER_ID"], customer_id=legacy_order["CUST_NUM"], status=self._translate_status(legacy_order["STATUS_CD"]), lines=[ OrderLine( product_id=item["PROD_ID"], quantity=item["QTY"], price=Decimal(item["PRICE"]) ) for item in legacy_order["ITEMS"] ] )
def _translate_status(self, legacy_status: str) -> str: """Translate legacy status codes to our ubiquitous language""" mapping = { "N": "draft", "C": "confirmed", "S": "shipped", "D": "delivered", } return mapping.get(legacy_status, "unknown")/** * Anticorruption Layer: Translates between new domain model and legacy system */public class LegacyOrderAdapter {
private final LegacySystem legacySystem;
public Order getOrder(String orderId) { // Fetch from legacy system Map<String, Object> legacyOrder = legacySystem.fetchOrder(orderId);
// Translate legacy data to our domain model Order order = new Order( (String) legacyOrder.get("ORDER_ID"), (String) legacyOrder.get("CUST_NUM") );
// Convert items List<Map<String, Object>> items = (List<Map<String, Object>>) legacyOrder.get("ITEMS");
for (Map<String, Object> item : items) { order.addItem( (String) item.get("PROD_ID"), (Integer) item.get("QTY"), new BigDecimal((String) item.get("PRICE")) ); }
return order; }
private OrderStatus translateStatus(String legacyStatus) { return switch (legacyStatus) { case "N" -> OrderStatus.DRAFT; case "C" -> OrderStatus.CONFIRMED; case "S" -> OrderStatus.SHIPPED; case "D" -> OrderStatus.DELIVERED; default -> OrderStatus.UNKNOWN; }; }}What is Event Storming?
Process:
List domain events (orange sticky notes)
Add commands that trigger events (blue sticky notes)
Identify aggregates (yellow sticky notes)
Draw boundaries (pink lines)
List all business capabilities:
E-Commerce Business Capabilities:├── Product Catalog│ └── Manage products, search, browse├── Order Management│ └── Place orders, track orders, cancel orders├── Payment Processing│ └── Process payments, refunds, payment methods├── Inventory Management│ └── Track stock, reserve inventory, replenish├── Shipping & Fulfillment│ └── Ship orders, track shipments, returns├── Customer Management│ └── Register customers, profiles, preferences└── Customer Service └── Support tickets, returns, complaintsEach capability = potential bounded context!
If “Product” means different things in different parts of system:
Product in Catalog Context:
class Product: name: str description: str images: list[str] category: str attributes: dict # Focus: browsing and discoveryProduct in Inventory Context:
class Product: sku: str quantity_on_hand: int reorder_point: int warehouse_location: str # Focus: stock managementProduct in Order Context:
class OrderLineProduct: product_id: str name: str price: Decimal # Focus: what was ordered at what priceDifferent models = different contexts!
Abstracts data access for aggregates.
from abc import ABC, abstractmethodfrom typing import Optional, List
class OrderRepository(ABC): """Repository interface - part of domain layer"""
@abstractmethod def save(self, order: Order) -> None: """Save aggregate""" pass
@abstractmethod def find_by_id(self, order_id: str) -> Optional[Order]: """Load aggregate by ID""" pass
@abstractmethod def find_by_customer(self, customer_id: str) -> List[Order]: """Query by customer""" pass
class PostgresOrderRepository(OrderRepository): """Concrete implementation - infrastructure layer"""
def __init__(self, db_session): self._session = db_session
def save(self, order: Order) -> None: # Persist aggregate self._session.add(order) self._session.commit()
# Publish domain events for event in order.get_events(): event_bus.publish(event)
def find_by_id(self, order_id: str) -> Optional[Order]: return self._session.query(Order).filter_by(id=order_id).first()
def find_by_customer(self, customer_id: str) -> List[Order]: return self._session.query(Order)\ .filter_by(customer_id=customer_id)\ .all()// Repository interface - part of domain layerpublic interface OrderRepository { void save(Order order); Optional<Order> findById(String orderId); List<Order> findByCustomer(String customerId);}
// Concrete implementation - infrastructure layerpublic class JpaOrderRepository implements OrderRepository {
private final EntityManager entityManager; private final EventBus eventBus;
@Override public void save(Order order) { // Persist aggregate entityManager.persist(order); entityManager.flush();
// Publish domain events for (DomainEvent event : order.getEvents()) { eventBus.publish(event); } }
@Override public Optional<Order> findById(String orderId) { Order order = entityManager.find(Order.class, orderId); return Optional.ofNullable(order); }
@Override public List<Order> findByCustomer(String customerId) { return entityManager .createQuery( "SELECT o FROM Order o WHERE o.customerId = :customerId", Order.class ) .setParameter("customerId", customerId) .getResultList(); }}When business logic doesn’t fit in an entity:
class PricingService: """Domain service - business logic that spans multiple aggregates"""
def __init__(self, discount_policy_repo: DiscountPolicyRepository): self._discount_policies = discount_policy_repo
def calculate_order_total( self, order: Order, customer: Customer ) -> Decimal: """Calculate total with business rules"""
# Base total from order subtotal = order.calculate_subtotal()
# Apply customer discounts discount = self._calculate_customer_discount(customer, subtotal)
# Apply promotional discounts promo_discount = self._calculate_promo_discount(order)
# Calculate tax tax = self._calculate_tax(order, customer)
return subtotal - discount - promo_discount + tax
def _calculate_customer_discount( self, customer: Customer, subtotal: Decimal ) -> Decimal: """Apply VIP discounts""" if customer.is_vip(): return subtotal * Decimal("0.10") # 10% VIP discount return Decimal("0")
def _calculate_promo_discount(self, order: Order) -> Decimal: """Apply promotional discounts""" # Complex business logic here policies = self._discount_policies.find_active_policies() # ... apply policies return Decimal("0")public class PricingService { /** * Domain service - business logic that spans multiple aggregates */
private final DiscountPolicyRepository discountPolicies;
public BigDecimal calculateOrderTotal(Order order, Customer customer) { // Base total from order BigDecimal subtotal = order.calculateSubtotal();
// Apply customer discounts BigDecimal discount = calculateCustomerDiscount(customer, subtotal);
// Apply promotional discounts BigDecimal promoDiscount = calculatePromoDiscount(order);
// Calculate tax BigDecimal tax = calculateTax(order, customer);
return subtotal.subtract(discount) .subtract(promoDiscount) .add(tax); }
private BigDecimal calculateCustomerDiscount( Customer customer, BigDecimal subtotal ) { if (customer.isVip()) { return subtotal.multiply(new BigDecimal("0.10")); // 10% VIP } return BigDecimal.ZERO; }
private BigDecimal calculatePromoDiscount(Order order) { // Complex business logic here List<DiscountPolicy> policies = discountPolicies.findActivePolicies(); // ... apply policies return BigDecimal.ZERO; }}# Order Context publishes eventclass OrderService: def place_order(self, cart: Cart, customer: Customer): order = Order.from_cart(cart, customer) self._order_repo.save(order)
# Publish domain event event_bus.publish( OrderPlacedEvent( order_id=order.id, customer_id=customer.id, items=order.items, total=order.total ) )
# Payment Context subscribesclass PaymentService: @subscribe_to(OrderPlacedEvent) def handle_order_placed(self, event: OrderPlacedEvent): # Process payment in Payment context payment = Payment.for_order(event.order_id, event.total) result = self._payment_processor.charge(payment)
if result.success: event_bus.publish( PaymentReceivedEvent( order_id=event.order_id, payment_id=payment.id, amount=event.total ) )
# Fulfillment Context subscribesclass FulfillmentService: @subscribe_to(PaymentReceivedEvent) def handle_payment_received(self, event: PaymentReceivedEvent): # Create shipment in Fulfillment context shipment = Shipment.for_order(event.order_id) self._shipment_repo.save(shipment)
event_bus.publish( ShipmentCreatedEvent( order_id=event.order_id, shipment_id=shipment.id ) )Clear Boundaries
Bounded contexts provide natural service boundaries for microservices. No guessing where to split!
Business Alignment
Software structure mirrors business structure. Easier to understand and evolve with business.
Team Autonomy
Each bounded context owned by one team. Minimize coordination overhead.
Maintainability
Ubiquitous language and clear models make code self-documenting and easier to maintain.
Bounded Context = Service
Each bounded context becomes a microservice with its own database and domain model.
Speak the Language
Use ubiquitous language in code. If business says “place order”, method should be place_order(), not createOrderEntity().
Aggregate Boundaries
Transactions don’t cross aggregate boundaries. Use eventual consistency between aggregates.
Events Connect Contexts
Use domain events for loose coupling between bounded contexts. Enables eventual consistency.