skip to content

Embeddables & Composite Keys

Value objects that live inside an entity's table and the two ways to model multi-column primary keys. Interviewers use @EmbeddedId vs @IdClass to test whether you can map legacy schemas, not just greenfield auto-increment tables.

part ofHibernateoverview, primer and where to startread it →
on this pageshow

questions

6

In JPA/Hibernate, what does annotating a class @Embeddable and referencing it with @Embedded from another class actually do, and how does such a value type differ from a class annotated @Entity?

level: juniorimportance: must knowfreq 68%

answer

  1. Component = value type, no id
  2. Columns inlined into owner's table
  3. No persist/remove, no cascade
  4. Never share one instance between entities
  5. All-null columns read back as null

basics

~20 s

@Embeddable marks a value type with no identity. Its fields become extra columns in the owner's table: no separate table, no primary key, no lifecycle of its own. An @Entity has an identifier, its own row, and is tracked individually by the persistence context.

solid answer

~50 s

An `@Embeddable` is a **value type** (Hibernate calls it a *component*). It has no identifier and no independent existence: when an entity declares `@Embedded Address address`, Hibernate inlines the embeddable's properties as **extra columns in the entity's own table**. No join, no second row, no `find()` by id, nothing to cascade. The value is loaded, dirty-checked and written as part of its owner. An `@Entity` has a primary key, its own row, its own lifecycle (`persist`/`remove`), and exactly one instance per id inside a persistence context. Embeddables exist to give a name and a type to a group of related columns (`Address`, `Money`, `Period`) and to reuse that grouping across entities. Requirements: a no-arg constructor and non-final fields (Hibernate 6 also supports Java records as embeddables). Because they are values, do **not** share one instance across two entities — copy it, or mutating it writes to both rows.

code

java · 13 lines
java
@Embeddable
public class Money {
    @Column(name = "amount") private BigDecimal amount;
    @Column(name = "currency") private String currency;
    protected Money() {}
    public Money(BigDecimal amount, String currency) { this.amount = amount; this.currency = currency; }
}

@Entity
public class Invoice {
    @Id @GeneratedValue private Long id;
    @Embedded private Money total;
}

go deeper

for a junior

Recall the core contrast: value type versus entity, columns inlined into the owner's table, no id and no lifecycle. Show a two-field Address example.

for a middle

Add the mechanics: dirty checking through the owner's snapshot, query paths without a join, what an embeddable may contain, and the all-null-reads-as-null asymmetry.

for a senior

Argue for immutable value objects, explain why shared instances are unsafe, and connect embeddables to @EmbeddedId and @ElementCollection where equals/hashCode start to matter.

for a principal

Frame it as domain modelling: which concepts deserve a type without deserving a table, how value objects keep invariants (amount+currency) together, and the schema-stability cost of promoting an embeddable to an entity later.

## Entities versus value types JPA splits persistent classes into two families. An **entity** has identity: a primary key, its own table row, and a guarantee of one instance per id inside a persistence context. A **value type** has no identity — it is state that belongs entirely to whoever holds it. `@Embeddable` declares a value type; `@Embedded` uses one inside an entity. ```java @Embeddable public class Address { private String street; private String city; private String zip; protected Address() {} } @Entity public class Customer { @Id @GeneratedValue private Long id; private String name; @Embedded private Address address; } ``` This produces **one table**: `customer(id, name, street, city, zip)`. There is no `address` table, no foreign key, no join at read time. `@Embedded` on the field is optional in practice — Hibernate recognises a type annotated `@Embeddable` — but writing it is clearer. ## Consequences of having no identity 1. **No lifecycle operations.** You never `persist()` or `remove()` an `Address`. It is written when the `Customer` is inserted and deleted when the row is deleted. Cascade settings are meaningless for it. 2. **Dirty checking is per column.** Hibernate snapshots the embeddable's columns as part of the owner's state, so `customer.getAddress().setCity("Berlin")` marks the owner dirty and issues an `UPDATE customer SET city=?`. This works only if the embeddable is genuinely mutable and reachable — replacing the whole value (`setAddress(new Address(...))`) is equally fine and usually cleaner. 3. **No shared references.** Two entities must never hold the same `Address` instance. Hibernate has no way to represent sharing (there is no row to point at), so a mutation would be flushed into both owners' rows. Value types should ideally be immutable: final-ish fields, no setters, replace instead of mutate. 4. **Query paths navigate through the attribute** without a join: `select c from Customer c where c.address.city = :city` compiles to a plain predicate on `customer.city`. ## What an embeddable may contain Basic attributes, other embeddables (nesting is allowed to any depth), `@ManyToOne`/`@OneToOne` associations, and even `@ElementCollection`. An embeddable can therefore carry a foreign key column into its owner's table. It can also serve as a composite identifier via `@EmbeddedId`, and as the element type of `@ElementCollection Set<Address>` — both of those uses require a correct `equals`/`hashCode`, whereas a plain `@Embedded` field does not strictly need one. ## Null semantics — the classic gotcha There is no null marker column for an embedded value. When Hibernate reads a row whose embeddable columns are all `NULL`, it hands you `null` for the attribute rather than an `Address` with null fields. So writing a non-null `Address` whose fields happen to be all null and reading it back gives `null` — the round trip is asymmetric. Code defensively, or make at least one column non-nullable. ## Why bother Embeddables buy domain expressiveness at zero schema cost: `Money(amount, currency)` used in five entities keeps the two columns paired and the arithmetic in one class, and `@AttributeOverride` lets each owner rename the columns. They are the JPA answer to "I want a type, not a table".

  • Can an @Embeddable contain a @ManyToOne association, and where does the foreign key column live?
    Yes. An embeddable may declare `@ManyToOne` or `@OneToOne` associations, and the join column is added to the owning entity's table just like any basic column. The owner can rename it with `@AssociationOverride(name = "...", joinColumns = @JoinColumn(name = "..."))`. Embeddables may also declare `@ElementCollection`, which then maps to its own collection table keyed by the owner's id.
  • Does an @Embeddable need equals() and hashCode()?
    Not for a plain `@Embedded` field — Hibernate compares the underlying columns for dirty checking, not the object. It becomes mandatory when the embeddable is used as an `@EmbeddedId` (Hibernate keys the persistence context and the second-level cache by identifier equality) or as the element type of a `Set` in an `@ElementCollection`, where set semantics depend on it.

