ROM (Ruby Object Mapper)

ROM (Ruby Object Mapper) is "a Ruby data mapping toolkit based on functional programming principles" designed as a new approach to data persistence that serves as an alternative to the traditional Active Record pattern. Adopting the concepts of "Command Query Responsibility Segregation (CQRS)" and "functional programming," it achieves high maintainability and predictability through immutable objects, pure functions, and explicit dependencies. It can interact with multiple databases (PostgreSQL, MySQL, SQLite, Redis, Elasticsearch, etc.) through a unified API, significantly improving data layer design in complex enterprise applications.

RubyDatabaseFunctional ProgrammingPersistenceData MapperRepository

Library

ROM (Ruby Object Mapper)

Overview

ROM (Ruby Object Mapper) is "a Ruby data mapping toolkit based on functional programming principles" designed as a new approach to data persistence that serves as an alternative to the traditional Active Record pattern. Adopting the concepts of "Command Query Responsibility Segregation (CQRS)" and "functional programming," it achieves high maintainability and predictability through immutable objects, pure functions, and explicit dependencies. It can interact with multiple databases (PostgreSQL, MySQL, SQLite, Redis, Elasticsearch, etc.) through a unified API, significantly improving data layer design in complex enterprise applications.

Details

ROM 2025 edition fully leverages the latest features of Ruby 3.3, providing a more expressive and safe data access layer. Through the combination of Repository Pattern, Data Mapper Pattern, and Unit of Work patterns, it has very high affinity with Domain-Driven Design (DDD) and strongly supports clean architecture implementation. It comprehensively provides features necessary for modern web application development, including multi-tenant support, read-write separation, custom data type support, and comprehensive validation systems. Additionally, leveraging the advantages of functional programming, it achieves predictable data operations with minimal side effects.

Key Features

  • Functional Approach: Predictable processing through immutable objects and pure functions
  • CQRS Support: Clear separation of commands and queries
  • Repository Pattern: Data access abstraction and dependency inversion
  • Multi-Adapter: Unified operations across multiple databases
  • Schema Definition: Type-safe schema definition and validation
  • DDD Integration: Natural integration with Domain-Driven Design

Pros and Cons

Pros

  • Functional design solving Active Record's "god class" problem
  • Improved predictability and safety through immutable objects
  • High flexibility to operate multiple databases with the same API
  • Significant improvement in testability and explicit side effect management
  • Excellent affinity with Domain-Driven Design and clean architecture
  • Maintainable code leveraging functional programming advantages

Cons

  • High learning curve requiring understanding of functional programming
  • Conceptual shift difficult for developers accustomed to Active Record
  • Complex configuration and setup, excessive for small projects
  • Integration with Rails ecosystem not as natural as Active Record
  • Limited Japanese documentation and community resources
  • Cannot leverage rich Active Record plugins

Reference Pages

Code Examples

Setup

# Gemfile
gem 'rom', '~> 5.3'
gem 'rom-sql', '~> 3.6'
gem 'rom-postgres', '~> 3.0'  # For PostgreSQL
gem 'rom-repository', '~> 5.3'

# For development/testing
gem 'rom-factory', '~> 0.11', group: [:development, :test]

# Install
bundle install
# config/database.yml (when using Rails)
default: &default
  adapter: postgresql
  encoding: unicode
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>

development:
  <<: *default
  database: myapp_development
  username: postgres
  password: password
  host: localhost

test:
  <<: *default
  database: myapp_test
  username: postgres
  password: password
  host: localhost

production:
  <<: *default
  database: myapp_production
  username: <%= ENV['DATABASE_USER'] %>
  password: <%= ENV['DATABASE_PASSWORD'] %>
  host: <%= ENV['DATABASE_HOST'] %>
# config/rom.rb
require 'rom'
require 'rom-sql'
require 'rom-repository'

# ROM configuration
ROM_CONFIG = ROM::Configuration.new(:sql, ENV['DATABASE_URL'])

# Load schema definitions
ROM_CONFIG.auto_registration('./app/persistence')

# Create container
ROM_CONTAINER = ROM.container(ROM_CONFIG)

Schema and Relation Definition

