Primary Keys and Generated Values: IDENTITY, SEQUENCE, UUID

Introduction

Primary key choice affects performance, scalability, and application design. This article covers every strategy JPA supports — from the simple AUTO_INCREMENT to UUID and composite keys — with the trade-offs of each.


IDENTITY Strategy (MySQL AUTO_INCREMENT)

GenerationType.IDENTITY delegates key generation to the database column’s auto-increment feature. This is the standard for MySQL.

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

Generated DDL (when ddl-auto=create):

id BIGINT NOT NULL AUTO_INCREMENT

How IDENTITY works with Hibernate

IDENTITY has one important behaviour: Hibernate cannot batch INSERT statements when using IDENTITY.

When you call repository.save(entity), Hibernate must execute the INSERT immediately (not wait for the flush) to get the generated id back from the database. This breaks JDBC batch insert optimisation.

// These three saves generate three separate INSERT statements immediately
// even within the same transaction — not batched
categoryRepository.save(cat1);
categoryRepository.save(cat2);
categoryRepository.save(cat3);

For most web applications this is fine — the performance difference is negligible. For bulk-insert batch jobs, use SEQUENCE strategy with a Hibernate sequence generator instead (PostgreSQL, Oracle).


SEQUENCE Strategy

SEQUENCE uses a database sequence object. Hibernate can pre-fetch sequence values in batches, which is much faster for bulk inserts.

@Id
@GeneratedValue(
    strategy = GenerationType.SEQUENCE,
    generator = "product_seq"
)
@SequenceGenerator(
    name = "product_seq",
    sequenceName = "product_id_seq",
    allocationSize = 50          // prefetch 50 ids at a time
)
private Long id;

With allocationSize = 50, Hibernate fetches 50 ids from the sequence in one call and assigns them locally — dramatically reducing database round trips for bulk operations.

MySQL does not natively support sequences before version 8.0. If you are on MySQL 8, you can create sequences:

CREATE SEQUENCE product_id_seq START WITH 1 INCREMENT BY 50;

For MySQL applications, IDENTITY is simpler. For PostgreSQL or Oracle, prefer SEQUENCE.


UUID Primary Keys

UUID primary keys are increasingly popular in distributed systems and microservices — they can be generated without a database round trip, making them safe to assign before persisting.

Java UUID generation (Spring Boot 3.x / Hibernate 6)

@Id
@GeneratedValue(strategy = GenerationType.UUID)
private UUID id;

Hibernate 6 generates a UUID using the Java runtime (UUID.randomUUID()).

Column type in MySQL: CHAR(36) or BINARY(16) (more efficient).

-- Flyway migration for UUID primary key
CREATE TABLE tags (
    id   CHAR(36)     NOT NULL,
    name VARCHAR(50)  NOT NULL UNIQUE,
    PRIMARY KEY (id)
) ENGINE=InnoDB;

UUID as String

@Id
@GeneratedValue(strategy = GenerationType.UUID)
@Column(length = 36)
private String id;

Pros and cons of UUID keys

ProsCons
Globally unique — safe in distributed systemsLarger storage (16 bytes vs 8 bytes for Long)
Generated without database round tripRandom UUIDs fragment B-tree indexes
Ids can be assigned before persistHarder to read in logs and URLs
No sequence/auto-increment bottleneckSlower JOIN performance on large tables

For most web applications on a single database, Long + IDENTITY is the right choice. UUID is justified when you need globally unique ids across multiple databases or services.

Sequential UUIDs (UUID v7)

If you want UUID primary keys without index fragmentation, use sequential UUIDs (UUID v7) which are time-ordered:

import java.util.UUID;

// UUID v7 — time-ordered, reduces index fragmentation
// Available from Java 21 via third-party libs or custom generator

Composite Primary Keys

Sometimes a table’s primary key consists of multiple columns (a composite key). JPA provides two approaches.

Approach 1: @EmbeddedId

Create a separate embeddable class for the composite key:

// The composite key class
@Embeddable
public class OrderItemId implements Serializable {

    @Column(name = "order_id")
    private Long orderId;

    @Column(name = "product_id")
    private Long productId;

    // Required by JPA
    public OrderItemId() {}

    public OrderItemId(Long orderId, Long productId) {
        this.orderId = orderId;
        this.productId = productId;
    }

