Familia v2.0.0-pre Series Technical Reference

Familia is a Ruby ORM for Valkey/Redis providing object mapping, relationships, and advanced features like encryption, connection pooling, and permission systems. This technical reference covers the major classes, methods, and usage patterns introduced in the v2.0.0-pre series.

For conceptual understanding and getting started with Familia, see the Overview Guide. This document provides detailed implementation patterns and advanced configuration options.


Core Architecture

Base Classes

Familia::Horreum - Primary ORM Base Class

The main base class for Valkey/Redis-backed objects, similar to ActiveRecord models.

class User < Familia::Horreum
  # Basic field definitions
  field :name, :email, :created_at

  # Valkey/Redis data types as instance variables
  list :sessions      # Valkey/Redis list
  set :tags          # Valkey/Redis set
  zset :scores       # Valkey/Redis sorted set
  hashkey :settings  # Valkey/Redis hash
end

Key Methods:

  • save - Persist object to Valkey/Redis
  • save_if_not_exists - Conditional persistence, returns false if exists
  • save_if_not_exists! - Conditional persistence, raises RecordExistsError if exists
  • load - Load object from Valkey/Redis
  • exists? - Check if object exists in Valkey/Redis
  • destroy - Remove object from Valkey/Redis

Familia::DataType - Valkey/Redis Data Type Wrapper

Base class for Valkey/Redis data type implementations.

Registered Types:

  • String - Valkey/Redis strings
  • List - Valkey/Redis lists
  • Set - Valkey/Redis sets
  • SortedSet - Valkey/Redis sorted sets
  • HashKey - Valkey/Redis hashes
  • Counter - Atomic counters
  • Lock - Distributed locks

Feature System

Feature Architecture

Modular system for extending Horreum classes with reusable functionality.

class Customer < Familia::Horreum
  feature :expiration      # TTL management
  feature :safe_dump       # API-safe serialization
  feature :encrypted_fields # Field encryption
  feature :transient_fields # Non-persistent fields
  feature :relationships   # Object relationships

  field :name, :email
  encrypted_field :api_key
  transient_field :password
end

Built-in Features

1. Expiration Feature

TTL (Time To Live) management with cascading expiration.

class Session < Familia::Horreum
  feature :expiration

  field :user_id, :token
  default_expiration 24.hours

  # Cascade expiration to related objects
  cascade_expiration_to :user_activity
end

session = Session.new(user_id: 123, token: "abc123")
session.save
session.expire_in(1.hour)  # UnsortedSet custom expiration
session.ttl                # Check remaining time

2. SafeDump Feature

API-safe serialization excluding sensitive fields.

class User < Familia::Horreum
  feature :safe_dump

  field :name, :email, :password_hash
  safe_dump_field :name, :email  # Only these fields in safe_dump
end

user = User.new(name: "Alice", email: "alice@example.com", password_hash: "secret")
user.safe_dump  # => {"name" => "Alice", "email" => "alice@example.com"}

3. Encrypted Fields Feature

Transparent field-level encryption with multiple providers.

# Configuration
Familia.configure do |config|
  config.encryption_keys = {
    v1: ENV['FAMILIA_ENCRYPTION_KEY'],
    v2: ENV['FAMILIA_ENCRYPTION_KEY_V2']
  }
  config.current_key_version = :v2
end

class Vault < Familia::Horreum
  feature :encrypted_fields

  field :name                    # Plaintext
  encrypted_field :secret_key    # Encrypted with XChaCha20-Poly1305
  encrypted_field :api_token     # Field-specific key derivation
  encrypted_field :private_data  # Transparent access
end

vault = Vault.new(
  name: "Production Secrets",
  secret_key: "super-secret-123",
  api_token: "sk-1234567890"
)
vault.save

# Transparent access - automatically encrypted/decrypted
vault.secret_key  # => "super-secret-123" (decrypted on access)

Encryption Providers:

  • XChaCha20-Poly1305 (preferred) - Requires rbnacl gem
  • AES-256-GCM (fallback) - Uses OpenSSL, no dependencies

Advanced Security Implementation:

# Multiple encryption providers with fallback
class CriticalData < Familia::Horreum
  feature :encrypted_fields

  encrypted_field :credit_card
  encrypted_field :ssn
  encrypted_field :notes
end

# Key versioning and rotation
Familia.configure do |config|
  config.encryption_keys = {
    v1: ENV['OLD_KEY'],      # Legacy key
    v2: ENV['CURRENT_KEY'],  # Current key
    v3: ENV['NEW_KEY']       # New key for rotation
  }
  config.current_key_version = :v2
  config.encryption_personalization = 'MyApp-2024'  # XChaCha20 BLAKE2b domain separation (<= 16 bytes)
  config.encryption_hkdf_salt = 'MyApp-2024'        # AES-GCM HKDF salt domain separation (any length)
  # When rotating either value, keep prior values for backward-compatible decryption:
  # config.encryption_personalization_history = ['MyApp-2023']
  # config.encryption_hkdf_salt_history = ['MyApp-2023']
