Back to Entity Mapping: Keys, Columns & Annotations
Q7Question 7 of 8

IDENTITY vs SEQUENCE vs TABLE vs AUTO vs UUID?

Key Concepts
  • IDENTITY relies on database auto-increment; the id is assigned during INSERT, which breaks JDBC batch inserts because Hibernate needs each id immediately.
  • SEQUENCE uses a database sequence object and pre-fetches id blocks via allocationSize, enabling batch inserts and fewer round trips.
  • TABLE emulates a sequence with a dedicated key table and row locking; it is portable but the slowest due to lock contention.
  • AUTO lets the provider choose the strategy based on the dialect; prefer explicit strategies in production.
  • UUID generates a 128-bit id in the application with no database round trip, ideal for distributed systems.
  • Random UUIDv4 as a clustered primary key causes index fragmentation; use time-ordered UUIDv7 or ULID instead.

Definition

IDENTITY uses DB auto-increment (no batching); SEQUENCE pre-fetches id blocks (preferred, batchable); TABLE emulates sequences portably but slowly; AUTO lets the provider choose; UUID is app-generated and distribution-friendly.
  • GenerationType.IDENTITY – relies on a database auto-increment column; the id is assigned during INSERT, so Hibernate cannot batch inserts because it needs each id immediately.
  • GenerationType.SEQUENCE – uses a database sequence object with an allocationSize to pre-fetch a block of ids, enabling batch inserts and fewer round trips.
  • GenerationType.TABLE – emulates a sequence using a dedicated key table with row locking; portable but slow due to lock contention.
  • GenerationType.AUTO – lets the JPA provider choose the strategy based on the database dialect.
  • GenerationType.UUID – generates a 128-bit UUID in the application with no database round trip; ideal for distributed systems but random UUIDv4 fragments clustered indexes.

When National Bank of India opens a new account it needs a unique number. IDENTITY is like a ticket machine at the branch that stamps the next number only when you reach the counter (one at a time). SEQUENCE is like being handed a strip of fifty pre-printed numbers to use at your own desk (fast, batchable). TABLE is like a shared logbook where everyone writes the last-used number under lock (portable but creates a queue). AUTO lets the bank's system pick the method. UUID is like generating a globally-unique reference yourself without asking the central registry at all.

Understanding the Concepts

What is GenerationType.IDENTITY?

IDENTITY relies on the database's auto-increment feature. The column is defined as SERIAL/IDENTITY on PostgreSQL or AUTO_INCREMENT on MySQL. The database assigns the value during the INSERT. The id is not known until the row is inserted, which means Hibernate CANNOT do JDBC batch inserts because it must execute each INSERT individually to read back the generated key.

JAVA
package com.bank.model;

import jakarta.persistence.*;
import java.math.BigDecimal;

@Entity
@Table(name = "accounts")
public class Account {

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

    @Column(name = "holder_name")
    private String holderName;

    @Column(name = "balance", precision = 15, scale = 2)
    private BigDecimal balance;
}
TEXT
IDENTITY behaviour:
- INSERT INTO accounts (holder_name, balance) VALUES ('Rajesh', 150000.00)
- Database assigns id = 1
- Hibernate reads back the generated id immediately
- Cannot batch: each INSERT is a separate round trip
- Good for: MySQL where sequences are not available (before 8.0)

What is GenerationType.SEQUENCE?

SEQUENCE uses a database sequence object. Hibernate asks the sequence for the next value and, with allocationSize, pre-allocates a block of ids in one call. This keeps batch inserts working and reduces round trips. It is the preferred strategy on PostgreSQL and Oracle.

JAVA
package com.bank.model;

import jakarta.persistence.*;
import java.math.BigDecimal;

