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:
An aggregate is a cluster of objects treated as a single unit for data changes.
Rules:
Implementation:
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
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.
When business logic doesn’t fit in an entity:
# 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.