end

# Operations with encrypted fields
record = CriticalData.new(
  credit_card: "4111-1111-1111-1234",
  ssn: "123-45-6789",
  notes: "Customer notes"
)
record.save

# Verify encryption status
puts "Record #{record.identifier}: #{status}"
# => {credit_card: {encrypted: true, algorithm: "xchacha20poly1305", cleared: false}}

ConcealedString Security Features:

# Automatic protection against accidental exposure
user = CriticalData.load("user123")

# Safe operations
user.credit_card.class           # => ConcealedString
user.credit_card.to_s           # => "[CONCEALED]"
user.credit_card.inspect        # => "[CONCEALED]"
user.credit_card.to_json        # => "\"[CONCEALED]\""

# Explicit access when needed
actual_card = user.credit_card.reveal  # => "4111-1111-1111-1234"

# Logging safety
Rails.logger.info "Processing card: #{user.credit_card}"
# => "Processing card: [CONCEALED]" (safe for logs)

# JSON serialization safety
user_data = user.to_json
# All encrypted fields show as "[CONCEALED]" in JSON

4. Transient Fields Feature

Non-persistent fields with memory-safe handling.

class LoginForm < Familia::Horreum
  feature :transient_fields

  field :username              # Persistent
  transient_field :password    # Never stored in Redis
  transient_field :csrf_token  # Runtime only
end

form = LoginForm.new(username: "alice", password: "secret123")
form.save  # Only saves 'username', password is transient

form.password.class  # => Familia::Features::TransientFields::RedactedString
form.password.to_s   # => "[REDACTED]" (safe for logging)
form.password.reveal # => "secret123" (explicit access)

RedactedString - Security wrapper preventing accidental exposure:

  • to_s returns "[REDACTED]"
  • inspect returns "[REDACTED]"
  • reveal method for explicit access
  • Safe for logging and serialization

5. Relationships Feature

Comprehensive object relationship system with automatic management, clean Ruby-idiomatic syntax, and simplified method generation.

class Customer < Familia::Horreum
  feature :relationships

  identifier_field :custid
  field :custid, :name, :email

  # Collections for storing related object IDs
  set :domains           # Simple set
  zset :activity   # Scored/sorted collection

  # Class-level indexed lookups (automatically managed on save/destroy)
  class_indexed_by :email, :email_lookup

  # Class-level tracking with scoring (automatically managed on save/destroy)
  class_participates_in :all_customers, score: :created_at
end

class Domain < Familia::Horreum
  feature :relationships

  identifier_field :domain_id
  field :domain_id, :name, :status

  # Bidirectional membership with clean << operator support
  participates_in Customer, :domains

  # Relationship-scoped indexing (per-customer domain lookups)
  indexed_by :name, :domain_index, target: Customer

  # Class-level conditional tracking with lambda scoring
  class_participates_in :active_domains,
                   score: -> { status == 'active' ? Familia.now.to_i : 0 }
end

Relationship Operations with Automatic Management:

# Create and save objects (automatic indexing and tracking)
customer = Customer.new(custid: "cust123", name: "Acme Corp", email: "admin@acme.com")
customer.save  # Automatically adds to email_lookup and all_customers

domain = Domain.new(domain_id: "dom456", name: "acme.com", status: "active")
domain.save    # Automatically adds to active_domains

# Establish relationships using clean Ruby-like << operator
customer.domains << domain  # Clean, Ruby-idiomatic collection syntax

# Query relationships
domain.in_customer_domains?(customer.custid)  # => true
customer.domains.member?(domain.identifier)   # => true

# O(1) indexed lookups (automatic management - no manual calls needed)
found_id = Customer.email_lookup.get("admin@acme.com")
found_customer = Customer.find_by_email("admin@acme.com")  # Convenience method

# Relationship-scoped lookups
customer_domain = customer.find_by_name("acme.com")  # Find within customer

# Class-level tracking queries
recent_customers = Customer.all_customers.range_by_score(
  (Familia.now - 24.hours).to_i, '+inf'
)
active_domains = Domain.active_domains.members

Key Features:

  • Automatic Management: Objects automatically added/removed from class-level collections on save/destroy
  • Clean Syntax: Collections support Ruby-like customer.domains << domain syntax
  • Simplified Methods: No complex "global" terminology - uses clear class_ prefixes
  • Performance: O(1) hash lookups and efficient sorted set operations
  • Flexibility: Supports class-level and relationship-scoped indexing patterns

6. Object Identifier Feature

Automatic generation of unique object identifiers with configurable strategies.

Default generator is :uuid_v7 (UUID version 7 with embedded timestamp).