    @Override
    public boolean equals(Object o) {
        if (!(o instanceof OrderItemId)) return false;
        OrderItemId other = (OrderItemId) o;
        return Objects.equals(orderId, other.orderId) &&
               Objects.equals(productId, other.productId);
    }

    @Override
    public int hashCode() {
        return Objects.hash(orderId, productId);
    }
}
// The entity using the composite key
@Entity
@Table(name = "order_items")
public class OrderItem {

    @EmbeddedId
    private OrderItemId id;

    @Column(nullable = false)
    private Integer quantity;

    @Column(name = "unit_price", nullable = false, precision = 10, scale = 2)
    private BigDecimal unitPrice;
}

Usage:

OrderItemId id = new OrderItemId(orderId, productId);
OrderItem item = orderItemRepository.findById(id).orElseThrow();

Approach 2: @IdClass

An alternative where the key fields are declared directly on the entity:

// Key class — must implement Serializable, have equals/hashCode
public class OrderItemId implements Serializable {
    private Long orderId;
    private Long productId;
    // constructors, equals, hashCode
}
@Entity
@Table(name = "order_items")
@IdClass(OrderItemId.class)
public class OrderItem {

    @Id
    @Column(name = "order_id")
    private Long orderId;

    @Id
    @Column(name = "product_id")
    private Long productId;

    private Integer quantity;
}

@EmbeddedId vs @IdClass

@EmbeddedId@IdClass
Key fieldsIn a separate @Embeddable classRepeated in both entity and key class
JPQL accessitem.id.orderIditem.orderId
Code clarityBetter separationSimpler entity class
Spring DatafindById(id)findById(id)

@EmbeddedId is generally preferred — the key is explicitly an object, and you can pass it around cleanly.


Assigning IDs Before Persisting

Sometimes you need the id before the entity is saved (e.g., to build a URL or reference in another entity). With IDENTITY, this is not possible — the id is assigned by the database on INSERT.

With UUID or application-assigned ids:

// UUID assigned before save
Tag tag = new Tag();
tag.setId(UUID.randomUUID());   // assigned now
tag.setName("sale");
tagRepository.save(tag);        // INSERT with the pre-assigned id
System.out.println(tag.getId()); // available immediately

With IDENTITY, the id is available only after save() executes:

// IDENTITY — id is null before save
Category cat = new Category();
cat.setName("Electronics");
System.out.println(cat.getId()); // null

categoryRepository.save(cat);
System.out.println(cat.getId()); // 1 — assigned after INSERT

Natural IDs with @NaturalId (Hibernate Extension)

If your entity has a business key (a unique, meaningful identifier like a product SKU or customer email), Hibernate’s @NaturalId lets you look up entities by it efficiently:

@Entity
public class Product {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @NaturalId
    @Column(nullable = false, unique = true, length = 50)
    private String sku;
}
// Lookup by natural id — uses second-level cache if configured
Product product = em.unwrap(Session.class)
    .byNaturalId(Product.class)
    .using("sku", "PROD-001")
    .load();

@NaturalId is a Hibernate-specific extension — not part of the JPA standard.


Best Practices

  1. Use Long + IDENTITY for most entities on MySQL — simple, fast, familiar
  2. Use UUID when you need distributed unique ids or want to generate ids before persisting
  3. Use SEQUENCE with allocationSize for bulk-insert performance on PostgreSQL
  4. Use @EmbeddedId for composite keys — cleaner than @IdClass
  5. Never use GenerationType.AUTO in production — its behaviour differs by dialect and Hibernate version
  6. Never use a mutable business field as the primary key — it causes cascading updates

Key Takeaways

  • GenerationType.IDENTITY delegates to database AUTO_INCREMENT — simplest for MySQL but disables JDBC batch inserts
  • GenerationType.SEQUENCE with a custom allocationSize is faster for bulk operations (PostgreSQL/Oracle)
  • GenerationType.UUID generates a UUID — globally unique, no database round-trip needed, but slower B-tree performance
  • Composite keys use either @EmbeddedId (preferred) or @IdClass
  • The composite key class must implement Serializable and override equals() and hashCode()

What’s Next

Article 7 covers embedded types and value objects — how to model reusable concepts like Address as @Embeddable components that are stored in the owning entity’s table rather than a separate table.