🔑 ID Generation in Hibernate (Summary)

Hibernate/JPA gives you several ways to generate primary keys. Each has different trade-offs depending on your database and performance needs.

Here’s the all-in-one master summary of Hibernate ID generation strategies (AUTO, SEQUENCE, IDENTITY, TABLE, UUID), including everything about batching, allocation, gaps, pros/cons, and when to use what.


1. GenerationType.AUTO

Meaning Hibernate chooses the best strategy for the underlying database.

Behavior (typical)

  • PostgreSQL / Oracle → SEQUENCE
  • MySQL / MariaDB / SQL Server → IDENTITY
  • If nothing else fits → TABLE

Pros

  • Very simple, portable, no DB-specific code.

Cons

  • Hibernate’s choice may change when you switch DBs → unpredictable performance.

Use when

  • Portability > performance. Good for prototypes or apps that may switch DBs.
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;

2. GenerationType.SEQUENCE

Meaning Use a database sequence object (nextval in PostgreSQL, seq.NEXTVAL in Oracle).

How it works

  • Hibernate fetches IDs from the sequence.
  • Can pre-fetch blocks of IDs (allocationSize).
  • IDs are known before insert, so batching works well.

Pros

  • Best performance with JDBC batching.
  • IDs assigned in memory before flush.
  • Efficient for high-volume inserts.

Cons

  • Requires sequence object in DB (Hibernate can create one if allowed).
  • With pooled allocation (allocationSize > 1), gaps appear.

Use when

  • On PostgreSQL/Oracle (databases with strong sequence support).
  • High-throughput insert-heavy workloads.

Example

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

SQL (PostgreSQL)

CREATE SEQUENCE user_id_seq START WITH 1 INCREMENT BY 1;

3. GenerationType.IDENTITY

Meaning Use DB’s auto-increment/identity column. The DB assigns IDs during INSERT.

How it works

  • Hibernate issues the INSERT.
  • The DB generates the ID.
  • Hibernate fetches it afterward.

Pros

  • Very simple.
  • Fits MySQL / SQL Server defaults.
  • No sequence object needed.

Cons

  • Hibernate doesn’t know ID until after insert → batching is limited.
  • Insert throughput lower than SEQUENCE.

Use when

  • On MySQL / SQL Server (legacy schemas).
  • When simplicity matters more than insert performance.

Example

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

DDL

id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY;

4. GenerationType.TABLE

Meaning Use a special table as a pseudo-sequence.

How it works

  • Hibernate stores current value in a table (hibernate_sequences or custom).
  • Reads/updates the row to generate IDs.

Pros

  • Portable to any DB, even those without sequences or identity.

Cons

  • Slowest, requires extra table reads/writes.
  • High contention under concurrency.

Use when

  • Extreme portability is needed.
  • Rare in modern production apps.

Example

@Id
@TableGenerator(
    name = "user_tbl_gen",
    table = "id_generator",
    pkColumnName = "gen_name",
    valueColumnName = "gen_val",
    pkColumnValue = "user_id",
    allocationSize = 50
)
@GeneratedValue(strategy = GenerationType.TABLE, generator = "user_tbl_gen")
private Long id;

SQL

CREATE TABLE id_generator (
  gen_name VARCHAR(64) PRIMARY KEY,
  gen_val BIGINT NOT NULL
);
INSERT INTO id_generator (gen_name, gen_val) VALUES ('user_id', 1);

5. UUID / GUID (Hibernate-specific)

Meaning Use UUID (128-bit identifier) instead of numeric IDs.

How it works

  • Hibernate (6+) can generate UUIDs in the app (@UuidGenerator).
  • Stored as BINARY(16) (compact) or CHAR(36) (readable) in DB.

Pros

  • Globally unique — good for distributed systems.
  • IDs generated without DB round-trip.
  • Known before persist — useful for references/events.

Cons

  • Larger (16 bytes vs 8).
  • Random UUIDs fragment B-trees → prefer time-ordered UUIDs (v7/ULID).
  • Harder to debug than numbers.

Use when

  • You need distributed ID generation across multiple services/nodes.
  • You want IDs before hitting the DB.
  • Microservices, offline clients, event-driven systems.

Example (MySQL, compact storage)

@Id
@UuidGenerator
@JdbcTypeCode(SqlTypes.BINARY) // store UUID in 16 bytes
@Column(columnDefinition = "BINARY(16)")
private UUID id;

DDL

CREATE TABLE users (
  id BINARY(16) NOT NULL PRIMARY KEY,
  name VARCHAR(100) NOT NULL,
  uuid_txt CHAR(36) AS (BIN_TO_UUID(id)) STORED
);

⚙️ Key Cross-Cutting Concepts