# app/persistence/relations/users.rb
module Relations
  class Users < ROM::Relation[:sql]
    schema(:users, infer: true) do
      attribute :id, ROM::Types::Integer.meta(primary_key: true)
      attribute :name, ROM::Types::String
      attribute :email, ROM::Types::String
      attribute :age, ROM::Types::Integer
      attribute :is_active, ROM::Types::Bool
      attribute :created_at, ROM::Types::DateTime
      attribute :updated_at, ROM::Types::DateTime
      
      primary_key :id
      
      associations do
        has_many :posts, foreign_key: :user_id
        has_one :profile, foreign_key: :user_id
      end
    end
    
    # Query methods
    def active
      where(is_active: true)
    end
    
    def by_age_range(min_age, max_age)
      where { age >= min_age }.where { age <= max_age }
    end
    
    def search_by_name(pattern)
      where { name.ilike("%#{pattern}%") }
    end
    
    def recent(limit = 20)
      order(:created_at).reverse.limit(limit)
    end
    
    def with_post_count
      select_append { integer::count(posts[:id]).as(:post_count) }
        .left_join(:posts)
        .group(:users[:id])
    end
    
    def adults
      where { age >= 18 }
    end
    
    def by_email(email)
      where(email: email)
    end
  end
end

# app/persistence/relations/posts.rb
module Relations
  class Posts < ROM::Relation[:sql]
    schema(:posts, infer: true) do
      attribute :id, ROM::Types::Integer.meta(primary_key: true)
      attribute :title, ROM::Types::String
      attribute :content, ROM::Types::String
      attribute :excerpt, ROM::Types::String.optional
      attribute :is_published, ROM::Types::Bool
      attribute :published_at, ROM::Types::DateTime.optional
      attribute :user_id, ROM::Types::Integer.meta(foreign_key: true)
      attribute :category_id, ROM::Types::Integer.meta(foreign_key: true).optional
      attribute :view_count, ROM::Types::Integer
      attribute :created_at, ROM::Types::DateTime
      attribute :updated_at, ROM::Types::DateTime
      
      primary_key :id
      
      associations do
        belongs_to :user, foreign_key: :user_id
        belongs_to :category, foreign_key: :category_id
        has_many :comments, foreign_key: :post_id
      end
    end
    
    def published
      where(is_published: true)
    end
    
    def by_user(user_id)
      where(user_id: user_id)
    end
    
    def by_category(category_id)
      where(category_id: category_id)
    end
    
    def popular(limit = 10)
      where(is_published: true)
        .order(:view_count)
        .reverse
        .limit(limit)
    end
    
    def recent_published(limit = 20)
      published
        .order(:published_at)
        .reverse
        .limit(limit)
    end
    
    def search_content(pattern)
      where { title.ilike("%#{pattern}%") | content.ilike("%#{pattern}%") }
    end
    
    def with_author
      combine(:user)
    end
    
    def with_category
      combine(:category)
    end
    
    def with_comments_count
      select_append { integer::count(comments[:id]).as(:comments_count) }
        .left_join(:comments)
        .group(:posts[:id])
    end
  end
end

# app/persistence/relations/categories.rb
module Relations
  class Categories < ROM::Relation[:sql]
    schema(:categories, infer: true) do
      attribute :id, ROM::Types::Integer.meta(primary_key: true)
      attribute :name, ROM::Types::String
      attribute :description, ROM::Types::String.optional
      attribute :color, ROM::Types::String
      attribute :is_active, ROM::Types::Bool
      attribute :sort_order, ROM::Types::Integer
      attribute :created_at, ROM::Types::DateTime
      
      primary_key :id
      
      associations do
        has_many :posts, foreign_key: :category_id
      end
    end
    
    def active
      where(is_active: true)
    end
    
    def ordered
      order(:sort_order, :name)
    end
    
    def with_post_count
      select_append { integer::count(posts[:id]).as(:post_count) }
        .left_join(:posts)
        .group(:categories[:id])
    end
  end
end

Repository Pattern Implementation

