1. Diferencia entre entity y value object
Si alguna vez has visto un proyecto donde "todo es una entidad", conoces esa sensación: muchas tablas, aún más JOIN, y para cambiar un campo tienes que hacer una búsqueda en el grafo. Si has visto un proyecto donde "todo son simplemente cadenas y números", entonces generalmente hay otro problema: parámetros en métodos de 12 tipos, algunos de ellos mezclados, y la validación está dispersa por todo el código. En realidad, queremos un tercer camino: entidades donde haya una identidad separada y un ciclo de vida, y value objects donde sea simplemente un valor que describe el estado del propietario.
En nuestro proyecto transversal Commerce Persistence Lab esto se nota especialmente en los ejemplos del precio y la dirección. El precio de un producto no es un "objeto que vive por sí mismo", sino parte del estado de Product. La dirección de envío en un pedido también es parte del estado de PurchaseOrder, y además una "instantánea" en un momento concreto. Y aquí comienza el principal riesgo metodológico: si por costumbre hacemos entidad para todo, Hibernate empezará a servir esta elección honestamente, con todos sus efectos secundarios. No discutirá, solo añadirá una tabla, un identificador, relaciones, cascadas y en algún momento estarás arreglando no un error, sino tu propio modelo.
Para no fallar con embeddables y converters, primero hay que hacer un diagnóstico correcto a nivel del modelo de dominio: qué es entity, qué es value object, y qué preguntas hacerse para no equivocarse.
2. Entity: identidad y ciclo de vida
Entity en un modelo Java para Hibernate es un objeto que "existe como una unidad separada" y normalmente tiene identidad, vinculada a la base de datos. Se puede buscar, se puede referenciar desde otras partes del sistema, puede vivir mucho tiempo, cambiar de estado, participar en transacciones y casi siempre tiene sentido preguntar: "¿Es el mismo objeto u otro?". Una buena analogía (sí, habrá un chiste): entity es una persona con pasaporte. Puedes cambiarte de peinado, dirección e IDE favorito, pero según el pasaporte sigues siendo la misma persona.
Es importante recordar que en Hibernate una entity no es solo una "clase con @Entity". Es un objeto que pasa por los estados transient → managed → detached → removed, entra en la first-level cache, participa en dirty checking y arrastra consigo reglas sobre igualdad y proxies. Por lo tanto, entity es una decisión fuerte: no solo "describes el modelo de forma bonita", sino que le das a Hibernate la tarea de gestionar el ciclo de vida de ese objeto.
Ejemplo en un dominio conocido: Product es claramente una entity. Un producto existe independientemente de quién lo mire, se puede buscar por id, por sku, actualizar, aplicar soft-delete y auditar.
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
@Entity
public class Product {
@Id
private Long id; // identidad de la entidad (por ella distinguimos "el mismo producto")
private String sku; // clave de negocio (normalmente única), pero no identity de Hibernate
private String name; // parte del estado, cambia sin cambiar la identidad
}
Aquí lo importante no es que haya poco código (aunque es agradable), sino que el significado está claro: el producto es un objeto independiente del sistema. Del mismo modo, son entity en nuestro caso Customer, PurchaseOrder, InventoryItem, porque su ciclo de vida e identidad son importantes independientemente de cualquier propietario.
Cabe destacar que una entity normalmente lleva invariantes y reglas de vida. Por ejemplo, un pedido cambia de estado no "al azar", sino a través de escenarios de negocio. Protegemos el inventario con @Version porque la competencia es real. Y eso también es una señal a favor de entity: si un objeto participa en lógica de negocio independiente y entra en conflicto con otras operaciones, "vive su propia vida", no solo almacena un conjunto de campos.
3. Value object: valor sin "personalidad"
Value object es un objeto que no tiene identidad propia en la base de datos ni en el dominio. Se define por su contenido, no por su id. Si dos value object contienen los mismos campos, desde el punto de vista del modelo son intercambiables. Otra analogía cotidiana: entity es un pasaporte, value object es la cantidad de dinero en una etiqueta de precio. La etiqueta no tiene pasaporte. Si en dos etiquetas pone 100.00 USD, desde el punto de vista del dominio es el mismo valor (aunque los papeles sean diferentes).
Lo más útil que aporta un value object a un desarrollador principiante es que cura la "sopa de primitivos". En lugar de arrastrar por el código un BigDecimal amount y un String currency por separado, arrastramos un solo objeto Money. En lugar de city/street/house/apartment por todos los métodos, pasamos un único Address. Esto hace que el código sea más corto y honesto: pasas un valor, no un montón de detalles.
A nivel de Java, a menudo se ve así:
import java.math.BigDecimal;
public class Money {
private final BigDecimal amount; // parte numérica del valor (p.ej., 100.00)
private final String currency; // moneda (p.ej., "USD")
public Money(BigDecimal amount, String currency) {
// Importante: un value object suele tener validación de invariantes
// para que no se propague "suciedad" por todo el sistema.
this.amount = amount;
this.currency = currency;
}
}
Observa dos cosas. Primero, Money no tiene id — y no es "porque nos olvidamos", sino porque no lo necesita. Segundo, si decidimos que Money(100, "USD") y Money(100, "USD") son lo mismo, nuestra lógica no se rompe, al contrario, se vuelve natural.
En el contexto de Hibernate, es importante entender otro punto. Un value object puede ser parte del estado de una entity, y entonces cambia junto con el propietario. Es decir, no "guardamos Money por separado", sino que cambiamos el estado del producto o del ítem del pedido. A nivel de almacenamiento, aquí importa un hecho: esos valores suelen vivir como parte del estado del propietario, sin un ciclo de vida propio en la base de datos. Pero la decisión de "value object o entity" se toma antes de las anotaciones, a nivel de significado.
4. Criterios de elección
El error más común de un principiante en ORM no es que no conozca una anotación. El error más común es que no se hace la pregunta adecuada antes de empezar a codificar. Por eso, hagamos lo que me gusta llamar "un pequeño seguro psicológico": un conjunto de preguntas que repasas mentalmente antes de decir "esto será una entity".
A propósito, lo presentaré no como una lista, sino como una tabla, para que el cerebro lo perciba como una matriz de verificación, no como otro conjunto de mandamientos.
| Pregunta al objeto en el modelo | Si la respuesta es "sí" — inclina hacia entity | Si la respuesta es "no" — a menudo inclina hacia value object |
|---|---|---|
| ¿Tiene identidad propia (distinguimos dos instancias incluso con los mismos campos)? | Necesita identidad estable: Product, Customer, PurchaseOrder. | Si el significado está completamente en los campos — es un valor: Money, Address. |
| ¿Es necesario buscarlo y referenciarlo independientemente del propietario? | Se buscará, relacionará, eliminará y reutilizará por separado. | No se busca por separado, "vive dentro" del propietario como estado. |
| ¿Puede existir sin propietario (o sobrevivir al propietario)? | Vive por sí mismo, no desaparece al cambiar otro objeto. | Sin propietario no tiene sentido, es parte del estado. |
| ¿Tiene su propio ciclo de vida, estados, conflictos, auditoría, reglas? | Sí, su vida es importante como historia separada. | No, es solo un conjunto de campos que describen al propietario. |
| ¿Es necesario cambiarlo independientemente del propietario? | Lo actualizamos como un objeto separado. | Cambiamos al propietario por completo (o una parte del propietario), el valor es solo un componente. |
| ¿Es natural comparar "por contenido"? | Generalmente no (una entity se compara por identidad). | Sí, un value object casi siempre se compara por campos. |
En esta tabla, la palabra clave es "independientemente". Si el objeto es independiente (se puede pensar en él separadamente del propietario), es candidato a entity. Si el objeto describe al propietario y no se necesita por separado, es candidato a value object.
En este punto, los estudiantes a menudo sienten la tentación de hacer trampa: "¿Y si hago MoneyEntity para que sea 'de verdad'?" Y aquí la pregunta honesta es: ¿para qué? Si no puedes explicar un ciclo de vida separado del dinero como entidad en tu sistema, simplemente crearás un id innecesario y una capa adicional de relaciones. Hibernate, por supuesto, lo gestionará, pero al final pagarás con complejidad por algo que no aporta beneficio al dominio.
5. Casos límite en Commerce Persistence Lab
Las decisiones más difíciles no están donde todo es obvio, sino donde un objeto se parece tanto a entity como a value object. Por eso, veamos dos ejemplos límite de nuestro proyecto: la dirección y el precio. Precisamente en ejemplos como estos suele nacer la sopa arquitectónica en sistemas reales, porque "bueno, se parece" es un mal criterio.
Dirección: CustomerAddress como entity y Address como value object
En nuestro proyecto tenemos CustomerAddress — es una entidad. ¿Por qué? Porque tiene sus propias reglas de vida: tipo de dirección (shipping/billing), indicador de dirección predeterminada, vinculación con el cliente, posiblemente soft delete. La dirección del cliente tiene un ciclo de vida separado: el cliente puede tener varias direcciones, cambiar la predeterminada, eliminar una dirección y conservar otra. Todo esto son signos de pensamiento de entity.
Al mismo tiempo, dentro de CustomerAddress vive el propio "valor de la dirección" — ciudad/calle/número/piso — y esto ya es un value object. Porque "Tokio, Shibuya, 1" es un valor, no un objeto independiente que deba buscarse por id en nuestro dominio.
De forma muy aproximada (sin anotaciones, solo el significado) se ve así:
public class CustomerAddress {
private Long id; // identity de la entidad de dirección del cliente (p.ej., para gestionar la lista de direcciones)
private Long customerId; // simplificado: referencia al propietario (en realidad a menudo ManyToOne)
private Address address; // value object: los propios campos de la dirección como valor
}
Y ahora compáralo con PurchaseOrder.deliveryAddress. La dirección de envío de un pedido es una historia completamente diferente. No es una "referencia a la dirección del cliente", sino una instantánea: el pedido debe recordar a dónde debía dirigirse en el momento de su creación. Si luego el cliente cambia la dirección en su perfil, el pedido no debe trasladarse mágicamente a la nueva dirección. Esta es una señal directa del dominio: la dirección en el pedido es un value object que se almacena como estado del pedido.
public class PurchaseOrder {
private Long id; // identity del pedido
private Address deliveryAddress; // valor instantáneo en el momento de realizar el pedido
}
Es decir, el mismo tipo Address puede ser un value object en dos lugares, pero el significado es diferente: en el cliente — valor actual, en el pedido — instantánea. Y eso es normal. El significado no lo da la clase, sino el propietario y el escenario.
Precio: Product y OrderItem
El precio de un producto es un valor en el momento actual. Pero OrderItem.priceAtPurchase también es un precio, solo que histórico: cuánto costaba en el momento de la compra. Si intentas hacer del precio una entidad y reutilizarla, te crearás un error: cambias el precio del producto y de repente el precio en pedidos antiguos "se actualiza". En contabilidad, eso suele considerarse una broma de mal gusto.
Por eso, en el proyecto aparecen casi de inmediato dos precios, y ambos son value object: el precio actual del producto y el precio en el momento de la compra. Tienen una estructura similar (amount+currency), pero difieren en significado y propietario.
public class OrderItem {
private Long id; // identity del ítem del pedido (si lo modelamos como entity)
private Money priceAtPurchase; // precio histórico (se fija en la compra)
private int quantity; // cantidad (también parte del estado del ítem)
}
Este ejemplo muestra muy bien el criterio principal de un value object: describe el estado del propietario y puede ser "copiado" como valor. El precio en un pedido no tiene ningún ciclo de vida separado: vive exactamente mientras vive el ítem del pedido, y cambia solo a través del cambio del ítem del pedido (si es que cambia).
6. Coste del error de elección en Hibernate
En este punto puede surgir la pregunta: "Vale, en código Java es bonito. Pero ¿qué tiene que ver Hibernate?" Tiene que ver directamente. Hibernate es un sistema que implementa su modelo de forma muy honesta y meticulosa. Si creas una entity, él gestionará sus estados, escribirá SQL para guardarla, manejará relaciones, cascadas y carga. Si creas un value object como parte del estado del propietario, actualizará al propietario y la vida será más sencilla.
Si conviertes el precio en una entity, casi inevitablemente necesitarás relaciones. Y si hay relación, aparecen owning side, cascadas, orphan removal, preguntas sobre SELECT adicionales al cargar, y más lugares donde puede saltar una LazyInitializationException o un N+1. Es decir, no solo "complicas un poco el esquema", sino que abres toda una clase de problemas de ORM que no son necesarios para "cantidad+moneda".
También hay una parte más insidiosa: accidental update. Ya hemos hablado de que Hibernate rastrea los cambios en las entidades gestionadas y las sincroniza en el flush. Cuantas más entidades y relaciones, más lugares donde alguien puede "accidentalmente" cambiar algo. Cuando el valor se representa con un solo objeto (value object) y se modifica como parte del propietario, el punto de cambio suele estar más cerca del método de negocio del propietario y es más fácil de controlar.
Es útil representar esto como un esquema simple de responsabilidades:
flowchart TD A[Entity] --> B[tiene identity y ciclo de vida] B --> C[vive en el persistence context por separado] C --> D["afecta a fetching / cascades / equality / SQL"] E[Value object] --> F[se define por el valor] F --> G[no requiere ciclo de vida separado] G --> H[cambia junto con el propietario]
Observa: no es "entity es malo, value object es bueno". Es "entity es una herramienta potente, por lo que hay que usarla donde realmente se necesita". Y value object es una buena manera de no invocar esa artillería pesada para la tarea de "almacenar un valor".
Si no tomas una decisión a nivel de significado, las anotaciones no te salvarán — simplemente darás una forma bonita a un modelo incorrecto.
7. Errores típicos al elegir entre entity y value object
Conviene mencionar los errores típicos que veo regularmente en proyectos reales. Casi siempre empiezan con una buena intención ("quiero hacerlo bien"), pero terminan en complejidad innecesaria que luego hay que pagar en registros SQL, migraciones y interminables "¿por qué se carga tan caro?".
Error #1: hacer entity solo porque hay más de un campo.
Un principiante tiene la falsa intuición: "si el objeto es complejo, entonces es entity". En la práctica, la cantidad de campos no es un criterio en absoluto. Una dirección puede tener 10 campos y seguir siendo un value object si no vive por separado. El criterio es la identidad y el ciclo de vida: ¿se puede buscar, cambiar y entender este objeto independientemente del propietario?
Error #2: confundir "se usa a menudo" con "debe tener id".
Money se usa tanto en el producto como en el pedido y en el ítem del pedido — y ese es un argumento muy frecuente para crear una entidad separada "Precio". Pero el uso frecuente solo indica que es un buen tipo de valor, no que necesite un ciclo de vida separado. Si no puedes describir un escenario como "búscame un precio por id y cámbialo independientemente", la entity será superflua aquí.
Error #3: convertir valores instantáneos en entidades y romper accidentalmente el historial.
La dirección de envío de un pedido y el precio en el momento de la compra son "instantáneas". Si los representas como entidades que se reutilizan y viven por separado, inevitablemente crearás el riesgo de que los cambios en un lugar "en cadena" modifiquen el historial de pedidos. Los valores históricos casi siempre es más natural modelarlos como value object perteneciente a un propietario concreto.
Error #4: comparar value object "por referencia" y obtener errores extraños.
Si un value object es un valor, entonces es normal pensar en la igualdad por campos. Cuando en el código se empieza a comparar money1 == money2, suele ser señal de que el value object se usa como una estructura mutable cualquiera y se pierde el sentido de "valor". Incluso sin un DDD profundo, esta es una regla simple: value object es "de qué se trata", no "quién es".
Error #5: dejar en el modelo un montón de primitivas y esperar que "luego las juntaremos en un objeto".
Al principio parece más fácil mantener city, street, house por separado — "es más rápido". Pero luego estos campos se multiplican en CustomerAddress, PurchaseOrder, Supplier, Warehouse… y al final mantienes una docena de conjuntos iguales de cadenas, cada uno con su propia validación y errores. Un value object no es un adorno, sino una forma de centralizar el significado y las reglas.
GO TO FULL VERSION