Skip to content
Leo Jung
Go back

[DDD Intro #21] ORM and DDD: Is a JPA Entity a domain model?

Edit page

When talking about DDD in a Java and Spring environment, JPA almost inevitably comes up. The word Entity overlaps. JPA has Entity, and DDD has Entity. So many teams naturally use JPA Entities as their domain models.

This choice is not always wrong. One class can be both a JPA Entity and play the role of a DDD Entity. But problems appear if we think the two concepts are the same.

ORM is a tool for storing the domain model. ORM does not design the domain model for us.

JPA Entity and DDD Entity have different concerns

JPA Entity is a technical concept for mapping database tables and objects. Concerns such as identifiers, columns, relationships, lazy loading, and persistence context are important.

DDD Entity is a modeling concept with identity and lifecycle in the domain. What state changes are allowed, what invariants must be protected, and what domain behavior should be expressed are important.

The two concerns can overlap, but they are not identical.

For example, suppose a subscription table looks like this:

subscriptions
- id
- member_id
- plan_id
- status
- started_at
- expired_at
- canceled_at

If we design the Subscription domain model only by looking at this table structure, important questions can be missed.

What state transitions does a subscription have?
Can an expired subscription be reactivated?
How are cancellation and expiration different?
What state represents suspension due to payment failure?
Does a plan change apply immediately or from the next billing date?

DB columns do not answer these questions. DDD Entity should be the result of answering domain questions, not the result of moving tables into objects.

The risk of table-centered models

When using ORM, table structure easily becomes object structure. We create an Entity for each table, create relationships for each foreign key, and follow those relationships to fetch data needed by screens.

At first, this is convenient. But Aggregate boundaries can collapse.

@Entity
public class Subscription {
    @ManyToOne
    private Member member;

    @ManyToOne
    private Plan plan;

    @OneToMany(mappedBy = "subscription")
    private List<Payment> payments;
}

This structure looks like an object graph. But from a domain perspective, it does not mean everything is one Aggregate. Member, Plan, and Payment can each have different lifecycles and rules.

In DDD, it is often better to reference other Aggregates by identifier rather than by object reference.

public class Subscription {
    private SubscriptionId id;
    private MemberId memberId;
    private PlanId planId;
}

This way, the subscription Aggregate does not directly depend on the internal state of Member and Plan. Necessary information can be loaded separately by Application Service or Domain Service and passed in.

Relationships are convenient but blur boundaries

JPA relationships are powerful. We can follow objects and use data, like subscription.getMember().getEmail(). But this convenience blurs boundaries.

At some point, subscription domain logic may check detailed member state, follow payment histories, and directly change internal properties of a plan.

subscription.getMember().changeGrade(VIP);
subscription.getPayments().get(0).cancel();

The fact that this code is naturally possible may be a warning sign. An Aggregate should protect consistency inside its own boundary. If it freely changes the internals of other Aggregates, the responsibility of each model collapses.

Relationships should not be opened just because they are technically possible. We should first ask whether the domain boundary allows that relationship.

Lazy loading makes domain logic hard to predict

JPA lazy loading is convenient, but it can make it hard to know when DB queries happen inside domain logic.

public void cancel() {
    if (payments.stream().anyMatch(Payment::isRefundRequired)) {
        // ...
    }
    this.status = CANCELED;
}

If payments is lazy loaded in this code, a DB query can occur at the moment the domain method is called. If the transaction is closed, an exception may occur. Performance problems are also hidden.

Domain methods are easier to predict when they receive explicit inputs for judgment.

subscription.cancel(refundPolicy, paymentSummary, now);

In this structure, the subscription receives the information it needs from outside. The timing of DB queries can be clearly managed in the Application Service or Repository.

Should domain model and persistence model be separated?

When applying DDD seriously, we eventually wonder whether to separate the domain model and persistence model.

Domain model: Subscription
Persistence model: SubscriptionJpaEntity

Separation allows the domain model to be designed freely from JPA constraints. It can reduce the influence of default constructors, protected setters, bidirectional relationships, and proxy constraints. Even if the external storage structure changes, the domain model is less shaken.

But the cost is also large. Mapping code is needed, models are duplicated, and even simple features require more work.

Therefore we cannot say separation is always required. In areas such as the Core Domain, where rules are complex and model protection is important, separation can be considered. In simple CRUD or Supporting Subdomains, using JPA Entity together as the domain model may be more practical.

What matters is the reason for the choice. Combining because it is convenient is different from combining after judging cost and complexity.

Practical compromises

Many projects use the same class as both JPA Entity and domain Entity. Even in this case, a few principles can protect the domain model to some degree.

First, do not open setters carelessly.

subscription.cancelByMember(reason, now);

Change state through domain behavior.

Second, minimize relationships. For other Aggregates, consider ID references rather than object references.

private MemberId memberId;
private PlanId planId;

Third, prioritize Aggregate boundaries over JPA convenience. A table relationship does not mean the same Aggregate.

Fourth, distinguish query requirements from the change model. Do not excessively expand Aggregates for screen queries.

Fifth, do not bring external system DTOs or API response models into Entity.

Following just these principles can significantly reduce conflict between JPA and DDD.

ORM is only a tool

ORM is a powerful tool. It reduces repetitive mapping between objects and tables, and makes transactions and dirty checking convenient. But ORM does not design the domain model.

The domain model should start from domain questions.

What lifecycle does this object have?
What state changes are allowed?
What rules must it protect by itself?
How far is the same Aggregate?
Which relationships are sufficiently represented by ID?

ORM only provides a way to store these answers. If table mapping questions come before domain questions, the model is pulled by data structure.

Closing

A JPA Entity can become a DDD Entity. But it does not become one automatically. JPA Entity is a concept of persistence technology, and DDD Entity is a concept of domain modeling.

Even if the two concepts are implemented in the same class, we must not confuse their concerns. Table relationships do not determine Aggregate boundaries. Foreign keys do not force object references. The convenience of lazy loading is not more important than the clarity of domain logic.

ORM is a tool for storing the domain model. When the tool starts driving the model, DDD returns to table-centered design.

A good DDD implementation does not reject ORM. It only prevents ORM from deciding the language and boundaries of the domain.


Edit page
Share this post:

Previous Post
[DDD Intro #22] Transactions and consistency: What must be protected at once?
Next Post
[DDD Intro #20] Hexagonal Architecture: A structure that places the domain at the center