# app/repositories/user_repository.rb
class UserRepository < ROM::Repository[:users]
  include ROM::Repository::Plugins::CommandAPIDefinition
  
  commands :create, :update, :delete
  
  # Relation loading configuration
  def all
    users.to_a
  end
  
  def find(id)
    users.by_pk(id).one!
  end
  
  def find_by_email(email)
    users.by_email(email).one
  end
  
  def active_users
    users.active.to_a
  end
  
  def users_by_age_range(min_age, max_age)
    users.by_age_range(min_age, max_age).to_a
  end
  
  def search_users(name_pattern)
    users.search_by_name(name_pattern).to_a
  end
  
  def recent_users(limit = 20)
    users.recent(limit).to_a
  end
  
  def users_with_post_count
    users.with_post_count.to_a
  end
  
  def adult_users
    users.adults.to_a
  end
  
  # User creation (with validation)
  def create_user(attributes)
    changeset = users.changeset(:create, attributes)
    command = users.command(:create)
    
    if changeset.valid?
      command.call(changeset)
    else
      raise ROM::ValidationError, changeset.errors
    end
  end
  
  # User update
  def update_user(id, attributes)
    user = find(id)
    changeset = users.changeset(:update, attributes)
    command = users.command(:update)
    
    if changeset.valid?
      command.by_pk(id).call(changeset)
    else
      raise ROM::ValidationError, changeset.errors
    end
  end
  
  # User deletion (with related data)
  def delete_user(id)
    # Delete related posts first
    posts.command(:delete).where(user_id: id).call
    
    # Delete user
    users.command(:delete).by_pk(id).call
  end
  
  # User statistics
  def user_statistics
    total_users = users.count
    active_users_count = users.active.count
    average_age = users.dataset.avg(:age)
    
    {
      total: total_users,
      active: active_users_count,
      average_age: average_age&.to_f&.round(2) || 0.0
    }
  end
  
  # Complex query example
  def users_with_recent_posts(days = 7)
    cutoff_date = Date.current - days.days
    
    users
      .join(:posts)
      .where { posts[:created_at] >= cutoff_date }
      .select(:users[:id], :users[:name], :users[:email])
      .group(:users[:id])
      .to_a
  end
  
  # Transaction processing
  def transfer_posts(from_user_id, to_user_id)
    ROM_CONTAINER.gateways[:default].transaction do
      from_user = find(from_user_id)
      to_user = find(to_user_id)
      
      raise ArgumentError, "One or both users not found" unless from_user && to_user
      
      posts.command(:update)
        .where(user_id: from_user_id)
        .call(user_id: to_user_id, updated_at: Time.current)
    end
  end
  
  private
  
  def posts
    ROM_CONTAINER.relations[:posts]
  end
end

# app/repositories/post_repository.rb
class PostRepository < ROM::Repository[:posts]
  include ROM::Repository::Plugins::CommandAPIDefinition
  
  commands :create, :update, :delete
  
  def all
    posts.to_a
  end
  
  def find(id)
    posts.by_pk(id).one!
  end
  
  def published_posts
    posts.published.to_a
  end
  
  def posts_by_user(user_id)
    posts.by_user(user_id).to_a
  end
  
  def posts_by_category(category_id)
    posts.by_category(category_id).to_a
  end
  
  def popular_posts(limit = 10)
    posts.popular(limit).to_a
  end
  
  def recent_published_posts(limit = 20)
    posts.recent_published(limit).to_a
  end
  
  def search_posts(search_term)
    posts.search_content(search_term).to_a
  end
  
  def posts_with_author
    posts.combine(:user).to_a
  end
  
  def posts_with_category
    posts.combine(:category).to_a
  end
  
  def posts_with_comments_count
    posts.with_comments_count.to_a
  end
  
  # Post creation
  def create_post(attributes)
    # Automatic timestamp setting
    attributes_with_timestamps = attributes.merge(
      created_at: Time.current,
      updated_at: Time.current,
      view_count: 0
    )
    
    # Automatic excerpt generation
    if attributes_with_timestamps[:excerpt].blank? && attributes_with_timestamps[:content].present?
      attributes_with_timestamps[:excerpt] = attributes_with_timestamps[:content][0..200]
    end
    
    changeset = posts.changeset(:create, attributes_with_timestamps)
    
    if changeset.valid?
      posts.command(:create).call(changeset)
    else
      raise ROM::ValidationError, changeset.errors
    end
  end
  
  # Post update
  def update_post(id, attributes)
    attributes_with_timestamp = attributes.merge(updated_at: Time.current)
    
    changeset = posts.changeset(:update, attributes_with_timestamp)
    
    if changeset.valid?
      posts.command(:update).by_pk(id).call(changeset)
    else
      raise ROM::ValidationError, changeset.errors
    end
  end
  
  # Publish post
  def publish_post(id)
    update_post(id, {
      is_published: true,
      published_at: Time.current
    })
  end
  
  # Unpublish post
  def unpublish_post(id)
    update_post(id, {
      is_published: false,
      published_at: nil
    })
  end
  
  # Increment view count
  def increment_view_count(id)
    post = find(id)
    update_post(id, view_count: post.view_count + 1)
  end
  
  # Delete post
  def delete_post(id)
    posts.command(:delete).by_pk(id).call
  end
  
  # Post statistics
  def post_statistics
    total_posts = posts.count
    published_posts_count = posts.published.count
    draft_posts_count = total_posts - published_posts_count
    average_view_count = posts.dataset.avg(:view_count)
    
    {
      total: total_posts,
      published: published_posts_count,
      drafts: draft_posts_count,
      average_views: average_view_count&.to_f&.round(2) || 0.0
    }
  end
  
  # Recent post statistics
  def recent_post_statistics(days = 7)
    cutoff_date = Date.current - days.days
    
    recent_posts = posts.where { created_at >= cutoff_date }
    recent_published = recent_posts.where(is_published: true)
    
    {
      recent_total: recent_posts.count,
      recent_published: recent_published.count,
      recent_drafts: recent_posts.count - recent_published.count
    }
  end
  
  # Posts count by user
  def posts_count_by_user
    posts
      .join(:users)
      .select(:users[:id], :users[:name])
      .select_append { integer::count(posts[:id]).as(:post_count) }
      .group(:users[:id])
      .to_a
  end
