Embedded Types and Value Objects: @Embeddable and @Embedded
Introduction
Not every concept in your domain deserves its own table. An Address — street, city, state, postal code — is a value object: it has no identity of its own, it belongs to an entity. JPA’s @Embeddable lets you model this as a separate Java class while storing it in the owning entity’s table.
What Is an Embeddable?
An @Embeddable class is a Java class whose fields are mapped to columns in the owning entity’s table — not a separate table.
customers table:
┌──────────┬──────┬─────────┬────────────────┬──────────┬────────────┬─────────┐
│ id │ name │ email │ street │ city │ state │ country │
├──────────┼──────┼─────────┼────────────────┼──────────┼────────────┼─────────┤
│ 1 │Alice │alice@.. │ 123 Main St │ Austin │ TX │ US │
The Address columns live in the customers table alongside name and email. In Java, Address is a separate class for code organisation and reuse.
Defining an @Embeddable
package com.devopsmonk.jpademo.domain;
import jakarta.persistence.Column;
import jakarta.persistence.Embeddable;
import jakarta.validation.constraints.NotBlank;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Embeddable
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class Address {
@Column(length = 200)
private String street;
@Column(length = 100)
private String city;
@Column(length = 100)
private String state;
@Column(name = "postal_code", length = 20)
private String postalCode;
@Column(length = 50)
private String country;
}
Key rules for @Embeddable classes:
- Must have a no-argument constructor
- Must NOT have
@Entityor@Id - Should implement
equals()andhashCode()(value object semantics) - Is not a JPA entity — cannot be queried directly, has no lifecycle of its own
Using @Embedded in an Entity
@Entity
@Table(name = "customers")
@Getter
@Setter
@NoArgsConstructor
public class Customer {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 100)
private String name;
@Column(nullable = false, length = 150, unique = true)
private String email;
@Embedded
private Address address;
@Column(name = "created_at", nullable = false, updatable = false)
private LocalDateTime createdAt;
@Column(name = "updated_at", nullable = false)
private LocalDateTime updatedAt;
}
The @Embedded annotation is technically optional when the class is annotated with @Embeddable — Hibernate infers it. But @Embedded makes the intent explicit and is recommended for clarity.
Working with Embedded Objects
@Transactional
public Customer createCustomer(String name, String email, String street,
String city, String state, String postalCode) {
Customer customer = new Customer();
customer.setName(name);
customer.setEmail(email);
customer.setCreatedAt(LocalDateTime.now());
customer.setUpdatedAt(LocalDateTime.now());
Address address = new Address(street, city, state, postalCode, "US");
customer.setAddress(address);
return customerRepository.save(customer);
}
Hibernate generates:
INSERT INTO customers (name, email, street, city, state, postal_code, country, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
The Address fields are included in the same INSERT as the Customer fields.
Querying by embedded fields
You can query by embedded fields using JPQL or Spring Data derived queries:
// Spring Data derived query — access embedded field via dot notation
List<Customer> findByAddressCity(String city);
// JPQL
@Query("SELECT c FROM Customer c WHERE c.address.city = :city")
List<Customer> findByCity(@Param("city") String city);
@AttributeOverride: Renaming Embedded Columns
If you embed the same type twice in one entity, column names clash. Use @AttributeOverride to give each embedding its own column names.
Example: Order has both a shipping address and a billing address:
@Entity
@Table(name = "orders")
@Getter
@Setter
@NoArgsConstructor
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Embedded
@AttributeOverrides({
@AttributeOverride(name = "street", column = @Column(name = "shipping_street")),
@AttributeOverride(name = "city", column = @Column(name = "shipping_city")),
@AttributeOverride(name = "state", column = @Column(name = "shipping_state")),
@AttributeOverride(name = "postalCode", column = @Column(name = "shipping_postal_code")),
@AttributeOverride(name = "country", column = @Column(name = "shipping_country"))
})
private Address shippingAddress;
@Embedded
@AttributeOverrides({
@AttributeOverride(name = "street", column = @Column(name = "billing_street")),
@AttributeOverride(name = "city", column = @Column(name = "billing_city")),
@AttributeOverride(name = "state", column = @Column(name = "billing_state")),
@AttributeOverride(name = "postalCode", column = @Column(name = "billing_postal_code")),
@AttributeOverride(name = "country", column = @Column(name = "billing_country"))
})
private Address billingAddress;
}
The resulting table has 10 address columns — 5 for shipping, 5 for billing — clearly named:
orders table:
shipping_street | shipping_city | shipping_state | shipping_postal_code | shipping_country |
billing_street | billing_city | billing_state | billing_postal_code | billing_country
Null Embeddables
If the entire embedded object is null, all its columns are NULL in the database:
customer.setAddress(null); // all address columns stored as NULL
When loading: if all embedded columns are NULL, Hibernate sets the embedded object to null (by default).
You can change this with @EmbeddedId or by configuring hibernate.create_empty_composites.enabled=true to get an empty object instead of null.
Nested Embeddables
Embeddables can contain other embeddables:
@Embeddable
public class ContactInfo {
@Column(length = 20)
private String phone;
@Embedded
private Address address; // nested embeddable
}
@Entity
public class Customer {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@Embedded
private ContactInfo contactInfo; // includes Address inside
}
All fields from ContactInfo and Address are flattened into the customers table. Nesting beyond two levels becomes hard to read — prefer flat embeddables for clarity.
@Embeddable for Composite Primary Keys (Revisited)
As covered in Article 6, @Embeddable is also used for composite primary keys with @EmbeddedId:
@Embeddable
public class OrderItemId implements Serializable {
private Long orderId;
private Long productId;
// equals, hashCode
}
@Entity
public class OrderItem {
@EmbeddedId
private OrderItemId id;
// ...
}
The same @Embeddable concept serves both value objects and composite keys.
When to Use Embeddables vs. Separate Entities
Use @Embeddable when | Use a separate @Entity when |
|---|---|
| The object has no identity of its own | The object has its own identity and lifecycle |
| It is always loaded with its owner | It can exist independently |
| You never query it in isolation | You query it directly |
| Simple value concept (Address, Money, DateRange) | Richer domain object (Customer, Product) |
| One-to-one conceptually | Many entities share this object |
Address is a classic value object — every customer has their own address, and an address has no meaning outside its customer. Category is an entity — it exists independently and products reference it.
Flyway Migration for Embedded Columns
The embedded columns appear in the owning entity’s table in the migration:
-- V1__create_schema.sql (excerpt)
CREATE TABLE customers (
id BIGINT NOT NULL AUTO_INCREMENT,
name VARCHAR(100) NOT NULL,
email VARCHAR(150) NOT NULL UNIQUE,
-- Address embedded columns
street VARCHAR(200),
city VARCHAR(100),
state VARCHAR(100),
postal_code VARCHAR(20),
country VARCHAR(50),
created_at DATETIME(6) NOT NULL,
updated_at DATETIME(6) NOT NULL,
PRIMARY KEY (id)
) ENGINE=InnoDB;
Key Takeaways
@Embeddabledefines a value object whose fields are stored in the owning entity’s table@Embeddedin the owning entity references the embeddable — Hibernate flattens its columns into the table@AttributeOverriderenames columns when the same embeddable type is embedded more than once- If the embedded object is
null, all its columns are stored asNULL - Embeddables are also used for composite primary keys (
@EmbeddedId) - Use embeddables for value concepts (Address, Money, DateRange) — use entities when the object has its own identity
What’s Next
Article 8 covers custom type conversions — mapping Java enums to database values, converting LocalDate to DATE, and building AttributeConverter for types JPA doesn’t support natively.