Hibernate & Entity Mapping

Embeddables & @Embedded

18 min Lesson 8 of 13

Embeddables & @Embedded

Not every object in your domain model needs its own database table. Sometimes a cluster of related fields — a postal address, a money amount, a date range — forms a coherent value object that belongs together conceptually, yet it makes perfect sense to store all its columns in the owning entity's table. JPA formalises this pattern through two annotations: @Embeddable and @Embedded.

The Problem: Flat Columns, Rich Domain

Consider a Customer entity that has a shipping address. A naive mapping spreads the address fields directly on the entity:

@Entity public class Customer { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; // address fields scattered across the entity private String street; private String city; private String postalCode; private String country; }

This compiles and works, but you lose the domain concept of an address. You cannot reuse the address structure on an Order entity, you cannot add address-specific validation in one place, and you cannot pass an Address object around your service layer. The database schema is identical either way — the goal is richer Java without changing the schema.

@Embeddable and @Embedded

Mark a plain class with @Embeddable to tell Hibernate it is a value object whose fields should be inlined into the owning table. Then annotate the field in the owning entity with @Embedded.

import jakarta.persistence.Embeddable; @Embeddable public class Address { private String street; private String city; private String postalCode; private String country; // JPA requires a no-arg constructor (can be protected) protected Address() {} public Address(String street, String city, String postalCode, String country) { this.street = street; this.city = city; this.postalCode = postalCode; this.country = country; } // getters omitted for brevity }
import jakarta.persistence.*; @Entity @Table(name = "customers") public class Customer { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; @Embedded private Address shippingAddress; protected Customer() {} public Customer(String name, Address shippingAddress) { this.name = name; this.shippingAddress = shippingAddress; } // getters / setters omitted }

Hibernate maps this to a single customers table with columns id, name, street, city, postal_code, and country. No join, no extra table, no foreign key — just flat columns.

@Embedded is optional when the field type is @Embeddable. Hibernate 6 infers the embedded mapping automatically. Still, writing @Embedded explicitly is considered good practice because it communicates intent clearly to readers who may not remember that Address carries the annotation.

Overriding Column Names with @AttributeOverride

The default column names come from the field names in the @Embeddable class. Problems arise when you embed the same type twice in one entity — for example, a Customer that has both a shipping address and a billing address. Both would try to create a column named street, causing a mapping conflict.

Resolve the conflict with @AttributeOverride:

@Entity @Table(name = "customers") public class Customer { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; @Embedded @AttributeOverrides({ @AttributeOverride(name = "street", column = @Column(name = "ship_street")), @AttributeOverride(name = "city", column = @Column(name = "ship_city")), @AttributeOverride(name = "postalCode", column = @Column(name = "ship_postal_code")), @AttributeOverride(name = "country", column = @Column(name = "ship_country")) }) private Address shippingAddress; @Embedded @AttributeOverrides({ @AttributeOverride(name = "street", column = @Column(name = "bill_street")), @AttributeOverride(name = "city", column = @Column(name = "bill_city")), @AttributeOverride(name = "postalCode", column = @Column(name = "bill_postal_code")), @AttributeOverride(name = "country", column = @Column(name = "bill_country")) }) private Address billingAddress; }

Now the table has eight distinct columns prefixed with ship_ and bill_, yet the Java code works with two cleanly typed Address objects.

Null Embeddables

When all columns belonging to an embedded value are NULL in the database, Hibernate returns null for the entire embedded field by default. This can produce a NullPointerException the first time you call customer.getShippingAddress().getCity(). Guard against it with a null check, or initialise the field to a sentinel instance in the entity constructor.

Never call nullable embedded fields without a null guard. If shippingAddress was never set before a record was saved, Hibernate will hand you null when you load it — not an empty Address object. Always initialise embedded fields to a safe default in your entity constructor, or check for null at the call site.

Nesting Embeddables

An @Embeddable class can itself contain another @Embeddable. A common real-world example is an Address that embeds a GeoPoint:

@Embeddable public class GeoPoint { private double latitude; private double longitude; protected GeoPoint() {} public GeoPoint(double lat, double lon) { this.latitude = lat; this.longitude = lon; } } @Embeddable public class Address { private String street; private String city; private String postalCode; private String country; @Embedded private GeoPoint coordinates; // nested embeddable protected Address() {} // constructor + getters omitted }

Hibernate flattens all nested columns into the same table. Keep nesting shallow — one or two levels is the practical limit before readability and column-override bookkeeping become painful.

Embeddables as Value Objects: Immutability and equals/hashCode

A value object should be immutable and compared by value, not identity. Make @Embeddable classes immutable where possible — provide only getters, set all fields in the constructor, and implement equals() and hashCode() based on the field values:

@Embeddable public class Money { private BigDecimal amount; private String currency; protected Money() {} public Money(BigDecimal amount, String currency) { this.amount = amount.setScale(2, RoundingMode.HALF_UP); this.currency = currency; } public BigDecimal getAmount() { return amount; } public String getCurrency() { return currency; } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof Money m)) return false; return amount.compareTo(m.amount) == 0 && currency.equals(m.currency); } @Override public int hashCode() { return Objects.hash(amount.stripTrailingZeros(), currency); } }
Treat @Embeddable classes like Java records. Immutable fields, all-args constructor, value-based equals()/hashCode(), and a protected no-arg constructor for JPA. In Java 16+ you can use an actual record annotated with @Embeddable — Hibernate 6.2+ supports this natively, with the canonical constructor replacing the all-args constructor and JPA using the compact constructor for hydration.

Performance Trade-offs

Embedded values are stored in the same row as their owner, which means:

  • No join required — reading a Customer always retrieves the Address in the same SELECT. There is no lazy-loading option and no N+1 risk for the embedded portion.
  • No partial loading — if you need only the customer name, the address columns are still fetched. Use projections (constructor expressions in JPQL or Spring Data Projections) when you genuinely need to avoid loading large embedded objects.
  • Wide tables — embedding many value objects in one entity produces tables with many columns. This is usually fine; relational engines handle wide rows well. Consider normalisation only when the embedded data is genuinely optional and large.

Summary

Use @Embeddable and @Embedded to break a flat column set into rich, reusable value objects without changing your database schema. Apply @AttributeOverride whenever the same embeddable type appears more than once in an entity. Make embedded classes immutable and implement value-based equality. The trade-off is straightforward: you gain domain richness and reusability with zero extra joins, but you cannot lazily load an embedded object or share it across multiple rows. These properties make embeddables the right choice for address, money, date-range, and coordinate types that naturally belong with their owner.