class Document < Familia::Horreum
  feature :object_identifier  # Uses default :uuid_v7

  field :title, :content, :created_at
end

class Session < Familia::Horreum
  feature :object_identifier, generator: :hex, length: 16

  field :user_id, :data, :expires_at
end

class ApiKey < Familia::Horreum
  feature :object_identifier, generator: :custom

  field :name, :permissions, :created_at

  # Custom generator implementation
  def self.generate_identifier
    "ak_#{SecureRandom.alphanumeric(32)}"
  end
end

Generator Types:

  • :uuid_v7 - UUID version 7 with embedded timestamp (default, 36 characters)
  • :uuid_v4 - Standard UUID v4 format (36 characters)
  • :hex - High-entropy hexadecimal strings (256-bit via SecureIdentifier)
  • Proc/Lambda - Custom generation logic provided as a callable

Technical Implementation:

# Auto-generated on object creation
doc = Document.create(title: "My Document")
doc.objid  # => "01234567-89ab-7def-8000-123456789abc"  # UUID v7 format

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

# Custom identifier with proc
api_key = ApiKey.create(name: "Production API")
api_key.objid  # => "ak_Xy9ZaBcD3fG8HjKlMnOpQrStUvWxYz12"

# Feature options per-class isolation
Document.feature_options(:object_identifier)
#=> {generator: :uuid_v7}

7. External Identifier Feature

Derives deterministic external identifiers from object identifiers.

class ExternalUser < Familia::Horreum
  feature :external_identifier

  identifier_field :id
  field :id, :name

  # External ID is automatically derived from objid
  # Format: 'ext_' + base36(truncated_hash(objid))
end

class APIKey < Familia::Horreum
  feature :external_identifier, format: 'api-%{id}'

  field :name, :permissions
end

External ID Management:

# External ID is deterministically derived from objid
user = ExternalUser.new(name: "John Doe")
user.save
user.objid  # => "01234567-89ab-7def-8000-123456789abc"
user.extid  # => "ext_abc123def456ghi789"  # Deterministic from objid

# Same objid always produces same extid
user2 = ExternalUser.new(objid: user.objid, name: "John Doe")
user2.extid  # => "ext_abc123def456ghi789"  # Identical

# Custom format with APIKey
key = APIKey.new(name: "Production")
key.extid  # => "api-xyz789abc123"

8. Quantization Feature

Time-based data bucketing for analytics and caching.

class DailyMetric < Familia::Horreum
  feature :quantization

  identifier_field :metric_key
  field :metric_key, :bucket_timestamp, :value_count, :sum_value

  # Example: Basic time quantization
  string :counter, default_expiration: 1.day, quantize: [10.minutes, '%H:%M']
end

Basic Usage:

# Create metric with quantized timestamp
metric = DailyMetric.new(metric_key: "page_views")
metric.counter.increment

# Time-based data grouping occurs automatically
# All data within the same 10-minute window shares the same key

Key Benefits:

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

Advanced Feature System Architecture

Feature Autoloader for Project Organization

Automatically load features from directory structure.

# app/models/customer.rb - Main model file
class Customer < Familia::Horreum
  include Familia::Features::Autoloader
  # Automatically loads all .rb files from app/models/customer/*.rb

  # Core model definition
  identifier_field :custid
  field :custid, :name, :email, :created_at
end

# app/models/customer/notifications.rb
class Customer < Familia::Horreum
  def send_welcome_email
    NotificationService.send_template(
      email: email,
      template: 'customer_welcome',
      variables: { name: name, custid: custid }
    )
  end

  def send_invoice_reminder(invoice_id)
    NotificationService.send_template(
      email: email,
      template: 'invoice_reminder',
      variables: { invoice_id: invoice_id }
    )
  end
end

# app/models/customer/analytics.rb
class Customer < Familia::Horreum
  # Analytics methods added to Customer
  def track_activity(activity_type,  = {})
    activity_data = {
      custid: custid,
      activity: activity_type,
      timestamp: Familia.now.to_i,
      metadata: 
    }

    # Store in customer's activity stream
    activities.unshift(activity_data.to_json)
    activities.trim(0, 999)  # Keep last 1000 activities
  end

  def recent_activities(limit = 10)
    activities.range(0, limit - 1).map { |json| JSON.parse(json) }
  end
end

Feature Dependencies

Features can declare dependencies that are automatically resolved.

# External identifier depends on object_identifier
class User < Familia::Horreum
  feature :external_identifier  # Automatically includes :object_identifier
  field :name
end

# Feature dependency resolution
Familia::Base.add_feature ExternalIdentifier, :external_identifier, depends_on: [:object_identifier]

# When external_identifier is included, object_identifier is automatically loaded first
end

Per-Class Feature Registration

Register custom features for specific model classes with ancestry chain lookup.

