Back to Entity Mapping: Keys, Columns & Annotations
Q3Question 3 of 8

What are the @GeneratedValue strategies?

Key Concepts
  • IDENTITY uses database auto-increment but disables JDBC batch inserts because the ID is only known after each INSERT.
  • SEQUENCE uses a database sequence object with configurable allocationSize, enabling batch-friendly ID pre-fetching.
  • TABLE simulates a sequence using a key-generation table with row-level locking, making it the slowest strategy.
  • AUTO lets the JPA provider choose the strategy based on the database, which can change between Hibernate versions.
  • UUID generates IDs in the JVM with no database round-trip but causes B-tree index fragmentation with random UUIDs.
  • SEQUENCE with allocationSize=50 is the recommended production strategy for PostgreSQL and Oracle.
  • For batch inserts, SEQUENCE is up to 10x faster than IDENTITY because it avoids per-row flush cycles.

Definition

IDENTITY uses database auto-increment but disables batch inserts, SEQUENCE pre-fetches IDs in blocks for batch-friendly inserts, TABLE is slow with row locking, AUTO lets the provider decide, and UUID generates IDs in the JVM with no database round-trip.
  • @Id -- marks the primary key field on a JPA entity.
  • @GeneratedValue -- tells JPA to generate the primary key value automatically using one of five strategies.
  • IDENTITY -- uses the database auto-increment feature (SERIAL in PostgreSQL, AUTO_INCREMENT in MySQL), but disables JDBC batch inserts because Hibernate must flush each INSERT immediately to read the generated ID.
  • SEQUENCE -- uses a database sequence object with allocationSize to pre-fetch IDs in blocks, enabling batch inserts and making it the fastest strategy for high-throughput systems.
  • TABLE -- uses a key-generation table with row-level locking, portable across databases but the slowest strategy.
  • AUTO -- lets the JPA provider choose the strategy based on the database dialect.
  • UUID -- generates java.util.UUID values in the JVM with no database round-trip, but random UUIDs cause B-tree index fragmentation.

At National Bank of India, think of assigning transaction reference numbers. IDENTITY is the counter that gives you a number only AFTER you submit your form, so you must wait after each submission before handing in the next one (no batching). SEQUENCE is a pre-printed book of 50 reference numbers: tear off the next one instantly without visiting the counter (batch-friendly). TABLE is a ledger book where only one clerk writes the next number at a time (slow, row lock). AUTO is "let the bank decide which system to use." UUID is generating your own globally unique number on your phone (no counter needed) but the number is long and hard to sort.

Understanding the Concepts

What is @Id and @GeneratedValue?

@Id marks a field as the primary key. @GeneratedValue tells JPA to generate the value automatically. Without @GeneratedValue, you must set the ID manually before persisting. The strategy parameter controls how the ID is generated.

  • @Id is required on every JPA entity.
  • @GeneratedValue is optional: omit it when you assign IDs manually (natural keys like account numbers).
  • The five strategies differ in performance, portability, and batch-insert support.
JAVA
package com.bank.model;

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

// Manual ID assignment (no @GeneratedValue)
@Entity
@Table(name = "accounts")
public class Account {
    @Id
    @Column(name = "account_id")
    private String accountId;  // set manually: "SAV2001"

    @Column(precision = 15, scale = 2)
    private BigDecimal balance;
}

How does IDENTITY strategy work?

IDENTITY uses the database auto-increment feature. The database generates the ID during the INSERT statement. Hibernate must execute each INSERT individually and immediately read back the generated ID to set it on the entity in the persistence context.

  • PostgreSQL: column type BIGSERIAL.
  • MySQL: column attribute AUTO_INCREMENT.
  • Simple to use but disables JDBC batch inserts.
  • Hibernate cannot delay the INSERT because it needs the ID immediately.
JAVA
package com.bank.model;

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

@Entity
@Table(name = "transactions")
public class Transaction {

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