end

Commands and Validation

# app/persistence/commands/create_user.rb
module Commands
  class CreateUser < ROM::Commands::Create[:sql]
    relation :users
    register_as :create_user
    
    # Validation rules
    schema do
      required(:name).filled(:string)
      required(:email).filled(:string, format?: /@/)
      required(:age).filled(:integer, gt?: 0, lt?: 150)
      optional(:is_active).filled(:bool)
    end
    
    # Hook processing
    def call(attributes)
      # Default value setting
      attributes_with_defaults = attributes.merge(
        is_active: attributes.fetch(:is_active, true),
        created_at: Time.current,
        updated_at: Time.current
      )
      
      # Email duplicate check
      if users.by_email(attributes_with_defaults[:email]).exist?
        raise ROM::ValidationError, { email: ['already exists'] }
      end
      
      super(attributes_with_defaults)
    end
  end
end

# app/persistence/commands/update_user.rb
module Commands
  class UpdateUser < ROM::Commands::Update[:sql]
    relation :users
    register_as :update_user
    
    schema do
      optional(:name).filled(:string)
      optional(:email).filled(:string, format?: /@/)
      optional(:age).filled(:integer, gt?: 0, lt?: 150)
      optional(:is_active).filled(:bool)
    end
    
    def call(attributes)
      attributes_with_timestamp = attributes.merge(updated_at: Time.current)
      
      # Email duplicate check (excluding self)
      if attributes[:email] && relation.dataset.exclude(id: restriction[:id]).where(email: attributes[:email]).any?
        raise ROM::ValidationError, { email: ['already exists'] }
      end
      
      super(attributes_with_timestamp)
    end
  end
end

# app/persistence/commands/create_post.rb
module Commands
  class CreatePost < ROM::Commands::Create[:sql]
    relation :posts
    register_as :create_post
    
    schema do
      required(:title).filled(:string, min_size?: 1)
      required(:content).filled(:string, min_size?: 10)
      required(:user_id).filled(:integer)
      optional(:category_id).filled(:integer)
      optional(:is_published).filled(:bool)
      optional(:excerpt).filled(:string)
    end
    
    def call(attributes)
      # Validation
      user_exists = ROM_CONTAINER.relations[:users].by_pk(attributes[:user_id]).exist?
      raise ROM::ValidationError, { user_id: ['does not exist'] } unless user_exists
      
      if attributes[:category_id]
        category_exists = ROM_CONTAINER.relations[:categories].by_pk(attributes[:category_id]).exist?
        raise ROM::ValidationError, { category_id: ['does not exist'] } unless category_exists
      end
      
      # Default values and timestamp setting
      attributes_with_defaults = attributes.merge(
        is_published: attributes.fetch(:is_published, false),
        view_count: 0,
        created_at: Time.current,
        updated_at: Time.current
      )
      
      # Automatic excerpt generation
      if attributes_with_defaults[:excerpt].blank?
        attributes_with_defaults[:excerpt] = attributes_with_defaults[:content][0..200]
      end
      
      # Published date setting
      if attributes_with_defaults[:is_published]
        attributes_with_defaults[:published_at] = Time.current
      end
      
      super(attributes_with_defaults)
    end
  end
end

Service Layer Implementation

