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 |
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
0so the value stays a positivelongin 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).
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
StatefulSetpod ordinal — aStatefulSetguarantees pod names of the form<name>-0,<name>-1, and so on; parsing the trailing ordinal out of the pod’sHOSTNAMEenvironment 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:
-
Give the identifier column a
UNIQUEconstraint (or rely on it already being the primary key) so a collision fails fast as a constraint violation rather than silently overwriting a row. -
Catch that violation specifically (
DataIntegrityViolationException/ConstraintViolationExceptionin Spring Data JPA) at the point of insert, distinguishing it from other failure causes. -
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.
-
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.
Related schemes
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 |
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
-
Twitter Engineering — "Announcing Snowflake" (archived; the original blog post and the
twitter-archive/snowflakeproject itself are no longer maintained) -
IETF RFC 9562 — Universally Unique IDentifiers (UUIDs) (defines UUIDv7, and obsoletes RFC 4122’s original UUIDv4 definition)