Familia - Overview

[!NOTE] This document refers to Valkey throughout, but all examples and patterns work identically with Valkey/Redis. Familia supports both Valkey and Valkey/Redis as they share the same protocol and data structures.

Introduction

Familia is a Ruby ORM for Valkey (Redis) that provides object-oriented access to Valkey's native data structures. Unlike traditional ORMs that map objects to relational tables, Familia preserves Valkey's performance and flexibility while offering a familiar Ruby interface.

Why Familia?

  • Maps Ruby objects directly to Valkey's native data structures (strings, lists, sets, etc.)
  • Maintains Valkey's atomic operations and performance characteristics
  • Handles complex patterns (quantization, encryption, expiration, relationships) out of the box
  • Modular feature system for organizing functionality across complex projects

Core Concepts

What is a Horreum Class?

The Horreum class is Familia's foundation, representing Valkey-compatible objects. It's named after ancient Roman storehouses, reflecting its purpose as a structured data repository.

class Flower < Familia::Horreum
  identifier_field :token
  field :name
  field :color
  field :species
  list :owners
  set :tags
  zset :metrics
  hashkey :props
  string :counter
end

This pattern lets you work with Valkey data as Ruby objects while maintaining direct access to Valkey's native operations.

Flexible Identifiers

Horreum classes require identifiers to determine Valkey key names. You can define them in various ways:

class User < Familia::Horreum
  # Simple field-based identifier
  identifier_field :email

  # Computed identifier
  identifier_field ->(user) { "user:#{user.id}" }

  # Multi-field composite identifier
  identifier_field [:type, :email]

  field :email
  field :type
  field :id
end

This flexibility allows you to adapt to different Valkey key naming strategies while maintaining clean Ruby object interfaces.

Data Types Mapping

Familia provides direct mappings to Valkey's native data structures:

class Product < Familia::Horreum
  identifier_field :sku

  # Basic fields
  field :sku
  field :name
  field :price

  # String fields (for counters, simple values)
  string :view_count, default: '0'
  # Usage: view_count.increment (atomic increment)

  # JSON string fields (type-preserving storage)
  json_string :last_synced_at, default: 0.0
  # Usage: last_synced_at stores Float, retrieves Float (not String)

  # Lists (ordered, allows duplicates)
  list :categories
  # Usage: categories.push('fruit'), categories.pop

  # Sets (unordered, unique)
  set :tags
  # Usage: tags.add('organic'), tags.include?('organic')

  # Sorted sets (scored, ordered)
  zset :ratings
  # Usage: ratings.add(4.5, 'customer123'), ratings.rank('customer123')

  # Hash keys (dictionaries)
  hashkey :attributes
  # Usage: attributes['color'] = 'red', attributes.to_h
end

Each type maintains Valkey's native operations while providing Ruby-friendly interfaces.

DataType Naming Options

Familia provides both traditional concise names and explicit names for DataType methods to avoid namespace confusion with Ruby core types:

class Product < Familia::Horreum
  # Traditional naming (concise, safe for lowercase)
  string :view_count     # Creates StringKey instance
  list :categories       # Creates ListKey instance

  # Explicit naming (clear intent, namespace-safe)
  stringkey :description # Creates StringKey instance
  listkey :history       # Creates ListKey instance

  # JSON string (type-preserving alternative to StringKey)
  json_string :metadata  # Creates JsonStringKey instance
  json_stringkey :config # Creates JsonStringKey instance

  # Both work identically - choose based on preference
  set :tags              # UnsortedSet (unchanged)
  sorted_set :ratings    # SortedSet (unchanged)
  hashkey :attributes    # HashKey (unchanged)
end

# Access patterns are identical
product.view_count.class        # => Familia::StringKey
product.description.class       # => Familia::StringKey
product..class          # => Familia::JsonStringKey
product.categories.class        # => Familia::ListKey
product.history.class           # => Familia::ListKey

Key Benefits:

  • Developer Choice: Use concise (string, list) or explicit (stringkey, listkey) method names
  • Namespace Safety: No confusion with Ruby's core String, Array, Set, Hash types
  • Backward Compatibility: All existing code continues to work unchanged
  • Future-Proof: Clear naming convention for any new DataTypes

Generated Method Patterns

