Backend Development 22 min read

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.

macrozheng
macrozheng
macrozheng
Mastering DDD with Spring Data JPA: Repositories, Lazy Loading, and Real‑World Order Example

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

<code>&lt;dependency&gt;
    &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
    &lt;artifactId&gt;spring-boot-starter-data-jpa&lt;/artifactId&gt;
&lt;/dependency&gt;</code>

Also add the MySQL driver:

<code>&lt;dependency&gt;
    &lt;groupId&gt;com.mysql&lt;/groupId&gt;
    &lt;artifactId&gt;mysql-connector-j&lt;/artifactId&gt;
    &lt;scope&gt;runtime&lt;/scope&gt;
&lt;/dependency&gt;</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

Order

aggregate:

<code>public interface OrderCommandRepository extends JpaRepository<Order, Long> {
}</code>

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:

<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

save

persists 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

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.

JavaBackend DevelopmentDDDRepository PatternSpring Data JPA
macrozheng
Written by

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.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.