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
24 changes: 12 additions & 12 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,16 +156,16 @@ is `systems.riddler:uxid`, and its module and package are
`systems.riddler.uxid`.

The tree on `main` today holds these agent instructions, the wurk manifest
(`.claude/wurk.json`), the tracker files under `.beads/`, the build
(`pom.xml`, `mise.toml` and `.mvn/jvm.config`), the module descriptor, the
package it exports, `LICENSE`, `CHANGELOG.md` with the fragment rule in
`changelog.d/README.md`, the vendored conformance corpus under `conformance/`
(with `.gitattributes`, which keeps its line endings as committed), and the two
workflows: CI (`.github/workflows/ci.yml`) and the release workflow
(`.github/workflows/release.yml`). The package holds time-based and
deterministic generation (`Uxid.generate` and `UxidOptions`), decoding
(`Uxid.decode`, `DecodeOptions` and `DecodeResult`), the validity checks
(`Uxid.isValid` and `Uxid.isDeterministic`) and the monotonic generator
(`.claude/wurk.json`), the tracker files under `.beads/`, the build (`pom.xml`,
`mise.toml` and `.mvn/jvm.config`), the module descriptor, the package it
exports, `LICENSE`, `README.md`, `docs/wire-format.md`, `CHANGELOG.md` with the
fragment rule in `changelog.d/README.md`, the vendored conformance corpus under
`conformance/` (with `.gitattributes`, which keeps its line endings as
committed), and the two workflows: CI (`.github/workflows/ci.yml`) and the
release workflow (`.github/workflows/release.yml`). The package holds
time-based and deterministic generation (`Uxid.generate` and `UxidOptions`),
decoding (`Uxid.decode`, `DecodeOptions` and `DecodeResult`), the validity
checks (`Uxid.isValid` and `Uxid.isDeterministic`) and the monotonic generator
(`Monotonic`, with its `DrawSource`). The sentences in this file about the
1.0.0 package describe the target, not the current tree: it is not yet
published.
Expand All @@ -181,8 +181,8 @@ published.
worked round here. An answer the corpus does not fix follows uxid-ex's spec
at that tag.

`README.md`, which a later change adds, is the user reference; `CHANGELOG.md`
records each release.
`README.md` is the user reference, and `docs/wire-format.md` points at the
reference spec at its tag; `CHANGELOG.md` records each release.

## Build & Test

Expand Down
212 changes: 212 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
# uxid

