Design software systems that model complex business domains. Covers bounded contexts, aggregates, value objects, domain events, context mapping, and the patterns that align code structure with business understanding.
Domain-Driven Design is an approach to software development that aligns code, models, and team structure around the core business domain. It matters when a system must evolve with complex, changing business logic—when the software is not just a tool, but the engine of the business.
A bounded context defines the scope of a domain model, where a set of terms, rules, and processes are consistent and mutually understood. A bounded context is not a module or a service—it is a conceptual boundary.
To define a bounded context:
Example: In a logistics platform, *Order Management*, *Inventory*, and *Billing* are distinct bounded contexts.
Each context owns its domain model, including:
Order, InventoryItem)OrderShipped, StockLow)OrderFulfillmentService)A context map might show:
Order Management depends on Inventory via a *published event*.Billing consumes OrderShipped and InvoiceGenerated from Order Management.Inventory and Billing share a Customer model via *shared kernel*.Ubiquitous Language is the common vocabulary used by developers, domain experts, and business stakeholders. It is not a glossary—it is a living contract.
To implement Ubiquitous Language:
Order, ShippingAddress, LineItem, PaymentStatus. public class Order {
private UUID id;
private Customer customer;
private List<LineItem> items;
private PaymentStatus paymentStatus;
private Instant orderDate;
public void addLineItem(Product product, int quantity) { ... }
public boolean isFullyPaid() { ... }
public void ship(ShippingAddress shippingAddress) {
this.shippingAddress = shippingAddress;
publish(new OrderShipped(this.id));
}
}
Order order = OrderRepository.findById(orderId);
order.ship(new ShippingAddress("123 Main St", "Portland", "OR", "97201"));
public class OrderFulfillmentService {
public void fulfill(Order order) {
if (order.isFullyPaid()) {
InventoryService.reserve(order.items());
order.markAsFulfilled();
order.publish(new OrderFulfilled(order.id));
} else {
throw new UnpaidOrderException(order.id);
}
}
}
An aggregate is a cluster of related objects treated as a unit for data changes. The root entity of the aggregate ensures consistency.
Example: Order is an aggregate root. It enforces:
public class Order {
private final List<LineItem> lineItems = new ArrayList<>();
public void addLineItem(Product product, int quantity) {
LineItem item = new LineItem(product, quantity);
lineItems.add(item);
ensureInvariants();
}
private void ensureInvariants() {
if (lineItems.isEmpty()) {
throw new InvalidOrderException("Order must have at least one item");
}
BigDecimal total = lineItems.stream()
.map(li -> li.product().price().multiply(BigDecimal.valueOf(li.quantity())))
.reduce(BigDecimal.ZERO, BigDecimal::add);
if (!total.equals(this.total)) {
throw new InconsistentTotalException("Total does not match line item sum");
}
}
public void markAsPaid(Money amount) {
if (amount.compareTo(this.total) < 0) {
throw new InsufficientPaymentException(this.id, amount, this.total);
}
this.paymentStatus = PaymentStatus.PAID;
publish(new PaymentConfirmed(this.id, amount));
}
}
Domain events are immutable facts that describe something significant that happened in the domain.
To publish an event:
public class OrderShipped {
private final UUID orderId;
private final Instant shippedAt;
private final ShippingAddress destination;
public OrderShipped(UUID orderId, Instant shippedAt, ShippingAddress destination) {
this.orderId = orderId;
this.shippedAt = shippedAt;
this.destination = destination;
}
// getters
}
public class Order {
public void ship(ShippingAddress shippingAddress) {
this.shippingAddress = shippingAddress;
this.shippedAt = Instant.now();
DomainEventPublisher.publish(new OrderShipped(this.id, this.shippedAt, shippingAddress));
}
}
public class BillingService {
public BillingService(EventBus eventBus) {
eventBus.subscribe(OrderShipped.class, this::handleOrderShipped);
}
public void handleOrderShipped(OrderShipped event) {
Invoice invoice = new Invoice(event.orderId(), event.shippedAt());
InvoiceRepository.save(invoice);
}
}
Repositories provide a collection-like interface for accessing aggregates.
public interface OrderRepository {
Order findById(UUID id);
void save(Order order);
List<Order> findUnshippedOrders();
}
public class JdbcOrderRepository implements OrderRepository {
private final DataSource dataSource;
public JdbcOrderRepository(DataSource dataSource) {
this.dataSource = dataSource;
}
@Override
public Order findById(UUID id) {
String sql = "SELECT * FROM orders WHERE id = ?";
try (Connection conn = dataSource.getConnection();
PreparedStatement stmt = conn.prepareStatement(sql)) {
stmt.setObject(1, id);
try (ResultSet rs = stmt.executeQuery()) {
if (rs.next()) {
return mapRowToOrder(rs);
}
throw new OrderNotFoundException(id);
}
} catch (SQLException e) {
throw new RepositoryException("Failed to load order", e);
}
}
private Order mapRowToOrder(ResultSet rs) throws SQLException {
UUID id = rs.getObject("id", UUID.class);
Instant orderDate = rs.getTimestamp("order_date").toInstant();
String status = rs.getString("payment_status");
// ... map items from separate table
return new Order(id, orderDate, PaymentStatus.valueOf(status), items);
}
@Override
public void save(Order order) {
// ... insert or update order and line items
}
}
The most common failure is overloading aggregates with too much logic. A Customer aggregate might handle:
But when the business introduces a new concept—customer tier—the Customer aggregate becomes bloated.
Symptoms:
Customer updates.Customer table during peak load.Solution:
Customer into Customer, CustomerAddress, CustomerLoyalty, CustomerBilling. public class Customer {
public void upgradeToPremium() {
this.tier = CustomerTier.PREMIUM;
publish(new CustomerUpgradedToPremium(this.id, Instant.now()));
}
}
CustomerUpgradedToPremium: public class LoyaltyService {
public void handleCustomerUpgradedToPremium(CustomerUpgradedToPremium event) {
// grant bonus points, send welcome email
}
}
When two contexts share a model (e.g., Customer), they must agree on structure and semantics.
Shared Kernel:
Customer class.Customer require coordination.Conformist:
Billing) consumes Customer from another (e.g., Order Management).Billing does not own Customer; it conforms to the model published by Order Management.Failure mode:
Order Management adds a preferredShippingMethod field to Customer.Billing does not update its model and fails to read the new field.org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'billingService': Could not autowire field: private com.example.customer.Customer customerService; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'customerService': Injection of autowired dependencies failedSolution:
Order Management publishes CustomerUpdated event with full Customer data.Billing listens and updates its Customer model.Customer has customer_id, name, email, preferred_shipping_method.Billing reads preferred_shipping_method only when available.When a business process spans multiple aggregates and bounded contexts, use sagas.
Example: *Order Fulfillment Saga*:
Order is created.Inventory reserves stock.Payment processes payment.Order ships and publishes OrderShipped.Each step is a compensating transaction in case of failure.
public class OrderFulfillmentSaga {
private final SagaRepository sagaRepository;
public void startFulfillment(UUID orderId) {
OrderFulfillmentSaga saga = new OrderFulfillmentSaga(orderId);
saga.addStep(new ReserveStockStep(orderId));
sagaRepository.save(saga);
}
public void handleStockReserved(StockReserved event) {
OrderFulfillmentSaga saga = sagaRepository.findById(event.sagaId());
saga.markStepAsCompleted(Step.RESERVE_STOCK);
if (saga.allStepsCompleted()) {
sagaRepository.save(saga);
publish(new PaymentRequested(saga.orderId()));
}
}
public void handlePaymentFailed(PaymentFailed event) {
OrderFulfillmentSaga saga = sagaRepository.findById(event.sagaId());
saga.markStepAsFailed(Step.PAYMENT);
sagaRepository.save(saga);
publish(new CompensateStockReservation(saga.orderId()));
}
public void handleStockReservationFailed(CompensateStockReservation event) {
InventoryService.release(event.orderId());
}
}
com.example.order.InvalidOrderException: Order must have at least one itemOrder.addLineItem() called before Order was saved.Order is created and persisted before calling addLineItem().OrderShipped event published, but BillingService takes 500ms to process.Order returns shippedAt = null even though OrderShipped was published.OrderReadModel listens to OrderShipped, OrderPaid, etc.OrderReadModel updates OrderView table with derived data.OrderShipped event is published, but BillingService is not running.Invoice is created.OrderShipped appears in events table.BillingService starts, but handleOrderShipped is never called.BillingService is not subscribed to the event bus. # Check event bus subscription
curl http://event-bus:8080/api/subscriptions
# Expected: {"service": "billing-service", "events": ["OrderShipped"]}
If missing, register subscription:
POST /api/subscriptions
{
"service": "billing-service",
"events": ["OrderShipped"]
}
Response: 201 Created with {"id": "sub-123", "service": "billing-service"}
Money, Address, Email. {
"eventId": "1a2b3c",
"eventType": "OrderShipped",
"version": 2,
"timestamp": "2024-01-15T10:00:00Z",
"data": {
"orderId": "d4e5f6",
"shippedAt": "2024-01-15T10:05:00Z",
"destination": {
"addressLine1": "123 Main St",
"city": "Portland",
"state": "OR",
"zipCode": "97201"
}
}
}
Domain-Driven Design is not a checklist—it is a discipline of thinking. It builds software that evolves with the business, not just in response to it.
This page was rewritten on 15 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.
We use cookies for analytics (Google Analytics) and advertising (Google AdSense) to improve your experience and support free content. Privacy Policy