Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,26 @@ All notable changes to this project are documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]
## [1.2.0] - 2026-07-29

### Changed
- **Breaking:** `SnowflakeConfig` now splits the node identifier into a genuine 5-bit datacenter id and 5-bit
worker id, matching Twitter's original Snowflake bit layout, instead of a single 10-bit node id.
`SnowflakeConfig.defaultConfig(epoch, nodeId)` is replaced by
`SnowflakeConfig.defaultConfig(epoch, datacenterId, workerId)`. `snowflake.node-id` is replaced by
`snowflake.datacenter-id` and `snowflake.worker-id` in `snowflake-jpa-spring`. The generated id's binary format
and total addressable node count (1024) are unchanged; only how a node's identity is split and configured
changes.

### Added
- `IdGenerator.nextIds(int count)` default method, generating a batch of ids in one call for bulk-insert
scenarios. Implementations may override it for a more efficient batch path; the default simply loops over
`nextId()`.
- Epoch overflow warning: `SnowflakeIdGenerator` now logs a `WARNING` (via `System.Logger`, no extra dependency)
when the configured epoch is within a year of overflowing its 41-bit timestamp field, so misconfiguration is
caught early instead of silently approaching a hard failure decades later.

## [1.1.1] - 2026-07-25

### Fixed
- `SnowflakeId.fromLong()` used a signed right shift (`>>`) instead of an unsigned one (`>>>`), inconsistent with
Expand Down
36 changes: 21 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,18 +34,18 @@ A consumer picks the module that matches their stack:

## How ID generation works

Every generated ID is a 64-bit signed `long`, composed of four bit fields packed together:
Every generated ID is a 64-bit signed `long`, composed of five bit fields packed together, following the same datacenter/worker split as Twitter's original Snowflake design:

```
1 bit 41 bits 10 bits 12 bits
unused timestamp node id sequence
(sign) (ms since epoch) (0-1023) (0-4095)
1 bit 41 bits 5 bits 5 bits 12 bits
unused timestamp datacenter id worker id sequence
(sign) (ms since epoch) (0-31) (0-31) (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 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. If the configured epoch is within a year of overflowing, the generator logs a `WARNING` (via the JDK's built-in `System.Logger`, no extra dependency) as soon as it is constructed, rather than failing silently decades later.
- The datacenter id and worker id fields together identify which application instance produced the ID: up to 32 datacenters, each with up to 32 workers, for 1024 independent instances in total. Every instance in a deployment must be configured with a distinct `(datacenterId, workerId)` pair; 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 worker, resetting to 0 when the clock advances to the next millisecond. This allows a single worker to produce up to 4096 IDs per millisecond, over 4 million per second, without colliding with itself.

### Generation algorithm

Expand Down Expand Up @@ -94,30 +94,35 @@ This is all `snowflake-core` provides: an `IdGenerator` interface and a thread-s
```java
SnowflakeConfig config = SnowflakeConfig.defaultConfig(
Instant.parse("2024-01-01T00:00:00Z").toEpochMilli(), // epoch
3L // node id
1L, // datacenter id
3L // worker id
);
IdGenerator generator = new SnowflakeIdGenerator(config, new SystemClock());

long id = generator.nextId();

// Generating many ids at once, e.g. before a bulk insert:
long[] ids = generator.nextIds(1_000);
```

`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.
`SnowflakeConfig.defaultConfig` uses the standard 41/5/5/12 bit split described above. A custom split is available through the full constructor if a deployment needs, for example, more datacenter or worker 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.

### Spring Boot application

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:
Configure a datacenter id and worker id in `application.yml`. Both are required; the application fails to start without them, rather than silently defaulting to a value that could collide with another instance:

```yaml
snowflake:
node-id: 3
datacenter-id: 1
worker-id: 3
epoch: 2024-01-01T00:00:00Z
```

That is the entire setup: add the dependency, set `snowflake.node-id`, annotate the entity field shown below.
That is the entire setup: add the dependency, set `snowflake.datacenter-id` and `snowflake.worker-id`, annotate the entity field shown below.

Annotate the entity's identifier field:

