1. The difference between entity and value object
If you have ever seen a project where “everything is an entity,” you know the feeling: lots of tables, even more JOINs, and to change a single field you have to go on a graph-navigation quest. If you have seen a project where “everything is just strings and numbers,” then there is usually a different kind of pain: method parameters come in sets of 12, some of them are swapped, and validation is spread all over the code. In reality, we want a third path: entities where there is a separate identity and lifecycle, and value objects where it is just a value describing the owner's state.
In our end-to-end project Commerce Persistence Lab, this is especially noticeable in the examples of price and address. A product price is not an “object that lives on its own,” but part of the state of Product. The shipping address in an order is also part of the state of PurchaseOrder, and also a “snapshot” at a specific moment in time. And this is where the main methodological risk begins: if, out of habit, you make an entity for everything, Hibernate will faithfully serve that choice — with all its side effects. It will not argue; it will simply add a table, an identifier, relations, cascades, and at some point you will be fixing not a bug, but your own model.
To avoid missing with embeddables and converters, you first need to make the right diagnosis at the domain-model level: what is an entity, what is a value object, and what questions to ask yourself so you do not miss.
2. Entity: identity and lifecycle
A entity in a Java model for Hibernate is an object that “exists as a separate unit” and usually has an identity tied to the database. It can be found, it can be referenced from other parts of the system, it can live a long time, change state, take part in transactions, and it almost always makes sense to ask: “Is this the same object or a different one?” A good analogy (yes, there will be a joke): an entity is a person with a passport. You can change the hairstyle, the address, and the favorite IDE, but by passport it is still the same person.
It is important to remember that in Hibernate a entity is not just “a class with @Entity.” It is an object that goes through the states transient → managed → detached → removed, gets into the first-level cache, participates in dirty checking, and brings along rules about equality and proxies. So a entity is a strong decision: you are not just “describing the model nicely,” you are giving Hibernate the task of supporting the lifecycle of this object.
A familiar domain example: Product is clearly an entity. A product exists independently of who looks at it, it can be found by id or by sku, updated, soft-deleted, and audited.
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
@Entity
public class Product {
@Id
private Long id; // entity identity (we use it to distinguish "the same product")
private String sku; // business key (usually unique), but not the Hibernate identity
private String name; // part of the state, changes without changing identity
}
What matters here is not that the code is short (though that is nice), but that the meaning is clear: a product is a standalone system object. In the same way, our entitys are Customer, PurchaseOrder, and InventoryItem, because their lifecycle and identity matter separately from any owner.
It is also worth noting that a entity usually carries invariants and rules of life. For example, an order does not change status “however it wants,” but through business scenarios. We protect inventory with @Version because concurrency is real. And that is also a sign in favor of a entity: if an object participates in independent business logic and conflicts with other operations, it “has a life of its own” instead of simply storing a set of fields.
3. Value object: value without a “personality”
A value object is an object that has no own identity in the database or in the domain. It is defined by its content, not by id. If two value objects contain the same fields, then from the model's point of view they are interchangeable. Another everyday analogy: a entity is a passport, a value object is the amount of money on a price tag. A price tag has no passport. If two price tags say 100.00 USD, then from the domain point of view it is the same value (even if the pieces of paper are different).
The most useful thing a value object gives a beginner developer is that it cures “primitive soup.” Instead of carrying BigDecimal amount and String currency separately through the code, we carry one Money object. Instead of passing city/street/house/apartment through every method, we pass one Address. This makes the code shorter and more honest: you pass a value, not a scatter of details.
At the Java level, this often looks like this:
import java.math.BigDecimal;
public class Money {
private final BigDecimal amount; // numeric part of the value (for example, 100.00)
private final String currency; // currency (for example, "USD")
public Money(BigDecimal amount, String currency) {
// Important: a value object usually has invariant validation,
// so that "dirt" does not spread through the whole system.
this.amount = amount;
this.currency = currency;
}
}
Pay attention to two things. First, Money has no id — and that is not because we forgot, but because it does not need one. Second, if we decide that Money(100, "USD") and Money(100, "USD") are the same thing, our logic does not break; on the contrary, it becomes natural.
In the Hibernate context, there is one more important point. A Value object can be part of the state of an entity, and then it changes together with its owner. That is, we do not “save Money separately”; we change the state of the product or order item. At the storage level, one fact matters here: such values usually live as part of the owner's state, without their own lifecycle in the database. But the decision “value object or entity” is made before annotations, at the level of meaning.
4. Selection criteria
The most common beginner mistake in ORM is not that they do not know an annotation. The most common mistake is that they do not ask the right question before they start coding. So let us do what I like to call a “small psychological safety net”: a set of questions you run through your head before you say “this will be a entity.”
I will intentionally format this not as a list, but as a table, so the brain perceives it as a checklist matrix rather than another set of commandments.
| Question for the object in the model | If the answer is “yes,” it leans toward entity | If the answer is “no,” it often leans toward value object |
|---|---|---|
| Does it have its own identity (we distinguish two instances even with identical fields)? | It needs stable identity: Product, Customer, PurchaseOrder. | If the meaning is fully in the fields, it is a value: Money, Address. |
| Does it need to be searched for and referenced independently of its owner? | It will be found, linked, deleted, and reused separately. | It is not searched for separately; it “lives inside” the owner as state. |
| Can it exist without an owner (or outlive the owner)? | It lives on its own, does not disappear when another object changes. | Without an owner it makes no sense; it is part of the state. |
| Does it have its own lifecycle, its own statuses, conflicts, audit, and rules? | Yes, its life matters as a separate story. | No, it is just a set of fields describing the owner. |
| Does it need to be changed independently of the owner? | We update it as a separate object. | We change the owner as a whole (or part of the owner); the value is just a component. |
| Is it natural to compare “by content”? | Usually no (entity is compared by identity). | Yes, a value object is almost always compared by fields. |
The key word in this table is “independently”. If an object is independent (you can think about it separately from the owner), it is a candidate for entity. If an object describes the owner and is not needed separately, it is a candidate for value object.
At this point students often get the temptation to cheat: “What if I make a MoneyEntity so that it is ‘real’?” And here is the honest question: why? If you cannot explain a separate lifecycle for money as an entity in your system, then you are just creating an extra id and an extra layer of relationships. Hibernate will, of course, cope, but in the end you will pay for complexity that brings no domain benefit.
5. Borderline cases in Commerce Persistence Lab
The hardest decisions are not where everything is obvious, but where an object looks like both a entity and a value object. So let us look at two borderline examples from our project: address and price. It is in examples like these that architectural mush usually appears in real systems, because “well, it looks similar” is a bad criterion.
Address: CustomerAddress as entity and Address as value object
In our project we have CustomerAddress — this is an entity. Why? Because it has its own rules of life: address type (shipping/billing), a default flag, a link to the customer, possibly soft delete. A customer's address has a separate lifecycle: the customer can have several addresses, switch the default one, delete one address, and keep another. These are all signs of entity thinking.
Inside CustomerAddress lives the actual “address value” — city/street/house/apartment — and this is already a value object. Because “Tokyo, Shibuya, 1” is a value, not a standalone object that needs to be searched by id in our domain.
Very roughly (without annotations, just the meaning) it looks like this:
public class CustomerAddress {
private Long id; // entity identity of the customer's address (for example, to manage the address list)
private Long customerId; // simplified: a link to the owner (in reality often ManyToOne)
private Address address; // value object: the address fields themselves as a value
}
Now compare that with PurchaseOrder.deliveryAddress. The shipping address in an order is a completely different story. It is not a “link to the customer's address,” but a snapshot: the order must remember where it was supposed to go at the moment it was created. If the customer later changes the address in the profile, the order should not magically move to the new address. This is a direct domain signal: the address in the order is a value object stored as part of the order's state.
public class PurchaseOrder {
private Long id; // order identity
private Address deliveryAddress; // snapshot value at the moment the order is placed
}
So the same Address type can be a value object in two places, but the meaning is different: for the customer it is the current value, for the order it is a snapshot. And that is normal. Meaning is determined not by the class, but by the owner and the scenario.
Price: Product and OrderItem
A product price is a value at the current moment. But OrderItem.priceAtPurchase is also a price, only historical: how much it cost at the moment of purchase. If you try to make the price an entity and reuse it, you will create a bug for yourself: change the product price — and suddenly the price in old orders is “pulled along.” In accounting, that is usually considered a bad joke.
That is why in the project we almost immediately get two prices, and both are value objects: the current product price and the price at purchase time. They are similar in structure (amount+currency), but differ in meaning and owner.
public class OrderItem {
private Long id; // identity of the order item (if we model it as an entity)
private Money priceAtPurchase; // historical price (fixed at purchase time)
private int quantity; // quantity (also part of the item's state)
}
This example very well shows the main value object criterion: it describes the owner's state and can be “copied” as a value. There is no separate lifecycle for the price in an order — it lives exactly as long as the order item lives, and changes only through changing the order item (if it changes at all).
6. The cost of choosing wrong in Hibernate
At this stage, you may ask: “Okay, in Java code this looks nice. But what does Hibernate have to do with it?” It is directly involved. Hibernate is a system that very faithfully and meticulously implements your model. If you made an entity, it will track its states, write SQL for saving it, and manage relations, cascades, and loading. If you made a value object as part of the owner's state, it will update the owner, and life will be easier.
If you make price an entity, you will almost inevitably need relations. And where there is a relation, there is an owning side, there are cascades, there are orphan removal questions, there are additional SELECTs when loading, there are more places where a LazyInitializationException or N+1 can blow up. In other words, you do not just “slightly complicate the schema”; you open up an entire class of ORM problems that are not needed at all for “amount + currency.”
There is also a more insidious part: accidental update. We already discussed that Hibernate tracks changes to managed entities and synchronizes them on flush. The more entities and relations you have, the more places there are where someone can “accidentally” change something. When a value is represented by one object (value object) and changes as part of the owner, the point of change is usually closer to the owner's business method, and it is easier to control.
It is useful to think of this as a simple responsibility diagram:
flowchart TD A[Entity] --> B[has identity and lifecycle] B --> C[lives in persistence context separately] C --> D["affects fetching / cascades / equality / SQL"] E[Value object] --> F[defined by value] F --> G[does not require a separate lifecycle] G --> H[changes together with the owner]
Notice: this is not “entity bad, value object good.” It is “entity is a powerful tool, so you need to use it where it is really needed.” And a value object is a good way not to bring out this heavy artillery for the task of “store one value.”
If you have not made the decision at the level of meaning, annotations will not save you — you will just neatly format the wrong model.
7. Typical mistakes when choosing between entity and value object
It is useful to talk through the typical mistakes I regularly see in real projects. They almost always start with good intentions (“I want to do it right”), but end with extra complexity that later has to be paid for in SQL logs, migrations, and endless “why is it loading so expensively?”
Mistake #1: making an entity just because there is more than one field.
Beginners often have a false intuition: “if the object is complex, then it is a entity.” In practice, the number of fields is not a criterion at all. An address may have 10 fields and still remain a value object if it does not live separately. The criterion is identity and lifecycle: can this object be found, changed, and understood separately from its owner?
Mistake #2: confusing “used often” with “must have an id.”
Money is used in the product, in the order, and in the order item — and that is a very common argument for making a separate “Price” entity. But frequent use only says that this is a good value type, not that it needs a separate lifecycle. If you cannot describe the scenario “find me the price by id and change it independently,” then a entity is unnecessary here.
Mistake #3: pulling snapshot values into entities and accidentally breaking history.
The shipping address of an order and the price at purchase time are “snapshots.” If you represent them as entities that are reused and live separately, you will inevitably create the risk that changes in one place will chain-update order history. Historical values are almost always more naturally modeled as a value object belonging to a specific owner.
Mistake #4: comparing value objects “by reference” and getting strange bugs.
If a value object is a value, then it is normal to think of equality by fields. When code starts comparing money1 == money2, this is usually a sign that the value object is being used as a normal mutable structure, and the meaning of “value” is lost. Even without deep DDD, this is a simple rule: a value object is “what about,” not “who.”
Mistake #5: leaving a scatter of primitives in the model and hoping “we will assemble them into an object later.”
At first it seems easier to keep city, street, and house separately — “it is faster.” But then these fields multiply in CustomerAddress, PurchaseOrder, Supplier, Warehouse… and in the end you are supporting a dozen identical sets of strings, each with its own validation and errors. A value object is not decoration, but a way to centralize meaning and rules.
GO TO FULL VERSION