Distributed ID Generation

This section documents Spring Boot 4.1.x and Spring Framework 7.0.x on the Java 17/21+ baseline, as described by the official Spring Boot reference documentation and each sub-project’s own site (Spring Data, Spring for Apache Kafka, Micrometer, Project Reactor, springdoc-openapi, Spring gRPC, and the others listed in this section’s Bibliography) — which are the references these pages are written and verified against.

This content was generated with the assistance of AI and should be verified against those official docs before being relied on in production. Spring Boot and its ecosystem continue to evolve; the examples here target the current 4.1.x / 7.0.x releases.

This section’s bibliography lists the reference material consulted while preparing these pages.

Entities and Identifiers already lists GenerationType.IDENTITY/SEQUENCE/TABLE/UUID in its @GeneratedValue strategies table. This page picks up where that table’s "Custom" row leaves off: it explains why a distributed application often wants a locally-generated identifier in the first place, the bit-packed Snowflake scheme that made the idea popular, its ULID/TSID/UUIDv7 relatives, and how to plug one of them into Spring Boot — both through Hibernate’s IdentifierGenerator SPI and as a plain bean for non-JPA use.

The coordination problem

GenerationType.IDENTITY, SEQUENCE, and TABLE all solve uniqueness the same way: one database owns "the next value" and hands it out on request, whether that is an auto-increment column, a sequence object, or a row in a dedicated allocation table. That works well for a single writer, but every one of those strategies becomes a shared, serializing resource the moment several application instances insert concurrently — a set of microservices, or the same monolith scaled out across a server farm. Every instance now contends for the same identity column, sequence, or allocation-table row before it can even begin its own insert.

GenerationType.UUID sidesteps the bottleneck entirely — a random (v4) UUID is generated client-side with no database round-trip at all — but Entities and Identifiers already flags its cost: 16 random bytes scatter across the keyspace, which is exactly what breaks index locality on a B-tree-backed primary key, and nothing about the value itself says which row was inserted before which.

To be precise about what problem Snowflake actually solves: a random UUID already is a fully distributed, coordination-free identifier — that problem is solved the moment GenerationType.UUID is chosen. Snowflake does not solve a different problem; it solves a narrower one. It generates coordination-free identifiers as compact, roughly sequential 64-bit numbers instead of random 128-bit tokens — which matters specifically when a numeric, sortable, index-friendly ID is required (e.g. a smaller primary key, a value that sorts by insertion order, or an ID format some downstream system expects to be numeric) and a random UUID’s shape is the part that doesn’t fit.

That raises the actual question this page answers: what if an identifier needs to be unique and roughly sequential and still require no cross-node coordination at all?

The Snowflake approach

Twitter’s original Snowflake service (2010) answered that question by packing a single 64-bit signed long out of four fields, generated entirely locally on each node:

  • 1 unused sign bit — kept 0 so the value stays a positive long in every language that lacks unsigned 64-bit integers.

  • ~41-bit timestamp — milliseconds since a custom epoch (not the Unix epoch) chosen close to the service’s launch date, to keep more of the 41 bits usable before the field wraps.

  • Datacenter ID and worker ID bits — Twitter’s original split used 5 bits each (32 datacenters x 32 workers per datacenter); some later implementations combine these into one larger "node/worker ID" field instead, but the principle is the same either way.

  • 12-bit sequence — a counter reset to zero every millisecond, incremented for each additional ID generated by the same worker within that same millisecond (up to 4096 IDs/ms per worker).

block-beta columns 64 block:sign:1 S["1 bit\nsign (0)"] end block:timestamp:41 T["41 bits -- ms since custom epoch"] end block:datacenter:5 D["5 bits\ndatacenter ID"] end block:worker:5 W["5 bits\nworker ID"] end block:sequence:12 Q["12 bits -- per-ms sequence"] end

Crucially, the datacenter ID and worker ID are assigned to a node once, at startup or deploy time — never looked up per generated ID — so producing an ID never touches the network or a shared data store. Two things fall out of this layout for free:

  • Uniqueness without locking. Two nodes can never collide as long as each has a distinct datacenter/worker-ID pair: even if their clocks read the exact same millisecond, their worker-ID bits differ.

  • Rough time-ordering. Because the timestamp occupies the highest-order bits (right after the sign bit), IDs generated later always sort numerically after IDs generated earlier, independent of which node produced either — unlike a random UUID, an index built on a Snowflake ID stays append-mostly instead of scattering inserts across the whole B-tree.

Trade-offs and operational concerns

