Skip to content
Low Level Design Mastery Logo
LowLevelDesign Mastery

Domain-Driven Design (HLD View)

Organizing complex systems around business domains, not technical layers

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:

  • Classrooms (for teaching)
  • Library (for books and studying)
  • Cafeteria (for food)
  • Gym (for sports)

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 Layer
Service Layer
API Layer

Problem: Business logic scattered across technical layers!

DDD Approach (Business):

Order Management Context
Payment Processing Context
Inventory Management Context
Customer Management Context

Benefit: Each context is self-contained with clear business responsibility!


A bounded context is a explicit boundary where a particular domain model applies.

Diagram

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:

  1. One aggregate root (entry point)
  2. Maintain consistency within aggregate
  3. References between aggregates by ID only
  4. Transactions don’t cross aggregate boundaries
Diagram

Implementation:

Capture significant business events that domain experts care about.

from dataclasses import dataclass
from datetime import datetime
@dataclass
class OrderPlacedEvent:
"""Domain event - something that happened"""
order_id: str
customer_id: str
total: Decimal
timestamp: datetime
@dataclass
class OrderCancelledEvent:
order_id: str
reason: str
timestamp: datetime
@dataclass
class PaymentReceivedEvent:
order_id: str
amount: Decimal
payment_method: str
timestamp: datetime
# In aggregate
class 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 events

Why Events?

  • Loose coupling between bounded contexts
  • Audit trail of what happened
  • Event sourcing (store events instead of state)
  • Eventual consistency across contexts

Each bounded context becomes a microservice!

Diagram

How bounded contexts relate to each other:

Diagram

Context Mapping Patterns:

  1. Customer-Supplier

    • Order Context (customer) depends on Payment Context (supplier)
    • Payment Context provides API that Order Context consumes
  2. Conformist

    • Order Context conforms to Customer Context’s model
    • Accepts Customer’s definition of “customer”
  3. Published Language

    • Order Context publishes events in common format
    • Shipping Context subscribes to events
  4. Anticorruption Layer

    • Translation layer between contexts
    • Protects your model from external influences

Finding Bounded Contexts (Practical Guide)

Section titled “Finding Bounded Contexts (Practical Guide)”

What is Event Storming?

  • Workshop with developers and domain experts
  • Use sticky notes to map out business processes
  • Identify domain events, commands, aggregates, and contexts

Process:

  1. List domain events (orange sticky notes)

    • OrderPlaced
    • PaymentReceived
    • ItemShipped
    • CustomerRegistered
  2. Add commands that trigger events (blue sticky notes)

    • PlaceOrder → OrderPlaced
    • ProcessPayment → PaymentReceived
    • ShipItem → ItemShipped
  3. Identify aggregates (yellow sticky notes)

    • Order (handles PlaceOrder, CancelOrder)
    • Payment (handles ProcessPayment, RefundPayment)
    • Shipment (handles ShipItem, TrackShipment)
  4. Draw boundaries (pink lines)

    • Where language changes = bounded context boundary!
    • Where team ownership changes = bounded context boundary!

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, complaints

Each capability = potential bounded context!

Method 3: Look for Different Models of Same Concept

Section titled “Method 3: Look for Different Models of Same Concept”

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 discovery

Product in Inventory Context:

class Product:
sku: str
quantity_on_hand: int
reorder_point: int
warehouse_location: str
# Focus: stock management

Product in Order Context:

class OrderLineProduct:
product_id: str
name: str
price: Decimal
# Focus: what was ordered at what price

Different models = different contexts!


Abstracts data access for aggregates.

When business logic doesn’t fit in an entity:


Diagram
# Order Context publishes event
class 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 subscribes
class 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 subscribes
class 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.



  • “Domain-Driven Design” by Eric Evans (the blue book)
  • “Implementing Domain-Driven Design” by Vaughn Vernon (the red book)
  • “Domain-Driven Design Distilled” by Vaughn Vernon (quick intro)
  • “Learning Domain-Driven Design” by Vlad Khononov (modern take)
  • Domain-Driven Design Community - dddcommunity.org