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 (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