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
Orderobject (Version V1).
Then the user changes the address, invoking
modifyAddresson the
Orderobject, moving it from V1 to V2.
After payment, the user calls
paySuccess, moving the
Orderfrom 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
Orderobjects.
Additional
save,
load, and
updateoperations aligned with the order lifecycle.
Creating an
Ordercalls
saveto persist it.
Business operations invoke
loadto reconstruct the object from the DB.
After modifications,
updatesynchronizes 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,
updatemethods.
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
<code><dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency></code>Also add the MySQL driver:
<code><dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency></code>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:
<code>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: true</code>Enable Spring Data JPA on the main class:
<code>@SpringBootApplication
@EnableJpaRepositories(basePackages = "com.geekhalo.springdata4ddd.order.repository")
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}</code>4.3. Using a Repository
Create a repository for the
Orderaggregate:
<code>public interface OrderCommandRepository extends JpaRepository<Order, Long> {
}</code>5. Practical Example – Order
The domain model consists of:
One
Orderper order.
One
OrderAddressper order.
Multiple
OrderItemobjects per order.
Core entity code:
<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<>();
}</code>5.1. Creating an Order
Service method:
<code>@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;
}</code>Unit test verifies that a single
savepersists the whole aggregate.
5.2. Modifying the Address
<code>@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);
}</code>Tests cover both adding a new address and updating an existing one, demonstrating lazy loading and automatic synchronization.
5.3. Paying for an Order
<code>@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);
}</code>Unit tests confirm that lazy loading fetches
OrderItemcollections only when accessed, and that both the order and its items are updated with a single
savecall.
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.
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.