Familia automatically generates methods for all field and data type declarations:

class Product < Familia::Horreum
  # Field declarations generate three methods each
  field :name, :price
  # → name, name=, name!        # getter, setter, fast writer
  # → price, price=, price!     # getter, setter, fast writer

  # Data type declarations generate accessor, setter, and type check
  set :tags
  list :categories              # Traditional method
  listkey :search_history       # Explicit method (same functionality)
  hashkey :attributes
  # → tags, tags=, tags?        # accessor, setter, type check
  # → categories, categories=, categories?
  # → search_history, search_history=, search_history?
  # → attributes, attributes=, attributes?

  # Class-level data types
  class_set :global_tags
  class_counter :total_products
  # → Product.global_tags, Product.global_tags?
  # → Product.total_products, Product.total_products?
end

# Field method usage
product.name = "Ruby Gem"      # Set field value
product.name                   # Get field value
product.name!("New Name")      # Set and save immediately

# Data type method usage
product.tags                   # Get UnsortedSet instance
product.tags = new_set         # Replace UnsortedSet instance
product.tags?                  # => true (confirms it's an UnsortedSet)

# Method conflict resolution
field :type, on_conflict: :skip      # Skip if method exists
field :id, on_conflict: :overwrite   # Force overwrite existing method

Essential Features

Automatic Expiration

Set default TTL for objects that should expire:

class Session < Familia::Horreum
  feature :expiration
  default_expiration 30.minutes

  field :user_id
  field :token
end

# Auto-expires in 30 minutes
session = Session.create(user_id: '123', token: 'abc')

This is ideal for temporary data like authentication tokens or cache entries.

Safe Dumping for APIs

Control which fields are exposed when serializing objects using the clean DSL:

class User < Familia::Horreum
  feature :safe_dump

  # Use clean DSL methods instead of @safe_dump_fields
  safe_dump_field :id
  safe_dump_field :email
  safe_dump_field :full_name, ->(user) { "#{user.first_name} #{user.last_name}" }

  field :id, :email, :first_name, :last_name, :password_hash
end

user.safe_dump
# => {id: "123", email: "alice@example.com", full_name: "Alice Windows"}

The new DSL prevents accidental exposure of sensitive data and makes field definitions easier to organize in feature modules.

Time-based Quantization

Group time-based metrics into buckets:

class DailyMetric < Familia::Horreum
  feature :quantization
  string :counter, default_expiration: 1.day, quantize: [10.minutes, '%H:%M']
end

This automatically groups metrics into 10-minute intervals formatted as "HH:MM", ideal for analytics dashboards.

Key Benefits:

  • Time Bucketing: Group time-based data into configurable intervals (minutes, hours, days)
  • Reduced Storage: Aggregate similar data points to optimize memory usage
  • Analytics Ready: Perfect for dashboards and time-series data visualization

For advanced quantization strategies, value bucketing, geographic quantization, and performance patterns, see the Technical Reference.

Object Identifiers

Automatically generate unique identifiers for objects:

class Document < Familia::Horreum
  feature :object_identifier, generator: :uuid_v4
  field :title
  field :content
end

class Session < Familia::Horreum
  feature :object_identifier, generator: :hex
  field :user_id
  field :data
end

# Objects get automatic IDs
doc = Document.create(title: "My Doc")
doc.objid  # => "550e8400-e29b-41d4-a716-446655440000" (UUID)

session = Session.create(user_id: "123")
session.objid  # => "a1b2c3d4e5f6" (hex)

Available Generators:

  • :uuid_v4 - Standard UUID format for global uniqueness
  • :hex - Compact hexadecimal identifiers for internal use

For custom generators, collision detection, and advanced identifier patterns, see the Technical Reference.

Specialized Field Types

Familia provides specialized field types beyond basic fields:

class SecureModel < Familia::Horreum
  feature :encrypted_fields
  feature :transient_fields
  feature :object_identifier

  # Regular fields generate: name, name=, name!
  field :name, :email

  # Encrypted fields return ConcealedString instances
  encrypted_field :api_key, :credit_card
  # → api_key, api_key=, api_key!
  # → Values wrapped in ConcealedString for safety

  # Transient fields never persist to database
  transient_field :password, :session_token
  # → password, password= (no fast writer method)
  # → Values wrapped in RedactedString

  # Note: All transient field values are automatically wrapped in RedactedString
  # for security - they never persist to the database

  # Object identifier fields auto-generate unique IDs when using the feature
  # → objid, objid= (lazy generation, preserves initialization values)
