A Value Object is identified by value. 10,000 won is 10,000 won, and a period with the same start date and end date is the same period. What matters is the value itself.
Entity is different. The core of Entity is identity. There are domain concepts that must be tracked as the same target even when their attributes change. There are objects whose state changes over time, but that must remain “the same thing” despite those changes.
In a subscription service, Subscription is a good example. A subscription can be created, renewed, have its plan changed, be paused, canceled, and expired. Its state keeps changing, but we still see it as the same subscription.
This is an Entity.
Entity is not a data bundle
If we think of Entity simply as an object corresponding to a database table, we miss the important point. In DDD, Entity is not just a data bundle. It is a concept in the domain with identity and lifecycle.
Suppose two subscriptions have the same plan, same start date, and same expiration date.
Subscription A
- Plan: PRO
- Start date: 2026-07-01
- Expiration date: 2026-08-01
Subscription B
- Plan: PRO
- Start date: 2026-07-01
- Expiration date: 2026-08-01
Looking only at their attributes, they are the same. But in reality, they may be different subscriptions of different users. Refunds, renewals, access rights, and payment histories continue separately.
Therefore Entity does not judge sameness only by attribute values. It needs an identifier.
public class Subscription {
private SubscriptionId id;
private MemberId memberId;
private PlanId planId;
private SubscriptionStatus status;
private SubscriptionPeriod period;
}
Here, SubscriptionId has meaning beyond a simple database primary key. It is a domain identifier for tracking this subscription over time.
When is the identifier assigned?
When designing an Entity, we also need to think about when the identifier is assigned. We can use an identifier generated after saving to the database, or we can assign an identifier when creating the domain object.
In a simple system, an ID generated by the database may be enough. But if we publish domain events, need to identify the object before saving, or need identifiers across several systems, the application can create the identifier first.
Subscription subscription = Subscription.start(
SubscriptionId.newId(),
memberId,
plan,
now
);
What matters is not the identifier generation method itself, but how the domain tracks this object. An Entity is not data at a single point in time. It is a target that continues through time.
Entity has a lifecycle
After an Entity is created, its state changes. These changes do not happen arbitrarily. There are changes allowed by domain rules and changes that are not allowed.
For a subscription, a lifecycle like this may exist:
Free trial started
Converted to recurring subscription
Plan changed
Grace period started due to payment failure
Renewal succeeded
Canceled by user
Period expired
Terminated by administrator
If this lifecycle is expressed with simple setters, domain rules disappear.
subscription.setStatus(SubscriptionStatus.CANCELED);
This code does not answer questions such as “Why was it canceled?”, “Was it in a cancelable state?”, “Is a refund needed?”, or “Can it be used until the expiration date?” It only changes a state value.
Entity should express its lifecycle rules as behavior.
subscription.cancelByMember(reason, now);
subscription.expire(now);
subscription.renew(paymentResult, now);
subscription.changePlan(targetPlan, policy, now);
These methods do not simply change values. They represent domain behavior.
State changes and domain behavior are different
The most common mistake in Entity design is confusing state changes with domain behavior.
subscription.setPlanId(targetPlanId);
subscription.setStatus(ACTIVE);
subscription.setExpiredAt(nextMonth);
This code looks like it changes the plan, but in reality it only changes field values. Rules such as whether the subscription is in a state where the plan can be changed, whether additional payment is needed, how to calculate the remaining period, and whether the minimum seat count of an enterprise plan is satisfied are not visible.
Domain behavior brings these questions into the object.
subscription.changePlan(targetPlan, planChangePolicy, now);
Inside this method, the subscription checks its own state, determines whether the change is allowed according to policy, and performs the necessary state changes. External code says “what to request,” not “how to change fields.”
This difference makes Entity a domain object, not a data object.
JPA Entity and DDD Entity are not the same term
In teams using Java and Spring, many people first think of JPA Entity when they hear the word Entity. JPA Entity also has an identifier and is mapped to a database table. So it looks similar to DDD Entity.
But they are not the same concept.
JPA Entity is a persistence unit of an ORM. It is a technical concept for mapping databases and objects. DDD Entity is a modeling concept with identity and lifecycle in the domain.
The two concepts can exist in the same class. In real projects, DDD Entities are often implemented as JPA Entities. But even then, what matters is preventing technology from overwhelming the domain.
For example, if every relationship is opened bidirectionally for JPA convenience, Aggregate boundaries can blur. Because of default constructors or proxy constraints, a domain object may be created in an incomplete state. Requirements of persistence technology can break invariants of the domain model.
Therefore even when implementing a DDD Entity as a JPA Entity, the questions must remain domain-centered.
What domain identity does this object have?
What lifecycle does it have?
What state changes are allowed?
What invariants should it protect by itself?
The database table structure does not answer these questions for us.
Entity should protect itself as much as possible
A good Entity does not change into any arbitrary state. It must protect its invariants.
Suppose an expired subscription cannot change plans.
public void changePlan(Plan targetPlan, PlanChangePolicy policy, Instant now) {
if (this.status == SubscriptionStatus.EXPIRED) {
throw new IllegalStateException("An expired subscription cannot change plans.");
}
PlanChangeResult result = policy.calculate(this, targetPlan, now);
this.planId = targetPlan.id();
this.period = result.nextPeriod();
}
If this rule exists only in the Application Service, it can easily be bypassed through another path. Admin features, batch jobs, or event handlers may modify the same Entity and forget the rule.
When Entity protects itself, at least the minimum domain rules are maintained no matter which use case calls it.
Of course, this does not mean every rule must be placed inside Entity. Judgments involving several Aggregates or calculations requiring external policy can be separated into Domain Services or Policy objects. But it is natural for Entity to know the core invariants about itself.
Closing
Entity is not a database row. Entity is a domain concept that maintains its identity over time. Even when its attributes change, it must be tracked as the same target, and those changes must happen according to domain rules.
To design Entity well, we should ask “What lifecycle does this object have?” before asking “What fields should it have?” And we should ask “What behavior happens in the domain?” before asking “How should the state be changed?”
setStatus(CANCELED) and cancelByMember(reason, now) can create the same result. But the two pieces of code tell completely different stories. The former modifies data, and the latter expresses domain behavior.
In DDD, Entity is a tool for leaving that difference inside the code.