# app/services/user_service.rb
class UserService
  include ROM::Repository::Plugins::Helpers
  
  def initialize(container = ROM_CONTAINER)
    @container = container
    @user_repo = UserRepository.new(container)
    @post_repo = PostRepository.new(container)
  end
  
  # User creation service
  def create_user(params)
    # Parameter validation
    clean_params = sanitize_user_params(params)
    
    @container.gateways[:default].transaction do
      user = @user_repo.create_user(clean_params)
      
      # Welcome processing (example)
      # send_welcome_email(user)
      # create_default_profile(user)
      
      user
    end
  rescue ROM::ValidationError => e
    raise ServiceError, "User creation failed: #{e.message}"
  end
  
  # User update service
  def update_user(id, params)
    clean_params = sanitize_user_params(params)
    
    @container.gateways[:default].transaction do
      user = @user_repo.update_user(id, clean_params)
      
      # Post-update processing
      # update_search_index(user) if user
      
      user
    end
  rescue ROM::ValidationError => e
    raise ServiceError, "User update failed: #{e.message}"
  end
  
  # User deletion service (safe deletion)
  def delete_user(id)
    @container.gateways[:default].transaction do
      # Pre-deletion check
      user = @user_repo.find(id)
      posts_count = @post_repo.posts_by_user(id).length
      
      if posts_count > 0
        # Processing choice when posts exist
        # Option 1: Delete posts as well
        @user_repo.delete_user(id)
        
        # Option 2: Throw error
        # raise ServiceError, "Cannot delete user with existing posts"
        
        # Option 3: Transfer posts to another user
        # transfer_posts_to_system_user(id)
        # @user_repo.delete_user(id)
      else
        @user_repo.delete_user(id)
      end
    end
  rescue ROM::TupleCountMismatchError
    raise ServiceError, "User not found"
  end
  
  # Complex business logic example
  def activate_user_with_verification(id, verification_code)
    @container.gateways[:default].transaction do
      user = @user_repo.find(id)
      
      # Verification code check (hypothetical implementation)
      unless verify_activation_code(user, verification_code)
        raise ServiceError, "Invalid verification code"
      end
      
      # User activation
      @user_repo.update_user(id, {
        is_active: true,
        verified_at: Time.current
      })
      
      # Post-activation processing
      # send_activation_confirmation(user)
      # grant_default_permissions(user)
      
      @user_repo.find(id)
    end
  end
  
  # User search service
  def search_users(query_params)
    results = @user_repo.all
    
    if query_params[:name_pattern]
      results = @user_repo.search_users(query_params[:name_pattern])
    end
    
    if query_params[:min_age] || query_params[:max_age]
      min_age = query_params[:min_age] || 0
      max_age = query_params[:max_age] || 150
      results = @user_repo.users_by_age_range(min_age, max_age)
    end
    
    if query_params[:active_only]
      results = results.select { |user| user.is_active }
    end
    
    results
  end
  
  # Data export service
  def export_user_data(format = :json)
    users_with_stats = @user_repo.users_with_post_count
    
    case format
    when :json
      users_with_stats.map do |user|
        {
          id: user.id,
          name: user.name,
          email: user.email,
          age: user.age,
          is_active: user.is_active,
          post_count: user.post_count || 0,
          created_at: user.created_at.iso8601
        }
      end
    when :csv
      # CSV format output implementation
      CSV.generate do |csv|
        csv << ['ID', 'Name', 'Email', 'Age', 'Active', 'Post Count', 'Created At']
        users_with_stats.each do |user|
          csv << [user.id, user.name, user.email, user.age, user.is_active, user.post_count || 0, user.created_at.iso8601]
        end
      end
    else
      raise ServiceError, "Unsupported export format: #{format}"
    end
  end
  
  private
  
  def sanitize_user_params(params)
    {
      name: params[:name]&.strip,
      email: params[:email]&.strip&.downcase,
      age: params[:age]&.to_i,
      is_active: params.key?(:is_active) ? params[:is_active] : true
    }.compact
  end
  
  def verify_activation_code(user, code)
    # In actual implementation, retrieve and compare verification code from database
    true # Simplified
  end
  
  # Custom exception
  class ServiceError < StandardError; end
end

# app/services/post_service.rb
class PostService
  def initialize(container = ROM_CONTAINER)
    @container = container
    @post_repo = PostRepository.new(container)
    @user_repo = UserRepository.new(container)
  end
  
  # Post creation service
  def create_post(params)
    clean_params = sanitize_post_params(params)
    
    @container.gateways[:default].transaction do
      # Author existence check
      author = @user_repo.find(clean_params[:user_id])
      raise ServiceError, "Author not found" unless author
      
      post = @post_repo.create_post(clean_params)
      
      # Post-creation processing
      # notify_followers(author, post) if post.is_published
      # update_search_index(post)
      
      post
    end
  rescue ROM::ValidationError => e
    raise ServiceError, "Post creation failed: #{e.message}"
  end
  
  # Post publishing service
  def publish_post(id, publish_at = nil)
    @container.gateways[:default].transaction do
      post = @post_repo.find(id)
      
      # Pre-publishing validation
      validate_post_for_publishing(post)
      
      @post_repo.publish_post(id)
      
      # Post-publishing processing
      # notify_subscribers(post)
      # update_sitemap
      # social_media_post(post)
      
      @post_repo.find(id)
    end
  rescue ROM::TupleCountMismatchError
    raise ServiceError, "Post not found"
  end
  
  # Bulk operation service
  def bulk_update_posts(post_ids, attributes)
    @container.gateways[:default].transaction do
      updated_posts = []
      
      post_ids.each do |id|
        updated_post = @post_repo.update_post(id, attributes)
        updated_posts << updated_post if updated_post
      end
      
      updated_posts
    end
  rescue => e
    raise ServiceError, "Bulk update failed: #{e.message}"
  end
  
  # Content analysis service
  def analyze_post_performance(days = 30)
    cutoff_date = Date.current - days.days
    
    # Get posts within period
    recent_posts = @post_repo.posts.where { created_at >= cutoff_date }.to_a
    
    total_views = recent_posts.sum(&:view_count)
    avg_views = recent_posts.empty? ? 0 : total_views.to_f / recent_posts.length
    
    top_posts = recent_posts
      .select { |post| post.is_published }
      .sort_by(&:view_count)
      .reverse
      .first(10)
    
    {
      period_days: days,
      total_posts: recent_posts.length,
      published_posts: recent_posts.count(&:is_published),
      total_views: total_views,
      average_views: avg_views.round(2),
      top_posts: top_posts.map { |post| 
        { 
          id: post.id, 
          title: post.title, 
          views: post.view_count 
        } 
      }
    }
  end
  
  private
  
  def sanitize_post_params(params)
    {
      title: params[:title]&.strip,
      content: params[:content]&.strip,
      excerpt: params[:excerpt]&.strip,
      user_id: params[:user_id]&.to_i,
      category_id: params[:category_id]&.to_i,
      is_published: params.key?(:is_published) ? params[:is_published] : false
    }.compact
  end
  
  def validate_post_for_publishing(post)
    errors = []
    
    errors << "Title cannot be empty" if post.title.blank?
    errors << "Content too short" if post.content.length < 100
    errors << "Author must be active" unless post.user&.is_active
    
    raise ServiceError, errors.join(", ") if errors.any?
  end
  
  class ServiceError < StandardError; end
