Bezeichner is a small Go service that generates and maps identifiers, exposed via gRPC and HTTP.
The API contract lives in:
api/bezeichner/v1/service.protoDistributed systems often need globally unique identifiers across multiple languages and runtimes. Bezeichner centralizes identifier generation so:
The v1 service supports:
GenerateIdentifiers: generate count identifiers for a configured applicationListApplications: report configured application names and safe capability metadataMapIdentifiers: map identifiers for a configured application, preserving input orderResponses contain generated identifiers, ordered mapping results, or
application discovery data, plus a meta map reserved for transport/service
metadata.
[!NOTE] HTTP is not a separate REST API. It is an RPC gateway over the same protobuf service contract used by gRPC. [!WARNING]
GenerateIdentifiersandMapIdentifiersenforce request-size limits in the domain layer for basic DoS protection. By default,GenerateIdentifiers.countand theMapIdentifiers.idslist length are capped at1000; larger requests fail withInvalidArgument.
Unknown generator applications, unknown mapper applications, unresolved generator kinds, and omitted mapper configuration fail with NotFound.
ListApplications is intended for client and operator discovery. It reports
configured generator application names and kinds, configured mapper application
names, supported generator kinds, and the effective request limits. It does not
expose mapper identifier entries, raw config, transport settings, telemetry
settings, or resolved secrets.
Bezeichner uses the go-service configuration conventions. A representative configuration used by development and feature tests is:
test/.config/server.yaml[!TIP] Use
test/.config/server.yamlas the copy-paste source for local examples. The usage examples below use application and mapping names from that file.
The snippets below document the Bezeichner-owned blocks. A runnable service
configuration also includes shared go-service keys, such as environment,
id, telemetry, and transport.http / transport.grpc addresses. The sample
config binds HTTP to tcp://:11000 and gRPC to tcp://:12000, which is what
the examples below assume.
Startup requires the top-level health and generator blocks, plus the shared
service configuration embedded by go-service. The mapper block is optional,
but omitting it makes every MapIdentifiers request fail with NotFound.
Generator configuration selects applications, each of which has:
name (the public application key you pass on requests),kind (the generator implementation to use).Application entries must have non-empty name and kind values, and
application names must be unique. Malformed application lists fail config
validation during startup.
Generated identifiers are prefixed with the application name followed by _.
For example, an application named uuid returns identifiers like uuid_<uuid>.
The typeid generator uses the application name as the native TypeID prefix.
For kind: typeid, choose application names that are valid TypeID prefixes:
lowercase ASCII letters and underscores only ([a-z_]), at most 63 characters,
and not starting or ending with _.
Supported built-in kinds (at time of writing):
uuid (application-prefixed UUIDv7 string)ksuid (application-prefixed KSUID string)ulid (application-prefixed ULID string)xid (application-prefixed XID string)snowflake (application-prefixed Sonyflake-based numeric ID as a decimal string)nanoid (application-prefixed NanoID string)typeid (TypeID string using the application name as the TypeID prefix)Example:
generator:
applications:
- name: uuid
kind: uuid
- name: ulid
kind: ulid
Mapper configuration defines application-scoped identifier translations (useful for legacy migrations):
mapper:
applications:
- name: uuid
identifiers:
req1: resp1
req2: resp2
Each configured mapper application must be non-null, have a non-empty name,
and use a name that is unique within the list. Invalid application lists fail
configuration validation during startup.
[!IMPORTANT] Mapping returns one result for each input ID in the same order. Known IDs include
mapped; missing IDs omitmapped. Consumers decide whether unmapped IDs should be ignored, reported, retried, or treated as failures. Themapperblock is optional at startup. If it is omitted, allMapIdentifiersrequests fail withNotFound.
Request limits configure Bezeichner-owned per-request item caps:
limits:
generate_count: 1000
map_ids: 1000
Both values are optional. Omitted or zero values use the default of 1000;
zero does not disable the operation. Set a positive lower value for smaller
deployments or stricter abuse controls. Requests above the effective limit fail
with InvalidArgument.
The representative test/.config/server.yaml overrides both limits to 2, so
the local transport examples below stay within that fixture’s limits.
Health checks are provided via go-health integration. Timing is configured as durations:
health:
duration: 1s # how often to run checks
timeout: 1s # max time a single check may take
Both values must be positive durations.
The service registers:
noop and online checks.healthz intentionally uses the online check. Bezeichner follows the shared
service convention that all services should report whether they can reach the
outside world if they need public egress later, even when the current generate
and map paths do not require outbound network access.
The service exposes these health observers:
| Observer | Check |
|---|---|
HTTP healthz |
online |
HTTP livez |
noop |
HTTP readyz |
noop |
| gRPC health | noop |
With the sample config, operational HTTP routes are service-prefixed:
| Endpoint | Local path |
|---|---|
| Health | /bezeichner/healthz |
| Liveness | /bezeichner/livez |
| Readiness | /bezeichner/readyz |
| Prometheus metrics | /bezeichner/metrics |
The gRPC health service name is bezeichner.v1.Service.
After setup:
make dev
make dev runs the server using air with the test config:
cd test && ../bezeichner server -config file:.config/server.yamlmake build # builds ./bezeichner (release)
make build-test # builds ./bezeichner test binary (features, race, coverage)
Below are examples for both transports. Exact request/response schemas are defined in api/bezeichner/v1/service.proto.
Assuming the service is listening on localhost:12000 (default in the sample config):
List configured applications and limits:
grpcurl -plaintext \
-d '{}' \
localhost:12000 \
bezeichner.v1.Service/ListApplications
Generate 2 IDs for application uuid:
grpcurl -plaintext \
-d '{"application":"uuid","count":"2"}' \
localhost:12000 \
bezeichner.v1.Service/GenerateIdentifiers
Map identifiers:
grpcurl -plaintext \
-d '{"application":"uuid","ids":["req1","req3"]}' \
localhost:12000 \
bezeichner.v1.Service/MapIdentifiers
The response keeps one result per input ID:
{
"meta": {
"request_id": "...",
"user_agent": "..."
},
"ids": [
{
"id": "req1",
"mapped": "resp1"
},
{
"id": "req3"
}
]
}
HTTP routes are keyed by the gRPC full method name. That means your HTTP client calls the same method identifiers as gRPC.
Assuming the service is listening on localhost:11000 (default in the sample config):
List configured applications and limits:
curl -sS \
-X POST \
-H 'content-type: application/json' \
--data '{}' \
http://localhost:11000/bezeichner.v1.Service/ListApplications
Generate identifiers:
curl -sS \
-X POST \
-H 'content-type: application/json' \
--data '{"application":"uuid","count":2}' \
http://localhost:11000/bezeichner.v1.Service/GenerateIdentifiers
Map identifiers:
curl -sS \
-X POST \
-H 'content-type: application/json' \
--data '{"application":"uuid","ids":["req1","req2"]}' \
http://localhost:11000/bezeichner.v1.Service/MapIdentifiers
HTTP errors use the same domain classification as gRPC, rendered as HTTP
statuses with safe text/error response bodies:
| gRPC/domain error | HTTP status | Common triggers |
|---|---|---|
InvalidArgument |
400 |
GenerateIdentifiers.count or MapIdentifiers.ids exceeds the configured request limit |
NotFound |
404 |
unknown generator application, unknown mapper application, unresolved generator kind, or omitted mapper config |
[!NOTE] The generated gRPC full method names include a leading slash, for example
/bezeichner.v1.Service/GenerateIdentifiers. In HTTP URLs, that slash is the path separator after the host;grpcurluses theservice/methodform without the leading slash.
Bezeichner is typically deployed as a shared internal service. Depending on your scale and domain boundaries, you can:
Non-master branch CI validates Docker images for both supported platforms. To reproduce that locally, run:
make platform=amd64 test-docker
make platform=arm64 test-docker
Published Docker images use the alexfalkowski/bezeichner repository. Pin a
released version tag such as alexfalkowski/bezeichner:<version> for
deployments.
Docker release, manifest publication, and deploy targets are CI-owned workflows
that require release artifacts plus DockerHub or GitHub credentials. Use the
local test-docker targets for image validation before pushing changes.
[!CAUTION] The
snowflakegenerator uses Sonyflake defaults. The intended deployment assumes normal Kubernetes pod networking where each concurrently running pod has a suitable private IPv4-derived machine ID. Re-evaluate that assumption for local multi-process deployments,hostNetwork, overlapping pod CIDRs, multi-cluster shared ID spaces, IPv6-only environments, or environments without private IPv4 addresses. If Sonyflake cannot derive a machine ID, a generation request panics; use another generator kind when the deployment cannot meet this assumption. [!IMPORTANT] Deployments should pin released version tags instead of depending on the movinglatestDocker manifest. The release pipeline may updatelatestafter versioned images are published, but the versioned image tag is the deployment contract.
Bezeichner builds on established ID generation libraries:
Service scaffolding and transport/DI patterns:
The project follows:
go.mod for version)Initialize the bin/ submodule and install dependencies:
git submodule sync
git submodule update --init
make dep
Go unit/spec tests:
make specs
Lint checks:
make lint
End-to-end feature tests:
make features
make features starts its own test server from test/nonnative.yml, so the
sample HTTP and gRPC ports (11000 and 12000) must be free. Harness and server
logs are written under test/reports/.
End-to-end benchmark scenarios:
make benchmarks
The protobuf contract in api/bezeichner/v1/service.proto owns the service API
schema. After changing it, regenerate and check the generated Go and Ruby stubs:
make proto-generate
make proto-stale
Before pushing API contract changes, also run:
make proto-breaking
For a broader local pass before pushing, run the repository-owned checks that mirror the main CI gates without publishing artifacts:
make lint
make proto-stale
make sec
make features
make benchmarks
make analyse
make coverage
make proto-breaking is part of the API-change workflow above because it checks
the protobuf contract against the remote master baseline. Codecov upload,
release, manifest, deploy, and push targets are CI or credential-gated.