[![MIT License][badge_license_url]](LICENSE)
[![CI][badge_ci_url]](https://github.com/riddler/uxid-java/actions/workflows/ci.yml)

User eXperience focused IDentifiers for Java: prefixed, K-sortable,
Stripe-style ids a person can read, copy and route back to their resource. An
id looks like `usr_01hf7yat0004h1`: the prefix names the resource, and the
body carries a time and randomness in lower-case Crockford Base32. The ids
follow the wire format of the Elixir reference implementation,
[uxid-ex](https://github.com/riddler/uxid-ex), and the package is checked
against that implementation's conformance corpus, every case, with no skip
list.

## Why UXID

An auto-increment key leaks how many rows a table holds and invites
enumeration; a random UUID fixes both but is long, unordered and anonymous, so
in a log line, a URL or a support thread nobody can tell what it points at, and
a double-click selects only part of it. A UXID names its resource in its
prefix, selects whole on a double-click, reads aloud without ambiguity, sorts
by creation time so it indexes well, and is generated in the application with
no coordination between nodes. Its size is tunable for low-cardinality
resources, a monotonic generator keeps a burst within one millisecond unique
and in order, and a deterministic mode maps the same input to the same id.

Because the format is shared, an id this package makes is read by the other
implementations of the format, and the other way round.

## Install

Maven:

```xml
<dependency>
<groupId>systems.riddler</groupId>
<artifactId>uxid</artifactId>
<version>1.0.0</version>
</dependency>
```

Gradle (Kotlin or Groovy DSL, on `mavenCentral()`):

```kotlin
implementation("systems.riddler:uxid:1.0.0")
```

## Basic usage

```java
import systems.riddler.uxid.DecodeResult;
import systems.riddler.uxid.Monotonic;
import systems.riddler.uxid.Size;
import systems.riddler.uxid.Uxid;
import systems.riddler.uxid.UxidOptions;

public class Example {
public static void main(String[] args) {
// A prefix names the resource; the body is a time and randomness.
String id = Uxid.generate(UxidOptions.builder().prefix("usr").build());
System.out.println(id.startsWith("usr_")); // true

// Shrink the random part for low-cardinality resources.
// Sizes: XS, S, M, L and XL (the default).
String small = Uxid.generate(UxidOptions.builder().prefix("cus").size(Size.S).build());
System.out.println(small.length() == "cus_".length() + 14); // true

// Deterministic: the same prefix and input give the same id, every time.
String named =
Uxid.generate(UxidOptions.builder().prefix("usr").from("alice@example.com").build());
System.out.println(named); // usr_zcvt7epac0t1ebcsjfyf7cwz25
System.out.println(Uxid.isDeterministic(named)); // true

// Read an id back. A malformed id is a result naming a reason, never an exception.
DecodeResult result = Uxid.decode("cus_01hf7yat0004h1");
if (result instanceof DecodeResult.Decoded decoded) {
System.out.println(decoded.prefix()); // Optional[cus]
System.out.println(decoded.size()); // S
System.out.println(decoded.time()); // OptionalLong[1700000000000]
}
System.out.println(Uxid.isValid("cus_01hf7yat0004h1", "_", "cus")); // true
System.out.println(Uxid.decode("cus_")); // Invalid[reason=EMPTY]

// Monotonic: ids from one generator increase, also within one millisecond.
// You hold the generator; the package keeps no global state.
Monotonic ids = new Monotonic();
UxidOptions options = UxidOptions.builder().prefix("usr").size(Size.M).build();
String first = ids.generate(options);
String second = ids.generate(options);
System.out.println(first.compareTo(second) < 0); // true
}
}
```

A generation request the wire format refuses, such as a random width above 10
bytes, throws a `UxidException`, an `IllegalArgumentException`; switch on its
`code()`, an `ErrorCode`, or log its `wireName()`, a stable `snake_case` name,
rather than its message. A malformed argument of another kind, such as a null
prefix or a time outside the 48-bit range, throws the usual
`NullPointerException` or `IllegalArgumentException`.

`Uxid.generate` and `Monotonic.generate` read the system clock when they need a
time, and a `SecureRandom` when they need random bytes. `UxidOptions.Builder.time`
and `byteSource` replace them; `DecodeOptions.Builder.clock` replaces the
`java.time.Clock` that `Uxid.decode` reads for a compact time. `rand` takes
fixed random bytes for test vectors: an id built from them is exactly as
guessable as those bytes.

`Uxid.decode` never throws on malformed input. It answers a
`DecodeResult.Invalid` whose `InvalidReason` is the first rule the string
breaks, in the order `empty`, `invalid_length`, `invalid_character`. A compact
time carries only its low 40 bits, so a decoded compact time takes the rest
from the clock; the arithmetic is in `long`, so a clock within about 2^40
milliseconds of `Long.MIN_VALUE` wraps silently, which no real clock does.

`Uxid.isValid(id, delimiter, requiredPrefix)` and
`Uxid.isDeterministic(id, delimiter)` take the delimiter the id was generated
with, and read no clock. `isValid` answers whether the id decodes (a body of a
length the format defines, in Crockford Base32 characters) and, given a
required prefix, whether it carries exactly that prefix. `isDeterministic`
answers whether the id decodes and its body starts with the marker `z` or `Z`.
Both are stricter than the Elixir reference, whose `valid?` accepts any
alphabet body of at least 8 characters and whose `deterministic?` checks only
the first body character: here a malformed string is neither valid nor
deterministic. The released TypeScript, Rust and .NET ports read them the
same way, and the conformance corpus does not fix these two answers.

A deterministic id hashes the prefix and the input as UTF-8, so a prefix or an
input holding an unpaired surrogate, which has no UTF-8 encoding, is refused
with an `IllegalArgumentException`. A time-based id writes its prefix as given
and accepts one.

A `Monotonic` keeps the last time and random value for each prefix and random
width it is given, and the pair of time and random value increases with each
id (so does the id string, within one letter case, delimiter and time form).
The random value alone does not: when the clock reaches a later millisecond,
or the field overflows into the next one, the id carries a fresh seed, which
can be smaller than the last value. That state grows by one entry for each
distinct prefix and random width the generator is given and lives as long as
the generator.

## Compatibility

- **Java 17 and later.** The jars are built for `maven.compiler.release` 17.
CI proves the floor by running the tests on Temurin 17, 21 and 25.
- **No dependencies.** The package has no runtime dependency; JUnit 5 is a test
dependency only.
- **Module `systems.riddler.uxid`.** The jar declares the module and exports its
one package, `systems.riddler.uxid`. On the class path it is an ordinary
package of the same name.
- **Thread safety.** A `Monotonic` is safe for concurrent use: a lock inside
the instance orders its calls, so its ids increase in the order the calls
take the lock, whichever thread asks. Two generators never affect each
other, and ids from two generators carry no order between them within a
millisecond. `Uxid.generate`, `Uxid.decode` and the validity checks keep no
state and can be called from any thread.

## Documentation

- Look up
- The API reference is the Javadoc shipped with the package as a Javadoc
jar: every public type and member carries it, and a missing comment fails
the build.
- [The wire format](docs/wire-format.md): where the normative layout of an
id lives, and which version of it this package follows.
- [The changelog](CHANGELOG.md): what changed in each version.
- [The conformance corpus](conformance): the cases every implementation of
the wire format passes, vendored here from uxid-ex.
- Understand
- [The reference spec, at v2.10.0][guide_wire_format_url]: the normative
layout of an id - alphabet, bit packing, sizes, prefix grammar,
deterministic and monotonic bodies, and decoding.
- [Why a UXID has a prefix, a time and randomness][guide_why_url]: what each
part of an id is for, and what you trade when you tune it.
- [Why monotonic mode exists and what it costs][guide_monotonic_url]:
same-millisecond uniqueness and ordering, and the security tradeoff.
- [_Designing APIs for humans: object IDs_][stripe_ids_url]: the Stripe id
design many of UXID's choices follow.

## Development

After checking out the repository, run `mise install` for the JDK and Maven
`mise.toml` pins. Then `mise run gate:loop` runs the inner loop (compile and
tests), and `mise run gate` runs the full quality gate: the format check, the
compile with Error Prone and every javac lint, the tests with 90 percent line
coverage, the Javadoc and sources jars with the Javadoc lint on, and the
conformance corpus check. The tests include the conformance corpus, every case
with no skip list.

A release is published by CI, never from a checkout: pushing a tag `vX.Y.Z`
that names the version in `pom.xml` runs `.github/workflows/release.yml`, which
publishes the package to Maven Central.

## Contributing

Bug reports and pull requests are welcome on GitHub at
https://github.com/riddler/uxid-java.

## License

The package is available as open source under the terms of the
[MIT License](https://opensource.org/licenses/MIT).

<!-- LINKS -->
[guide_wire_format_url]: https://github.com/riddler/uxid-ex/blob/v2.10.0/guides/wire-format.md
[guide_why_url]: https://github.com/riddler/uxid-ex/blob/v2.10.0/guides/why-prefix-time-randomness.md
[guide_monotonic_url]: https://github.com/riddler/uxid-ex/blob/v2.10.0/guides/monotonic.md
[stripe_ids_url]: https://dev.to/stripe/designing-apis-for-humans-object-ids-3o5a

<!-- BADGES -->
[badge_license_url]: https://img.shields.io/badge/license-MIT-brightgreen.svg
[badge_ci_url]: https://github.com/riddler/uxid-java/actions/workflows/ci.yml/badge.svg
4 changes: 2 additions & 2 deletions changelog.d/uxjv-5tk.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
### Added

- `Monotonic` is a generator the caller holds whose ids for one prefix and random width increase strictly, also within one millisecond and when the clock moves back: `generate()` and `generate(UxidOptions)` keep the last time and random value per prefix and random width, step the random field by a value drawn uniformly from 1 to 2^(4w) within a millisecond, and move the time one millisecond forward with a fresh seed when the field overflows. A monotonic `xs` uses compact time unless the options set the time form.
- One `Monotonic` may be shared between threads: a lock inside the instance orders the calls, so its ids still increase strictly. Nothing is kept in a static field, and two generators never affect each other.
- `Monotonic` is a generator the caller holds whose ids for one prefix and random width increase, also within one millisecond and when the clock moves back (the pair of time and random value increases, and the id string with it within one letter case, delimiter and time form; a fresh seed after a later clock or an overflow can be a smaller random value on its own): `generate()` and `generate(UxidOptions)` keep the last time and random value per prefix and random width, step the random field by a value drawn uniformly from 1 to 2^(4w) within a millisecond, and move the time one millisecond forward with a fresh seed when the field overflows. A monotonic `xs` uses compact time unless the options set the time form.
- One `Monotonic` may be shared between threads: a lock inside the instance orders the calls, so its ids still increase in the order the calls take the lock. Its state grows by one entry for each distinct prefix and random width it is given and lives as long as the generator. Nothing is kept in a static field, and two generators never affect each other.
- `DrawSource` replaces a generator's seeds and steps to replay known draws; by default they come from each call's byte source, the step masked from its bytes with no modulo bias.
- `Monotonic.generate(UxidOptions)` refuses a `from` input with `from_with_monotonic` and fixed random bytes with `rand_with_monotonic`. A call that throws leaves the generator's state as it was.
15 changes: 15 additions & 0 deletions docs/wire-format.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Wire format

This package does not define the id format; it follows it. The normative
statement of what a UXID string is - the alphabet, the bit packing, the sizes,
the prefix grammar, deterministic and monotonic bodies, and what a decoder
reads back - is the reference specification in uxid-ex:

- [`guides/wire-format.md` at the tag `v2.10.0`](https://github.com/riddler/uxid-ex/blob/v2.10.0/guides/wire-format.md)

The tag is the one `conformance/SOURCE.json` names. That file also records
the commit and the hash of the vendored conformance corpus, the cases every
implementation of the format passes; the cases in `conformance/` are the
arbiter where the specification and a reference implementation could be read
two ways. When the corpus is bumped to a newer tag, `SOURCE.json` and this
pointer move together.
18 changes: 11 additions & 7 deletions src/main/java/systems/riddler/uxid/Monotonic.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,15 @@
import java.util.concurrent.locks.ReentrantLock;

/**
* A monotonic generator: for one prefix and random width, the time and random value of each id it
* makes are greater than the last one's, also when several fall in one millisecond and when the
* clock moves back, so among ids of one letter case, delimiter and time form each sorts after the
* last as a string. It follows the "Monotonic generation" rule of the wire format. A monotonic id
* is an ordinary time-based id of its size and length: nothing in it says how it was made, and
* {@link Uxid#decode(String)} reads it as any other.
* A monotonic generator: for one prefix and random width, the pair of time and random value of each
* id it makes is greater than the last one's (the time is at least the last time, and when it is
* equal the random value is greater), also when several fall in one millisecond and when the clock
* moves back, so among ids of one letter case, delimiter and time form each sorts after the last as
* a string. The random value alone is not monotonic: when the clock reaches a later millisecond, or
* the field overflows into the next one, the id carries a fresh seed, which can be smaller than the
* last value. It follows the "Monotonic generation" rule of the wire format. A monotonic id is an
* ordinary time-based id of its size and length: nothing in it says how it was made, and {@link
* Uxid#decode(String)} reads it as any other.
*
* <p>Each call takes a {@link UxidOptions} as {@link Uxid#generate(UxidOptions)} does, and the
* generator keeps its state per prefix and per random width: for each such pair it has seen, the
Expand Down Expand Up @@ -56,7 +59,8 @@
* whichever thread makes them. The draw source or byte source is called with the lock held. The
* state lives in the instance alone, with nothing kept in a static field, so two generators never
* affect each other, and ids from two generators carry no order between them within a millisecond.
* The state for each pair is kept for the generator's lifetime.
* The state for each pair is kept for the generator's lifetime, so it grows by one entry for each
* distinct prefix and random width the generator is given.
*
* <p><b>A refused call.</b> A call that throws leaves the generator as it was: the state of every
* pair stays what it was before the call, and the next call starts from it. Draws a refused call
Expand Down
Loading
Loading