end

Testing

# spec/repositories/user_repository_spec.rb
require 'spec_helper'

RSpec.describe UserRepository, type: :repository do
  let(:repo) { described_class.new(ROM_CONTAINER) }
  
  describe '#create_user' do
    it 'creates a user with valid attributes' do
      attributes = {
        name: 'John Doe',
        email: '[email protected]',
        age: 30
      }
      
      user = repo.create_user(attributes)
      
      expect(user.name).to eq('John Doe')
      expect(user.email).to eq('[email protected]')
      expect(user.age).to eq(30)
      expect(user.is_active).to be(true)
    end
    
    it 'raises error for invalid email' do
      attributes = {
        name: 'John Doe',
        email: 'invalid-email',
        age: 30
      }
      
      expect { repo.create_user(attributes) }.to raise_error(ROM::ValidationError)
    end
  end
  
  describe '#search_users' do
    before do
      repo.create_user(name: 'Alice Johnson', email: '[email protected]', age: 25)
      repo.create_user(name: 'Bob Smith', email: '[email protected]', age: 35)
    end
    
    it 'finds users by name pattern' do
      results = repo.search_users('Alice')
      
      expect(results.length).to eq(1)
      expect(results.first.name).to eq('Alice Johnson')
    end
  end
  
  describe '#user_statistics' do
    before do
      repo.create_user(name: 'User 1', email: '[email protected]', age: 25)
      repo.create_user(name: 'User 2', email: '[email protected]', age: 35)
    end
    
    it 'returns correct statistics' do
      stats = repo.user_statistics
      
      expect(stats[:total]).to eq(2)
      expect(stats[:active]).to eq(2)
      expect(stats[:average_age]).to eq(30.0)
    end
  end
end

# spec/services/user_service_spec.rb
require 'spec_helper'

RSpec.describe UserService do
  let(:service) { described_class.new }
  
  describe '#create_user' do
    it 'creates user successfully' do
      params = {
        name: 'Jane Doe',
        email: '[email protected]',
        age: 28
      }
      
      user = service.create_user(params)
      
      expect(user.name).to eq('Jane Doe')
      expect(user.email).to eq('[email protected]')
    end
    
    it 'handles validation errors gracefully' do
      params = {
        name: '',
        email: 'invalid',
        age: -5
      }
      
      expect { service.create_user(params) }.to raise_error(UserService::ServiceError)
    end
  end
  
  describe '#search_users' do
    before do
      service.create_user(name: 'Alice', email: '[email protected]', age: 25)
      service.create_user(name: 'Bob', email: '[email protected]', age: 35)
    end
    
    it 'searches by name pattern' do
      results = service.search_users(name_pattern: 'Ali')
      
      expect(results.length).to eq(1)
      expect(results.first.name).to eq('Alice')
    end
    
    it 'filters by age range' do
      results = service.search_users(min_age: 30, max_age: 40)
      
      expect(results.length).to eq(1)
      expect(results.first.name).to eq('Bob')
    end
  end
end

# spec/factories/user_factory.rb (using ROM::Factory)
require 'rom-factory'

ROM::Factory.configure do |config|
  config.rom = ROM_CONTAINER
end

