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_payment are part of this language.
  • Aggregates: Clusters of domain objects that can be treated as a single unit. Order is an aggregate root; OrderItem is part of the Order aggregate. You interact with the aggregate through its root.
  • Entities: Objects with a distinct identity that persists over time. Order is an entity, identified by order_id.
  • Value Objects: Objects that represent descriptive aspects of the domain and are defined by their attributes, not identity. OrderItem could 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, OrderShipped are domain events.
  • Domain Services: Operations or concepts that don’t naturally fit within a single entity or value object. OrderPlacementService encapsulates the logic for placing an order, which involves checking customer existence and creating an Order aggregate.

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.

Want structured learning?

Take the full Monolith course →