Definition
Three annotations work together to model value objects in JPA.
- @Embeddable – marks a class as a value object whose fields are stored as columns in the owning entity's table, not in a separate table.
- @Embedded – marks a field in an entity that holds an instance of an embeddable, merging its columns into the entity's table.
- Value Object – an object defined by its values rather than by an identity, such as Money or Address.
- @AttributeOverride – renames the columns of an embedded field to prevent column name clashes when the same embeddable type is used twice.
- @AttributeOverrides – groups multiple @AttributeOverride annotations when several columns need renaming.
At National Bank of India, a customer's address is not a separate account -- it is a bundle of street, city, and pincode that only exists as part of the customer record. @Embeddable says "address is a reusable bundle" and @Embedded says "stamp that bundle's fields directly onto the customer's row." If Priya has both a home address and an office address, both bundles try to write to columns named street, city, and pincode, which collides. @AttributeOverride is the instruction that says "for the office bundle, write to office_street, office_city, and office_pincode instead" so both fit on one record.
Understanding the Concepts
What is @Embeddable?
@Embeddable marks a class as a value object in JPA. A value object has no identity column and no database table of its own. Its fields are stored as columns directly in the owning entity's table. Common examples include Money (amount + currency), Address (street + city + pincode), and DateRange (startDate + endDate).
At National Bank of India, Rajesh models the account balance as a Money value object that bundles the amount and currency together, ensuring they always travel as a pair.
package com.bank.model;
import jakarta.persistence.Embeddable;
import jakarta.persistence.Column;
import java.math.BigDecimal;
@Embeddable
public class Money {
@Column(precision = 19, scale = 4)
private BigDecimal amount;
@Column(length = 3)
private String currency;
protected Money() {} // JPA requires a no-arg constructor
public Money(BigDecimal amount, String currency) {
this.amount = amount;
this.currency = currency;
}
public BigDecimal getAmount() { return amount; }
public String getCurrency() { return currency; }
}
What is @Embedded?
@Embedded marks a field in an entity that holds an instance of an embeddable class. When JPA sees this annotation, it merges the embeddable's columns into the entity's table. There is no separate table and no join required.
package com.bank.model;
import jakarta.persistence.*;
import java.math.BigDecimal;
@Entity
@Table(name = "accounts")
public class Account {
@Id
@Column(name = "account_id")
private String accountId; // SAV2001
@Column(name = "holder_name")
private String holderName; // Priya
@Embedded
private Money balance;
// getters and setters
}
The accounts table will have columns: account_id, holder_name, amount, and currency. The Money fields are stored inline, not in a separate table.
accounts table:
account_id | holder_name | amount | currency
SAV2001 | Priya | 50000.00 | INR
SAV2002 | Rajesh | 120000.00 | INR
Why do we need value objects in JPA?
Value objects solve a real problem in banking applications. Consider storing money without a value object.
// Without value object -- amount and currency are loose fields
@Entity
public class Account {
@Id private String accountId;
private BigDecimal amount;
private String currency;
}
This looks fine until a developer changes the amount without changing the currency, or copies the amount from one account to another without copying the currency. At National Bank of India, Rajesh found a bug where an INR amount was tagged as USD because the two fields drifted apart during a funds-transfer operation.
Bundling them into a Money value object solves this:
- The domain model is cleaner because
Moneyencapsulates its own invariants. - You cannot accidentally change one field without the other.
- You can reuse the same
Moneytype across Account, Transaction, and LoanApplication. - equals and hashCode are based on all fields, so two Money objects with the same amount and currency are equal.
package com.bank.model;
import java.math.BigDecimal;
import java.util.Objects;
@Embeddable
public class Money {
private BigDecimal amount;
private String currency;
protected Money() {}
public Money(BigDecimal amount, String currency) {
this.amount = amount;
this.currency = currency;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Money m)) return false;
return Objects.equals(amount, m.amount)
&& Objects.equals(currency, m.currency);
}
@Override
public int hashCode() {
return Objects.hash(amount, currency);
}
}
How does @Embeddable work internally?
When Hibernate processes an @Embedded field, it reads the @Embeddable class metadata and adds each of its mapped fields as columns to the owning entity's table. There is no separate INSERT or SELECT for the embedded value. The embeddable's fields become part of the entity's column list.
Entity: Account (table = accounts)
├── accountId → account_id (PK)
├── holderName → holder_name
└── @Embedded Money balance
├── amount → amount
└── currency → currency
Generated SQL:
INSERT INTO accounts (account_id, holder_name, amount, currency)
VALUES (?, ?, ?, ?)
The embeddable shares the entity's persistence context. It has no identity, no @Id, and no lifecycle of its own. When the entity is persisted, updated, or removed, the embeddable goes with it. Hibernate internally models the embeddable as a "component" in its metadata.
package com.bank.service;
import com.bank.model.Account;
import com.bank.model.Money;
import jakarta.persistence.EntityManager;
import java.math.BigDecimal;
public class AccountService {
private final EntityManager em;
public AccountService(EntityManager em) { this.em = em; }
public void createAccount() {
Account acc = new Account();
acc.setAccountId("SAV2001");
acc.setHolderName("Priya");
acc.setBalance(new Money(new BigDecimal("50000.00"), "INR"));
em.persist(acc); // single INSERT with all columns
}
}
How do you use @AttributeOverride when the same type appears twice?
When an entity embeds the same embeddable type twice, both instances map to the same column names by default. This causes a column name clash. @AttributeOverride renames the columns of one or both embedded fields.
At National Bank of India, Priya's loan application has both a disbursement amount and a repayment amount, both modeled as Money.
package com.bank.model;
import jakarta.persistence.*;
@Entity
@Table(name = "loan_applications")
public class LoanApplication {
@Id
@Column(name = "loan_id")
private String loanId;
@Column(name = "applicant_name")
private String applicantName; // Priya
@Embedded
private Money disbursement; // default columns: amount, currency
@Embedded
@AttributeOverrides({
@AttributeOverride(name = "amount",
column = @Column(name = "repay_amount", precision = 19, scale = 4)),
@AttributeOverride(name = "currency",
column = @Column(name = "repay_currency", length = 3))
})
private Money repayment;
// getters and setters
}
The resulting table has columns: loan_id, applicant_name, amount, currency, repay_amount, repay_currency. Without @AttributeOverride, Hibernate throws a MappingException at startup.
loan_applications table:
loan_id | applicant_name | amount | currency | repay_amount | repay_currency
LN001 | Priya | 500000.00 | INR | 550000.00 | INR
How is @Embeddable different from @Entity?
This is a common interview comparison. The two annotations serve fundamentally different purposes.
| Feature | @Embeddable | @Entity |
|---|---|---|
| Identity | No @Id, no identity | Has @Id, has identity |
| Table | No own table, stored in owner's table | Has its own table |
| Lifecycle | Tied to owning entity | Independent lifecycle |
| Queries | Cannot query directly | Can query with JPQL or Criteria |
| Relationships | Cannot be target of @ManyToOne | Can be target of relationships |
| Equals/HashCode | Based on all field values | Based on @Id |
| Loading | Always eager (shares owner's table) | Can be lazy loaded |
// @Embeddable -- value object, no identity
@Embeddable
public class Money {
private BigDecimal amount;
private String currency;
// No @Id!
}
// @Entity -- has identity, has its own table
@Entity
public class Account {
@Id
private String accountId; // SAV2001
private String holderName;
@Embedded
private Money balance;
}
Choose @Embeddable when the concept has no meaning outside its owner -- money, address, date range. Choose @Entity when it has an independent identity and lifecycle -- account, customer, transaction.
What are the rules and constraints?
Embeddable types have specific rules that you must follow.
- An embeddable class must have a no-argument constructor (public or protected). JPA uses it to instantiate the object.
- An embeddable class must not have an @Id field. It has no identity of its own.
- An embeddable class cannot be the target of a relationship annotation like
@ManyToOne. - Embeddable fields are always loaded eagerly. Because they share the owner's table, there is no way to lazy-load them.
- If all columns of an embedded field are null in the database, JPA returns a null reference for the embedded field, not an object with null fields.
- An embeddable can contain
@Basic,@Column,@Temporal,@Enumerated, and even@ManyToOnerelationships (with@AssociationOverrideto customize the join column).
package com.bank.model;
import jakarta.persistence.*;
import java.math.BigDecimal;
@Embeddable
public class Money {
@Column(name = "amount", precision = 19, scale = 4)
private BigDecimal amount;
@Column(name = "currency", length = 3)
private String currency;
protected Money() {} // Required no-arg constructor
public Money(BigDecimal amount, String currency) {
this.amount = amount;
this.currency = currency;
}
public BigDecimal getAmount() { return amount; }
public String getCurrency() { return currency; }
}
What are common mistakes?
Mistake 1: Forgetting @AttributeOverride when embedding the same type twice.
// WRONG -- both Money fields map to the same columns
@Entity
public class LoanApplication {
@Id private String loanId;
@Embedded
private Money disbursement; // maps to: amount, currency
@Embedded
private Money repayment; // ALSO maps to: amount, currency
}
// Hibernate throws:
// org.hibernate.MappingException: Repeated column in mapping for entity
// CORRECT -- use @AttributeOverride on the second field
@Embedded
@AttributeOverrides({
@AttributeOverride(name = "amount",
column = @Column(name = "repay_amount")),
@AttributeOverride(name = "currency",
column = @Column(name = "repay_currency"))
})
private Money repayment;
Mistake 2: Not handling null embedded values.
// WRONG -- NullPointerException if balance columns are all null
Account account = em.find(Account.class, "SAV2001");
BigDecimal amt = account.getBalance().getAmount(); // NPE!
// CORRECT -- check for null first
Money balance = account.getBalance();
if (balance != null) {
BigDecimal amt = balance.getAmount();
}
Mistake 3: Missing the no-argument constructor.
// WRONG -- JPA cannot instantiate this class
@Embeddable
public class Money {
private BigDecimal amount;
private String currency;
public Money(BigDecimal amount, String currency) {
this.amount = amount;
this.currency = currency;
}
// No no-arg constructor!
// JPA throws InstantiationException
}
// CORRECT -- add a protected no-arg constructor
@Embeddable
public class Money {
private BigDecimal amount;
private String currency;
protected Money() {} // JPA needs this
public Money(BigDecimal amount, String currency) {
this.amount = amount;
this.currency = currency;
}
}
What about nested embeddables?
An embeddable can contain another embeddable. At National Bank of India, Pankaj designs an AccountDetails embeddable that contains a Money balance and an Address branch address.
package com.bank.model;
import jakarta.persistence.*;
@Embeddable
public class Address {
private String street;
private String city;
private String pincode;
protected Address() {}
}
@Embeddable
public class AccountDetails {
@Embedded
private Money balance;
@Embedded
private Address branchAddress;
protected AccountDetails() {}
}
@Entity
@Table(name = "accounts")
public class Account {
@Id
private String accountId;
@Embedded
@AttributeOverrides({
@AttributeOverride(name = "balance.amount",
column = @Column(name = "bal_amount")),
@AttributeOverride(name = "balance.currency",
column = @Column(name = "bal_currency")),
@AttributeOverride(name = "branchAddress.street",
column = @Column(name = "branch_street")),
@AttributeOverride(name = "branchAddress.city",
column = @Column(name = "branch_city")),
@AttributeOverride(name = "branchAddress.pincode",
column = @Column(name = "branch_pin"))
})
private AccountDetails details;
}
Notice the dotted path syntax in @AttributeOverride: balance.amount reaches through the outer embeddable into the nested one. This works to any nesting depth.
What about collections of embeddables?
JPA supports collections of embeddable types using @ElementCollection. The collection is stored in a separate collection table, not inline in the entity's table.
package com.bank.model;
import jakarta.persistence.*;
import java.util.List;
@Entity
@Table(name = "customers")
public class Customer {
@Id
@Column(name = "customer_id")
private String customerId;
private String name; // Priya
@ElementCollection
@CollectionTable(
name = "customer_addresses",
joinColumns = @JoinColumn(name = "customer_id"))
private List<Address> addresses;
}
This creates a customer_addresses table with columns: customer_id (FK), street, city, pincode. Each address row references its owning customer. You can combine @ElementCollection with @AttributeOverride to rename the columns in the collection table.
customers table: customer_addresses table:
customer_id | name customer_id | street | city | pincode
C001 | Priya C001 | MG Road | Mumbai | 400001
C001 | Brigade Road | Delhi | 110001
What should a senior engineer know?
- Immutability. Value objects should be immutable. Once created, their fields should not change. Make fields final where possible and provide no setters. JPA can still populate them via reflection.
- equals and hashCode. Because embeddables are value objects, their equals and hashCode must be based on all fields, not on an id. Two Money objects with the same amount and currency must be equal.
- Hibernate component mapping. Under the hood, Hibernate models embeddables as "components" in its metadata. The component has no identifier and is always part of the owning entity's state.
- @AssociationOverride. If an embeddable contains a
@ManyToOnerelationship, you can use@AssociationOverrideto rename the join column when embedding the same type twice. - Performance. Embeddables add zero overhead at query time because they share the owner's table. There is no join. They are the most efficient way to model multi-field value objects.
- Java records (Java 16+). Hibernate 6 supports
@Embeddableon Java records, which gives you immutability and auto-generated equals/hashCode for free.
package com.bank.model;
import jakarta.persistence.Embeddable;
import java.math.BigDecimal;
// Java 16+ -- embeddable as a record
@Embeddable
public record Money(BigDecimal amount, String currency) {}
package com.bank.service;
import com.bank.model.Account;
import com.bank.model.Money;
import jakarta.persistence.EntityManager;
import java.math.BigDecimal;
public class AccountService {
private final EntityManager em;
public AccountService(EntityManager em) { this.em = em; }
public void demo() {
Money m1 = new Money(new BigDecimal("50000.00"), "INR");
Money m2 = new Money(new BigDecimal("50000.00"), "INR");
System.out.println("Equal: " + m1.equals(m2)); // true -- value equality
Account acc = em.find(Account.class, "SAV2001");
System.out.println("Balance: " + acc.getBalance());
}
}
Expected output:
Equal: true
Balance: Money[amount=50000.00, currency=INR]
Visual Overview
@Embeddable Money @Entity Account (table = accounts)
┌──────────────┐ ┌──────────────────────────────────────┐
│ amount │──embed──▶ │ account_id (PK) │
│ currency │ │ holder_name │
└──────────────┘ │ amount ◀── from @Embedded Money│
(no table, no @Id) │ currency ◀── from @Embedded Money│
└──────────────────────────────────────┘
Same @Embeddable twice with @AttributeOverride: @Entity LoanApplication (table = loan_applications) ┌─────────────────────────────────────────────────┐ │ loan_id (PK) │ │ applicant_name │ │ amount ◀── @Embedded Money disbursement (default) │ │ currency ◀── @Embedded Money disbursement (default) │ │ repay_amount ◀── @Embedded Money repayment (overridden) │ │ repay_currency ◀── @Embedded Money repayment (overridden) │ └─────────────────────────────────────────────────┘
Nested embeddable path:
Account
└── @Embedded AccountDetails details
├── @Embedded Money balance
│ ├── amount → override path: "balance.amount"
│ └── currency → override path: "balance.currency"
└── @Embedded Address branchAddress
├── street → override path: "branchAddress.street"
├── city → override path: "branchAddress.city"
└── pincode → override path: "branchAddress.pincode"
Key Takeaway
An @Embeddable class is a value object in JPA. It has no identity, no table of its own, and no independent lifecycle. Its fields are stored as columns directly in the owning entity's table. When you mark a field with @Embedded, JPA merges the embeddable's columns into the entity's table with no join and no extra query. This is the cleanest way to model concepts like Money, Address, or DateRange that have no meaning outside their owner.
The interesting challenge arises when the same embeddable type appears twice in one entity. At National Bank of India, Rajesh's loan application has both a disbursement amount and a repayment amount, both modeled as Money. By default, both fields try to create columns named amount and currency, which clashes. The @AttributeOverride annotation renames one set of columns so both fit in the same table without conflict.
Value objects should be immutable with equals and hashCode based on all their fields. A null embedded value means all its columns are null in the database, so always check for null before accessing embedded fields. Embeddables can be nested, collected with @ElementCollection, and even contain relationships. They add zero query overhead because they share the owner's table, making them the most efficient way to model multi-field value objects in a banking domain where Money, Address, and DateRange appear everywhere.
Remember one sentence: @Embeddable is a value object stored inline in the owner's table, @Embedded includes it, and @AttributeOverride renames its columns when the same type appears twice.
---