module Factories
  extend ROM::Factory::DSL
  
  define(:user) do |f|
    f.sequence(:name) { |n| "User #{n}" }
    f.sequence(:email) { |n| "user#{n}@example.com" }
    f.age { rand(18..80) }
    f.is_active { true }
    f.created_at { Time.current }
    f.updated_at { Time.current }
  end
  
  define(:post) do |f|
    f.sequence(:title) { |n| "Post #{n}" }
    f.content { "This is the content of the post." * 10 }
    f.association(:user)
    f.is_published { false }
    f.view_count { rand(0..1000) }
    f.created_at { Time.current }
    f.updated_at { Time.current }
  end
end

# Factory usage example in tests
RSpec.describe 'Integration test' do
  it 'creates users and posts' do
    user = Factories[:user]
    post = Factories[:post, user: user, title: 'Custom Title']
    
    expect(user.name).to start_with('User')
    expect(post.title).to eq('Custom Title')
    expect(post.user_id).to eq(user.id)
  end
end

Error Handling and Exception Processing

# app/errors/rom_errors.rb
module ROMErrors
  class BaseError < StandardError
    attr_reader :details
    
    def initialize(message, details = {})
      super(message)
      @details = details
    end
  end
  
  class ValidationError < BaseError; end
  class NotFoundError < BaseError; end
  class DuplicateError < BaseError; end
  class TransactionError < BaseError; end
  class ConfigurationError < BaseError; end
end

# app/services/safe_user_service.rb
class SafeUserService
  include ROMErrors
  
  def initialize(container = ROM_CONTAINER)
    @container = container
    @user_repo = UserRepository.new(container)
  end
  
  # Safe user creation
  def create_user_safely(params)
    # Input validation
    validate_user_input(params)
    
    @container.gateways[:default].transaction do
      @user_repo.create_user(params)
    end
  rescue ROM::ValidationError => e
    raise ValidationError.new("User validation failed", e.errors)
  rescue ROM::ConstraintError => e
    if e.message.include?('email')
      raise DuplicateError.new("Email already exists", { email: params[:email] })
    else
      raise ValidationError.new("Data constraint violation", { error: e.message })
    end
  rescue => e
    raise TransactionError.new("User creation failed", { original_error: e.message })
  end
  
  # Safe user retrieval
  def get_user_safely(id)
    @user_repo.find(id)
  rescue ROM::TupleCountMismatchError
    raise NotFoundError.new("User not found", { user_id: id })
  rescue => e
    raise BaseError.new("Failed to retrieve user", { user_id: id, error: e.message })
  end
  
  # Safe user update
  def update_user_safely(id, params)
    validate_user_input(params, updating: true)
    
    @container.gateways[:default].transaction do
      # User existence check
      user = get_user_safely(id)
      
      # Email duplicate check
      if params[:email] && params[:email] != user.email
        existing_user = @user_repo.find_by_email(params[:email])
        if existing_user && existing_user.id != id
          raise DuplicateError.new("Email already exists", { email: params[:email] })
        end
      end
      
      @user_repo.update_user(id, params)
    end
  rescue NotFoundError, DuplicateError
    raise
  rescue ROM::ValidationError => e
    raise ValidationError.new("User validation failed", e.errors)
  rescue => e
    raise TransactionError.new("User update failed", { user_id: id, error: e.message })
  end
  
  # Safe user deletion
  def delete_user_safely(id)
    @container.gateways[:default].transaction do
      user = get_user_safely(id)
      
      # Pre-deletion check
      posts_count = @container.relations[:posts].where(user_id: id).count
      if posts_count > 0
        raise ValidationError.new(
          "Cannot delete user with existing posts", 
          { user_id: id, posts_count: posts_count }
        )
      end
      
      @user_repo.delete_user(id)
      true
    end
  rescue NotFoundError, ValidationError
    raise
  rescue => e
    raise TransactionError.new("User deletion failed", { user_id: id, error: e.message })
  end
  
  # Safe execution of bulk operations
  def bulk_create_users_safely(users_data)
    results = { success: [], failed: [] }
    
    @container.gateways[:default].transaction do
      users_data.each_with_index do |user_params, index|
        begin
          user = create_user_safely(user_params)
          results[:success] << { index: index, user: user }
        rescue BaseError => e
          results[:failed] << { 
            index: index, 
            params: user_params, 
            error: e.message, 
            details: e.details 
          }
        end
      end
      
      # Rollback if many failures
      if results[:failed].length > users_data.length * 0.5
        raise TransactionError.new(
          "Bulk operation failed: too many errors", 
          { success_count: results[:success].length, failed_count: results[:failed].length }
        )
      end
    end
    
    results
  rescue TransactionError
    raise
  rescue => e
    raise TransactionError.new("Bulk operation failed", { error: e.message })
  end
  
  # Database integrity check
  def perform_data_integrity_check
    issues = []
    
    begin
      # Orphaned data check
      orphaned_posts = @container.relations[:posts]
        .left_join(:users, id: :user_id)
        .where(users[:id] => nil)
      
      if orphaned_posts.count > 0
        issues << {
          type: :orphaned_data,
          table: :posts,
          count: orphaned_posts.count,
          message: "Posts without valid user references found"
        }
      end
      
      # Duplicate email check
      duplicate_emails = @container.relations[:users]
        .dataset
        .group(:email)
        .having { count.function.* > 1 }
        .select(:email)
        .count
      
      if duplicate_emails > 0
        issues << {
          type: :duplicate_data,
          table: :users,
          count: duplicate_emails,
          message: "Duplicate email addresses found"
        }
      end
      
      # Invalid data check
      invalid_users = @container.relations[:users]
        .where { (age < 0) | (age > 150) }
        .count
      
      if invalid_users > 0
        issues << {
          type: :invalid_data,
          table: :users,
          count: invalid_users,
          message: "Users with invalid age values found"
        }
      end
      
    rescue => e
      raise BaseError.new("Integrity check failed", { error: e.message })
    end
    
    {
      healthy: issues.empty?,
      issues: issues,
      checked_at: Time.current
    }
  end
  
  # Data cleanup
  def cleanup_invalid_data
    cleanup_results = {}
    
    @container.gateways[:default].transaction do
      # Delete orphaned posts
      orphaned_posts_count = @container.relations[:posts]
        .left_join(:users, id: :user_id)
        .where(users[:id] => nil)
        .delete
      
      cleanup_results[:orphaned_posts_removed] = orphaned_posts_count
      
      # Fix invalid user data
      invalid_users_fixed = @container.relations[:users]
        .where { (age < 0) | (age > 150) }
        .update(age: 0)  # Reset invalid age to 0
      
      cleanup_results[:invalid_users_fixed] = invalid_users_fixed
      
      cleanup_results[:cleaned_at] = Time.current
    end
    
    cleanup_results
  rescue => e
    raise TransactionError.new("Data cleanup failed", { error: e.message })
  end
  
  private
  
  def validate_user_input(params, updating: false)
    errors = []
    
    if !updating || params.key?(:name)
      name = params[:name]
      errors << "Name cannot be empty" if name.nil? || name.strip.empty?
    end
    
    if !updating || params.key?(:email)
      email = params[:email]
      if email.nil? || email.strip.empty?
        errors << "Email cannot be empty"
      elsif !email.match?(/\A[^@\s]+@[^@\s]+\z/)
        errors << "Invalid email format"
      end
    end
    
    if !updating || params.key?(:age)
      age = params[:age]
      if age.nil?
        errors << "Age cannot be empty"
      elsif age < 0 || age > 150
        errors << "Age must be between 0 and 150"
      end
    end
    
    raise ValidationError.new("Input validation failed", { errors: errors }) if errors.any?
  end
