Configure a Custom ID Generator
Evidence status: Java 1.525-RELEASE interfaces and runtime wiring were
verified from released JAR signatures/bytecode. Rust 4.1.1 interfaces were
verified from the published crate source, and the generated SQLite Golden Path
registered its provider ID-space generator. Custom distributed generators and
Java persistence remain unexecuted in this documentation baseline.
Problem
Replace the default persistence-ID strategy or add a business-facing number without editing generated entities or assigning IDs inconsistently in service code.
Choose the Correct ID Boundary
| Need | Java boundary | Rust boundary |
|---|---|---|
| Persistence primary key and entity references | InternalIdGenerationService | InternalIdGenerator |
| Human/business-facing number such as an order number | BusinessIdGenerator capability | No equivalent current contract verified here |
Do not use a business number as a persistence key merely because both are called “ID.” Define uniqueness, immutability, exposure, retry, and migration rules separately.
All persistence IDs produced by a custom generator must follow the
Portable Entity ID Range: persisted values
are 1..=2^63-1, zero is reserved, and JSON transports IDs as decimal strings.
Java Internal ID Path
The released Java interface is exact:
public interface InternalIdGenerationService {
Long generateId(UserContext ctx, Entity entity);
default long nextId(String typeName) { /* unsupported by default */ }
}
Register the implementation while assembling TeaQLRuntime:
TeaQLRuntime runtime = TeaQLRuntime.builder()
.metadata(metaFactory)
.idGenerationService(internalIdGenerationService)
.build();
When saveGraph(...) sees an entity whose ID is null, runtime
1.525-RELEASE calls generateId(ctx, entity) and assigns the result through
its internal entity contract before mutation execution. Application code
should not call internal setters.
Implement nextId(String) as well only when framework components without an
entity instance need type-name allocation. Its default implementation throws
UnsupportedOperationException.
Rust Internal ID Path
Rust 4.1.1 defines:
pub trait InternalIdGenerator: Send + Sync {
fn generate_id(&self, entity: &str) -> Result<u64, RuntimeError>;
}
Register a generator during context assembly:
use teaql_runtime::{SnowflakeIdGenerator, UserContext};
let ctx = UserContext::new()
.with_module(my_domain::module())
.with_internal_id_generator(SnowflakeIdGenerator::new(3, 1));
The two constructor values are worker and datacenter IDs. They must be unique
within the deployment topology. The built-in implementation accepts values
from 0 through 31 for each and handles sequence rollover and clock
regression according to its source implementation.
For a provider-backed sequence, use the generator emitted or supplied by that
provider. The verified SQLite-generated runtime creates
SqliteIdSpaceGenerator::from_executor(...) and registers it with
set_internal_id_generator(...). Keep provider construction in runtime
assembly rather than in business services.
Java Business ID Path
Java business IDs use a separate exact interface:
public interface BusinessIdGenerator {
String generateBusinessId(
UserContext ctx,
Entity entity,
EntityDescriptor entityDesc,
PropertyDescriptor propertyDesc);
}
UserContext.generateBusinessId(...) resolves this interface as a capability.
DefaultUserContext stores capabilities by class name, so the source-verified
registration shape is:
ctx.put(BusinessIdGenerator.class.getName(), businessIdGenerator);
Runtime 1.525-RELEASE does not automatically call the business generator from
saveGraph(...). The handwritten creation policy must resolve the generated
entity/property descriptors, call generateBusinessId(...), and assign the
result through the exact generated update method. Inspect that method and the
generated metadata rather than guessing either one.
The released InMemoryBusinessIdGenerator reads a metadata entry named
business_id_rule; its PREFIX, LENGTH format is implementation-specific and
its sequence state is process-local. Do not use it as a cluster-wide guarantee.
Verification
Before production adoption, test:
- IDs are unique under the expected thread/process/node concurrency.
- New entities receive IDs before provider mutation; updates retain IDs.
- Graph saves allocate every missing ID without collisions.
- Retries and transaction rollback have an explicitly accepted gap policy.
- Restart, failover, clock rollback, and worker-ID duplication are covered.
- Provider sequence tables are created/migrated under controlled permissions.
- Business IDs remain immutable and unique in their documented tenant/global scope.
- Logs and public APIs expose only the intended identifier.
Record implementation, runtime/provider/database versions, topology, test command, collision count, and restart/failover results.
Failure Modes
| Symptom | Most likely cause | Smallest credible fix |
|---|---|---|
| Java entity reaches insert with no ID | No internal service was registered or generator returned null. | Verify runtime assembly and fail fast on invalid generator output. |
nextId throws | Custom Java service implements only generateId. | Implement nextId if the selected framework component requires it. |
| Duplicate Rust IDs | Worker/datacenter assignment or custom generator is not globally unique. | Centralize topology allocation and run multi-node collision tests. |
| Business ID capability is missing | BusinessIdGenerator was not placed in the context. | Register it by capability class name during context creation. |
| Business number changes on update | Creation policy regenerates an existing value. | Generate only when the exact generated field is empty and test retries. |
| Generated setter is missing | Handwritten code guessed a method. | Inspect the generated entity and use its emitted update method. |