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
47 changes: 47 additions & 0 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
name: Deploy Javadoc

on:
push:
branches: [ main ]

permissions:
contents: read
pages: write
id-token: write

concurrency:
group: pages
cancel-in-progress: true

jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up JDK 21
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '21'
cache: maven

- name: Generate aggregated Javadoc
run: mvn -B javadoc:aggregate

- name: Upload Pages artifact
uses: actions/upload-pages-artifact@v3
with:
path: target/site/apidocs

deploy:
needs: build
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
target/
dependency-reduced-pom.xml

.idea/
*.iws
Expand Down
38 changes: 38 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Changelog

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]

### Added
- `SnowflakeMetrics` output port in `snowflake-core`, with a `NoOpSnowflakeMetrics` default so the generator never
depends on a metrics backend directly.
- `MicrometerSnowflakeMetrics` adapter in `snowflake-jpa-spring`, publishing `snowflake.id.generated` and
`snowflake.id.spin_wait` counters through whatever `MeterRegistry` the application already has configured. No
metrics backend is required; if none is present, generator activity is simply not measured.
- Mutation testing via Pitest on `snowflake-core` (98% mutation score).
- `snowflake-benchmark` module with a light JMH benchmark (single fork, short warmup and measurement) measuring
single-thread and four-thread generator throughput, runnable standalone and not published as part of the library.
- Aggregated Javadoc site, rebuilt and deployed to GitHub Pages automatically on every push to `main`.

### Changed
- `SnowflakeIdGenerator` gained a new constructor overload accepting a `SnowflakeMetrics` instance. The existing
two-argument constructor is unchanged and still defaults to `NoOpSnowflakeMetrics`, so this is not a breaking
change.

## [1.0.0] - 2026-07-23

### Added
- `snowflake-core`: dependency-free, thread-safe Snowflake id generator. 64-bit ids composed of a sign bit, a
configurable-epoch timestamp, a node id, and a sequence counter, produced through a lock-free
compare-and-swap loop. Throws on system clock rollback instead of risking a duplicate id.
- `snowflake-jpa`: Hibernate integration via a `@SnowflakeGeneratedId` annotation (`@IdGeneratorType`-based) and
a static holder bridging Spring or manually configured `IdGenerator` instances into Hibernate's
reflection-instantiated `IdentifierGenerator`.
- `snowflake-jpa-spring`: Spring Boot auto-configuration registered through the standard
`META-INF/spring/...AutoConfiguration.imports` discovery mechanism. No `@Import` required; consumers only add
the dependency and set `snowflake.node-id`.
- Published on JitPack, verified by resolving the real artifact from a separate consumer project.
35 changes: 35 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
# Snowflake ID Java

