skip to content

Hibernate's sequence-backed id generators can use different optimizers — hi/lo, pooled, and pooled-lo. What does the value returned by the database sequence mean in each, and which are safe when another application inserts into the same table?

level: seniorimportance: nice to knowfreq 25%

answer

  1. hilo: value is a multiplier, not an id
  2. pooled: nextval = top of block
  3. pooled-lo: nextval = bottom of block
  4. pooled/pooled-lo are interop-safe
  5. allocationSize 1 = no-op optimizer

basics

~20 s

With legacy hi/lo the sequence value is a multiplier, so it bears no relation to stored ids and outside writers collide. With pooled, nextval returns the top of the reserved block; with pooled-lo it returns the bottom. Both keep sequence values inside the id space, so external callers using the same sequence stay safe.

solid answer

~60 s

All three amortise a sequence call over many ids; they differ in how the returned number maps to identifiers. - **hilo (legacy)** — the returned value is a *hi* multiplier: ids are `hi * incrementSize + lo`. A sequence at 3 with increment size 50 yields ids around 150. Nothing else may use that sequence or insert into the table, because the sequence value is not an id and gives no clue what is in use. - **pooled** — Hibernate's default when allocationSize > 1. Each `nextval` returns the **upper boundary** of a block; Hibernate uses the range below it. The sequence must be `increment by allocationSize`. Every value the sequence produces is itself a valid, unused id. - **pooled-lo** — same, but the returned value is the **lowest** id of the block and Hibernate uses it upward. This is the friendlier variant when adopting a sequence that already sits at the current maximum id. pooled and pooled-lo are interoperable: an outside writer that calls the same sequence gets a value nobody else will use. hi/lo is not.

code

java · 14 lines
java
@Entity
public class Order {
    @Id
    @GenericGenerator(
        name = "order_gen",
        strategy = "org.hibernate.id.enhanced.SequenceStyleGenerator",
        parameters = {
            @Parameter(name = "sequence_name", value = "order_seq"),
            @Parameter(name = "increment_size", value = "50"),
            @Parameter(name = "optimizer", value = "pooled-lo")
        })
    @GeneratedValue(generator = "order_gen")
    private Long id;
}

go deeper

for a junior

Enough to know that Hibernate reserves ids in blocks so it does not call the sequence for every row.

for a middle

State that pooled is the modern default and requires the sequence increment to match allocationSize, while hi/lo is legacy.

for a senior

Explain precisely what the returned value means in each optimizer and derive the interoperability conclusion from it; mention allocationSize = 1 as the escape hatch.

for a principal

Frame it as ownership of the id space: whether the sequence is a shared contract other systems may draw from, and what that implies for migrations and rolling deploys.

## Why optimizers exist Calling a sequence per row costs a round trip per insert. An *optimizer* sits between Hibernate's generator and the database: it fetches one value and derives several identifiers from it, so N inserts cost N/allocationSize sequence calls. The optimizer is chosen by Hibernate based on `allocationSize` and configuration; it can be set explicitly via `hibernate.id.optimizer.pooled.preferred` or generator parameters. The subtlety is what the fetched number *means*, because that determines whether anyone else may touch the same sequence or table. ## hi/lo — the legacy scheme The returned value is a high-order multiplier. With `incrementSize = 50`, hi = 1 produces ids 1–50 (or 51–100 depending on the variant), hi = 2 produces the next 50, and so on. The sequence itself increments by 1. Problems: - The sequence value is not an identifier. `select last_value from my_seq` returning 7 tells you nothing about which ids exist. - Any other writer — a second application, an ETL job, a DBA inserting a fix-up row using `nextval` — will produce values deep inside a range Hibernate already considers its own, so collisions are guaranteed rather than merely likely. - The id space is consumed in a jumpy pattern that is hard to reason about during migrations. Hibernate keeps it for backward compatibility with old mappings; new mappings should not use it. ## pooled — the modern default The sequence is declared `increment by allocationSize`, so successive `nextval` calls return, say, 50, 100, 150. The optimizer treats the returned value as the **top** of its block and hands out the values below it (roughly `(previous, returned]`). Two properties follow: 1. **Safety comes from the engine.** Because the sequence advances by a whole block per call, blocks are disjoint across threads, JVMs and nodes with no coordination. 2. **Interoperability.** Any value the sequence returns is a real, unused identifier. Another application, a native `insert ... values (nextval('order_seq'), ...)`, or a data-fix script can draw from the same sequence and be safe — it simply consumes one block's worth of range. The catch is the boundary at adoption: on a table whose maximum id is M, the sequence must start above M, and switching an existing entity from allocationSize 1 to 50 requires changing the sequence increment in the same migration. ## pooled-lo Identical arithmetic, opposite anchor: the returned value is the **lowest** id of the block, and Hibernate hands out `[returned, returned + incrementSize)`. This avoids the awkward "the first block starts below the sequence's start value" question and makes reasoning about legacy data easier, which is why some teams prefer it when converting an existing table. Interoperability is the same as pooled. ## Choosing, and the escape hatch - Default to **pooled** (or **pooled-lo** when converting a live table); both need the sequence increment to match `allocationSize`. - Use **`allocationSize = 1`** — the no-op optimizer, one `nextval` per row — when another system draws ids from the same sequence one at a time and expects strict single-stepping, or when dense, monotonic ids genuinely matter. You pay a round trip per insert. - Avoid **hilo** unless you are maintaining an old mapping, and if you inherit one, treat the table as exclusively owned by that application until it is migrated. ## Operational notes Whatever the optimizer, the reserved block lives in JVM memory: a redeploy discards the unused remainder, so ids have gaps and are not globally ordered across nodes. During a rolling deploy that changes `allocationSize`, old and new instances briefly disagree about block width — with pooled that is exactly the overlap scenario that produces duplicate keys, so change the increment and the mapping together in a controlled window, or introduce a new sequence and switch over. In an interview, the crisp discriminator to state is: *hi/lo's sequence value is a multiplier and therefore private to Hibernate; pooled and pooled-lo's sequence values are identifiers, which is what makes them safe to share.*

  • A reporting job inserts rows into the same table using `nextval` on the entity's sequence. Which optimizer settings keep that safe?
    pooled and pooled-lo both keep it safe: the value the job gets is a genuine identifier and consumes a whole block's worth of range, so it cannot land inside a block Hibernate has reserved. Legacy hi/lo is unsafe, because the sequence value there is a multiplier and the job's value will fall inside a range Hibernate is already using.
  • Why is changing allocationSize during a rolling deployment risky?
    Old instances still allocate blocks of the old width from a sequence whose INCREMENT BY has changed, so the two populations compute different, overlapping ranges from the same returned values — the same failure mode as an increment mismatch, and it produces intermittent duplicate-key errors. Change the sequence and the mapping in one controlled step, or introduce a new sequence and cut over.

saying these in an interview costs you the question

  • Thinking hi/lo's sequence value is a usable identifier
  • Believing Hibernate coordinates id blocks between JVM instances
  • Setting an optimizer without changing the sequence's INCREMENT BY
  • Assuming ids from pooled generators are globally ordered by insert time
  • Treating pooled and hi/lo as interchangeable when other systems share the table

context