end

# Usage examples
model = SecureModel.create(name: "Alice", api_key: "secret123")

# Encrypted field safety
model.api_key.class                      # => ConcealedString
model.api_key.to_s                       # => "[CONCEALED]" (safe for logs)
model.api_key.reveal                     # => "secret123" (actual value)

# Transient field behavior
model.password = "temp123"
model.save
model.reload
model.password                           # => nil (not persisted)

# Object identifier generation
model.objid                              # => Auto-generated UUID or hex
model.objid_generator_used               # => :uuid_v7 (provenance)

Field Type Features:

  • Method Conflict Resolution: Use on_conflict: :skip/:warn/:overwrite for existing methods
  • Fast Writer Control: Use fast_method: false to disable fast writers
  • Custom Method Names: Use as: :custom_name for different method names
  • Security by Default: Encrypted and transient fields prevent accidental exposure

External Identifiers

Integrate with external systems and validate identifiers:

class ExternalUser < Familia::Horreum
  feature :external_identifier

  field :external_id
  field :name
  field :sync_status

  # Validate external system identifiers
  def valid_external_id?
    external_id.present? && external_id.match?(/^ext_\d+$/)
  end
end

# Map external identifiers to internal objects
user = ExternalUser.create(external_id: "ext_12345", name: "Alice")

This feature helps maintain consistency when integrating with external APIs or legacy systems.

For advanced external identifier patterns, batch operations, and sync status management, see the Technical Reference.

Relationships

Manage complex object relationships with CRUD operations:


# Define relationships
class User < Familia::Horreum
  feature :relationships
  identifier_field :email
  field :email, :name

  participates_in Team, :teams
end

class Team < Familia::Horreum
  feature :relationships
  identifier_field :name
  field :name, :description
end

# Create relationships
alice = User.create(email: "alice@example.com", name: "Alice")
dev_team = Team.create(name: "developers", description: "Dev Team")

# Add relationships
alice.teams << dev_team

# Query relationships
alice.teams.to_a         # => [dev_team identifiers]
dev_team.in_user_teams?(alice)  # => true

# Remove relationships
alice.teams.delete(dev_team.identifier)

# Bulk operations
alice.teams.merge([qa_team.identifier, design_team.identifier])
alice.teams.clear

Generated Relationship Method Patterns

The relationships feature automatically generates comprehensive method patterns:

# Participation methods (on User class)
alice.in_team_teams?(dev_team)           # Check membership
alice.add_to_team_teams(dev_team, 1.0)   # Add with score
alice.remove_from_team_teams(dev_team)   # Remove membership
alice.score_in_team_teams(dev_team)      # Get participation score

# Target class methods (on Team class)
dev_team.teams                           # Collection getter (SortedSet)
dev_team.add_team(alice, 1.0)            # Add single member with score
dev_team.remove_team(alice)              # Remove single member
dev_team.add_teams([alice, bob])         # Bulk add members

# Indexing methods (if using indexed_by)
class User < Familia::Horreum
  indexed_by :email, :email_index, target: Team
end

# Generated index methods on User:
alice.add_to_team_email_index(dev_team)        # Add to index
alice.remove_from_team_email_index(dev_team)   # Remove from index
alice.update_in_team_email_index(dev_team, old_email)  # Update index

# Generated finder methods on Team:
Team.find_by_email("alice@example.com")        # Find user by email
Team.find_all_by_email(["alice@example.com"])  # Bulk find by emails
Team.email_index_for("alice@example.com")      # Direct index access

Key Features:

  • Bidirectional Links: Automatic reverse relationship management
  • Ruby-like Syntax: Clean customer.domains << domain collection operations
  • Automatic Indexing: Efficient O(1) lookups with automatic index maintenance
  • Performance Optimized: Bulk operations and efficient sorted set operations

For advanced relationship patterns, permission-encoded relationships, time-series tracking, and performance optimization, see the Technical Reference.

Transient Fields

Handle temporary or sensitive data that shouldn't persist:

class LoginAttempt < Familia::Horreum
  feature :transient_fields

  field :username
  field :timestamp
  transient_field :password
  redacted_field :security_token
end

attempt = LoginAttempt.new(
  username: "alice",
  password: "secret123",
  security_token: "sensitive_data"
)

# Transient fields aren't saved to Valkey
attempt.save
attempt.reload
attempt.password        # => nil (not persisted)

# Redacted fields return safe values
attempt.security_token.class    # => RedactedString
attempt.security_token.to_s     # => "[REDACTED]"
attempt.security_token.reveal   # => "sensitive_data"

Field Types:

  • Transient Fields: Exist only in memory, never persisted
  • Redacted Fields: Return [REDACTED] when converted to strings for logging safety

For RedactedString implementation details, single-use patterns, and security considerations, see the Technical Reference.

Permission Management

Permissions are not a separate subsystem with its own DSL. They are 8 bit flags packed into the fractional part of a sorted set score, alongside a timestamp in the integer part. A participates_in ... type: :sorted_set relationship is therefore also a permission grant, and one sorted set gives you both time-ordering and access control:

class Customer < Familia::Horreum
  feature :relationships
  identifier_field :custid
  field :custid
end

class Document < Familia::Horreum
  feature :relationships
  identifier_field :doc_id
  field :doc_id
  field :title

  participates_in Customer, :documents, type: :sorted_set
end

SE = Familia::Features::Relationships::ScoreEncoding

# Grant by encoding flags into the score you add with
customer.add_documents_instance(doc, SE.encode_score(Familia.now, [:read, :write]))

# Query the collection, filtered per-member by permission bits
customer.documents_with_permission(:write)   # => ["doc_1"]
customer.documents_with_permission(:delete)  # => []
customer.documents_with_permission           # defaults to :read

Both the target and the participant need feature :relationships. <collection>_with_permission is generated only for type: :sorted_set participations — a :set has no scores to carry permission bits — and it is an instance method on the target, not a class method.

Grant through add_<collection>_instance, which passes an explicit score through untouched and also records the reverse participation index. Writing to customer.documents directly with .add sets the score but skips that bookkeeping.

Reading and changing permission bits — these are ScoreEncoding module methods operating on a score, not on a user:

score = SE.encode_score(Familia.now, [:read, :write])

SE.decode_score(score)         # => {timestamp:, permissions: 5, permission_list: [:read, :write]}
SE.permission?(score, :write)  # => true  (all named flags present)
SE.add_permissions(score, :delete)
SE.remove_permissions(score, :write)
SE.category?(score, :content_editor)  # => true  (overlaps the category mask)
SE.permission_tier(score)             # => :content_editor  (single bucket)

Four symbol namespaces — do not mix them. This is the most common source of ArgumentError in this API:

Namespace Members Accepted by
PERMISSION_FLAGS :read :append :write :edit :configure :delete :transfer :admin every method that inspects or mutates bits
PERMISSION_ROLES :viewer :editor :moderator :admin encode_score/permission_encode only, and only as a bare symbol
PERMISSION_CATEGORIES :readable :content_editor :administrator :privileged :owner category?, filter_by_category, meets_category?
Tiers (return values) :administrator :content_editor :viewer :none returned by permission_tier; never an input

Passing a role where a flag is expected raises rather than silently expanding:

SE.encode_score(Familia.now, :editor)   # => role resolves to read|write|edit
SE.encode_score(Familia.now, [:editor]) # => ArgumentError -- array form takes flags only
customer.documents_with_permission(:editor)  # => ArgumentError -- flags only
customer.documents_with_permission(:edit)    # => works

:admin is deliberately in both namespaces and does not raise: as a role it is all eight bits, as a flag it is bit 7 alone.

category? and permission_tier also reuse the names :content_editor and :administrator while asking opposite questions. category? tests overlap and is non-exclusive; permission_tier assigns one bucket, checking most-privileged first. A :moderator score (read|write|edit|delete) answers true to every category — including :content_editor — yet its tier is :administrator, because :delete falls inside the administrator mask.

Key Features:

  • Granular Control: 8 independent flags, combinable in any pattern
  • Category-based Queries: Broad filtering via bitmask overlap
  • One Structure, Two Axes: Time ordering and permissions in a single score

