Object Identifiers Guide
💡 Quick Reference
Enable automatic object ID generation with configurable strategies:
class Document < Familia::Horreum feature :object_identifier, generator: :uuid_v4 field :title, :content end
Overview
The Object Identifier feature provides automatic generation of unique identifiers for Familia objects. Instead of manually creating identifiers, you can configure different generation strategies that suit your application's needs - from globally unique UUIDs to high-entropy hexadecimal strings.
Why Use Object Identifiers?
Automatic Generation: No manual ID management - identifiers are generated lazily when first accessed.
Configurable Strategies: Choose from UUID v7 (timestamped), UUID v4 (random), hex (high-entropy), or custom generators.
Provenance Tracking: System tracks which generator created each ID for security and debugging.
Data Integrity: Preserves existing IDs during initialization - never overwrites loaded data.
Lookup Support: Automatic bidirectional mapping enables finding objects by their objid.
Generation Strategies
UUID v7 (Default)
UUID version 7 with embedded timestamp for natural sorting:
class User < Familia::Horreum
feature :object_identifier # Uses :uuid_v7 by default
field :email, :name
end
user = User.new(email: 'alice@example.com')
user.objid # => "01234567-89ab-7def-8000-123456789abc"
Characteristics:
- 128-bit identifier with embedded timestamp
- Naturally sortable by creation time
- Globally unique across distributed systems
- May leak timing information (consider for security-sensitive apps)
UUID v4 (Random)
UUID version 4 for legacy compatibility and maximum randomness:
class LegacyUser < Familia::Horreum
feature :object_identifier, generator: :uuid_v4
field :email, :username
end
user = LegacyUser.new(email: 'bob@example.com')
user.objid # => "f47ac10b-58cc-4372-a567-0e02b2c3d479"
Characteristics:
- 122-bit random identifier (6 bits for version/variant)
- No timing correlation for enhanced security
- Widely supported format
- Compatible with existing UUID systems
High-Entropy Hex
256-bit hexadecimal for security-critical applications:
class SecureDocument < Familia::Horreum
feature :object_identifier, generator: :hex
field :title, :classification
end
doc = SecureDocument.new(title: 'Classified Report')
doc.objid # => "a1b2c3d4e5f6789012345678901234567890abcdef..." (64 chars)
Characteristics:
- Maximum entropy (256 bits of randomness)
- No structure or timing information
- Compact representation without hyphens
- Ideal for security-sensitive applications
Custom Generator
Provide your own generation logic:
class TimestampedItem < Familia::Horreum
feature :object_identifier,
generator: -> { "item_#{Familia.now.to_i}_#{SecureRandom.hex(4)}" }
field :data, :category
end
item = TimestampedItem.new(data: 'test')
item.objid # => "item_1693857600_a1b2c3d4"
Custom Generator Requirements:
- Must be callable (Proc, lambda, or respond to
call) - Should return unique strings
- Avoid collision-prone patterns
Basic Usage
Lazy Generation
Object identifiers are only generated when first accessed:
user = User.new(email: 'test@example.com')
# No objid generated yet
user.instance_variable_get(:@objid) # => nil
# First access triggers generation
user.objid # => "01234567-89ab-7def-8000-123456789abc"
# Subsequent access returns cached value
user.objid # => "01234567-89ab-7def-8000-123456789abc" (same)
Preserved During Initialization
Existing IDs are never overwritten:
# Loading existing object from database
existing_user = User.new(
objid: '01234567-89ab-7def-8000-existing123',
email: 'existing@example.com'
)
# Existing ID is preserved, not regenerated
existing_user.objid # => "01234567-89ab-7def-8000-existing123"
Finding by Object ID
# Save user first
user = User.new(email: 'findme@example.com')
user.save
puts user.objid # => "01234567-89ab-7def-8000-123456789abc"
# Find by object identifier
found = User.find_by_objid('01234567-89ab-7def-8000-123456789abc')
found.email # => "findme@example.com"
# Returns nil if not found
missing = User.find_by_objid('nonexistent') # => nil
Long-form Methods
user.object_identifier # Same as user.objid
user.object_identifier = 'new' # Same as user.objid = 'new'
Provenance Tracking
The system tracks which generator created each objid:
# Generated by the system
user = User.new
user.objid # Triggers generation
user.objid_generator_used # => :uuid_v7
# Loaded from database (provenance inferred from format)
loaded = User.new(objid: 'f47ac10b-58cc-4372-a567-0e02b2c3d479')
loaded.objid_generator_used # => :uuid_v4 (inferred from format)
# Unknown format
custom = User.new(objid: 'custom-format-id')
custom.objid_generator_used # => nil (unknown provenance)
Why Provenance Matters:
- Security features like ExternalIdentifier require known provenance
- Debugging and auditing benefit from generator tracking
- Format validation can be performed based on expected generator
Lookup Management
Automatic Mapping
The feature maintains lookup tables for objid-to-primary-key mapping:
class Product < Familia::Horreum
feature :object_identifier
identifier_field :product_code # Different from objid
field :product_code, :name, :price
end
product = Product.new(product_code: 'PROD123', name: 'Widget')
product.save
# Lookup table maps objid to primary key
Product.objid_lookup.class # => Familia::DataType::HashKey
Product.objid_lookup[product.objid] # => "PROD123"
Cleanup on Destroy
product.destroy!
# Lookup entry is automatically cleaned up
Product.objid_lookup[product.objid] # => nil
Integration Patterns
Using objid as Primary Key
class SimpleModel < Familia::Horreum
feature :object_identifier
identifier_field :objid # Use objid as the primary key
field :data, :status
end
model = SimpleModel.new(data: 'test')
model.save
# No separate lookup needed - objid is the primary key
SimpleModel.find_by_objid(model.objid) == SimpleModel.find(model.objid) # => true
Combining with External Identifiers
class User < Familia::Horreum
feature :object_identifier # Provides internal objid
feature :external_identifier # Derives public extid from objid
field :email, :name
end
user = User.new(email: 'test@example.com')
user.save
user.objid # => "01234567-89ab-7def-8000-123456789abc" (internal)
user.extid # => "ext_abc123def456ghi789jkl" (public-facing)
API Design Patterns
# Internal operations use objid
def sync_user_data(objid)
user = User.find_by_objid(objid)
# ... sync logic
end
# Public APIs use external identifiers
class UsersController
def show
@user = User.find_by_extid(params[:id]) # Public ID
render json: {
id: @user.extid, # Hide internal objid
email: @user.email,
name: @user.name
}
end
end
Error Handling
Invalid Generator Configuration
# ❌ Invalid generator type
class BadModel < Familia::Horreum
feature :object_identifier, generator: :invalid_type
end
model = BadModel.new
model.objid
# => Familia::Problem: Invalid object identifier generator: :invalid_type
Custom Generator Errors
# ❌ Non-callable generator
class BadCustomModel < Familia::Horreum
feature :object_identifier, generator: "not_callable"
end
model = BadCustomModel.new
model.objid
# => Familia::Problem: Invalid object identifier generator: "not_callable"
Testing Strategies
Basic Generation Testing
class ObjectIdentifierTest < Minitest::Test
def test_lazy_generation
user = User.new(email: 'test@example.com')
# No objid until first access
assert_nil user.instance_variable_get(:@objid)
# First access generates ID
objid = user.objid
assert_match UUID_V7_PATTERN, objid
# Subsequent access returns same ID
assert_equal objid, user.objid
end
def test_preserves_existing_objid
existing_id = '01234567-89ab-7def-8000-existing123'
user = User.new(objid: existing_id, email: 'test@example.com')
assert_equal existing_id, user.objid
end
def test_find_by_objid
user = User.new(email: 'findme@example.com')
user.save
found = User.find_by_objid(user.objid)
assert_equal user.email, found.email
end
end
Generator Strategy Testing
class GeneratorTest < Minitest::Test
def test_uuid_v7_format
user = User.new # Uses default :uuid_v7
objid = user.objid
assert_match(/\A[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\z/, objid)
assert_equal :uuid_v7, user.objid_generator_used
end
def test_uuid_v4_format
user = LegacyUser.new # Uses :uuid_v4
objid = user.objid
assert_match(/\A[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\z/, objid)
assert_equal :uuid_v4, user.objid_generator_used
end
def test_hex_format
doc = SecureDocument.new # Uses :hex
objid = doc.objid
assert_match(/\A[0-9a-f]{64}\z/, objid) # 256 bits = 64 hex chars
assert_equal :hex, doc.objid_generator_used
end
def test_custom_generator
item = TimestampedItem.new
objid = item.objid
assert_match(/\Aitem_\d+_[0-9a-f]{8}\z/, objid)
end
end
Provenance Testing
class ProvenanceTest < Minitest::Test
def test_provenance_inference
# UUID v7 format
user = User.new(objid: '01234567-89ab-7def-8000-123456789abc')
assert_equal :uuid_v7, user.objid_generator_used
# UUID v4 format
user = User.new(objid: 'f47ac10b-58cc-4372-a567-0e02b2c3d479')
assert_equal :uuid_v4, user.objid_generator_used
# Hex format
user = User.new(objid: 'a1b2c3d4e5f6789012345678901234567890abcdef1234567890123456789012')
assert_equal :hex, user.objid_generator_used
# Unknown format
user = User.new(objid: 'custom-format-id')
assert_nil user.objid_generator_used
end
end
Performance Considerations
Lookup Table Growth
Each class maintains its own lookup table:
# Monitor table sizes
puts User.objid_lookup.length # Number of objid mappings
puts Product.objid_lookup.length # Separate table per class
# Consider cleanup for large datasets
User.objid_lookup.clear if rebuilding_data
Lazy Generation Benefits
# ✅ Efficient - objid only generated if needed
users = 1000.times.map { User.new(email: "user#{_1}@example.com") }
# No objids generated yet
# objids generated only on access
users.each { |user| puts user.objid } # Now generated
Memory Usage
# Each objid uses approximately:
# - UUID: 36 bytes (with hyphens)
# - Hex: 64 bytes (256-bit)
# - Custom: varies by format
# Plus lookup table overhead per class
Best Practices
Choose Appropriate Generator
# ✅ UUID v7 for distributed systems needing sortability
class DistributedEvent < Familia::Horreum
feature :object_identifier # Default :uuid_v7
end
# ✅ UUID v4 for legacy system compatibility
class LegacyRecord < Familia::Horreum
feature :object_identifier, generator: :uuid_v4
end
# ✅ Hex for maximum security
class CryptographicKey < Familia::Horreum
feature :object_identifier, generator: :hex
end
Consistent Strategy Per Application
# Define a base class with consistent strategy
class ApplicationRecord < Familia::Horreum
feature :object_identifier, generator: :uuid_v7
end
class User < ApplicationRecord
field :email, :name
end
class Order < ApplicationRecord
field :total, :status
end
Use objid for Internal Operations
# ✅ Internal APIs use objid
def process_user(user_objid)
user = User.find_by_objid(user_objid)
# ... processing logic
end
# ✅ Public APIs use external identifiers
def public_user_profile(user_extid)
user = User.find_by_extid(user_extid)
# ... public profile logic
end
Troubleshooting
objid Returns Nil
Check that object has been properly initialized:
user = User.new
# Missing feature configuration?
user.class.features_enabled.include?(:object_identifier) # => Should be true
# Access objid to trigger generation
user.objid # Should generate and return ID
Lookup Not Working
Ensure object was saved to populate lookup table:
user = User.new(email: 'test@example.com')
objid = user.objid # Generates objid but doesn't save lookup
User.find_by_objid(objid) # => nil (lookup not saved)
user.save # Saves lookup mapping
User.find_by_objid(objid) # => user (now works)
Generator Not Applied
Check feature options syntax:
# ❌ Wrong - hash instead of keyword arguments
class User < Familia::Horreum
feature :object_identifier, { generator: :uuid_v4 }
end
# ✅ Correct - keyword arguments
class User < Familia::Horreum
feature :object_identifier, generator: :uuid_v4
end
Custom Generator Errors
# ✅ Valid custom generator
class Model < Familia::Horreum
feature :object_identifier,
generator: -> { "prefix_#{SecureRandom.hex(8)}" }
end
# ❌ Invalid - not callable
class Model < Familia::Horreum
feature :object_identifier, generator: "not_a_proc"
end
See Also
- External Identifiers - Public-facing IDs derived from objid
- Feature System - Understanding Familia's feature architecture
- Relationships - Using object identifiers in relationships