[![](https://jitpack.io/v/Fayupable/snowflake-id-java.svg)](https://jitpack.io/#Fayupable/snowflake-id-java)

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](https://fayupable.github.io/snowflake-id-java/), rebuilt automatically from `main` on every push.

## Why Snowflake instead of UUID or database auto-increment

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.
Expand Down Expand Up @@ -182,6 +186,37 @@ This requires no additional annotation and no auto-configuration; `SnowflakeIdGe

`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.

## Metrics

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.

## Benchmarks

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:

```bash
cd snowflake-benchmark
mvn clean package
java -jar target/benchmarks.jar
```

## Configuration reference

| Property | Required | Description |
Expand Down
14 changes: 14 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,18 @@
<module>snowflake-jpa-spring</module>
</modules>

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<version>3.6.3</version>
<configuration>
<doclint>none</doclint>
<quiet>true</quiet>
</configuration>
</plugin>
</plugins>
</build>

</project>
77 changes: 77 additions & 0 deletions snowflake-benchmark/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

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

<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<jmh.version>1.37</jmh.version>
</properties>

<dependencies>
<dependency>
<groupId>com.fayupable</groupId>
<artifactId>snowflake-core</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-core</artifactId>
<version>${jmh.version}</version>
</dependency>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-generator-annprocess</artifactId>
<version>${jmh.version}</version>
<scope>provided</scope>
</dependency>
</dependencies>

<build>
<finalName>benchmarks</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-generator-annprocess</artifactId>
<version>${jmh.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.5.1</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>org.openjdk.jmh.Main</mainClass>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>

</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package com.fayupable.snowflake.benchmark;

import com.fayupable.snowflake.SnowflakeIdGenerator;
import com.fayupable.snowflake.SystemClock;
import com.fayupable.snowflake.config.SnowflakeConfig;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Fork;
import org.openjdk.jmh.annotations.Measurement;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.annotations.OutputTimeUnit;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.Setup;
import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.annotations.Threads;
import org.openjdk.jmh.annotations.Warmup;

import java.util.concurrent.TimeUnit;

/**
* Measures SnowflakeIdGenerator throughput. Settings below are deliberately
* light (short warmup, few iterations, single fork) so this runs in under a
* minute on a laptop without spinning every core at 100% for long stretches.
* This is meant to give a realistic, honest number, not to squeeze out the
* absolute peak throughput JMH could report with a heavier configuration.
* <p>
* How to run:
* <pre>
* mvn clean package
* java -jar target/benchmarks.jar
* </pre>
* <p>
* To run only this class if more benchmarks are added later:
* <pre>
* java -jar target/benchmarks.jar SnowflakeIdGeneratorBenchmark
* </pre>
*/
@Fork(1)
// One JVM fork only. JMH normally recommends multiple forks (3+) to smooth
// out JIT/JVM warmup noise between runs, but each extra fork means another
// full JVM startup plus warmup cycle, so this trades some statistical rigor
// for a much shorter total run time.
@Warmup(iterations = 2, time = 1, timeUnit = TimeUnit.SECONDS)
// Only 2 warmup iterations of 1 second each (2 seconds total) instead of
// JMH's default 5 iterations, just enough for the JIT to compile the hot
// path before measurement starts.
@Measurement(iterations = 3, time = 1, timeUnit = TimeUnit.SECONDS)
// 3 measured iterations of 1 second each (3 seconds total) instead of the
// default 5, enough to get a stable average without a long-running session.
@BenchmarkMode(Mode.Throughput)
@OutputTimeUnit(TimeUnit.SECONDS)
@State(Scope.Benchmark)
public class SnowflakeIdGeneratorBenchmark {

private SnowflakeIdGenerator generator;

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

@Benchmark
@Threads(1)
public long singleThreadThroughput() {
return generator.nextId();
}

@Benchmark
@Threads(4)
// 4 threads, not a higher count: enough to demonstrate the CAS loop
// handling real contention without turning a laptop into a space heater.
public long fourThreadThroughput() {
return generator.nextId();
}
}
20 changes: 20 additions & 0 deletions snowflake-core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,26 @@
<artifactId>maven-surefire-plugin</artifactId>
<version>3.2.5</version>
</plugin>
<plugin>
<groupId>org.pitest</groupId>
<artifactId>pitest-maven</artifactId>
<version>1.15.8</version>
<dependencies>
<dependency>
<groupId>org.pitest</groupId>
<artifactId>pitest-junit5-plugin</artifactId>
<version>1.2.1</version>
</dependency>
</dependencies>
<configuration>
<targetClasses>
<param>com.fayupable.snowflake.*</param>
</targetClasses>
<targetTests>
<param>com.fayupable.snowflake.*</param>
</targetTests>
</configuration>
</plugin>
</plugins>
</build>

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.fayupable.snowflake;

import com.fayupable.snowflake.port.SnowflakeMetrics;

/**
* Default {@link SnowflakeMetrics} implementation that discards every event.
* Used when no metrics backend is configured, so the generator never has to
* null-check its metrics dependency.
*/
public final class NoOpSnowflakeMetrics implements SnowflakeMetrics {

public static final NoOpSnowflakeMetrics INSTANCE = new NoOpSnowflakeMetrics();

private NoOpSnowflakeMetrics() {
}

@Override
public void recordIdGenerated() {
}

@Override
public void recordSpinWait() {
}
}
Loading
Loading