Definition
- @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
allocationSizeto 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.UUIDvalues 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.
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.
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;
}
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.
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;
}
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 |
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.
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;
}
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.
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;
}
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.
// 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
// 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.
// 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.
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
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.
---