    @Column(name = "account_id")
    private String accountId;

    @Column(precision = 15, scale = 2)
    private BigDecimal amount;

    @Column(name = "created_at")
    private Instant createdAt;
}
TEXT
SQL generated:
INSERT INTO transactions (account_id, amount, created_at)
  VALUES ('SAV2001', 50000.00, '2024-01-15 10:30:00')
-- DB generates id=1, Hibernate reads it back via JDBC getGeneratedKeys()

Problem: Hibernate must flush EACH insert to get the ID.
10,000 inserts = 10,000 individual round-trips. Cannot batch.

How does SEQUENCE strategy work?

SEQUENCE uses a database sequence object. Hibernate pre-fetches IDs in blocks controlled by allocationSize. With allocationSize=50, one sequence call returns a range of 50 IDs. Hibernate assigns them locally without hitting the database for each entity, then batch-inserts all 50 rows at once.

  • This is the recommended strategy for PostgreSQL and Oracle.
  • allocationSize=50 means 50 IDs per sequence call.
  • Combined with hibernate.jdbc.batch_size=50, inserts are batched.
  • Up to 10x faster than IDENTITY for bulk operations.
JAVA
package com.bank.model;

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

@Entity
@Table(name = "transactions")
public class Transaction {

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

    @Column(name = "account_id")
    private String accountId;

    @Column(precision = 15, scale = 2)
    private BigDecimal amount;

    @Column(name = "created_at")
    private Instant createdAt;
}
TEXT
Sequence flow:
1. Hibernate calls: SELECT nextval('transaction_id_seq')  -> returns 1
2. Hibernate locally assigns IDs 1 through 50 to entities
3. Hibernate batch-inserts all 50 rows in one JDBC call
4. Hibernate calls nextval again only after ID 50 is used

For 10,000 inserts:
  IDENTITY:  10,000 individual INSERTs -> ~15 seconds
  SEQUENCE:  200 sequence calls + 200 batch INSERTs of 50 -> ~1.5 seconds
  Speedup:   ~10x

How is IDENTITY different from SEQUENCE?

| Aspect | IDENTITY | SEQUENCE |
|---|---|---|
| ID generation | Database during INSERT | Database sequence, pre-fetched |
| Batch inserts | Disabled (flush per INSERT) | Enabled (allocationSize batching) |
| Round-trips for 10K inserts | 10,000 | ~200 (allocationSize=50) |
| Database support | MySQL, PostgreSQL, SQL Server | PostgreSQL, Oracle, DB2 |
| Complexity | Simple, no configuration | Needs @SequenceGenerator |
| Production recommendation | Small-scale only | Preferred for throughput |

JAVA
package com.bank.service;

import com.bank.model.Transaction;
import com.bank.repository.TransactionRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;

@Service
public class BatchInsertService {

    private final TransactionRepository txnRepo;

    public BatchInsertService(TransactionRepository txnRepo) {
        this.txnRepo = txnRepo;
    }

    @Transactional
    public void importDailyTransactions(List<String[]> records) {
        List<Transaction> batch = new ArrayList<>();
        for (String[] record : records) {
            Transaction txn = new Transaction();
            txn.setAccountId(record[0]);
            txn.setAmount(new BigDecimal(record[1]));
            txn.setCreatedAt(Instant.now());
            batch.add(txn);
        }
        txnRepo.saveAll(batch);
        // With SEQUENCE + batch_size=50: efficient batch INSERT
        // With IDENTITY: 10,000 individual INSERTs
    }
}

How does TABLE strategy work?

TABLE simulates a sequence using a dedicated key-generation table. It stores the current value in a row and uses SELECT ... FOR UPDATE to atomically increment it. This is the slowest strategy because the row lock serializes all ID generation requests.

JAVA
package com.bank.model;

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

@Entity
@Table(name = "transactions")
public class Transaction {