🔄 Batching

  • JDBC batching = send multiple inserts in one round trip.
  • Best with SEQUENCE (IDs known up front).
  • Limited with IDENTITY (IDs only known after insert).
  • Enable in Hibernate:
hibernate.jdbc.batch_size=50

📦 Allocation / Prefetching

  • With SEQUENCE and TABLE, Hibernate can fetch blocks of IDs using allocationSize.
  • Example: allocationSize=50 → Hibernate grabs 50 IDs and consumes them gradually.
  • Benefits: fewer DB calls, better batching.
  • Downsides: gaps if app crashes or restarts.

⚠️ Gaps

  • All strategies may produce gaps:

    • Rollbacks
    • Crashes
    • Pooled allocations
    • Don’t use IDs as business order — always add a created_at column.

✅ When to Use What (Cheat Sheet)

Strategy Best Fit Pros Cons
AUTO Portability Easiest, automatic choice Unpredictable per DB
SEQUENCE PostgreSQL / Oracle Best batching, performance, pre-fetch Needs sequence object, gaps
IDENTITY MySQL / SQL Server (legacy schemas) Simple, DB-native Weak batching, slower inserts
TABLE Exotic DBs, portability needed Works everywhere Slow, contention-heavy
UUID Distributed / microservices / offline Global uniqueness, no DB round-trip Larger index, may fragment

🚀 Quick Rules of Thumb

  • High throughput inserts (Postgres/Oracle)SEQUENCE + batching.
  • MySQL/SQL Server legacyIDENTITY.
  • Distributed/microservicesUUID (prefer time-ordered).
  • Extreme portabilityTABLE.
  • Don’t care / just startingAUTO.

✅ In short:

  • Use SEQUENCE when you can (best batching).
  • Use IDENTITY if schema/DB requires it.
  • Use UUID if you need IDs outside the DB or in distributed systems.
  • Expect gaps with any method.
  • Never use PK for business ordering → add timestamps.

MySQL

UUID Primary Keys (Compact BINARY(16) Storage)

When using UUIDs as primary keys in MySQL, storing them as CHAR(36) strings is common but inefficient. A better approach is to store them in a compact binary format using BINARY(16). This reduces storage space and improves index performance.

Here’s a tiny, end-to-end example of UUID primary keys on MySQL, stored compactly as BINARY(16) and generated in the app with Hibernate 6.

1) MySQL table (compact PK + easy-to-read helper)

CREATE TABLE users (
  id BINARY(16) NOT NULL PRIMARY KEY,          -- packed UUID
  name VARCHAR(100) NOT NULL,
  created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
  -- Optional: human-readable mirror (handy for ad-hoc queries)
  uuid_txt CHAR(36) AS (BIN_TO_UUID(id)) STORED
) ENGINE=InnoDB;

You don’t have to keep uuid_txt—it’s just convenient. The real PK is BINARY(16).

2) Hibernate/JPA entity (Hibernate 6)

import jakarta.persistence.*;
import java.time.Instant;
import java.util.UUID;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UuidGenerator;
import org.hibernate.annotations.JdbcTypeCode;
import org.hibernate.type.SqlTypes;

@Entity
@Table(name = "users")
public class User {

  @Id
  @UuidGenerator // generates java.util.UUID in the app (no DB round-trip)
  @JdbcTypeCode(SqlTypes.BINARY)               // map UUID -> BINARY(16)
  @Column(name = "id", columnDefinition = "BINARY(16)")
  private UUID id;

  @Column(nullable = false)
  private String name;

  @CreationTimestamp
  @Column(name = "created_at", nullable = false, updatable = false)
  private Instant createdAt;

  // getters/setters ...
}

If your Hibernate exposes a time-ordered style, you can do @UuidGenerator(style = UuidGenerator.Style.TIME) to reduce index fragmentation vs random v4.

3) Spring/JPA settings (example)

spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL8Dialect
spring.jpa.hibernate.ddl-auto=none          # or update/create at your own risk
spring.jpa.properties.hibernate.jdbc.batch_size=50

4) Using it

User u = new User();
u.setName("Alice");
userRepository.save(u);          // Hibernate generates UUID in-memory and sends as 16 bytes
System.out.println(u.getId());   // you already have the ID before insert

5) Handy SQL when querying manually

-- Find by a textual UUID
SELECT id, name, created_at
FROM users
WHERE id = UUID_TO_BIN('550e8400-e29b-41d4-a716-446655440000');

-- See the UUIDs in text form (thanks to the generated column)
SELECT uuid_txt, name, created_at FROM users;