An entity is a person with a passport number; an embeddable is their home address written on the passport page. The address has no independent existence — copy the passport and you copy the address.

saying these in an interview costs you the question

  • Saying an @Embeddable gets its own table or requires a join
  • Sharing one embeddable instance across two entities and expecting independent rows
  • Calling persist() or remove() on an embeddable, or configuring cascade for it
  • Assuming an all-null embeddable is read back as a non-null object with null fields
  • Claiming an embeddable must have a primary key

context

open as a page

JPA offers two ways to map a composite primary key: @EmbeddedId and @IdClass. How do they differ in mapping, in query syntax, and in when you would choose one over the other?

level: middleimportance: must knowfreq 58%

basics

~20 s

@EmbeddedId puts the key in one @Embeddable field, so you access and query it through that field (entity.id.orderId). @IdClass declares the key properties directly on the entity with @Id each, plus a mirror class listing them; queries use flat paths. Both key classes need Serializable, a no-arg constructor and equals/hashCode.

open as a page

An entity needs two attributes of the same @Embeddable type — for example a billing address and a shipping address — and both would map to identical column names. How do you make that work in JPA/Hibernate, including when the embeddable itself contains a nested embeddable or an association?

level: middleimportance: should knowfreq 52%

basics

~10 s

Use @AttributeOverride on each embedded attribute to rename the columns, one per property. Nested embeddable properties are addressed with dotted paths like "geo.latitude". Associations inside the embeddable are renamed with @AssociationOverride instead.

open as a page

Why does Hibernate insist that a composite identifier class implement equals() and hashCode(), and what concretely goes wrong at runtime when the implementation is missing, incomplete, or based on mutable state?

level: seniorimportance: should knowfreq 44%

basics

~20 s

Hibernate keys the persistence context and the second-level cache by identifier value, comparing keys with equals/hashCode. Without a correct implementation, lookups miss: the same row loads as multiple instances, find() re-queries, merge misbehaves, and cache hits never happen. Keys must also be immutable, or the hash changes under the map.

open as a page

A link entity's primary key is (order_id, product_id) and the same entity also needs @ManyToOne references to Order and Product. How do you map that in JPA/Hibernate without mapping the same two columns twice?

level: seniorimportance: should knowfreq 38%

basics

~20 s

Use derived identity: keep an @EmbeddedId holding the two id values and annotate each association with @MapsId("orderId") / @MapsId("productId"). Hibernate then owns the columns once through the associations and fills the key fields from them, so you set only the associations.

open as a page

For an association table entity such as order_line, how would you decide between a composite primary key built from the two foreign keys and a single surrogate key with a unique constraint on the pair? What drives the call, and what does each choice cost you later?

level: principalimportance: nice to knowfreq 28%

basics

~20 s

Composite keys model the truth (a pair is unique) with no extra column, but every child table, cache key and query carries both columns and the key must never change. A surrogate key gives one stable, cheap handle to reference and evolve, at the cost of an extra column plus a unique constraint you must not forget.

open as a page