Permission bits are not a contiguous score range, so they cannot be filtered server-side by ZRANGEBYSCORE. <collection>_with_permission fetches members with scores and tests bits per member; there is deliberately no category-based score range method. The fetch is paged internally in batch_size: chunks (default 500), so peak memory is O(batch_size) rather than O(N).

For large collections, bound the result or stream it:

customer.documents_with_permission(:write, limit: 50)              # first 50 matches
customer.documents_with_permission(:write, limit: 50, offset: 50)  # next page
customer.each_documents_with_permission(:read) { |id| ... }        # stream, O(1) memory

limit: caps how many matching members are returned and offset: skips the first N matching members (post-filter semantics), so the two paginate cleanly in deterministic score order. Deep offsets cost server CPU — each page is a ZRANGEBYSCORE ... LIMIT scan, so the server re-walks skipped pages. Because the result is assembled across multiple paged reads rather than one command, the Array form is no longer an atomic snapshot: concurrent writes to the collection between pages can skip or duplicate a member. each_<collection>_with_permission(min_permission = :read, batch_size: 100) streams via ZSCAN instead: O(1) retained memory in a single pass, returning an Enumerator when called without a block. The trade is that iteration is unordered and at-least-once — a member can be yielded twice while the server rehashes — so collect into a Set when uniqueness matters.

Advanced Patterns

Custom Methods and Logic

Add domain-specific behavior to your models:

class User < Familia::Horreum
  field :first_name
  field :last_name
  field :status

  def full_name
    "#{first_name} #{last_name}"
  end

  def active?
    status == 'active'
  end
end

These methods work alongside Familia's persistence layer, letting you build rich domain models.

Transactional Operations

Execute multiple Valkey commands atomically:

user.transaction do |conn|
  conn.set("user:#{user.id}:status", "active")
  conn.zadd("active_users", Familia.now.to_i, user.id)
end

Preserves data integrity for complex operations that require multiple Valkey commands.

Connection Management and Pooling

Configure connection pooling for production environments:

require 'connection_pool'

pools = {
  'redis://localhost:6379/0' => ConnectionPool::Wrapper.new(size: 10) { Redis.new(db: 0) }
}

Familia.connection_provider = lambda do |uri|
  pools.fetch(uri)
end

This ensures efficient Valkey connection usage in multi-threaded applications.

The provider hands back a ConnectionPool::Wrapper, which checks a connection out for the duration of each command and checks it back in afterwards. Do not return pool.with { |conn| conn }: with checks the connection back in when its block returns, so the client Familia receives is simultaneously available to other callers. See the provider contract.

Encrypted Fields

Protect sensitive data at rest with transparent encryption:

# First, configure encryption keys
Familia.configure do |config|
  config.encryption_keys = {
    v1: ENV['FAMILIA_ENCRYPTION_KEY'],
    v2: ENV['FAMILIA_ENCRYPTION_KEY_V2']
  }
  config.current_key_version = :v2
  config.encryption_personalization = 'MyApp-2024'  # Optional
  # If you later rotate the personalization, list prior values so old data still decrypts:
  # config.encryption_personalization_history = ['MyApp-2023']
end

# Validate configuration before use
Familia::Encryption.validate_configuration!

class SecureUser < Familia::Horreum
  feature :encrypted_fields

  identifier_field :id
  field :id, :email               # Plaintext fields
  encrypted_field :ssn            # Encrypted with field-specific key
  encrypted_field :credit_card    # Another encrypted field
  encrypted_field :notes, aad_fields: [:id, :email]  # With tamper protection
end

# Usage is transparent
user = SecureUser.new(
  id: 'user123',
  email: 'alice@example.com',
  ssn: '123-45-6789',
  credit_card: '4111-1111-1111-1111',
  notes: 'VIP customer'
)
user.save

# Access returns ConcealedString to help prevent accidentally logging or displaying the value
user.ssn.class                  # => ConcealedString
user.ssn.reveal                 # => "123-45-6789" (actual value)
user.ssn.to_s                   # => "[CONCEALED]" (safe for logging)

# Performance optimization for multiple operations
Familia::Encryption.with_request_cache do
  user.ssn = "new-ssn"
  user.credit_card = "new-card"
  user.save  # Reuses derived keys
end