    @Id
    @GeneratedValue(strategy = GenerationType.TABLE, generator = "txn_table_gen")
    @TableGenerator(
        name = "txn_table_gen",
        table = "id_generator",
        pkColumnValue = "transaction_id",
        allocationSize = 50
    )
    private Long id;

    @Column(precision = 15, scale = 2)
    private BigDecimal amount;
}
DIAGRAM
Generated table:
id_generator
├── gen_name   VARCHAR  (e.g., 'transaction_id')
└── gen_value  BIGINT   (e.g., 50)

SQL executed:
SELECT gen_value FROM id_generator
  WHERE gen_name = 'transaction_id' FOR UPDATE
UPDATE id_generator SET gen_value = 100
  WHERE gen_name = 'transaction_id'

Row-level lock means only ONE thread generates IDs at a time.
Portable across all databases but slowest strategy. Avoid in production.

How does UUID strategy work?

UUID generates identifiers in the JVM with no database round-trip. This makes it batch-friendly and ideal for distributed systems where multiple instances generate IDs independently. The downside is that random UUIDs (v4) cause B-tree index fragmentation because inserts land at random positions in the index.

JAVA
package com.bank.model;

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

@Entity
@Table(name = "transactions")
public class Transaction {

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

    @Column(name = "account_id")
    private String accountId;

    @Column(precision = 15, scale = 2)
    private BigDecimal amount;
}
TEXT
UUID pros:
  + No database round-trip for ID generation
  + Batch-friendly (ID is known before INSERT)
  + Works in distributed systems (no shared sequence)

UUID cons:
  - 16 bytes vs 8 bytes for BIGINT (larger indexes)
  - Random UUID v4: B-tree index fragmentation (random inserts)
  - Harder to read and debug than sequential numbers

Fix for fragmentation: use UUIDv7 (time-ordered)
  - UUIDv7 has a timestamp prefix so values are roughly sequential
  - Index inserts are mostly append-only, like a sequence

What are common mistakes with @GeneratedValue?

The most frequent mistakes involve mismatched allocationSize and misunderstanding when IDENTITY prevents batching.

JAVA
// WRONG: allocationSize on @SequenceGenerator does not match DB sequence INCREMENT
@SequenceGenerator(
    name = "txn_seq",
    sequenceName = "transaction_id_seq",
    allocationSize = 50  // Hibernate thinks it gets 50 IDs per call
)
// But DB sequence: CREATE SEQUENCE transaction_id_seq INCREMENT BY 1
// Mismatch! In multi-instance deployment, two instances get overlapping ranges
// causing duplicate key violations.

// CORRECT: DB sequence INCREMENT must match allocationSize
// CREATE SEQUENCE transaction_id_seq INCREMENT BY 50;
// allocationSize = 50
JAVA
// WRONG: Using IDENTITY and wondering why batch inserts are slow
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// With hibernate.jdbc.batch_size=50, IDENTITY still executes one INSERT at a time
// Batching is silently disabled.

// CORRECT: Switch to SEQUENCE for batch support
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "txn_seq")
@SequenceGenerator(name = "txn_seq", sequenceName = "txn_id_seq", allocationSize = 50)
private Long id;

When should you use each strategy?

The choice depends on your database, throughput requirements, and deployment model. Here is a decision guide for production banking systems.

JAVA
// Decision guide:
//
// PostgreSQL + high throughput:  SEQUENCE (allocationSize=50)
// Oracle + high throughput:      SEQUENCE (allocationSize=50)
// MySQL (pre-8.0):               IDENTITY (no native sequences)
// MySQL 8.0+:                    SEQUENCE (MySQL added sequences)
// Distributed / microservices:   UUID (no shared sequence needed)
// Legacy / portable:             TABLE (last resort)
// Unknown database:              AUTO (but be explicit in production)
//
// For National Bank of India trade batch service:
//   SEQUENCE + allocationSize=50 + hibernate.jdbc.batch_size=50
//   100,000 daily trade inserts: ~4 minutes (vs ~45 minutes with IDENTITY)

