Bezeichner is a small Go service that generates and maps identifiers, exposed via gRPC and HTTP.
- gRPC is the primary API surface.
- HTTP is implemented as an RPC gateway that routes by gRPC full method name (so both transports share the same contract).
The API contract lives in:
api/bezeichner/v1/service.proto
Distributed systems often need globally unique identifiers across multiple languages and runtimes. Bezeichner centralizes identifier generation so:
- you don't re-implement ID generation logic per service/language,
- you can standardize generator choices per domain/application,
- you can migrate/translate legacy identifiers via mapping.
The v1 service supports:
GenerateIdentifiers: generatecountidentifiers for a configuredapplicationListApplications: report configured application names and safe capability metadataMapIdentifiers: map identifiers for a configuredapplication, preserving input order
Responses 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]
GenerateIdentifiers and MapIdentifiers enforce request-size limits in the domain layer for basic DoS protection. By default, GenerateIdentifiers.count and the MapIdentifiers.ids list length are capped at 1000; larger requests fail with InvalidArgument.
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.yml
Tip
Use test/.config/server.yml as 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:
- a
name(the public application key you pass on requests), - a
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: ulidMapper configuration defines application-scoped identifier translations (useful for legacy migrations):
mapper:
applications:
- name: uuid
identifiers:
req1: resp1
req2: resp2Each 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 omit mapped. Consumers decide whether unmapped IDs should be ignored, reported, retried, or treated as failures.
The mapper block is optional at startup. If it is omitted, all MapIdentifiers requests fail with NotFound.
Request limits configure Bezeichner-owned per-request item caps:
limits:
generate_count: 1000
map_ids: 1000Both 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.yml 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 takeBoth values must be positive durations.
The service registers:
noopandonlinechecks.
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 devmake dev runs the server using air with the test config:
cd test && ../bezeichner server -config file:.config/server.yml
make 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/ListApplicationsGenerate 2 IDs for application uuid:
grpcurl -plaintext \
-d '{"application":"uuid","count":"2"}' \
localhost:12000 \
bezeichner.v1.Service/GenerateIdentifiersMap identifiers:
grpcurl -plaintext \
-d '{"application":"uuid","ids":["req1","req3"]}' \
localhost:12000 \
bezeichner.v1.Service/MapIdentifiersThe response keeps one result per input ID:
{
"meta": {
"requestId": "...",
"userAgent": "..."
},
"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/ListApplicationsGenerate identifiers:
curl -sS \
-X POST \
-H 'content-type: application/json' \
--data '{"application":"uuid","count":2}' \
http://localhost:11000/bezeichner.v1.Service/GenerateIdentifiersMap identifiers:
curl -sS \
-X POST \
-H 'content-type: application/json' \
--data '{"application":"uuid","ids":["req1","req2"]}' \
http://localhost:11000/bezeichner.v1.Service/MapIdentifiersHTTP 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; grpcurl uses the service/method form without the leading slash.
Bezeichner is typically deployed as a shared internal service. Depending on your scale and domain boundaries, you can:
- run a single global instance,
- shard by bounded context,
- run per region/cluster.
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-dockerPublished 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 snowflake generator 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 moving
latest Docker manifest. The release pipeline may update latest after
versioned images are published, but the versioned image tag is the deployment
contract.
Bezeichner builds on established ID generation libraries:
- https://github.com/alexfalkowski/go-service/tree/master/id
- https://github.com/sony/sonyflake
- https://go.jetify.com/typeid
Service scaffolding and transport/DI patterns:
The project follows:
- Go (see
go.modfor version) - Ruby with Bundler (used for end-to-end feature tests; no repository-pinned Ruby version is currently declared)
Initialize the bin/ submodule and install dependencies:
git submodule sync
git submodule update --init
make depGo unit/spec tests:
make specsLint checks:
make lintEnd-to-end feature tests:
make featuresmake 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 benchmarksThe 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-staleBefore pushing API contract changes, also run:
make proto-breakingFor 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 coveragemake 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.
