Domain-Driven Design without microservices is less about how you structure your code and more about how you think about your business problems.
Let’s see what that looks like in practice. Imagine we’re building an e-commerce platform. We’ve identified a core business capability: managing customer orders. Instead of thinking about "order services" or "payment services" as separate deployable units, we focus on the domain of "Order Management."
Here’s a simplified representation of what that might look like in code, using a common object-oriented approach. We’re not talking about RPC calls or network latency here; we’re talking about method calls within the same process.
# --- Domain Model ---
class Order:
def __init__(self, customer_id: int, items: list[OrderItem]):
self.order_id = generate_unique_id() # Domain concept: unique identifier
self.customer_id = customer_id
self.items = items
self.status = OrderStatus.PENDING # Domain concept: lifecycle state
self.total_amount = sum(item.price * item.quantity for item in items)
def add_item(self, item: OrderItem):
if self.status != OrderStatus.PENDING:
raise OrderCannotBeModifiedError("Cannot add items to a finalized order.")
self.items.append(item)
self.total_amount = sum(i.price * i.quantity for i in self.items)
def confirm_payment(self, payment_transaction_id: str):
if self.status != OrderStatus.PENDING:
raise OrderAlreadyConfirmedError("Order payment already confirmed.")
self.status = OrderStatus.PAID
# Domain event: OrderPaid(order_id=self.order_id, transaction_id=payment_transaction_id)
def ship(self):
if self.status != OrderStatus.PAID:
raise OrderCannotBeShippedError("Order must be paid before shipping.")
self.status = OrderStatus.SHIPPED
# Domain event: OrderShipped(order_id=self.order_id)
class OrderItem:
def __init__(self, product_id: str, quantity: int, price: float):
self.product_id = product_id
self.quantity = quantity
self.price = price # Price at the time of order
# --- Domain Services (if needed for complex operations) ---
class OrderPlacementService:
def __init__(self, order_repository: OrderRepository, customer_service: CustomerService):
self.order_repository = order_repository
self.customer_service = customer_service
def place_order(self, customer_id: int, items_data: list[dict]) -> Order:
# Validate customer existence
if not self.customer_service.customer_exists(customer_id):
raise CustomerNotFoundError(f"Customer {customer_id} not found.")
order_items = [OrderItem(item_data['product_id'], item_data['quantity'], item_data['price']) for item_data in items_data]
new_order = Order(customer_id, order_items)
# Persist the order
self.order_repository.save(new_order)
# Publish domain events (e.g., OrderPlaced)
publish_domain_event(OrderPlaced(order_id=new_order.order_id, customer_id=new_order.customer_id))
return new_order
# --- Infrastructure (Repositories, External Services) ---
class OrderRepository:
def __init__(self):
self._orders = {} # In-memory for example
def save(self, order: Order):
self._orders[order.order_id] = order
def get_by_id(self, order_id: str) -> Order:
return self._orders.get(order_id)
class CustomerService: # Represents interaction with a separate "Customer" domain/service
def customer_exists(self, customer_id: int) -> bool:
# ... actual check ...
return True
# --- Application/Use Case Layer ---
class OrderApplicationService:
def __init__(self, order_repository: OrderRepository, order_placement_service: OrderPlacementService):
self.order_repository = order_repository
self.order_placement_service = order_placement_service
def create_new_order(self, customer_id: int, items_data: list[dict]) -> str:
order = self.order_placement_service.place_order(customer_id, items_data)
return order.order_id
def pay_for_order(self, order_id: str, transaction_id: str):
order = self.order_repository.get_by_id(order_id)
if order:
order.confirm_payment(transaction_id)
self.order_repository.save(order) # Save changes
# Publish domain event: OrderPaid
else:
raise OrderNotFoundError(f"Order {order_id} not found.")
The core problem DDD addresses, whether in a monolith or microservices, is managing complexity. It forces you to deeply understand the business domain you’re modeling. In a monolith, this means organizing your code around these domain concepts (like Order, Customer, Product) rather than by technical layers (like Controllers, Services, Repositories) or by superficial business functions. You build "Bounded Contexts" within your monolith, which are logical boundaries for your domain models.
The key levers you control are:
- Ubiquitous Language: A shared language between developers and domain experts. In the code above, terms like
Order,OrderStatus.PENDING,confirm_paymentare part of this language. - Aggregates: Clusters of domain objects that can be treated as a single unit.
Orderis an aggregate root;OrderItemis part of theOrderaggregate. You interact with the aggregate through its root. - Entities: Objects with a distinct identity that persists over time.
Orderis an entity, identified byorder_id. - Value Objects: Objects that represent descriptive aspects of the domain and are defined by their attributes, not identity.
OrderItemcould be considered a value object if we don’t care about the identity of a specific line item on an order, only its properties. - Domain Events: Significant occurrences within the domain that other parts of the system might react to.
OrderPlaced,OrderPaid,OrderShippedare domain events. - Domain Services: Operations or concepts that don’t naturally fit within a single entity or value object.
OrderPlacementServiceencapsulates the logic for placing an order, which involves checking customer existence and creating anOrderaggregate.
The one thing most people don’t realize is that DDD in a monolith is more about strategic design and less about tactical code organization than people think. It’s about drawing clear boundaries between different parts of your business logic within the single codebase, ensuring that the language and concepts used in one area don’t leak into or corrupt another. This prevents the monolith from becoming a "big ball of mud."
The next challenge you’ll face is how to evolve these Bounded Contexts within your monolith as your business grows and changes.