What about AUTO strategy?

AUTO lets the JPA provider pick the strategy based on the database dialect. On PostgreSQL, Hibernate typically picks SEQUENCE. On MySQL (pre-8.0), it picks IDENTITY or TABLE. The problem is that the choice can change between Hibernate versions, so production code should always be explicit.

JAVA
package com.bank.model;

import jakarta.persistence.*;

@Entity
public class Transaction {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
    // Hibernate decides:
    //   PostgreSQL -> SEQUENCE
    //   MySQL      -> IDENTITY (or TABLE in older Hibernate)
    //   H2         -> SEQUENCE
    // Behavior may CHANGE between Hibernate versions!
    // Always be explicit: use SEQUENCE or IDENTITY directly.
}

Visual Overview

DIAGRAM
  IDENTITY flow (per insert):
  ┌─────────────┐    INSERT     ┌──────────────┐    ID back    ┌─────────────┐
  │  Hibernate   │ ──────────> │   Database    │ ──────────> │  Hibernate   │
  │  (no ID yet) │             │ (generates ID)│             │  (sets ID)   │
  └─────────────┘             └──────────────┘             └─────────────┘
  Repeated for EVERY insert. Cannot batch.

  SEQUENCE flow (batched):
  ┌─────────────┐  nextval()   ┌──────────────┐   IDs 1-50   ┌─────────────┐
  │  Hibernate   │ ──────────> │  DB Sequence  │ ──────────> │  Hibernate   │
  └─────────────┘             └──────────────┘             │  (assigns    │
                                                            │   IDs 1-50   │
                                                            │   locally)   │
                                                            └──────┬──────┘
                                                                   │
                                                          Batch INSERT
                                                          50 rows at once
                                                                   │
                                                                   ▼
                                                            ┌──────────────┐
                                                            │   Database    │
                                                            │ (50 rows)    │
                                                            └──────────────┘

  Performance comparison (10,000 inserts):
  ┌─────────────────────────────────────────┐
  │  IDENTITY:  10,000 round-trips  ~15 sec │
  │  SEQUENCE:  200 round-trips     ~1.5 sec│
  │  TABLE:     200 round-trips     ~5 sec  │
  │  UUID:      0 round-trips       ~1.2 sec│
  └─────────────────────────────────────────┘

Key Takeaway

JPA primary key generation uses @Id to mark the key field and @GeneratedValue to control how the value is produced. The five strategies differ in performance and portability. IDENTITY uses database auto-increment and is the simplest option, but it disables JDBC batch inserts because Hibernate must flush each INSERT individually to read back the generated ID. For bulk operations this is devastating: 10,000 inserts require 10,000 individual round-trips.

SEQUENCE uses a database sequence object with a configurable allocationSize. With allocationSize=50, Hibernate calls the sequence once and gets 50 IDs to assign locally, then batch-inserts all 50 rows in one JDBC call. This is up to 10x faster than IDENTITY for bulk operations and is the recommended strategy for production systems on PostgreSQL and Oracle. The allocationSize must match the database sequence INCREMENT to avoid duplicate key violations in multi-instance deployments.

In our National Bank of India trade batch service, switching from IDENTITY to SEQUENCE with allocationSize=50 reduced 100,000 daily trade inserts from 45 minutes to 4 minutes. TABLE strategy uses a key-generation table with row-level locking and should be avoided. UUID generates IDs in the JVM with no database round-trip, making it ideal for distributed systems, but random UUIDs cause B-tree index fragmentation. AUTO lets the provider decide and should be replaced with an explicit strategy in production.

Remember one sentence: use SEQUENCE with allocationSize matching the database sequence INCREMENT for production throughput, because IDENTITY disables batch inserts and TABLE is too slow.

---

Share
Use ← → arrow keys to navigate