A dependency-free Java implementation of the Snowflake distributed unique ID algorithm, with optional JPA and Spring Boot integration.
The core module has zero runtime dependencies and can be used in any Java application: Spring, Quarkus, Micronaut, or plain Java with no framework at all. The JPA and Spring modules are separate, opt-in layers on top of it.
Full API documentation is published at fayupable.github.io/snowflake-id-java, rebuilt automatically from main on every push.
Database auto-increment (SERIAL, IDENTITY) requires a round trip to the database before the application knows an entity's ID, and does not scale across multiple database instances or shards without coordination.
UUID solves the distributed generation problem but produces random, unsorted values. Random IDs fragment B-tree indexes as rows are inserted out of order, which degrades write and range-query performance at scale, and a UUID takes twice the storage of a long (128 bits versus 64 bits).
Snowflake generates a 64-bit long value entirely in application memory, with no network call. Because the most significant bits encode a timestamp, IDs generated later always sort after IDs generated earlier, which keeps index insertion sequential. Each node embeds its own identifier in every ID it produces, so up to 1024 independent nodes can generate IDs concurrently with no coordination and no possibility of collision.
snowflake-core Pure Java. No dependencies. The algorithm itself.
snowflake-jpa Depends on snowflake-core and Hibernate (provided scope).
Hibernate-level integration, no Spring dependency.
snowflake-jpa-spring Depends on snowflake-jpa and Spring Boot.
Auto-configuration and application.yml based setup.
A consumer picks the module that matches their stack:
- A plain Java service that just needs unique IDs: depend on
snowflake-coreonly. - A Hibernate application without Spring (Quarkus, Micronaut, plain JPA): depend on
snowflake-jpa. - A Spring Boot application: depend on
snowflake-jpa-spring.
Every generated ID is a 64-bit signed long, composed of four bit fields packed together:
1 bit 41 bits 10 bits 12 bits
unused timestamp node id sequence
(sign) (ms since epoch) (0-1023) (0-4095)
- The sign bit is always 0, so the value is always a positive
long. - The timestamp field holds the number of milliseconds elapsed since a configurable epoch, not since the Unix epoch. Using a recent epoch (for example, January 1st of the year the project started) rather than 1970 leaves the full 41-bit range, about 69 years, available before it overflows.
- The node id field identifies which application instance produced the ID. Every instance in a deployment must be configured with a distinct node id; otherwise two instances could produce the same ID at the same millisecond.
- The sequence field is a counter that increments for every ID produced within the same millisecond by the same node, resetting to 0 when the clock advances to the next millisecond. This allows a single node to produce up to 4096 IDs per millisecond, over 4 million per second, without colliding with itself.
The generator holds its internal state, the last-seen millisecond and the current sequence value, packed into a single AtomicLong. Producing an ID does not use a lock; it uses a compare-and-swap loop:
- Read the current state and the current time.
- If the current time is earlier than the last-seen millisecond, the system clock has moved backwards (for example, an NTP resync). The generator throws an exception rather than risk producing a duplicate ID, since silently continuing could hand out an ID that was already issued.
- If the current time is later than the last-seen millisecond, the millisecond has advanced: build a new state with that timestamp and sequence reset to 0.
- If the current time equals the last-seen millisecond and the sequence has not yet reached its maximum (4095), increment the sequence.
- If the current time equals the last-seen millisecond and the sequence has already reached its maximum, the node has exhausted its capacity for this millisecond. The generator spins, checking the clock again, until it advances to the next millisecond.
- Attempt to atomically replace the old state with the new one. If another thread updated the state first, the whole attempt is retried from step 1.
Because state changes go through a single atomic compare-and-swap rather than a lock, multiple threads calling the generator concurrently do not block each other; they simply retry on contention. No reflection, no unsafe memory operations, and no native calls are used anywhere in the algorithm.
Snowflake IDs are unique and are generated in non-decreasing order for a given node. They are not, and are not intended to be, a dense sequence of consecutive integers. Because most of the value comes from the timestamp field, an ID generated one millisecond after another can differ by millions, since the sequence portion resets and the timestamp portion shifts by a large amount for even a one millisecond difference. This is expected and does not indicate any problem: the design goal is uniqueness and rough time ordering, not density.
The library is published on JitPack, built directly from GitHub tags. Add the JitPack repository and the module(s) you need:
<repositories>
<repository>
<id>jitpack.io</id>
<url>https://jitpack.io</url>
</repository>
</repositories>
<dependency>
<groupId>com.github.Fayupable.snowflake-id-java</groupId>
<artifactId>snowflake-jpa-spring</artifactId>
<version>v1.0.0</version>
</dependency>Replace the artifact with snowflake-core or snowflake-jpa if your project does not need the Spring layer.
This is all snowflake-core provides: an IdGenerator interface and a thread-safe implementation. Nothing else in the library is required to use it.
SnowflakeConfig config = SnowflakeConfig.defaultConfig(
Instant.parse("2024-01-01T00:00:00Z").toEpochMilli(), // epoch
3L // node id
);
IdGenerator generator = new SnowflakeIdGenerator(config, new SystemClock());
long id = generator.nextId();SnowflakeConfig.defaultConfig uses the standard 41/10/12 bit split described above. A custom split is available through the full constructor if a deployment needs, for example, more node bits at the cost of sequence capacity.
This same IdGenerator can be used to key entries in a cache, produce message keys for a queue, or generate identifiers for a document store; it has no dependency on any persistence framework.
Add the snowflake-jpa-spring dependency. No @Import or other wiring is needed: the module registers its auto-configuration through Spring Boot's standard discovery mechanism (META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports), the same mechanism every official Spring Boot starter uses. As soon as the dependency is on the classpath, Spring Boot picks it up automatically.
Configure a node id in application.yml. This is required; the application fails to start without it, rather than silently defaulting to a value that could collide with another instance:
snowflake:
node-id: 3
epoch: 2024-01-01T00:00:00ZThat is the entire setup: add the dependency, set snowflake.node-id, annotate the entity field shown below.
Annotate the entity's identifier field:
@Entity
public class User {
@Id
@SnowflakeGeneratedId
private Long id;
private String name;
private String email;
}Calling userRepository.save(new User(...)) now produces an entity with a Snowflake ID assigned before the insert happens, with no further code required.
@SnowflakeGeneratedId is built on Hibernate's @IdGeneratorType mechanism; it replaces the older @GeneratedValue plus @GenericGenerator pair with a single annotation. It is still possible to use the classic two-annotation form if a codebase already follows that convention elsewhere, since the underlying generator class also implements Hibernate's classic IdentifierGenerator interface:
@Id
@GeneratedValue(generator = "snowflake")
@GenericGenerator(name = "snowflake", type = SnowflakeIdentifierGenerator.class)
private Long id;A plain @GeneratedValue(strategy = GenerationType.AUTO) on its own does not produce Snowflake IDs. GenerationType is a fixed JPA enum that has no Snowflake option, so the generator must always be referenced explicitly, either through @SnowflakeGeneratedId or through the generator name plus @GenericGenerator pairing shown above.
Depend on snowflake-jpa directly. The @SnowflakeGeneratedId annotation works the same way, but since there is no Spring context to wire the generator automatically, register it once at application startup, before the first entity is persisted:
IdGenerator generator = new SnowflakeIdGenerator(
SnowflakeConfig.defaultConfig(epochMillis, nodeId),
new SystemClock());
SnowflakeIdGeneratorHolder.setInstance(generator);For codebases that prefer not to use a custom Hibernate IdentifierGenerator, for example to keep full compatibility with JPA batch inserts, the same static holder can be called directly from a lifecycle callback:
@Entity
public class User {
@Id
private Long id;
private String name;
@PrePersist
void assignId() {
if (id == null) {
id = SnowflakeIdGeneratorHolder.getInstance().nextId();
}
}
}This requires no additional annotation and no auto-configuration; SnowflakeIdGeneratorHolder is a plain static utility that works whether or not Spring is on the classpath, as long as it has been initialized once as shown above.
SnowflakeIdGeneratorHolder is a static, process-wide bridge between dependency injection and Hibernate's reflection-based generator instantiation. Because Spring caches and reuses an ApplicationContext across test methods within a class, the bean that populates the holder is created once per context, not once per test. A test class that uses Spring and needs isolation from other test classes running in the same JVM should reset the holder once, after all of its tests have completed, rather than after each individual test.
snowflake-core is also covered by Pitest mutation testing (98% mutation score at the time of writing), a stronger signal than line coverage alone: it verifies the test suite actually fails when the generator's logic is subtly broken, not just that the lines were executed.
When snowflake-jpa-spring finds a MeterRegistry bean in the application context (for example from Spring Boot Actuator with a Prometheus or other registry configured), it automatically publishes two counters through it:
| Metric | Meaning |
|---|---|
snowflake.id.generated |
Total ids successfully generated |
snowflake.id.spin_wait |
Total times the generator spun waiting for the next millisecond because the current one's sequence capacity was exhausted |
No metrics backend is required. If no MeterRegistry bean is present, these events are simply not recorded; nothing needs to be configured or installed to use the library without metrics.
A JMH benchmark module (snowflake-benchmark, not published as part of the library) measures raw generator throughput on a single thread and under four-thread contention. Settings are intentionally light, one JVM fork, 2 short warmup iterations, 3 short measurement iterations, so the full run takes about 10 seconds instead of JMH's usual multi-minute default configuration:
Benchmark Mode Cnt Score Error Units
SnowflakeIdGeneratorBenchmark.fourThreadThroughput thrpt 3 3988155.259 ± 157852.642 ops/s
SnowflakeIdGeneratorBenchmark.singleThreadThroughput thrpt 3 4097146.271 ± 13640.308 ops/s
Measured on a single laptop, not a controlled benchmarking environment; treat these as a rough, honest order-of-magnitude figure rather than a precise guarantee. To run it yourself:
cd snowflake-benchmark
mvn clean package
java -jar target/benchmarks.jar| Property | Required | Description |
|---|---|---|
snowflake.node-id |
Yes | Unique identifier for this application instance, 0 to 1023 with the default bit layout. Must not overlap with any other concurrently running instance. |
snowflake.epoch |
No, defaults to 2024-01-01T00:00:00Z |
Reference instant that generated timestamps are measured from. Choosing a value close to the project's actual start date maximizes the years available before the 41-bit timestamp field overflows. |