Organizing Rails Models in Folders with Custom Table Names
As Rails applications grow in complexity, organizing models into logical folders becomes essential for maintainability and code organization. This comprehensive guide explores how to structure models in folders while configuring table names to reflect the folder hierarchy, creating a clean and scalable architecture.
Why Organize Models in Folders?
Benefits of Folder Organization
- Logical Grouping: Related models stay together
- Namespace Separation: Prevents naming conflicts
- Improved Navigation: Easier to find specific models
- Domain-Driven Design: Reflects business domains
- Team Collaboration: Clear ownership boundaries
Common Use Cases
- E-commerce:
products/,orders/,payments/ - CRM:
customers/,sales/,support/ - Content Management:
articles/,media/,comments/ - HR System:
employees/,payroll/,performance/
Basic Folder Structure
Let's start with a typical Rails application structure:
app/
├── models/
│ ├── user.rb
│ ├── product.rb
│ ├── order.rb
│ └── payment.rb
After organizing into folders:
app/
├── models/
│ ├── user.rb
│ ├── catalog/
│ │ ├── product.rb
│ │ ├── category.rb
│ │ └── variant.rb
│ ├── sales/
│ │ ├── order.rb
│ │ ├── line_item.rb
│ │ └── invoice.rb
│ └── billing/
│ ├── payment.rb
│ ├── subscription.rb
│ └── credit_card.rb
Setting Up Namespaced Models
Step 1: Create Folder Structure
# Create the directory structure
mkdir -p app/models/catalog
mkdir -p app/models/sales
mkdir -p app/models/billing
mkdir -p app/models/admin
Step 2: Generate Namespaced Models
# Generate models with namespace
rails generate model Catalog::Product name:string price:decimal
rails generate model Sales::Order total:decimal status:string
rails generate model Billing::Payment amount:decimal method:string
Step 3: Configure Table Names
By default, Rails will create table names like catalog_products, sales_orders, etc. Here's how to customize this behavior:
Option A: Explicit Table Name Configuration
# app/models/catalog/product.rb
class Catalog::Product < ApplicationRecord
self.table_name = 'catalog_products'
validates :name, presence: true
validates :price, presence: true, numericality: { greater_than: 0 }
end
Option B: Automatic Table Name Generation
# app/models/catalog/product.rb
class Catalog::Product < ApplicationRecord
# Rails automatically generates 'catalog_products' table name
validates :name, presence: true
validates :price, presence: true, numericality: { greater_than: 0 }
end
Option C: Custom Table Name Pattern
# app/models/catalog/product.rb
class Catalog::Product < ApplicationRecord
self.table_name = "#{name.deconstantize.underscore}_#{name.demodulize.underscore.pluralize}"
validates :name, presence: true
validates :price, presence: true, numericality: { greater_than: 0 }
end
Complete Example: E-commerce Application
Let's build a complete e-commerce application with organized models:
Database Migrations
# db/migrate/20240101000001_create_catalog_products.rb
class CreateCatalogProducts < ActiveRecord::Migration[7.0]
def change
create_table :catalog_products do |t|
t.string :name, null: false
t.text :description
t.decimal :price, precision: 8, scale: 2, null: false
t.string :sku, null: false
t.integer :stock_quantity, default: 0
t.boolean :active, default: true
t.references :catalog_category, null: false, foreign_key: true
t.timestamps
end
add_index :catalog_products, :sku, unique: true
add_index :catalog_products, :active
add_index :catalog_products, :catalog_category_id
end
end
# db/migrate/20240101000002_create_catalog_categories.rb
class CreateCatalogCategories < ActiveRecord::Migration[7.0]
def change
create_table :catalog_categories do |t|
t.string :name, null: false
t.string :slug, null: false
t.text :description
t.references :parent, null: true, foreign_key: { to_table: :catalog_categories }
t.boolean :active, default: true
t.timestamps
end
add_index :catalog_categories, :slug, unique: true
add_index :catalog_categories, :parent_id
end
end
# db/migrate/20240101000003_create_sales_orders.rb
class CreateSalesOrders < ActiveRecord::Migration[7.0]
def change
create_table :sales_orders do |t|
t.string :order_number, null: false
t.references :user, null: false, foreign_key: true
t.decimal :subtotal, precision: 8, scale: 2, null: false
t.decimal :tax_amount, precision: 8, scale: 2, default: 0
t.decimal :total, precision: 8, scale: 2, null: false
t.string :status, null: false, default: 'pending'
t.datetime :shipped_at
t.timestamps
end
add_index :sales_orders, :order_number, unique: true
add_index :sales_orders, :user_id
add_index :sales_orders, :status
end
end
Model Definitions
Catalog Models
# app/models/catalog/category.rb
class Catalog::Category < ApplicationRecord
self.table_name = 'catalog_categories'
has_many :products, class_name: 'Catalog::Product', foreign_key: 'catalog_category_id'
has_many :children, class_name: 'Catalog::Category', foreign_key: 'parent_id'
belongs_to :parent, class_name: 'Catalog::Category', optional: true
validates :name, presence: true
validates :slug, presence: true, uniqueness: true
scope :active, -> { where(active: true) }
scope :root_categories, -> { where(parent_id: nil) }
def self.find_by_slug(slug)
find_by(slug: slug)
end
def full_name
parent ? "#{parent.full_name} > #{name}" : name
end
end
# app/models/catalog/product.rb
class Catalog::Product < ApplicationRecord
self.table_name = 'catalog_products'
belongs_to :category, class_name: 'Catalog::Category', foreign_key: 'catalog_category_id'
has_many :line_items, class_name: 'Sales::LineItem', foreign_key: 'catalog_product_id'
has_many :orders, through: :line_items, source: :order
validates :name, presence: true
validates :price, presence: true, numericality: { greater_than: 0 }
validates :sku, presence: true, uniqueness: true
validates :stock_quantity, numericality: { greater_than_or_equal_to: 0 }
scope :active, -> { where(active: true) }
scope :in_stock, -> { where('stock_quantity > 0') }
scope :by_category, ->(category) { where(catalog_category_id: category.id) }
def in_stock?
stock_quantity > 0
end
def formatted_price
"$#{price.to_f}"
end
def reduce_stock!(quantity)
update!(stock_quantity: stock_quantity - quantity)
end
end
Sales Models
# app/models/sales/order.rb
class Sales::Order < ApplicationRecord
self.table_name = 'sales_orders'
belongs_to :user
has_many :line_items, class_name: 'Sales::LineItem', foreign_key: 'sales_order_id', dependent: :destroy
has_many :products, through: :line_items, source: :product
has_many :payments, class_name: 'Billing::Payment', foreign_key: 'sales_order_id'
validates :order_number, presence: true, uniqueness: true
validates :total, presence: true, numericality: { greater_than: 0 }
validates :status, inclusion: { in: %w[pending processing shipped delivered cancelled] }
scope :pending, -> { where(status: 'pending') }
scope :shipped, -> { where(status: 'shipped') }
scope :recent, -> { order(created_at: :desc) }
before_create :generate_order_number
after_create :reduce_product_stock
def can_cancel?
status.in?(%w[pending processing])
end
def total_items
line_items.sum(:quantity)
end
def formatted_total
"$#{total.to_f}"
end
private
def generate_order_number
self.order_number = "ORD-#{Time.current.strftime('%Y%m%d')}-#{SecureRandom.hex(4).upcase}"
end
def reduce_product_stock
line_items.each do |line_item|
line_item.product.reduce_stock!(line_item.quantity)
end
end
end
# app/models/sales/line_item.rb
class Sales::LineItem < ApplicationRecord
self.table_name = 'sales_line_items'
belongs_to :order, class_name: 'Sales::Order', foreign_key: 'sales_order_id'
belongs_to :product, class_name: 'Catalog::Product', foreign_key: 'catalog_product_id'
validates :quantity, presence: true, numericality: { greater_than: 0 }
validates :unit_price, presence: true, numericality: { greater_than: 0 }
before_save :calculate_total
def total_price
quantity * unit_price
end
private
def calculate_total
self.total = total_price
end
end
Billing Models
# app/models/billing/payment.rb
class Billing::Payment < ApplicationRecord
self.table_name = 'billing_payments'
belongs_to :order, class_name: 'Sales::Order', foreign_key: 'sales_order_id'
belongs_to :payment_method, class_name: 'Billing::PaymentMethod', foreign_key: 'billing_payment_method_id'
validates :amount, presence: true, numericality: { greater_than: 0 }
validates :status, inclusion: { in: %w[pending processing completed failed] }
scope :completed, -> { where(status: 'completed') }
scope :failed, -> { where(status: 'failed') }
def formatted_amount
"$#{amount.to_f}"
end
def success?
status == 'completed'
end
end
# app/models/billing/payment_method.rb
class Billing::PaymentMethod < ApplicationRecord
self.table_name = 'billing_payment_methods'
belongs_to :user
has_many :payments, class_name: 'Billing::Payment', foreign_key: 'billing_payment_method_id'
validates :method_type, inclusion: { in: %w[credit_card paypal bank_transfer] }
validates :is_default, uniqueness: { scope: :user_id }, if: :is_default?
scope :active, -> { where(active: true) }
scope :default, -> { where(is_default: true) }
def display_name
case method_type
when 'credit_card'
"Credit Card ending in #{last_four}"
when 'paypal'
"PayPal (#{email})"
when 'bank_transfer'
"Bank Transfer"
end
end
end
Advanced Configuration Patterns
Custom Table Name Helpers
# app/models/concerns/custom_table_naming.rb
module CustomTableNaming
extend ActiveSupport::Concern
class_methods do
def set_namespaced_table_name
namespace = name.deconstantize.underscore
model_name = name.demodulize.underscore
self.table_name = "#{namespace}_#{model_name.pluralize}"
end
def set_prefixed_table_name(prefix)
model_name = name.demodulize.underscore
self.table_name = "#{prefix}_#{model_name.pluralize}"
end
end
end
Usage:
# app/models/catalog/product.rb
class Catalog::Product < ApplicationRecord
include CustomTableNaming
set_namespaced_table_name # Results in 'catalog_products'
# ... rest of model
end
Base Classes for Namespaces
# app/models/catalog/base.rb
class Catalog::Base < ApplicationRecord
self.abstract_class = true
# Common behavior for all catalog models
scope :active, -> { where(active: true) }
def self.table_name_prefix
'catalog_'
end
end
# app/models/catalog/product.rb
class Catalog::Product < Catalog::Base
# Inherits table_name_prefix, so table becomes 'catalog_products'
validates :name, presence: true
# ... rest of model
end
Handling Associations Across Namespaces
Explicit Class Names
# app/models/user.rb
class User < ApplicationRecord
has_many :orders, class_name: 'Sales::Order'
has_many :payment_methods, class_name: 'Billing::PaymentMethod'
def recent_orders
orders.recent.limit(5)
end
end
Foreign Key Conventions
# app/models/sales/order.rb
class Sales::Order < ApplicationRecord
# Rails expects 'user_id' for belongs_to :user
belongs_to :user
# Explicit foreign key for namespaced associations
has_many :line_items, class_name: 'Sales::LineItem', foreign_key: 'sales_order_id'
end
Database Indexing Strategies
Composite Indexes for Namespaced Tables
# db/migrate/create_composite_indexes.rb
class CreateCompositeIndexes < ActiveRecord::Migration[7.0]
def change
# Catalog indexes
add_index :catalog_products, [:catalog_category_id, :active]
add_index :catalog_products, [:active, :stock_quantity]
# Sales indexes
add_index :sales_orders, [:user_id, :status]
add_index :sales_orders, [:status, :created_at]
# Billing indexes
add_index :billing_payments, [:sales_order_id, :status]
add_index :billing_payment_methods, [:user_id, :is_default]
end
end
Testing Namespaced Models
RSpec Configuration
# spec/rails_helper.rb
RSpec.configure do |config|
# Configure factory paths for namespaced models
config.include FactoryBot::Syntax::Methods
end
Factory Definitions
# spec/factories/catalog/products.rb
FactoryBot.define do
factory :catalog_product, class: 'Catalog::Product' do
name { "Sample Product" }
price { 29.99 }
sku { "SKU-#{SecureRandom.hex(4)}" }
stock_quantity { 10 }
active { true }
association :category, factory: :catalog_category
end
end
# spec/factories/catalog/categories.rb
FactoryBot.define do
factory :catalog_category, class: 'Catalog::Category' do
name { "Sample Category" }
slug { name.parameterize }
active { true }
end
end
Model Tests
# spec/models/catalog/product_spec.rb
RSpec.describe Catalog::Product, type: :model do
describe 'table name' do
it 'uses the correct table name' do
expect(described_class.table_name).to eq('catalog_products')
end
end
describe 'associations' do
it 'belongs to category' do
expect(described_class.reflect_on_association(:category).class_name).to eq('Catalog::Category')
end
end
describe 'validations' do
subject { build(:catalog_product) }
it { should validate_presence_of(:name) }
it { should validate_presence_of(:price) }
it { should validate_uniqueness_of(:sku) }
end
end
Controller Organization
Namespaced Controllers
# app/controllers/catalog/products_controller.rb
class Catalog::ProductsController < ApplicationController
before_action :set_product, only: [:show, :edit, :update, :destroy]
def index
@products = Catalog::Product.active.includes(:category)
end
def show
@related_products = @product.category.products.active.limit(4)
end
private
def set_product
@product = Catalog::Product.find(params[:id])
end
def product_params
params.require(:catalog_product).permit(:name, :price, :description, :sku, :stock_quantity, :catalog_category_id)
end
end
Routes Configuration
# config/routes.rb
Rails.application.routes.draw do
namespace :catalog do
resources :categories do
resources :products
end
resources :products
end
namespace :sales do
resources :orders do
resources :line_items
end
end
namespace :billing do
resources :payments
resources :payment_methods
end
end
Performance Considerations
Eager Loading
# app/models/sales/order.rb
class Sales::Order < ApplicationRecord
scope :with_details, -> { includes(:line_items, :user, line_items: :product) }
def self.detailed_orders
with_details.limit(10)
end
end
Query Optimization
# Efficient queries across namespaces
def recent_order_summary
Sales::Order.joins(:line_items, :user)
.joins('JOIN catalog_products ON sales_line_items.catalog_product_id = catalog_products.id')
.select('sales_orders.*, users.name as user_name, COUNT(sales_line_items.id) as item_count')
.group('sales_orders.id, users.name')
.order(created_at: :desc)
.limit(10)
end
Best Practices
1. Consistent Naming Conventions
# Good: Consistent namespace and table naming
class Catalog::Product < ApplicationRecord
self.table_name = 'catalog_products'
end
class Sales::Order < ApplicationRecord
self.table_name = 'sales_orders'
end
2. Clear Association Definitions
# Always specify class_name for cross-namespace associations
class Sales::Order < ApplicationRecord
has_many :line_items, class_name: 'Sales::LineItem', foreign_key: 'sales_order_id'
has_many :products, through: :line_items, source: :product, class_name: 'Catalog::Product'
end
3. Proper Index Management
# Add indexes for foreign keys in namespaced tables
add_index :sales_orders, :user_id
add_index :sales_line_items, :sales_order_id
add_index :sales_line_items, :catalog_product_id
4. Documentation
# Document namespace purpose and relationships
# app/models/catalog/README.md
# Catalog Namespace
#
# Contains all product catalog related models:
# - Product: Individual sellable items
# - Category: Product categorization
# - Variant: Product variations (size, color, etc.)
#
# Table naming convention: catalog_[model_name]
Common Pitfalls and Solutions
1. Association Confusion
# Problem: Ambiguous associations
class Sales::Order < ApplicationRecord
has_many :line_items # Rails doesn't know which LineItem
end
# Solution: Explicit class names
class Sales::Order < ApplicationRecord
has_many :line_items, class_name: 'Sales::LineItem', foreign_key: 'sales_order_id'
end
2. Migration Naming
# Problem: Unclear migration names
class CreateProducts < ActiveRecord::Migration[7.0]
def change
create_table :catalog_products do |t|
# ...
end
end
end
# Solution: Clear migration names
class CreateCatalogProducts < ActiveRecord::Migration[7.0]
def change
create_table :catalog_products do |t|
# ...
end
end
end
3. Factory Naming
# Problem: Factory name conflicts
factory :product, class: 'Catalog::Product' do
# ...
end
# Solution: Namespaced factory names
factory :catalog_product, class: 'Catalog::Product' do
# ...
end
Conclusion
Organizing Rails models in folders with properly configured table names provides significant benefits for large applications:
- Improved Organization: Logical grouping of related models
- Better Maintainability: Easier to navigate and understand code structure
- Domain Separation: Clear boundaries between different business areas
- Scalability: Supports growth without becoming unwieldy
- Team Productivity: Multiple developers can work on different domains
The key is to establish consistent patterns early and maintain them throughout the application's lifecycle. With proper setup, namespaced models with folder-based table names create a robust foundation for complex Rails applications.
Remember to always:
- Use explicit class names in associations
- Maintain consistent naming conventions
- Add proper indexes for foreign keys
- Document your namespace organization
- Test thoroughly, especially cross-namespace relationships
This approach scales well from small applications to large enterprise systems, providing the structure needed for long-term maintenance and development.