end

# Usage example and error handling
class UserController
  def initialize
    @user_service = SafeUserService.new
  end
  
  def create
    user = @user_service.create_user_safely(params)
    render json: { success: true, user: user }
  rescue ROMErrors::ValidationError => e
    render json: { 
      success: false, 
      error: "Validation failed", 
      details: e.details 
    }, status: 422
  rescue ROMErrors::DuplicateError => e
    render json: { 
      success: false, 
      error: "Duplicate data", 
      details: e.details 
    }, status: 409
  rescue ROMErrors::TransactionError => e
    render json: { 
      success: false, 
      error: "Transaction failed", 
      details: e.details 
    }, status: 500
  rescue => e
    render json: { 
      success: false, 
      error: "Unexpected error", 
      message: e.message 
    }, status: 500
  end
  
  def update
    user = @user_service.update_user_safely(params[:id], params)
    render json: { success: true, user: user }
  rescue ROMErrors::NotFoundError => e
    render json: { 
      success: false, 
      error: "User not found", 
      details: e.details 
    }, status: 404
  rescue ROMErrors::ValidationError, ROMErrors::DuplicateError => e
    render json: { 
      success: false, 
      error: e.message, 
      details: e.details 
    }, status: 422
  rescue => e
    render json: { 
      success: false, 
      error: "Update failed", 
      message: e.message 
    }, status: 500
  end
  
  def health_check
    integrity_result = @user_service.perform_data_integrity_check
    
    if integrity_result[:healthy]
      render json: { 
        status: "healthy", 
        checked_at: integrity_result[:checked_at] 
      }
    else
      render json: { 
        status: "issues_found", 
        issues: integrity_result[:issues], 
        checked_at: integrity_result[:checked_at] 
      }, status: 200
    end
  rescue => e
    render json: { 
      status: "check_failed", 
      error: e.message 
    }, status: 500
  end
end