Mastering DDD with Spring Data JPA: Repositories, Lazy Loading, and Real‑World Order Example
This article explains how domain‑driven design (DDD) leverages object‑oriented principles and the Repository pattern, demonstrates integrating Spring Data JPA for persistence, and walks through a complete order lifecycle—including creation, address modification, and payment—showcasing lazy loading, automatic synchronization, and practical unit‑test examples.
1. Object‑Oriented Design Is the Core of DDD
DDD focuses on mapping domain concepts and objects to code so that the object model reflects the real business situation, making the design more understandable and maintainable.
DDD (Domain‑Driven Design) is a methodology that solves business problems by building a clear understanding of the domain model. Unlike transaction scripts, DDD uses object‑oriented design to handle complex business scenarios.
In simple terms, DDD lets domain objects carry business logic; all operations are performed on model objects, and different operations on the same object constitute its lifecycle.
Consider an order example:
First, the user places an order, creating an Order object (Version V1).
Then the user changes the address, invoking modifyAddress on the Order object, moving it from V1 to V2.
After payment, the user calls paySuccess, moving the Order from V2 to V3.
All business logic is performed by the business objects, so object‑orientation is the core of DDD design.
2. Why Do We Need a Repository?
If we imagined a super‑powerful computer with infinite memory, we could keep all model objects in memory. In reality, we must persist objects to disk and reload them later.
Compared with the all‑in‑memory version, the persisted version adds:
Unchanged business operations (order, address change, payment).
Persistence storage (MySQL) to store Order objects.
Additional save, load, and update operations aligned with the order lifecycle.
Creating an Order calls save to persist it.
Business operations invoke load to reconstruct the object from the DB.
After modifications, update synchronizes the latest state back to the DB.
In DDD, a Repository is a design pattern that acts as a container for storing domain objects, providing a uniform way to query and persist them while abstracting the underlying data‑store technology.
3. What Makes a Good Repository?
A good Repository should satisfy business needs while exhibiting the following traits:
High cohesion – adheres to the Single Responsibility Principle, focusing on one aggregate root.
Loose coupling – interacts with other layers through abstract interfaces.
Ease of use – offers a simple set of methods for developers.
Maintainability – easy to understand without extensive code reading.
In plain language:
Provide a unified Repository interface with easy‑to‑use save, load, update methods.
Create a specific Repository interface for each aggregate root, inheriting from the unified one.
Keep the implementation as simple as possible, ideally relying on the framework.
4. Getting Started with Spring Data
Spring Data simplifies data‑access layer development by abstracting interactions with various storage technologies (relational, document, graph, cache, etc.).
Spring Data provides generic data‑access interfaces (e.g., Repository) and auto‑generates implementation code, allowing developers to focus on business logic.
4.1. Adding Spring Data JPA
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>Also add the MySQL driver:
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>Spring Data JPA’s default implementation is Hibernate, the most popular JPA provider with powerful mapping, query, and transaction capabilities.
4.2. Configuration
Add DB and JPA settings to application.yml:
spring:
application:
name: Spring-Data-for-DDD-demo
datasource:
# database configuration
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/books
username: root
password: root
jpa:
# show sql
show-sql: trueEnable Spring Data JPA on the main class:
@SpringBootApplication
@EnableJpaRepositories(basePackages = "com.geekhalo.springdata4ddd.order.repository")
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}4.3. Using a Repository
Create a repository for the Order aggregate:
public interface OrderCommandRepository extends JpaRepository<Order, Long> {
}5. Practical Example – Order
The domain model consists of:
One Order per order.
One OrderAddress per order.
Multiple OrderItem objects per order.
Core entity code:
@Data
@Entity
@Table(name = "tb_order")
@Setter(AccessLevel.PRIVATE)
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "user_id")
private Long userId;
@Column(name = "status")
@Enumerated(EnumType.STRING)
private OrderStatus status;
@Column(name = "price")
private int price;
// One‑to‑one address
@OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
@JoinColumn(name = "user_address_id")
private OrderAddress address;
// One‑to‑many items
@OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
@JoinColumn(name = "order_id")
private List<OrderItem> items = new ArrayList<>();
}5.1. Creating an Order
Service method:
@Transactional(readOnly = false)
public Order createOrder(CreateOrderCommand command) {
// create in‑memory object
Order order = Order.create(command);
// persist to DB
this.repository.save(order);
return order;
}
public static Order create(CreateOrderCommand command) {
Order order = new Order();
order.setUserId(command.getUserId());
String userAddress = command.getUserAddress();
if (!StringUtils.hasText(userAddress)) {
OrderAddress orderAddress = new OrderAddress();
orderAddress.setDetail(userAddress);
order.setAddress(orderAddress);
}
command.getProducts().stream()
.map(product -> OrderItem.create(product))
.forEach(order::addOrderItem);
order.init();
return order;
}Unit test verifies that a single save persists the whole aggregate.
5.2. Modifying the Address
@Transactional(readOnly = false)
public void modifyAddress(Long orderId, String address) {
Optional<Order> opt = repository.findById(orderId);
if (opt.isPresent()) {
Order order = opt.get();
order.modifyAddress(address);
this.repository.save(order);
}
}
public void modifyAddress(String address) {
if (this.address == null) {
this.address = new OrderAddress();
}
this.address.modify(address);
}
public void modify(String address) {
setDetail(address);
}Tests cover both adding a new address and updating an existing one, demonstrating lazy loading and automatic synchronization.
5.3. Paying for an Order
@Transactional(readOnly = false)
public void paySuccess(PaySuccessCommand command) {
Optional<Order> opt = repository.findById(command.getOrderId());
if (opt.isPresent()) {
Order order = opt.get();
order.paySuccess(command);
this.repository.save(order);
}
}
public void paySuccess(PaySuccessCommand cmd) {
this.setStatus(OrderStatus.PAID);
this.items.forEach(OrderItem::paySuccess);
}
public void paySuccess() {
setStatus(OrderItemStatus.PAID);
}Unit tests confirm that lazy loading fetches OrderItem collections only when accessed, and that both the order and its items are updated with a single save call.
With Spring Data JPA, developers can manage domain objects without writing any data‑access code.
6. Summary
DDD and JPA are pinnacle examples of object‑oriented design; combined, they provide powerful, concise persistence for domain models.
Benefits include improved readability, reduced boilerplate, higher reusability, and easier extensibility.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
macrozheng
Dedicated to Java tech sharing and dissecting top open-source projects. Topics include Spring Boot, Spring Cloud, Docker, Kubernetes and more. Author’s GitHub project “mall” has 50K+ stars.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