# Define a custom feature module
module CustomerAnalytics
  def track_purchase(amount)
    purchases.increment(amount)
  end
end

# Register feature only for Customer and its subclasses
Customer.add_feature CustomerAnalytics, :customer_analytics

class Customer < Familia::Horreum
  feature :customer_analytics  # Available via Customer's registry
end

class PremiumCustomer < Customer
  feature :customer_analytics  # Inherited via ancestry chain
end

class Session < Familia::Horreum
  # feature :customer_analytics  # Not available - would raise error
end

Benefits:

  • Features can have the same name across different model hierarchies
  • Natural inheritance through Ruby's class hierarchy
  • Better namespace management for large applications

Per-Class Feature Configuration Isolation

Each class maintains independent feature options.

class PrimaryCache < Familia::Horreum
  feature :expiration
  feature :object_identifier, generator: :uuid_v7

  field :cache_key, :value, :hit_count
  default_expiration 24.hours
end

class SecondaryCache < Familia::Horreum
  feature :expiration
  feature :object_identifier, generator: :hex  # Different generator

  field :cache_key, :backup_value, :backup_timestamp
  default_expiration 7.days
end

# Feature options are completely isolated
PrimaryCache.feature_options(:object_identifier)
#=> {generator: :uuid_v7}

SecondaryCache.feature_options(:object_identifier)
#=> {generator: :hex}

Runtime Feature Checking

Check which features are enabled on a class.

class SecureModel < Familia::Horreum
  feature :expiration
  feature :encrypted_fields
  feature :safe_dump

  field :name, :status
  encrypted_field :api_key
  safe_dump_field :name
end

# Check enabled features
SecureModel.features_enabled
#=> [:expiration, :encrypted_fields, :safe_dump]

# Check feature options
SecureModel.feature_options(:encrypted_fields)
#=> {} # Default options

# Each class tracks its own features
class BasicModel < Familia::Horreum
  feature :expiration
  field :name
end

BasicModel.features_enabled
#=> [:expiration]

Connection Management

Connection Provider Pattern

Flexible connection pooling with provider-based architecture.

# Basic Valkey/Redis connection
Familia.configure do |config|
  config.uri = "redis://localhost:6379/0"
end

# Custom connection provider with pooling
require 'connection_pool'

POOLS = {}
POOLS_MUTEX = Mutex.new

Familia.connection_provider = lambda do |uri|
  # Provider MUST return a connection already on the correct database.
  # Build each pool ONCE and hand back a wrapper (see the contract below).
  POOLS_MUTEX.synchronize do
    POOLS[uri] ||= ConnectionPool::Wrapper.new(size: 10, timeout: 5) do
      Redis.new(url: uri)
    end
  end
end

Provider contract

Familia calls the provider every time it needs a client and never hands the connection back — there is no check-in hook. The provider must therefore return a client that stays exclusively usable by the caller without a matching check-in, and it must not build a new pool per call.

Do not do this:

Familia.connection_provider = lambda do |uri|
  pool = ConnectionPool.new { Redis.new(url: uri) }  # a new pool every call
  pool.with { |conn| conn }                          # checked back in already
end

ConnectionPool#with checks the connection back in the moment its block returns, so the client handed to Familia is simultaneously available to every other checkout — the pool reports it as free while Familia is still using it:

pool = ConnectionPool.new(size: 5) { Redis.new }
conn = pool.with { |c| c }
pool.available  # => 5 of 5 — nothing is checked out, yet `conn` is in use

Under concurrency two threads then issue commands on one connection, and per-connection state (MULTI, WATCH, SELECT, SUBSCRIBE) crosses callers.

Use ConnectionPool::Wrapper (alias ConnectionPool.wrap) instead. It proxies each command through pool.with, so a connection is checked out for exactly the duration of that command and checked back in afterwards. ConnectionPool's checkout is reentrant per fiber, so a MULTI/EXEC, a pipeline, or a WATCH-guarded transaction still runs entirely on one connection.

Or check out explicitly when a request should pin one connection for its whole lifetime — then the checkout is yours to release at the boundary:

Familia.connection_provider = ->(uri) { POOLS.fetch(uri).checkout }

# Rack middleware / job wrapper
def call(env)
  @app.call(env)
ensure
  POOLS.each_value { |pool| pool.checkin(force: true) }
end

Without that ensure, a bare pool.checkout in the provider leaks a connection on every call and the pool exhausts.

Multi-Database Support

Configure different logical databases for different models.

class User < Familia::Horreum
  logical_database 0  # Use database 0
  field :name, :email
end

class Session < Familia::Horreum
  logical_database 1  # Use database 1
  field :user_id, :token
end

class Cache < Familia::Horreum
  logical_database 2  # Use database 2
  field :key, :value
end

Testing and Debugging

Database Command Logging

Monitor all Redis commands with DatabaseLogger middleware.