Notes & options

  • Why BINARY(16)? 2× smaller than CHAR(36), faster indexes.
  • Time-ordered UUIDs: Random v4 UUIDs fragment B-trees. Prefer v7 (time-ordered) if possible:

    • Generate UUIDv7 in your code (library) and assign it to id before save(), or
    • Use a Hibernate generator with a time-ordered style if your version supports it.
    • MySQL’s UUID_TO_BIN(..., 1) trick: When you generate IDs in SQL (not here), using UUID_TO_BIN(uuid, 1) stores bytes in a time-friendly order. If you go that route, be consistent and also read with BIN_TO_UUID(id, 1). With Hibernate generating IDs as Java UUID, you typically stick to the straightforward mapping above (no swap flag).

FAQs: UUID vs SEQUENCE vs IDENTITY

1) When to pick UUID / GUID (Hibernate 6)

Choose a UUID/GUID primary key when one or more of these are true:

  • You need to generate IDs outside the DB (e.g., in services, message producers, offline/mobile clients) and later upsert/merge in the DB.
  • You have multiple writer nodes / multi-region and want globally unique IDs without coordinating with the DB first.
  • You want to avoid an extra DB round-trip per ID assignment (helpful when using IDENTITY, or when you don’t want to call a sequence).
  • You want stable IDs before persistence (useful for building relationships in memory, emitting events, logs, URLs, etc.).
  • You’re using an event-driven or CQRS setup and need IDs as soon as aggregates are created.

Trade-offs

  • Pros: globally unique across services, no DB trip to get an ID, easy replication/sharding, IDs exist before insert.
  • Cons: bigger indexes; random UUIDs (v4) fragment B-trees and bloat indexes. Prefer time-ordered IDs (UUIDv7, v1, ULID-style) to keep inserts mostly append-only and indexes tighter.

Practical mapping tips

  • PostgreSQL: use uuid column type.
  • MySQL/MariaDB: use BINARY(16) and store packed UUIDs (don’t store as 36-char strings).
  • SQL Server: UNIQUEIDENTIFIER.
  • Oracle: RAW(16).

Hibernate 6 examples:

// Postgres-friendly
@Id
@org.hibernate.annotations.UuidGenerator // Hibernate 6
@org.hibernate.annotations.JdbcTypeCode(org.hibernate.type.SqlTypes.UUID)
@Column(columnDefinition = "uuid")
private java.util.UUID id;

// MySQL-friendly (packed)
@Id
@org.hibernate.annotations.UuidGenerator
@org.hibernate.annotations.JdbcTypeCode(org.hibernate.type.SqlTypes.BINARY)
@Column(columnDefinition = "BINARY(16)")
private java.util.UUID id;

(If your Hibernate version exposes a “time” style for the generator, use it to get time-ordered UUIDs.)


2) “If we can pre-order IDs with SEQUENCE, why do we need IDENTITY? Can we pre-fetch 50 and use them one by one?”

Yes, you can pre-fetch and reuse

With GenerationType.SEQUENCE plus @SequenceGenerator(allocationSize = 50), Hibernate will fetch a block of 50 IDs from the DB sequence and cache them in memory. It then consumes those IDs:

  • for batched inserts, or
  • for individual inserts over time, until the block is exhausted,
  • then it fetches the next block automatically.

This is not limited to a single “batch operation”. It’s a pool that gets used as your app persists entities. Note you may see gaps (e.g., app restarts with unused IDs in the block).

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

So… why use IDENTITY at all?

IDENTITY remains common because:

  • Database choice / legacy: MySQL and SQL Server schemas traditionally rely on identity/auto-increment. Many shops, tools, and DBAs default to it.
  • Simplicity: No separate sequence object to manage. One column with AUTO_INCREMENT/IDENTITY.
  • Existing systems & tooling: ETL, ORM defaults, migration scripts, audit triggers, etc., may expect identity columns.
  • DB feature set: Some environments don’t use sequences; identity is the “native” and operationally familiar option.

Downside: with IDENTITY, Hibernate learns the key after each insert, which weakens true JDBC insert batching. (That’s exactly why sequences or UUIDs tend to perform better for heavy write loads.)


Quick chooser

  • High write throughput on Postgres/Oracle: SEQUENCE + allocationSize (pooled) + JDBC batching.
  • Multi-service / offline ID generation / pre-persistence IDs: UUID (prefer time-ordered variants).
  • Legacy MySQL/SQL Server or simplest ops: IDENTITY is fine; accept weaker batching.
  • Maximum portability to odd DBs: TABLE (understand it’s slower).

Extra tips

  • Don’t rely on PK order for business sorting; use created_at or a business key.
  • Expect gaps with any strategy (rollbacks, pooling, restarts).
  • If you use UUIDs on MySQL, store as BINARY(16) and consider time-ordered UUIDs (e.g., v7) to reduce index fragmentation.