Appearance
ADR-005: ID Strategy
Status: Accepted Date: 2026-02-07
Context
Internal database IDs should not be exposed to users for security and usability reasons. The platform needs a user-friendly public identifier system, similar to Jira/YouTrack project-based IDs (e.g., PROJ-123).
Decision
- Internal IDs: UUIDv7 across all entities
- Public IDs: Project key + shared sequence number (e.g.,
VNO-42) - Sequence counter: DB-side atomic increment on the Project entity
Rationale
UUIDv7 for Internal IDs
- Globally unique like standard UUIDs
- Time-ordered prefix for chronological sorting and efficient B-tree indexing in PostgreSQL
- Native support in .NET 9+ via
Guid.CreateVersion7() - No collision risk across distributed systems
Project-Scoped Sequence Numbers
- User-friendly and memorable (
VNO-42vs a UUID) - Follows established patterns from project management tools (Jira, YouTrack, Linear)
- Each project has a
Key(e.g., "VNO") set at creation, unique across the platform - Each aggregate root gets a sequence number scoped to its project
Shared Sequence (not per-type)
- One counter per project for all entity types (characters, timelines, factions)
- Simplest to implement - no type prefixes, no multiple counters
- Adding new entity types doesn't require new counters
DB-Side Atomic Counter
NextSequenceNumbercolumn on the Project table- Incremented via
UPDATE ... SET "NextSequenceNumber" = "NextSequenceNumber" + 1 ... RETURNING - PostgreSQL row-level locking prevents race conditions under concurrent requests
- Application-side incrementing would require distributed locking or retry logic
Alternatives Considered
- Expose UUIDs directly - Technically simpler but not user-friendly. Long, unmemorable strings in URLs
- Sqids/Hashids - Short encoded strings (e.g.,
k5nZ3d). Compact but not meaningful to users - Slugs - Human-readable (e.g.,
vampire-new-orleans) but need uniqueness handling and rename logic - Per-type sequences - Separate counters with type prefixes (VNO-C1, VNO-T1). More complex, no real benefit
- Application-side counter - Race condition risk under concurrent requests without additional locking
Consequences
- Project
Keymust be validated (alphanumeric, unique, immutable after creation) - Only aggregate roots get sequence numbers (Section, Item, FactionMembership etc. are accessed through their parent)
- The public ID
VNO-42is composed at runtime, not stored as a single field - API routes use the key and sequence:
/api/projects/VNO/characters/42