Domain Driven Design is useful when the hardest part of a system is not syntax, frameworks, or database access, but understanding the business problem clearly enough to model it in code.
For a Java developer, DDD is a practical way to move from requirements and architecture diagrams toward software that reflects the real domain. It encourages teams to name concepts carefully, place behavior where it belongs, keep technology details away from the core model, and divide large systems into smaller models when one shared model becomes too difficult to maintain.
DDD is most valuable in complex domains. For small CRUD applications, it can be more structured than needed. But even when a team does not adopt DDD fully, its ideas are useful: build a shared language, avoid empty data objects, and protect business rules from accidental coupling.
The Problem
A common shortcut in enterprise applications is the anemic domain model.
In this style, domain objects mostly hold data. They have fields, getters, and setters, but very little behavior. Most of the real logic ends up inside service classes that operate on those objects.
class Payment {
UUID id;
UUID senderId;
UUID recipientId;
BigDecimal amount;
}
class PaymentService {
void validateAndSend(Payment payment) {
validate(payment);
send(payment);
}
void validate(Payment payment) {
}
void send(Payment payment) {
}
}
This can work for simple CRUD screens where the application mostly exposes database tables. The problem appears when the domain grows.
Service classes become larger. Data objects start to look alike. Rules become harder to find. The application slowly becomes procedural code wrapped in object-oriented syntax.
DDD points in the opposite direction. Instead of treating objects as passive records, it asks the team to build a rich model where important business concepts contain both state and behavior.
Core Idea
The core idea of DDD is to model software in a way that mirrors the real problem the system solves.
That requires collaboration between technical people and domain experts. Developers understand code structure, boundaries, and implementation tradeoffs. Domain experts understand the business rules, vocabulary, and exceptions that make the domain meaningful.
DDD joins those perspectives through ubiquitous language.
Ubiquitous language is a shared vocabulary used by everyone on the team. It should appear in conversations, diagrams, requirements, tests, and code. If the business says payment, recipient, sender, authorization, and transaction, the code should avoid unrelated or vague names that make the model harder to connect to the domain.
This is not solved by a magic template. It is built through repeated collaboration. The team discusses the domain, names the important concepts, corrects misunderstandings, and keeps the model aligned with the shared language.
Keep the Domain Protected with Layers
A domain model should not be shaped by database tables, web screens, or framework details. DDD commonly uses a layered architecture to keep responsibilities separated.
Presentation Layer
Collects input and presents output
Application Layer
Coordinates use cases
Domain Layer
Contains business model, state, and behavior
Infrastructure Layer
Connects to databases, external systems, and technical services
The presentation layer handles user interaction or machine-to-machine interaction, such as API calls.
The application layer coordinates the use case. It can call domain operations, order steps, and manage session-level needs. It should stay mostly stateless and should not become the place where business rules hide.
The domain layer is the heart of DDD. It contains the model: entities, value objects, services, aggregates, and the behavior that expresses the business rules.
The infrastructure layer contains technical glue. Persistence, database access, external service calls, and other technology-specific details belong here. This keeps the domain model cleaner and easier to reason about.
Model the Domain
DDD gives names to common building blocks in a rich domain model.
Entities
An entity is an object with a strong identity and a life cycle. Its identity matters more than its current field values.
A user is a typical entity. Two users can have the same name and still be different users. A payment can also be an entity because the system may need to track one exact transaction over time.
Value Objects
A value object is defined by its values, not by identity. It is usually immutable and easy to share.
An exchange rate is a useful example. It can contain currency symbols and a numeric rate. The exact object identity is not important. The values are what matter.
final class Money {
private final BigDecimal amount;
private final String currency;
Money(BigDecimal amount, String currency) {
this.amount = amount;
this.currency = currency;
}
BigDecimal amount() {
return amount;
}
String currency() {
return currency;
}
}
Immutability makes value objects easier to pass around safely. Since their values do not change, they are simpler to use in concurrent code and safer to share between methods.
Rich Entities
A rich entity contains behavior that naturally belongs to the business concept.
class Payment {
private final UUID id;
private final UUID senderId;
private final UUID recipientId;
private final Money amount;
private PaymentStatus status;
Payment(UUID id, UUID senderId, UUID recipientId, Money amount) {
this.id = id;
this.senderId = senderId;
this.recipientId = recipientId;
this.amount = amount;
this.status = PaymentStatus.CREATED;
}
void authorize() {
if (status != PaymentStatus.CREATED) {
throw new IllegalStateException("Payment is not ready for authorization");
}
status = PaymentStatus.AUTHORIZED;
}
}
enum PaymentStatus {
CREATED,
AUTHORIZED
}
The important difference is not the amount of code. The important difference is ownership. The rule for authorizing a payment is close to the payment itself.
Services
Some behavior does not belong naturally to one entity or value object. In that case, DDD uses services.
A peer-to-peer payment is a good example. It involves a sender, a recipient, and a transaction. Forcing that behavior into either the sender or the recipient can make the model confusing. A payment service can represent the operation more clearly.
interface PaymentRepository {
Payment findById(UUID id);
void save(Payment payment);
}
final class PaymentService {
private final PaymentRepository payments;
PaymentService(PaymentRepository payments) {
this.payments = payments;
}
void authorize(UUID paymentId) {
Payment payment = payments.findById(paymentId);
payment.authorize();
payments.save(payment);
}
}
The service coordinates the use case, while the entity still owns the rule that changes its state.
Aggregates
An aggregate is a consistency boundary. It groups entities and value objects that should be treated as one unit when state changes.
Each aggregate has a root entity. External code should interact with the aggregate through that root. This prevents unrelated parts of the system from changing internal objects in uncontrolled ways.
For example, a payment aggregate may contain a payment root and related objects needed to keep the transaction consistent. Changes should go through the payment root, not through random inner objects.
Use Factories and Repositories Carefully
DDD also describes patterns that support the domain model.
A factory helps create complex objects or aggregates. It is useful when construction requires coordination and should not be scattered throughout the codebase.
final class PaymentFactory {
Payment create(UUID senderId, UUID recipientId, Money amount) {
return new Payment(UUID.randomUUID(), senderId, recipientId, amount);
}
}
A repository provides a domain-friendly way to find, add, or remove objects. It hides persistence details from the domain. The repository can use a database, an external service, or another storage mechanism internally, but the domain should not need to know that.
The method names exposed by repositories should make sense in the domain, not only in the database implementation.
Split Large Models with Bounded Contexts
A single model can become impractical in a large system. The same word may mean different things in different areas. A customer in a CRM context may not be identical to a user in a payment context.
DDD handles this with bounded contexts.
A bounded context is the boundary where a particular model and language are valid. Inside that context, terms should be consistent. Outside it, another model may use different terms or different meanings.
Payments Context
Payment
Sender
Recipient
Customer Profile Context
Customer
Account
Contact Details
Interaction
Payments -> Anti-corruption layer -> Customer Profile
Different bounded contexts should not freely share internal objects or call arbitrary methods on each other. They should communicate through clear interfaces.
DDD describes several ways to connect contexts:
- Shared kernel: two contexts share a small part of the model.
- Customer supplier: one context provides an interface used by another.
- Conformity: one context adopts part of another model without controlling it.
- Anti-corruption layer: a translation layer prevents one model from leaking into another.
The anti-corruption layer is especially useful when integrating with legacy systems or with models that should remain independent.
Practical Workflow
A simple DDD workflow looks like this:
- Bring developers and domain experts into the same discussion.
- Identify the key business words and use them consistently.
- Find entities by looking for concepts with identity and history.
- Find value objects by looking for immutable descriptive data.
- Put behavior on the object that naturally owns the rule.
- Use services only when behavior spans multiple objects or does not fit one entity.
- Group related objects into aggregates around consistency boundaries.
- Keep database and infrastructure details outside the domain layer.
- Split large systems into bounded contexts when one model becomes too broad.
- Define explicit interfaces between contexts.
Common Mistakes
The first mistake is building an anemic model and calling it DDD. If objects only hold data and all rules live in services, the design misses one of the main goals of DDD.
The second mistake is designing the domain around database tables. Persistence matters, but it should not control the business model.
The third mistake is allowing technology details into the domain layer. Framework annotations, external protocols, and database-specific ideas can make the model harder to evolve.
The fourth mistake is skipping domain experts. Developers may understand the implementation, but domain experts bring the vocabulary and rules that make the model accurate.
The fifth mistake is sharing objects freely across bounded contexts. This creates hidden coupling and makes each model harder to change.
Checklist
Before implementing a DDD-inspired model, check the following:
- The main domain terms are understood by both business and technical people.
- Class names and method names match the shared language.
- Important entities have clear identities.
- Value objects are immutable where possible.
- Business behavior is close to the objects that own it.
- Application services coordinate use cases without absorbing all business logic.
- Infrastructure code does not shape the domain model.
- Aggregates have clear root entities.
- Bounded contexts have explicit interfaces.
- Context integration is intentional, not accidental.
Conclusion
Domain Driven Design helps Java teams model complex business problems with clearer boundaries and richer objects. Its value is not in adding patterns for their own sake. Its value is in making the code express the domain clearly.
Start with the language. Use entities, value objects, services, aggregates, repositories, and bounded contexts only where they clarify the model. Keep the domain protected from technology details. When the model reflects the business problem, the code becomes easier to discuss, test, and evolve.