@Entity
@Table(name = "accounts")
public class Account {

    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "acc_seq")
    @SequenceGenerator(name = "acc_seq", sequenceName = "account_seq", allocationSize = 50)
    private Long id;

    @Column(name = "holder_name")
    private String holderName;

    @Column(name = "balance", precision = 15, scale = 2)
    private BigDecimal balance;
}
TEXT
SEQUENCE behaviour:
- SELECT nextval('account_seq')  -> returns 50 (allocationSize=50)
- Hibernate assigns ids 1-50 in memory without hitting DB again
- 50 INSERTs can be batched in one JDBC batch call
- Next call: SELECT nextval('account_seq') -> returns 100, assigns 51-100
- Best performance for bulk inserts

What is GenerationType.TABLE?

TABLE emulates a sequence using a dedicated table that stores the last-used value per entity. Hibernate reads and increments that row under a lock. It is portable to any database but the slowest option because the row lock serializes id generation.

JAVA
package com.bank.model;

import jakarta.persistence.*;

@Entity
public class Account {

    @Id
    @GeneratedValue(strategy = GenerationType.TABLE, generator = "acc_tab")
    @TableGenerator(name = "acc_tab", table = "id_keys",
            pkColumnName = "key_name", valueColumnName = "next_val",
            allocationSize = 50)
    private Long id;
}
TEXT
TABLE behaviour:
- SELECT next_val FROM id_keys WHERE key_name = 'Account' FOR UPDATE
- UPDATE id_keys SET next_val = next_val + 50 WHERE key_name = 'Account'
- Row lock serializes id generation -> slowest strategy
- Use ONLY when the database has no sequence support

What is GenerationType.UUID?

UUID generates a 128-bit UUID in the application with no database round trip. It is ideal for distributed systems where multiple nodes need to generate ids without coordination.

JAVA
package com.bank.model;

import jakarta.persistence.*;

@Entity
public class Payment {

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

    private String fromAccount;
    private String toAccount;
    private java.math.BigDecimal amount;
}
TEXT
UUID behaviour:
- Application generates UUID, no database round trip
- Id known before persist -> can return it immediately
- Ideal as idempotency key for distributed payment service
- CAUTION: random UUIDv4 as clustered PK causes index fragmentation

How do the five strategies compare?

| Aspect | IDENTITY | SEQUENCE | TABLE | AUTO | UUID |
|---|---|---|---|---|---|
| Id source | DB auto-increment | DB sequence | Key table | Provider picks | Application |
| Batching | Not possible | Yes (allocationSize) | Yes (allocationSize) | Depends | Yes |
| Round trips | 1 per INSERT | 1 per block | 1 per block (locked) | Depends | 0 |
| Id known before flush | No | Yes | Yes | Depends | Yes |
| Distributed | No | No (single DB) | No (single DB) | Depends | Yes |
| Index impact | Sequential (good) | Sequential (good) | Sequential (good) | Depends | Random v4 (bad) |

JAVA
// IDENTITY: one round trip per INSERT (no batching)
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

// SEQUENCE: one round trip per block of 50 (batching works)
@Id @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "seq")
@SequenceGenerator(name = "seq", sequenceName = "account_seq", allocationSize = 50)
private Long id;

// UUID: zero round trips (app-generated)
@Id @GeneratedValue(strategy = GenerationType.UUID)
private java.util.UUID id;

What are common mistakes?

The most common mistake is using IDENTITY on a high-volume batch import, which makes every INSERT a separate round trip. A second mistake is using random UUIDv4 as a clustered primary key, causing index fragmentation.

JAVA
// WRONG: IDENTITY for batch import -- no batching possible
package com.bank.model;

import jakarta.persistence.*;

@Entity
public class Transaction {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    // 100,000 INSERTs = 100,000 round trips -- very slow!
}
JAVA
// CORRECT: SEQUENCE for batch import -- 50 ids per round trip
package com.bank.model;

import jakarta.persistence.*;

@Entity
public class Transaction {
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "txn_seq")
    @SequenceGenerator(name = "txn_seq", sequenceName = "txn_seq", allocationSize = 50)
    private Long id;
    // 100,000 INSERTs = 2,000 round trips for ids + batched INSERTs
}

How do you fix UUID index fragmentation?

Use a time-ordered identifier like UUIDv7 (Hibernate 6.2+) or ULID so inserts append near the end of the B-tree instead of scattering randomly.

