Domain-Driven Design (DDD) is a software engineering approach that solves complex problems by connecting the code directly to the business domain. When combined with Jakarta EE 11 and Java 25, you can build enterprise systems that are scalable, easy to maintain, and truly reflect the business rules.
In this guide, we will explore the core concepts of DDD, breaking it down into Strategic Design and Tactical Design. We will then show you how to implement these concepts using the latest features in Jakarta EE 11, including Jakarta Data 1.0 and Jakarta Persistence (Hibernate 7).
What is Domain-Driven Design (DDD)?
At its core, DDD focuses on understanding the business (the domain) before writing any code. Developers and domain experts work together to create a shared vocabulary. This is called the Ubiquitous Language. Every term used in discussions must appear exactly the same way in the Java code.
If the business talks about an Order, a Customer, and a Product, your code should have classes named exactly Order, Customer, and Product. DDD prevents the code from drifting away from the actual business reality.
Strategic Design: Bounded Contexts
Large enterprise systems are too complex to model as one giant application. Strategic DDD solves this by splitting the system into smaller, logical boundaries called Bounded Contexts.

A Bounded Context defines a specific area of the business. For example, an e-commerce platform might have:
- Inventory Context: Focuses on stock levels and warehouse locations.
- Orders Context: Focuses on shopping carts, checkout, and payment status.
- Shipping Context: Focuses on tracking numbers and delivery routes.
A Product in the Inventory context means “a physical box in a warehouse.” A Product in the Orders context means “a price and a name shown to the user.” By separating these contexts, we avoid creating a massive, confusing Product class that tries to do everything.
Tactical Design: The Building Blocks
Once you define your Bounded Contexts, you use Tactical DDD to write the code inside them. Tactical design provides specific patterns to structure your Java classes.

1. Entities and Value Objects
In DDD, there are two main types of domain objects:
- Entity: An object with a unique identity that changes over time. For example, an
Orderhas an ID and a status (Pending, Shipped). - Value Object: An object with no identity. It is defined only by its attributes. It is immutable. For example, a
Moneyobject (amount and currency) or aShippingAddress.
2. Aggregates and Aggregate Roots
An Aggregate is a group of related Entities and Value Objects treated as a single unit. The Aggregate Root is the main entity that controls access to the aggregate.
For example, an Order is an Aggregate Root. It contains OrderItem entities. You should never modify an OrderItem directly. You must ask the Order to add or remove items. The Aggregate Root protects the business rules (invariants).
Mapping DDD to Jakarta EE 11
Jakarta EE 11 provides the perfect annotations to map these DDD concepts to actual code. Let’s look at how they connect.

Implementing Value Objects with @Embeddable
We use the @Embeddable annotation from Jakarta Persistence to create Value Objects. In Java 25, we can use record types for pure immutability, but for JPA mapping, standard classes are often easier.
import jakarta.persistence.Embeddable;
@Embeddable
public class Address {
private String street;
private String city;
private String zipCode;
protected Address() {} // Required by JPA
public Address(String street, String city, String zipCode) {
this.street = street;
this.city = city;
this.zipCode = zipCode;
}
// Getters only. No setters to enforce immutability!
}
Implementing the Aggregate Root with @Entity
We use the @Entity annotation to define our Aggregate Root. Notice how the Order class protects its state. It does not have public setters.
import jakarta.persistence.*;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
@Entity
@Table(name = "orders")
public class Order {
@Id
private UUID id;
@Embedded
private Address shippingAddress;
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
private List<OrderItem> items = new ArrayList<>();
private String status;
protected Order() {} // JPA requirement
public Order(UUID id, Address shippingAddress) {
this.id = id;
this.shippingAddress = shippingAddress;
this.status = "PENDING";
}
// Business method inside the Aggregate Root
public void addItem(OrderItem item) {
// Enforce business rules here
if (this.status.equals("SHIPPED")) {
throw new IllegalStateException("Cannot add items to a shipped order");
}
this.items.add(item);
}
}
Repositories with Jakarta Data 1.0
In DDD, a Repository is responsible for saving and loading Aggregate Roots. You should only create repositories for Aggregate Roots, never for internal entities.

Jakarta Data 1.0 makes it incredibly easy to define DDD repositories. You simply create an interface and use the @Repository annotation. The framework generates the implementation automatically.
import jakarta.data.repository.CrudRepository;
import jakarta.data.repository.Repository;
import java.util.UUID;
@Repository
public interface OrderRepository extends CrudRepository<Order, UUID> {
// Custom query method generated by Jakarta Data
List<Order> findByStatus(String status);
}
Real-World Use Cases
Domain-Driven Design is not for simple CRUD (Create, Read, Update, Delete) applications. It shines in complex environments:
- Fintech and Banking: Handling complex transactions, ledgers, and currency exchanges where strict business rules must apply.
- Healthcare Systems: Managing patient records, appointments, and insurance claims across different hospital departments (Bounded Contexts).
- E-commerce Platforms: Handling dynamic pricing, advanced inventory reservations, and complex shipping logistics.
Advantages and Limitations
Advantages
- Clear Code: The code reflects the business reality. Developers and domain experts understand each other.
- Safe Modifications: Aggregate Roots enforce business rules, making it impossible to leave the system in an invalid state.
- Microservices Ready: Bounded Contexts naturally map to microservice boundaries.
Limitations
- High Learning Curve: DDD introduces many new terms and concepts that take time to learn.
- Overkill for Simple Apps: If your application simply reads and writes data to a database without complex rules, DDD adds unnecessary complexity.
Best Practices and Common Mistakes
Best Practices
- Focus on the Domain: Spend time talking to business experts before writing code.
- Use Value Objects: Replace primitive types (like
Stringorint) with Value Objects (likeEmailAddressorQuantity) whenever possible. - One Aggregate per Transaction: A single database transaction should only modify one Aggregate Root. If you need to update multiple aggregates, use Domain Events.
Common Mistakes
- Anemic Domain Model: Creating classes with only getters and setters, and putting all the business logic in separate “Service” classes. The logic should live inside the Entities.
- Repositories for Everything: Creating a repository for every single entity. Remember, only Aggregate Roots get repositories.
- Ignoring the Ubiquitous Language: Using different names in the code than the business uses in meetings.
Conclusion
Domain-Driven Design provides a powerful way to manage software complexity. By using Bounded Contexts, you can organize large systems. By using Tactical patterns like Aggregate Roots and Value Objects, you can write safe, expressive Java code.
Jakarta EE 11, with Jakarta Persistence and the new Jakarta Data 1.0 specification, offers standard, built-in tools to implement these DDD concepts seamlessly. If you want to see a more advanced architecture, check out our guide on Hexagonal Architecture and DDD.
Companion Workshop Code
Ready to see this running? Get the complete, compilable Java 25 and Jakarta EE 11 project from our GitHub repository. It includes all the entities, repositories, and Testcontainers integration tests.
👉 View the Companion Code on GitHub
References
- Jakarta Data 1.0 Official Specification
- Jakarta Persistence (JPA) Specification
- Domain-Driven Design: Tackling Complexity in the Heart of Software by Eric Evans (The Blue Book)