# Key rotation support
user.re_encrypt_fields!  # Re-encrypt with current key version
user.encrypted_fields_status  # Check encryption status

Key Features:

  • Transparent Encryption: Fields encrypted/decrypted automatically
  • Security by Default: ConcealedString prevents accidental value exposure
  • Key Rotation: Seamless updates with backward compatibility
  • Multiple Algorithms: XChaCha20-Poly1305 (preferred) with AES-256-GCM fallback

For advanced encryption configuration, multiple providers, request caching, and key rotation procedures, see the Technical Reference.

Open-ended Serialization

Customize how objects are serialized to Valkey:

class JsonModel < Familia::Horreum
  def serialize_value
    JSON.generate(to_h)
  end

  def self.deserialize_value(data)
    new(**JSON.parse(data, symbolize_names: true))
  end
end

Enables integration with custom serialization formats beyond Familia's defaults.

Feature System Architecture

Familia's modular feature system helps organize functionality across complex projects:

class ComplexModel < Familia::Horreum
  # Enable features as needed
  feature :expiration           # TTL management
  feature :safe_dump           # API-safe serialization
  feature :relationships       # Object relationships
  feature :encrypted_fields    # Secure field storage
end

Key Benefits:

  • Per-Class Configuration: Each model can configure features independently
  • Automatic Loading: Use autoloader for large projects to organize features in separate files
  • Dependency Management: Features can depend on other features for complex functionality
  • Reusable Modules: Share common functionality across multiple models

For advanced feature organization patterns, autoloader configuration, and complex dependency management, see the Technical Reference.

Configuration

Basic Setup

# Simple connection
Familia.uri = 'redis://localhost:6379/0'

# Multiple databases
Familia.redis_config = {
  host: 'localhost',
  port: 6379,
  db: 0,
  timeout: 5
}

Production Configuration

Environment-based Setup:

# config/familia.rb
case ENV['RAILS_ENV'] || ENV['RACK_ENV']
when 'production'
  Familia.redis_config = {
    host: ENV['REDIS_HOST'],
    port: ENV['REDIS_PORT'],
    password: ENV['REDIS_PASSWORD'],
    ssl: true,
    timeout: 10,
    reconnect_attempts: 3
  }
when 'development'
  Familia.uri = 'redis://localhost:6379/0'
when 'test'
  Familia.uri = 'redis://localhost:2525/3'
end

Advanced Connection Pooling:

# Multi-database with connection pooling
require 'connection_pool'

primary_pool = ConnectionPool::Wrapper.new(size: 20) { Redis.new(url: ENV['PRIMARY_REDIS_URL']) }
cache_pool = ConnectionPool::Wrapper.new(size: 10) { Redis.new(url: ENV['CACHE_REDIS_URL']) }

Familia.connection_provider = lambda do |uri|
  case uri
  when /primary/
    primary_pool
  when /cache/
    cache_pool
  else
    Redis.new(url: uri)
  end
end

Encryption Setup

Development Keys:

# Generate base64-encoded 32-byte keys
Familia.configure do |config|
  config.encryption_keys = {
    v1: Base64.strict_encode64(SecureRandom.bytes(32)),
    v2: Base64.strict_encode64(SecureRandom.bytes(32))
  }
  config.current_key_version = :v2
  config.encryption_personalization = "#{Rails.application.class.name}-#{Rails.env}"
end

Production Security:

# Use secure key management
Familia.configure do |config|
  # Load keys from secure key management service
  config.encryption_keys = {
    v1: ENV['FAMILIA_ENCRYPTION_KEY_V1'],
    v2: ENV['FAMILIA_ENCRYPTION_KEY_V2'],
    v3: ENV['FAMILIA_ENCRYPTION_KEY_V3']  # For rotation
  }
  config.current_key_version = :v3
  config.encryption_personalization = ENV['FAMILIA_ENCRYPTION_CONTEXT']
  # After rotating the personalization, keep prior values for decryption
  # (mirrors config.encryption_hkdf_salt_history for the AES-GCM salt):
  # config.encryption_personalization_history = [ENV['FAMILIA_ENCRYPTION_CONTEXT_PREV']]

  # Validate configuration on startup
  Familia::Encryption.validate_configuration!
end

For production configuration patterns, advanced connection pooling, multi-database setup, and environment-based configuration, see the Technical Reference.