# Enable command logging (middleware registered automatically)
Familia.enable_database_logging = true

# Capture commands in tests
commands = DatabaseLogger.capture_commands do
  user = User.create(name: "Test User")
  user.save
end

puts commands.first.command  # => "SET user:123 {...}"
puts commands.first.μs       # => 567 (microseconds)

# Enable sampling for production
DatabaseLogger.sample_rate = 0.01  # Log 1% of commands

# Structured logging format
DatabaseLogger.structured_logging = true
# => "Redis command cmd=SET args=[key, value] duration_ms=0.42 db=0"

Debug Mode

Enable comprehensive debugging output.

# Via environment variable
ENV['FAMILIA_DEBUG'] = '1'
ENV['FAMILIA_TRACE'] = '1'

# Via configuration
Familia.configure do |config|
  config.debug = true
end

# Check database contents
Familia.dbclient.keys('user:*')

# Trace specific operations
Familia.trace :LOAD, redis_client, "user:123", "from cache"

Connection Chain Debugging

Understand connection resolution order.

# Connection resolution order:
# 1. Instance @dbclient if set
# 2. Class logical_database if configured
# 3. Connection provider if set
# 4. Global Familia connection

class DebugModel < Familia::Horreum
  logical_database 3
  field :name
end

model = DebugModel.new(name: "test")
# Uses database 3 from logical_database

model.dbclient = custom_connection
# Now uses custom_connection instead

Performance Considerations

Encryption Performance

  • XChaCha20-Poly1305 is ~2x faster than AES-256-GCM
  • Key derivation is NOT cached by default for security
  • Use request-level caching for bulk operations:
Familia::Encryption.with_request_cache do
  # Bulk operations with cached key derivation
  1000.times do |i|
    User.create(email: "user#{i}@example.com")
  end
end

Connection Pooling

  • Use connection_provider for multi-threaded environments
  • Pool size should match thread count
  • Connections are thread-safe when using provider pattern

Feature Performance Impact

  • Encryption: ~10-20% overhead for field access
  • Relationships: O(1) for indexed lookups
  • Quantization: Minimal overhead, improves storage efficiency
  • Safe Dump: Lazy evaluation, only computed when called

Migration Guide

From Familia v1.x to v2.0

  • Replace redis_uri with uri in configuration
  • Update feature syntax from mixins to feature declarations
  • Migrate from global_ prefix to class_ for class-level methods
  • Update encryption configuration to new provider system

Connection Provider Pattern

  • Familia v2.0 uses redis-rb gem internally
  • Connection providers must return Redis connections
  • Uses RedisClient middleware architecture internally via redis-rb

Summary

Familia v2.0.0-pre series provides a comprehensive ORM for Valkey/Redis with:

  • Modular Feature System: Isolated, configurable features per class
  • Advanced Security: Field-level encryption with multiple providers
  • Flexible Relationships: Automatic management with clean Ruby syntax
  • Performance Optimized: Connection pooling, sampling, and caching
  • Production Ready: Debug logging, monitoring, and thread safety

For additional documentation and examples, see the Familia GitHub repository.

class ActivityTracker < Familia::Horreum feature :relationships

identifier_field :activity_id field :activity_id, :user_id, :activity_type, :data, :created_at

# Class-level tracking with automatic management class_participates_in :user_activities, score: :created_at class_participates_in :activity_by_type, score: -> { "#activity_type:#created_at".hash } end

Create and save activity (automatic tracking)

activity = ActivityTracker.new( activity_id: 'act123', user_id: 'user456', activity_type: 'login', created_at: Familia.now.to_i ) activity.save # Automatically added to both tracking collections

Query recent activities (last hour)

hour_ago = (Familia.now - 1.hour).to_i recent_activities = ActivityTracker.user_activities.range_by_score( hour_ago, '+inf' )

Get activities by type in time range

login_hash_start = "login:#hour_ago".hash login_hash_end = "login:#Familia.now.to_i".hash login_activities = ActivityTracker.activity_by_type.range_by_score( login_hash_start, login_hash_end )


---

## Data Type Usage Patterns

### Advanced Sorted Set Operations
Leverage Valkey/Redis sorted sets for rankings, time series, and scored data.

