Core DDD Concepts
Domain and Subdomains
A domain is the subject area your software addresses—the business problem space. An e-commerce platform’s domain includes everything from catalog browsing and shopping carts to payment processing and logistics. Complex domains are divided into subdomains, each focused on a distinct business capability:
Identifying subdomain types guides where to invest custom DDD modeling effort (core subdomains) versus where to use off-the-shelf solutions (generic subdomains).
Bounded Context
A bounded context is an explicit boundary within which a particular domain model applies. Inside the boundary, every term in the model has a precise and unambiguous meaning. The same word can mean different things in different bounded contexts, and that is by design. Example: The word “customer” means something different to yourOrderContext (a party placing an order, with a shipping address) than to your MarketingContext (a contact record with campaign history and segmentation tags). If you force a single Customer entity to satisfy both contexts, you end up with a bloated model that serves neither well.
A context map documents how your bounded contexts relate to each other:
- Partnership: Two teams coordinate closely; changes are made together.
- Shared Kernel: A small shared model that both contexts agree on and co-own.
- Customer/Supplier: The downstream context (customer) relies on the upstream context (supplier) to provide what it needs; the supplier has a published API.
- Anti-Corruption Layer: The downstream context translates the upstream model to protect its own domain model from external influence.
- Conformist: The downstream context adopts the upstream model as-is (useful when you have no influence over the upstream).
Ubiquitous Language
Ubiquitous language is the shared vocabulary that developers and domain experts build together and use consistently in all conversations, documentation, and code. If the business calls it an “invoice,” your code has anInvoice class—not a BillingDocument or a PaymentRequest. If a business process is called “fulfilling an order,” you have a method order.fulfill(), not order.setStatus("fulfilled").
Maintaining ubiquitous language requires ongoing collaboration. When a domain expert uses a new term or corrects your terminology, update the code to match. Terminology drift between business and engineering is an early warning sign of a deteriorating model.
Building Blocks
Entity vs. Value Object
Entities have a distinct identity that persists across state changes. TwoOrder objects with the same items but different IDs are different orders—their identity matters more than their current attributes.
Value Objects have no identity; they are defined entirely by their attribute values. Two Money objects both representing “50 USD” are interchangeable. Value objects should be immutable—to change a value, you replace it rather than mutating it.
Address, Email, DateRange, Coordinates, Money. Examples of entities: Order, User, Product, Invoice.
Aggregate and Aggregate Root
An aggregate is a cluster of related entities and value objects that must remain consistent with each other. The aggregate root is the single entity within the cluster through which all external access occurs. No outside object holds a direct reference to any non-root member of the aggregate; they can only hold a reference to the root. The aggregate root is responsible for enforcing all invariants (business rules) within its boundary. Example: AnOrder aggregate contains Order (root), OrderItem entities, and a ShippingAddress value object. You access order items only through the Order root; you never inject an OrderItem into another service directly.
- Keep aggregates small. A large aggregate is a performance and contention risk.
- Reference other aggregates by ID only, not by object reference.
- Apply one repository per aggregate root.
- Enforce invariants within the aggregate boundary on every state change.
Domain Events
A domain event is a record of something meaningful that happened in the domain. Events use past tense:OrderConfirmed, PaymentReceived, ShipmentDispatched. They capture what happened, when it happened, and the data relevant to that event.
Domain events decouple bounded contexts from each other. When OrderContext confirms an order, it publishes an OrderConfirmed event. InventoryContext listens to that event and decrements stock; NotificationContext listens and sends a confirmation email. Neither listener knows anything about the other, and OrderContext knows nothing about either listener.
Repository
The Repository pattern provides collection-like access to aggregates while abstracting away the data storage mechanism. Your domain layer defines repository interfaces; the infrastructure layer provides implementations (JPA, MyBatis, MongoDB driver, etc.).Anti-Corruption Layer
The Anti-Corruption Layer (ACL) is a translation layer between your domain model and an external system or legacy model that you do not control. It prevents the external model’s concepts and terminology from “leaking” into your domain and corrupting its purity. When you need it: You are integrating with a legacy system whose data model is incompatible with your domain model, but you cannot modify the legacy system (perhaps because it is still serving an older version of the product or another team owns it). Example: Suppose your new DDD-based system has anEmployee domain model with a clean resigned: boolean field, but the legacy system uses a table with an integer is_resigned column (0 or 1) that you cannot rename or alter. The ACL sits at the boundary and translates:
- Translating field names and types between models.
- Converting enumerations and status codes.
- Validating and sanitizing inputs from external systems.
- Protecting the domain model from invalid external data.
- Encapsulating database query details (e.g.,
QueryWrapperconstruction for ORM frameworks).
DDD and Microservices
Bounded contexts and microservices align naturally: one bounded context maps to one (or a small set of) microservices. Each microservice owns its data store and is responsible for the consistency of its own aggregate root. Services communicate through domain events (asynchronous) or lightweight APIs (synchronous), but never through shared databases.Mapping Patterns
Data Isolation and Eventual Consistency
Each microservice owns its own data store—no shared databases across service boundaries. Cross-service operations use domain events and eventual consistency rather than distributed transactions. WhenOrderService confirms an order, it publishes OrderConfirmed; InventoryService consumes the event and decrements stock asynchronously. The two services may be briefly inconsistent, but they eventually converge.
For scenarios that truly require coordination across services (e.g., “reserve inventory AND confirm order atomically”), use the Saga pattern: a sequence of local transactions each publishing an event, with compensating transactions to undo completed steps if a later step fails.
Layered Architecture
DDD recommends a four-layer architecture that enforces a dependency direction (outer layers depend inward; the domain layer has no external dependencies):- User Interface Layer: Receives HTTP or gRPC requests and translates them into application commands. Returns results as DTOs or view objects (VO).
- Application Layer: Coordinates domain objects to fulfill use cases. Contains no business logic itself—it sequences domain operations. Uses Data Transfer Objects (DTO) for input/output.
- Domain Layer: The heart of the system. Contains all business logic in entities, aggregates, value objects, domain services, and domain events. Defines repository interfaces but does not implement them.
- Infrastructure Layer: Implements repository interfaces (using JPA, MyBatis, etc.), adapters for external services, and message brokers. Contains Persistent Objects (PO) that map to database tables.
When you adopt DDD with microservices, start by identifying your bounded contexts from the business domain—not from your existing database schema or service structure. Premature decomposition based on technical boundaries (e.g., “one service per table”) leads to chatty, tightly coupled services that are harder to maintain than a well-structured monolith.