JAVA
package com.bank.model;

import jakarta.persistence.*;
import org.hibernate.annotations.UuidGenerator;

@Entity
public class Payment {

    // Time-ordered UUIDv7: inserts append, no fragmentation
    @Id
    @UuidGenerator(style = UuidGenerator.Style.TIME)
    private java.util.UUID id;

    private String fromAccount;
    private String toAccount;
    private java.math.BigDecimal amount;
}
TEXT
UUIDv7 vs UUIDv4:
- UUIDv4: random -> scatters across B-tree -> page splits -> fragmentation
- UUIDv7: timestamp prefix -> appends near end -> no splits -> fast inserts

What should a senior engineer know?

A senior engineer defaults to SEQUENCE with allocationSize on PostgreSQL and Oracle because it batches well and the id is known before flush. They keep the allocationSize aligned with the actual sequence increment. They use UUID only when distributed generation is needed and choose UUIDv7 over UUIDv4. They know that with IDENTITY, Hibernate must flush an INSERT immediately on persist because it needs the generated id, while with SEQUENCE and UUID it can defer.

YAML
# application.yml: enable JDBC batching (works with SEQUENCE, not IDENTITY)
spring:
  jpa:
    properties:
      hibernate:
        jdbc:
          batch_size: 50
        order_inserts: true
        order_updates: true

Visual Overview

TEXT
  @GeneratedValue Strategy Comparison
  ============================================

  IDENTITY (auto-increment)
  persist() -> INSERT -> DB assigns id -> return id
  [Cannot batch: 1 INSERT = 1 round trip]

  SEQUENCE (database sequence)
  persist() -> SELECT nextval (1 call per block)
            -> assign ids 1..50 in memory
            -> batch INSERT 50 rows in 1 call
  [Batch-friendly: 1 sequence call per allocationSize]

  TABLE (key table with lock)
  persist() -> SELECT FOR UPDATE id_keys
            -> UPDATE id_keys (increment)
            -> assign ids in memory
  [Portable but SLOW: row lock serializes]

  UUID (application-generated)
  persist() -> generate UUID in app (0 round trips)
            -> batch INSERT
  [Distributed: no DB needed for id]

  Performance Ranking (batch INSERT 100K rows):
  ============================================
  1. SEQUENCE (allocationSize=50): ~2s   (2K id calls + batched INSERTs)
  2. UUID:                         ~3s   (0 id calls + batched INSERTs)
  3. TABLE (allocationSize=50):    ~5s   (2K locked id calls + batched)
  4. IDENTITY:                     ~45s  (100K individual INSERTs)

Key Takeaway

The @GeneratedValue annotation tells JPA how to produce primary-key values, and there are five strategies. IDENTITY relies on database auto-increment, and the important consequence is that Hibernate cannot batch inserts because it must execute each INSERT individually to read back the generated id. SEQUENCE uses a database sequence with an allocationSize that pre-fetches a block of ids, keeping batching working, and is Pankaj's default on PostgreSQL and Oracle at National Bank of India. TABLE emulates a sequence with a key table and row locking, portable but the slowest. AUTO lets the provider pick. UUID generates a 128-bit id in the application with no database round trip, ideal for the distributed payments service.

In the trade and account tables at National Bank of India, Rajesh uses SEQUENCE with allocationSize=50 so the nightly import batches inserts and finishes in seconds instead of making one round trip per row. The distributed payments service uses UUID so it can generate an idempotency reference immediately and return it to the caller before any database interaction.

The senior caution Pankaj always gives is about UUIDs as primary keys. A random UUIDv4 scatters inserts across the B-tree index, causing page splits and fragmentation. The fix is a time-ordered UUIDv7 or ULID, which stays globally unique but keeps inserts append-friendly. He also keeps the sequence allocationSize aligned with the actual sequence increment to avoid collisions.

Remember one sentence: SEQUENCE with allocationSize is the preferred strategy for batch-friendly inserts, IDENTITY breaks batching, and UUID needs time-ordering (v7) to avoid index fragmentation.

---

Share
Use ← → arrow keys to navigate