```ruby
class Leaderboard < Familia::Horreum
  identifier_field :game_id
  field :game_id, :name
  sorted_set :scores
end

leaderboard = Leaderboard.new(game_id: "game1", name: "Daily Challenge")

# Add player scores
leaderboard.scores.add(1500, "player1")
leaderboard.scores.add(2300, "player2")
leaderboard.scores.add(1800, "player3")

# Get top 10 players (highest scores first)
top_players = leaderboard.scores.revrange(0, 9, withscores: true)
# => [["player2", 2300.0], ["player3", 1800.0], ["player1", 1500.0]]

# Get player rank (0-indexed, lower scores = lower rank)
rank = leaderboard.scores.rank("player1")  # => 0
rev_rank = leaderboard.scores.revrank("player1")  # => 2 (highest to lowest)

# Get score range
mid_tier = leaderboard.scores.rangebyscore(1000, 2000, withscores: true)

# Increment score atomically
leaderboard.scores.increment("player1", 100)  # Add 100 to existing score

List-Based Queues and Feeds

Use Valkey/Redis lists for queues, feeds, and ordered data.

class TaskQueue < Familia::Horreum
  identifier_field :queue_name
  field :queue_name
  list :tasks
end

class ActivityFeed < Familia::Horreum
  identifier_field :user_id
  field :user_id
  list :activities
end

# Task queue operations
queue = TaskQueue.new(queue_name: "email_processing")

# Add tasks to queue (right push)
queue.tasks.push({
  type: "send_email",
  recipient: "user@example.com",
  template: "welcome"
}.to_json)

# Process tasks (left pop - FIFO)
next_task = queue.tasks.pop  # Atomic pop from left
task_data = JSON.parse(next_task) if next_task

# Activity feed with size limit
feed = ActivityFeed.new(user_id: "user123")

# Add activity (keep last 100)
feed.activities.unshift("User logged in at #{Familia.now}")
feed.activities.trim(0, 99)  # Keep only last 100 items

# Get recent activities
recent = feed.activities.range(0, 9)  # Get 10 most recent

Hash-Based Configuration Storage

Store structured configuration and key-value data.

class UserPreferences < Familia::Horreum
  identifier_field :user_id
  field :user_id
  hash :settings
  hash :feature_flags
end

prefs = UserPreferences.new(user_id: "user123")

# UnsortedSet individual preferences
prefs.settings["theme"] = "dark"
prefs.settings["notifications"] = "true"
prefs.settings["timezone"] = "UTC-5"

# Batch set multiple values
prefs.feature_flags.update(
  "beta_ui" => "true",
  "new_dashboard" => "false",
  "advanced_features" => "true"
)

# Get preferences
theme = prefs.settings["theme"]     # => "dark"
all_settings = prefs.settings.to_h  # => Hash of all settings

# Check feature flags
beta_enabled = prefs.feature_flags["beta_ui"] == "true"

Error Handling and Validation

Connection Error Handling

Robust error handling for Valkey/Redis connection issues.

class ResilientService < Familia::Horreum
  field :name, :data

  def self.with_fallback(&block)
    retries = 3
    begin
      yield
    rescue Redis::ConnectionError, Redis::TimeoutError => e
      retries -= 1
      if retries > 0
        sleep(0.1 * (4 - retries))  # Exponential backoff
        retry
      else
        Familia.warn "Database operation failed after retries: #{e.message}"
        nil  # Return nil or handle gracefully
      end
    end
  end

  def save_with_fallback
    self.class.with_fallback { save }
  end
end

Data Validation Patterns

Implement validation in model classes.

class User < Familia::Horreum
  field :email, :username, :age

  def valid?
    errors.clear
    validate_email
    validate_username
    validate_age
    errors.empty?
  end

  def errors
    @errors ||= []
  end

  private

  def validate_email
    unless email&.include?('@')
      errors << "Email must be valid"
    end
  end

  def validate_username
    if username.nil? || username.length < 3
      errors << "Username must be at least 3 characters"
    end
  end

  def validate_age
    unless age.is_a?(Integer) && age > 0
      errors << "Age must be a positive integer"
    end
  end
end

# Usage
user = User.new(email: "invalid", username: "ab", age: -5)
if user.valid?
  user.save
else
  puts "Validation errors: #{user.errors.join(', ')}"
end

Performance Optimization

Pipelined Bulk Loading

Load multiple objects efficiently with a single pipelined Redis batch.

# Before: N×2 commands (EXISTS + HGETALL per object)
users = ids.map { |id| User.find_by_id(id) }
# For 14 objects: 28 Redis commands

# After: 1 pipelined batch
users = User.load_multi(ids)
# For 14 objects: 1 batch with 14 HGETALL commands (2× faster)

# Load by full dbkeys
users = User.load_multi_by_keys(['user:123:object', 'user:456:object'])

# Filter out nils for missing objects
existing_users = User.load_multi(ids).compact

Optional EXISTS Check Optimization

Skip the EXISTS check for 50% reduction in Redis commands when keys are known to exist.

# Default behavior (2 commands: EXISTS + HGETALL)
user = User.find_by_id(123)

# Optimized (1 command: HGETALL only)
user = User.find_by_id(123, check_exists: false)

When to use check_exists: false:

  • Loading from sorted set results (keys guaranteed to exist)
  • High-throughput API endpoints
  • Bulk operations with known-existing keys

Batch Operations

Minimize Valkey/Redis round trips with batch operations.

# Instead of multiple individual operations
users = []
100.times do |i|
  user = User.new(name: "User #{i}", email: "user#{i}@example.com")
  users << user
end

# Use transactions for batch saves
User.pipelined do
  users.each do |user|
    # All operations batched in pipeline
    user.save
  end
end

Index Rebuilding

Auto-generated rebuild methods for unique and multi indexes with zero downtime.

class User < Familia::Horreum
  feature :relationships
  unique_index :email, :email_lookup
end

# Rebuild class-level unique index
User.rebuild_email_lookup

# With progress tracking
User.rebuild_email_lookup(batch_size: 100) do |progress|
  puts "#{progress[:completed]}/#{progress[:total]}"
end

# Instance-scoped index rebuild
company.rebuild_badge_index

When to use:

  • After data migrations or bulk imports
  • Recovering from index corruption
  • Adding indexes to existing data

Each rebuild holds an advisory lock at <index_key>:rebuild-lock. A rebuild of an index that is already being rebuilt raises Familia::RebuildInProgressError rather than running concurrently. A rebuild whose batch overran the lock TTL and lost the lock to another rebuild raises Familia::RebuildLockLostError at the next batch or before the swap, discards its own temporary key, and leaves the live index untouched. Calling a rebuild inside an enclosing Familia.transaction or pipeline raises Familia::OperationModeError, because the lock's SET NX would only be queued there. The temporary key used for the swap expires after 300 seconds between batches, and the swap fails closed: it is one Lua script that verifies the lock token before the RENAME, and a rebuild that cannot complete the swap raises rather than leaving a partial or stale index reported as current. Any batch failure discards the whole temporary key. See Concurrency and temporary keys for the sweep used to clear temporary keys left by older versions, and the precondition for running it.

Memory Optimization

Efficient memory usage patterns.

class CacheEntry < Familia::Horreum
  feature :expiration

  field :key, :value, :created_at
  default_expiration 1.hour

  # Use shorter field names to reduce memory
  field :k, :v, :c  # Instead of key, value, created_at

  # Compress large values
  def value=(val)
    @value = val.length > 1000 ? Zlib.deflate(val) : val
  end

  def value
    val = @value || ""
    val.start_with?("\x78\x9c") ? Zlib.inflate(val) : val
  rescue Zlib::DataError
    @value  # Return original if decompression fails
  end
end

Connection Pool Sizing

Configure connection pools based on application needs.

# High-throughput application
HIGH_THROUGHPUT_POOL = ConnectionPool::Wrapper.new(size: 25, timeout: 5) do
  Redis.new(url: ENV['REDIS_URL'])
end
Familia.connection_provider = ->(_uri) { HIGH_THROUGHPUT_POOL }

# Memory-constrained environment
SMALL_POOL = ConnectionPool::Wrapper.new(size: 5, timeout: 10) do
  Redis.new(url: ENV['REDIS_URL'])
end
Familia.connection_provider = ->(_uri) { SMALL_POOL }

Both build the pool once at boot. A single-pool provider like this ignores the uri it is given, so it only works when every model shares one logical database; see Connection Provider Pattern for the per-URI form.


Migration and Upgrading

From v1.x to v2.0

Key changes and migration steps.

# OLD v1.x syntax
class User < Familia
  identifier :email
  string :name
  list :sessions
end

# NEW v2.0 syntax
class User < Familia::Horreum
  identifier_field :email  # Updated method name
  field :name              # Generic field method
  list :sessions           # Data types unchanged
end

# Feature activation (NEW)
class User < Familia::Horreum
  feature :expiration      # Explicit feature activation
  feature :safe_dump

  identifier_field :email
  field :name
  list :sessions

  default_expiration 24.hours  # Feature-specific methods
  safe_dump_field :name       # Feature-specific methods
end

Encryption Migration

Migrating existing fields to encrypted storage.

# Step 1: Add feature without changing existing fields
class User < Familia::Horreum
  feature :encrypted_fields  # Add feature

  field :name, :email
  field :api_key    # Still plaintext during migration
end

# Step 2: Migrate data with dual read/write
class User < Familia::Horreum
  feature :encrypted_fields

  field :name, :email
  encrypted_field :api_key  # Now encrypted

  # Temporary migration method
  def migrate_api_key!
    if raw_api_key = dbclient.hget(dbkey, "api_key")  # Read old plaintext
      self.api_key = raw_api_key                       # Write as encrypted
      dbclient.hdel(dbkey, "api_key")                 # Remove plaintext
      save
    end
  end
end

# Step 3: Run migration for existing users
User.instances.each(&:migrate_api_key!)

Testing Patterns

Test Helpers and Utilities

Common patterns for testing Familia applications.

# test_helper.rb
require 'familia'

# Use separate Valkey/Redis database for tests
Familia.configure do |config|
  config.uri = ENV.fetch('REDIS_TEST_URI', 'redis://localhost:2525/3')
end

module TestHelpers
  def setup_redis
    # Clear test database
    Familia.dbclient.flushdb
  end

  def teardown_redis
    Familia.dbclient.flushdb
  end

  def create_test_user(**attrs)
    User.new({
      email: "test@example.com",
      name: "Test User",
      created_at: Familia.now.to_i
    }.merge(attrs))
  end
end

# In test files
class UserTest < Minitest::Test
  include TestHelpers

  def setup
    setup_redis
  end

  def teardown
    teardown_redis
  end

  def test_user_creation_with_automatic_indexing
    user = create_test_user(name: "Alice")
    user.save  # Automatically adds to class-level indexes

    assert user.exists?
    assert_equal "Alice", user.name

    # Test automatic indexing (if using unique_index)
    if User.respond_to?(:find_by_email)
      found_user = User.find_by_email(user.email)
      assert_equal user.identifier, found_user.identifier
    end
  end

  def test_relationships_with_clean_syntax
    user = create_test_user
    user.save  # Automatic class-level tracking

    domain = Domain.new(domain_id: "test_domain", name: "test.com")
    domain.save  # Automatic class-level tracking

    # Test clean relationship syntax
    user.domains << domain  # Ruby-like collection syntax
    assert domain.in_user_domains?(user.identifier)
    assert user.domains.member?(domain.identifier)
  end

  def test_encrypted_fields_concealment
    setup_encryption_keys

    vault = SecureVault.new(
      name: "Test Vault",
      secret_key: "super-secret-123",
      api_token: "sk-1234567890"
    )
    vault.save

    # Test ConcealedString behavior
    assert_instance_of Familia::Features::EncryptedFields::ConcealedString, vault.secret_key
    assert_equal "[CONCEALED]", vault.secret_key.to_s
    assert_equal "super-secret-123", vault.secret_key.reveal

    # Test JSON safety
    json_data = vault.to_json
    refute_includes json_data, "super-secret-123"
    assert_includes json_data, "[CONCEALED]"
  end

  def test_transient_fields_non_persistence
    form = LoginForm.new(
      username: "testuser",
      password: "secret123",
      csrf_token: "abc123"
    )
    form.save

    # Reload and verify transient fields not persisted
    reloaded = LoginForm.load(form.identifier)
    assert_equal "testuser", reloaded.username
    assert_nil reloaded.password
    assert_nil reloaded.csrf_token
  end

  def test_object_identifier_generation
    # Test UUID generation
    doc = UuidDocument.create(title: "Test Doc")
    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/i, doc.objid)

    # Test hex generation
    session = HexSession.create(user_id: "123")
    assert_match(/\A[0-9a-f]+\z/i, session.objid)
    assert_equal 16, session.objid.length
  end

  def test_quantization_time_bucketing
    timestamp = Time.parse("2024-12-15 14:37:23")
    bucket = MetricsBucket.quantize_timestamp(timestamp)
    assert_equal "20241215_1430", bucket  # 10-minute bucket

    # Test multiple timestamps in same bucket
    timestamp2 = Time.parse("2024-12-15 14:39:45")
    bucket2 = MetricsBucket.quantize_timestamp(timestamp2)
    assert_equal bucket, bucket2
  end

  def test_external_identifier_mapping
    user = ExternalUser.new(name: "External User")
    user.save

    # Test external ID is derived from objid
    assert_not_nil user.objid
    assert_not_nil user.extid
    assert user.extid.start_with?("ext_")

    # Test deterministic generation
    user2 = ExternalUser.new(objid: user.objid, name: "External User")
    assert_equal user.extid, user2.extid
  end

  private

  def setup_encryption_keys
    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
end

Resources and References

Key Configuration

Essential configuration options for Familia v2.0.0-pre.

Familia.configure do |config|
  # Basic Valkey/Redis connection
  config.uri = ENV['REDIS_URL'] || 'redis://localhost:6379/0'

  # Connection provider for pooling (optional)
  config.connection_provider = MyConnectionProvider

  # Encryption configuration (for encrypted_fields feature)
  config.encryption_keys = {
    v1: ENV['FAMILIA_ENCRYPTION_KEY_V1'],
    v2: ENV['FAMILIA_ENCRYPTION_KEY_V2']
  }
  config.current_key_version = :v2

  # Debugging and logging
  config.debug = ENV['FAMILIA_DEBUG'] == 'true'
  config.enable_database_logging = ENV['FAMILIA_LOG_REDIS'] == 'true'
end

Version Information

  • Current Version: v2.0.0
  • Ruby Compatibility: 3.2+
  • Redis Compatibility: 6.0+ (Valkey compatible)

This technical reference covers the major components and usage patterns available in Familia v2.0. For complete API documentation, see the generated YARD docs and wiki guides.