Expand Down Expand Up @@ -162,7 +167,7 @@ This check is implemented as an annotation processor bundled in `snowflake-jpa`.
<path>
<groupId>com.github.Fayupable.snowflake-id-java</groupId>
<artifactId>snowflake-jpa</artifactId>
<version>v1.1.1</version>
<version>v1.2.0</version>
</path>
</annotationProcessorPaths>
</configuration>
Expand All @@ -175,7 +180,7 @@ Depend on `snowflake-jpa` directly. The `@SnowflakeGeneratedId` annotation works

```java
IdGenerator generator = new SnowflakeIdGenerator(
SnowflakeConfig.defaultConfig(epochMillis, nodeId),
SnowflakeConfig.defaultConfig(epochMillis, datacenterId, workerId),
new SystemClock());
SnowflakeIdGeneratorHolder.setInstance(generator);
```
Expand Down Expand Up @@ -243,5 +248,6 @@ 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.datacenter-id` | Yes | Datacenter identifier for this application instance, `0` to `31` with the default bit layout. |
| `snowflake.worker-id` | Yes | Worker identifier for this application instance within its datacenter, `0` to `31` with the default bit layout. The `(datacenter-id, worker-id)` pair 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. |
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

<groupId>com.fayupable</groupId>
<artifactId>snowflake-id-java</artifactId>
<version>1.1.1</version>
<version>1.2.0</version>
<packaging>pom</packaging>

<properties>
Expand Down
4 changes: 2 additions & 2 deletions snowflake-benchmark/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

<groupId>com.fayupable</groupId>
<artifactId>snowflake-benchmark</artifactId>
<version>1.1.1</version>
<version>1.2.0</version>

<properties>
<maven.compiler.source>21</maven.compiler.source>
Expand All @@ -19,7 +19,7 @@
<dependency>
<groupId>com.fayupable</groupId>
<artifactId>snowflake-core</artifactId>
<version>1.1.1</version>
<version>1.2.0</version>
</dependency>
<dependency>
<groupId>org.openjdk.jmh</groupId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ public class SnowflakeIdGeneratorBenchmark {

@Setup
public void setup() {
SnowflakeConfig config = SnowflakeConfig.defaultConfig(1_700_000_000_000L, 1L);
SnowflakeConfig config = SnowflakeConfig.defaultConfig(1_700_000_000_000L, 1L, 1L);
generator = new SnowflakeIdGenerator(config, new SystemClock());
}

Expand Down
2 changes: 1 addition & 1 deletion snowflake-core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<parent>
<groupId>com.fayupable</groupId>
<artifactId>snowflake-id-java</artifactId>
<version>1.1.1</version>
<version>1.2.0</version>
</parent>

<artifactId>snowflake-core</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
import com.fayupable.snowflake.port.IdGenerator;
import com.fayupable.snowflake.port.SnowflakeMetrics;

import java.lang.System.Logger;
import java.lang.System.Logger.Level;

import java.util.concurrent.atomic.AtomicLong;

/**
Expand All @@ -15,6 +18,9 @@
*/
public final class SnowflakeIdGenerator implements IdGenerator {

private static final Logger LOGGER = System.getLogger(SnowflakeIdGenerator.class.getName());
private static final long OVERFLOW_WARNING_THRESHOLD_MILLIS = 365L * 24 * 60 * 60 * 1000; // 1 year

private final SnowflakeConfig config;
private final ClockSource clock;
private final SnowflakeMetrics metrics;
Expand All @@ -28,6 +34,7 @@ public SnowflakeIdGenerator(SnowflakeConfig config, ClockSource clock, Snowflake
this.config = config;
this.clock = clock;
this.metrics = metrics;
warnIfEpochNearOverflow();
}

@Override
Expand All @@ -54,4 +61,17 @@ public long nextId() {
}
}
}

private void warnIfEpochNearOverflow() {
long maxRelativeMillis = (1L << config.timestampBits()) - 1;
long overflowInstant = config.epoch() + maxRelativeMillis;
long remainingMillis = overflowInstant - clock.millis();

if (remainingMillis < OVERFLOW_WARNING_THRESHOLD_MILLIS) {
LOGGER.log(Level.WARNING,
"Snowflake epoch will overflow its {0}-bit timestamp in less than a year (at {1}). "
+ "Consider migrating to a new epoch before that date.",
config.timestampBits(), java.time.Instant.ofEpochMilli(overflowInstant));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,48 +2,64 @@

/**
* Configuration for the Snowflake id layout.
* Bit layout (MSB to LSB): 1 unused sign bit | timestampBits | nodeBits | sequenceBits.
* Default: 41-bit timestamp (~69 years from epoch), 10-bit node (up to 1024 nodes),
* 12-bit sequence (up to 4096 ids per millisecond per node).
* Bit layout (MSB to LSB): 1 unused sign bit | timestampBits | datacenterBits | workerBits | sequenceBits.
* Default: 41-bit timestamp (~69 years from epoch), 5-bit datacenter (up to 32 datacenters),
* 5-bit worker (up to 32 workers per datacenter), 12-bit sequence (up to 4096 ids per millisecond per worker).
*/
public record SnowflakeConfig(
long epoch,
long nodeId,
long datacenterId,
long workerId,
int timestampBits,
int nodeBits,
int datacenterBits,
int workerBits,
int sequenceBits
) {

private static final int TOTAL_USABLE_BITS = 63;

public SnowflakeConfig {
if (timestampBits <= 0 || nodeBits < 0 || sequenceBits < 0) {
if (timestampBits <= 0 || datacenterBits < 0 || workerBits < 0 || sequenceBits < 0) {
throw new IllegalArgumentException("bit counts must be positive");
}
if (timestampBits + nodeBits + sequenceBits != TOTAL_USABLE_BITS) {
if (timestampBits + datacenterBits + workerBits + sequenceBits != TOTAL_USABLE_BITS) {
throw new IllegalArgumentException(
"timestampBits + nodeBits + sequenceBits must equal %d".formatted(TOTAL_USABLE_BITS));
"timestampBits + datacenterBits + workerBits + sequenceBits must equal %d"
.formatted(TOTAL_USABLE_BITS));
}
long maxNodeId = (1L << nodeBits) - 1;
if (nodeId < 0 || nodeId > maxNodeId) {
long maxDatacenterId = (1L << datacenterBits) - 1;
if (datacenterId < 0 || datacenterId > maxDatacenterId) {
throw new IllegalArgumentException(
"nodeId must be between 0 and %d".formatted(maxNodeId));
"datacenterId must be between 0 and %d".formatted(maxDatacenterId));
}
long maxWorkerId = (1L << workerBits) - 1;
if (workerId < 0 || workerId > maxWorkerId) {
throw new IllegalArgumentException(
"workerId must be between 0 and %d".formatted(maxWorkerId));
}
}

public static SnowflakeConfig defaultConfig(long epoch, long nodeId) {
return new SnowflakeConfig(epoch, nodeId, 41, 10, 12);
public static SnowflakeConfig defaultConfig(long epoch, long datacenterId, long workerId) {
return new SnowflakeConfig(epoch, datacenterId, workerId, 41, 5, 5, 12);
}

public long maxSequence() {
return (1L << sequenceBits) - 1;
}

public long nodeId() {
return (datacenterId << workerBits) | workerId;
}

public int nodeBits() {
return datacenterBits + workerBits;
}

public int nodeShift() {
return sequenceBits;
}

public int timestampShift() {
return sequenceBits + nodeBits;
return sequenceBits + nodeBits();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,24 @@
public interface IdGenerator {

long nextId();

/**
* Generates {@code count} ids in one call. The default implementation simply
* calls {@link #nextId()} in a loop; implementations that can do better under
* contention (e.g. advancing the internal sequence in a single CAS) may override
* this for a more efficient batch path.
*
* @param count how many ids to generate, must be positive
* @return an array of {@code count} ids, in generation order
*/
default long[] nextIds(int count) {
if (count <= 0) {
throw new IllegalArgumentException("count must be positive");
}
long[] ids = new long[count];
for (int i = 0; i < count; i++) {
ids[i] = nextId();
}
return ids;
}
}
Loading
Loading