Common Patterns

Bulk Operations

# Load multiple objects
users = User.multiget('alice@example.com', 'bob@example.com')

# Batch operations
User.transaction do |conn|
  conn.set('user:alice:status', 'active')
  conn.zadd('active_users', Familia.now.to_i, 'alice')
end

Error Handling

begin
  user = User.load('nonexistent@example.com')
rescue Familia::Problem => e
  puts "User not found: #{e.message}"
end

# Safe loading
user = User.load('maybe@example.com') || User.new

Troubleshooting

Common Issues

Connection Errors:

# Check connection
Familia.connect_to_uri('redis://localhost:6379/0')

Missing Keys:

# Debug key names
user = User.new(email: 'test@example.com')
puts user.dbkey  # Shows the Valkey key that would be used

Encryption Issues:

# Validate encryption config
Familia::Encryption.validate_configuration!

# Check encryption status for specific fields
user.encrypted_fields_status
# => {ssn: {encrypted: true, algorithm: "xchacha20poly1305", cleared: false}, credit_card: {encrypted: false, value: nil}}

# Re-encrypt all fields with current key
user.re_encrypt_fields!

Relationship Issues:

# Debug relationship indexes
alice.relationships_debug_info
# => Shows internal relationship state and indexes

# Check relationship consistency
User.validate_relationship_indexes!  # Raises if inconsistent

Feature Conflicts:

# Check which features are enabled
MyModel.features_enabled
# => [:safe_dump, :encrypted_fields, :relationships]

# Check feature dependencies
MyModel.feature_dependencies(:relationships)
# => Shows required features

# Verify feature loading order
Familia.debug = true  # Shows feature loading sequence

Debug Mode

# Enable debug logging
Familia.debug = true

# Check what's in Valkey
Familia.dbclient.keys('*')  # List all keys (use carefully in production)

Testing

Test Configuration

# test_helper.rb or spec_helper.rb
require 'familia'

# Use separate test database
Familia.uri = 'redis://localhost:2525/3'

# Setup encryption for tests
test_keys = {
  v1: Base64.strict_encode64('a' * 32),
  v2: Base64.strict_encode64('b' * 32)
}
Familia.config.encryption_keys = test_keys
Familia.config.current_key_version = :v1

# Clear data between tests
def clear_redis
  Familia.dbclient.flushdb
end

# Feature-specific testing patterns
def setup_encryption_for_tests
  test_keys = {
    v1: Base64.strict_encode64('a' * 32),
    v2: Base64.strict_encode64('b' * 32)
  }
  Familia.configure do |config|
    config.encryption_keys = test_keys
    config.current_key_version = :v1
    config.encryption_personalization = 'TestApp-Test'
  end
end

def test_relationships_cleanup
  # Clean up relationship indexes
  Familia.dbclient.keys('*:relationships:*').each do |key|
    Familia.dbclient.del(key)
  end
end

Feature Testing Strategies

Testing with Encrypted Fields:

# test/models/secure_user_test.rb
require 'test_helper'

class SecureUserTest < Minitest::Test
  def setup
    setup_encryption_for_tests
    clear_redis
  end

  def test_encrypted_field_concealment
    user = SecureUser.create(
      id: 'test123',
      email: 'test@example.com',
      ssn: '123-45-6789'
    )

    assert_instance_of Familia::Features::EncryptedFields::ConcealedString, user.ssn
    assert_equal '[CONCEALED]', user.ssn.to_s
    assert_equal '123-45-6789', user.ssn.reveal
  end
end

Testing Relationships:

def test_relationship_bidirectionality
  alice = User.create(email: "alice@test.com")
  team = Team.create(name: "test-team")

  alice.add_membership(team)

  assert_includes alice.memberships, team
  assert_includes team.members, alice
end

Testing Transient Fields:

def test_transient_field_not_persisted
  attempt = LoginAttempt.new(
    username: "alice",
    password: "secret"
  )
  attempt.save

  reloaded = LoginAttempt.load(attempt.identifier)
  assert_nil reloaded.password  # Not persisted
  assert_equal "alice", reloaded.username  # Regular field persisted
end

For comprehensive testing patterns, advanced test helpers, and feature-specific testing strategies, see the Technical Reference.