Clock drift and NTP. The entire scheme leans on each node’s system clock advancing monotonically. If NTP steps the clock backwards — a leap-second correction, a misbehaving time source, a VM host pausing and resuming a guest — a generator that naively re-reads the clock can produce a timestamp smaller than the last one it already used, silently breaking both uniqueness (if the sequence also happens to repeat) and time-ordering. A correct generator must detect this explicitly and pick one of two responses: block and wait until the clock catches back up past the last-used timestamp, or reject the request outright (throwing rather than emitting a possibly-colliding ID). Which is acceptable depends on whether the caller can tolerate a stalled insert or needs a fast failure instead.

Assigning worker/datacenter IDs in Spring Boot. Nothing in the algorithm assigns these IDs automatically — that is entirely the deploying application’s responsibility, typically one of:

  • Static per-instance configuration — a distinct value baked into each instance’s Spring profile or externalized configuration (an environment variable injected per deployment slot).

  • A Kubernetes StatefulSet pod ordinal — a StatefulSet guarantees pod names of the form <name>-0, <name>-1, and so on; parsing the trailing ordinal out of the pod’s HOSTNAME environment variable at startup gives each replica a stable, distinct worker ID with no extra coordination service at all.

  • Startup-only coordination through a shared store — a node claims the next free ID from a shared table, ZooKeeper znode, or etcd/Consul key exactly once, at boot, then caches it in memory for the rest of the process’s life. This reintroduces a coordination point, but only once per process lifetime rather than once per generated ID, which is a very different cost profile than the SEQUENCE/TABLE strategies above.

Detecting and recovering from a duplicate. The in-process clock-drift guard above is not the only place a collision can surface — a bug in one of the worker-ID assignment schemes just described, a misconfigured deployment that starts two instances with the same worker ID, or a clock correction the generator’s own guard didn’t catch can all still let a duplicate reach the database. The persistence layer is the last line of defense, and the pattern is the same one already used for any INSERT that can violate a unique constraint:

  1. Give the identifier column a UNIQUE constraint (or rely on it already being the primary key) so a collision fails fast as a constraint violation rather than silently overwriting a row.

  2. Catch that violation specifically (DataIntegrityViolationException/ConstraintViolationException in Spring Data JPA) at the point of insert, distinguishing it from other failure causes.

  3. If the underlying cause is transient — a clock correction that has since passed, a worker-ID collision that a supervisor has already resolved by restarting one of the instances — generate a fresh ID and retry the insert, capped at a small, fixed number of attempts so a persistent problem doesn’t retry forever.

  4. If retries are exhausted or the cause is not transient (e.g. two instances are still misconfigured with the same worker ID), fail the operation outright and surface it as an application error/alert rather than looping indefinitely — a stuck retry loop is worse than a fast, visible failure.

Finite timestamp lifespan. A fixed-width timestamp field eventually wraps. 41 bits of milliseconds is roughly 69 years from whatever custom epoch a given scheme chooses — comfortably long for most systems, but worth actually computing (2^41 ms from the chosen epoch) rather than assuming, especially for a scheme that allocates fewer than 41 bits to the timestamp in exchange for a wider worker-ID or sequence field.

Snowflake was influential enough that several later schemes adopt the same "locally generated, timestamp in the high bits" shape while changing the encoding, the field widths, or the standardization story:

Scheme Requires DB coordination? Time-sortable? Size Standardization

DB IDENTITY/SEQUENCE

Yes — every ID requires a round-trip (or a pre-fetched block) from the owning database

Yes — strictly monotonic

Typically 4 or 8 bytes

Database-vendor feature, not an interchange format

Random UUID (v4)

No

No — fully random

16 bytes / 128 bits

Standardized — IETF RFC 9562 (obsoletes RFC 4122)

Snowflake

No (worker/datacenter ID assigned once, not per ID)

Yes — timestamp in the high-order bits

8 bytes / 64 bits

Convention popularized by Twitter’s implementation, not a formal standard; the original project is archived

ULID

No

Yes — 48-bit millisecond timestamp prefix

16 bytes / 128 bits (26-character Crockford Base32 text form)

Community specification (ulid/spec), not an IETF/ISO standard

TSID

No (worker ID assigned once, not per ID)

Yes — timestamp in the high-order bits, Snowflake-derived layout

8 bytes / 64 bits

Convention (naming and layout defined by its reference implementations), not a formal standard

UUIDv7

No

Yes — 48-bit Unix-epoch millisecond timestamp prefix

16 bytes / 128 bits

Standardized — IETF RFC 9562

UUIDv7 is worth calling out specifically: it keeps the familiar 128-bit UUID wire format and type (so it drops into any column, API, or library already typed as UUID) while fixing v4’s lack of sortability by putting a millisecond Unix timestamp in the leading bits — the same idea Snowflake and TSID use, standardized into the UUID format itself rather than a bespoke 64-bit encoding.

Using it from Spring Boot

The examples below use tsid-creator (see "Choosing a Java library" below for why); add it once and both integration points share it:

<dependency>
    <groupId>com.github.f4b6a3</groupId>
    <artifactId>tsid-creator</artifactId>
    <version>5.2.6</version>
</dependency>

There are two distinct integration points, depending on whether the identifier backs a JPA entity or not.

Hooking into @GeneratedValue with a custom IdentifierGenerator

Entities and Identifiers's strategies table names the "Custom" row as implementing org.hibernate.id.IdentifierGenerator and referencing it through @GenericGenerator. Hibernate instantiates that class itself via reflection when it first needs a generator — it never resolves constructor arguments through Spring, so the class must have a public no-arg constructor. To still reach the Spring-managed TsidFactory bean (worker/datacenter ID configuration should flow through the normal Spring Boot configuration mechanism, not be hardcoded), a small ApplicationContextAware holder bridges the two:

@Component
class TsidFactoryHolder implements ApplicationContextAware {

    private static volatile TsidFactory factory;

    static TsidFactory get() {
        return factory;
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) {
        factory = applicationContext.getBean(TsidFactory.class);
    }
}
public class SnowflakeIdentifierGenerator implements IdentifierGenerator {

    // No-arg constructor required -- Hibernate instantiates this reflectively, it does not
    // autowire constructor arguments. The Spring-managed TsidFactory bean is reached through
    // the holder above instead.

    @Override
    public Object generate(SharedSessionContractImplementor session, Object object) {
        return TsidFactoryHolder.get().create().toLong();
    }
}
@Entity
public class Order {

    @Id
    @GeneratedValue(generator = "snowflake")
    @GenericGenerator(name = "snowflake", type = SnowflakeIdentifierGenerator.class)
    private Long id;

    // ...
}

A plain bean for non-JPA use

Not every identifier is a JPA entity’s primary key — a document store’s key, a message ID, or an event ID published onto a broker has no @GeneratedValue annotation to hook into at all. For those, expose a TsidFactory as an ordinary Spring bean instead, built from a @ConfigurationProperties-bound worker ID so the same static-configuration/pod-ordinal/startup-coordination choice from the previous section applies uniformly:

@ConfigurationProperties(prefix = "app.id-generation")
public record IdGenerationProperties(int workerId) {
}
@Configuration
@EnableConfigurationProperties(IdGenerationProperties.class)
public class IdGenerationConfig {

    @Bean
    public TsidFactory tsidFactory(IdGenerationProperties properties) {
        return TsidFactory.builder()
                .withNode(properties.workerId())
                .build();
    }
}

Any @Component/@Service needing an identifier — a message publisher building an event ID, a repository generating a document-store key — then just injects the TsidFactory bean and calls factory.create().toLong() (or .toString() for the 13-character Crockford Base32 text form), exactly like any other collaborator.

Choosing a Java library

Rolling a Snowflake/TSID generator from scratch means getting the clock-drift handling from "Trade-offs and operational concerns" above exactly right, which is easy to get subtly wrong — reaching for an existing, tested library is the safer default.

com.github.f4b6a3:tsid-creator generates TSIDs directly and has no runtime dependencies of its own, which makes it the simplest drop-in choice for either the IdentifierGenerator or plain-bean integration shown above, at the cost of leaving worker-ID assignment entirely to the caller. It is published to Maven Central under the coordinates already used above:

<dependency>
    <groupId>com.github.f4b6a3</groupId>
    <artifactId>tsid-creator</artifactId>
    <version>5.2.6</version>
</dependency>

Baidu’s uid-generator takes the opposite trade-off: it is a Spring-integrated Snowflake implementation that ships its own worker-ID assigner, backed by a small database table it queries once at application startup — convenient in a Spring Boot application that already has a datasource configured, but it reintroduces a (startup-only, not per-ID) database dependency that tsid-creator avoids entirely. Unlike the other two libraries here, it is not published to Maven Central and has no tagged releases — its pom.xml declares version 1.0.0-SNAPSHOT, so the only reliable way to use it is cloning the repository and installing that snapshot into the local Maven repository:

git clone https://github.com/baidu/uid-generator.git
cd uid-generator
mvn install
<dependency>
    <groupId>com.baidu.fsg</groupId>
    <artifactId>uid-generator</artifactId>
    <version>1.0.0-SNAPSHOT</version>
</dependency>

Hutool’s cn.hutool:hutool-core also bundles a Snowflake utility class (IdUtil.createSnowflake(workerId, datacenterId), then snowflake.nextId()) as one small piece of its much larger general-purpose utility library — a reasonable choice if the project already depends on Hutool for other utilities, but not compelling enough as a standalone dependency to justify pulling in the rest of the library just for ID generation:

<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-core</artifactId>
    <version>5.8.36</version>
</dependency>

References