9 min read
Organizing Rails Models in Folders with Custom Table Names
Ruby on RailsArchitecture
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.rbAfter 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.rbSetting 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/adminStep 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:stringStep 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 }
endOption 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 }
endOption 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 }
endComplete 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
endModel Definitions
Catalog Models
# app/models/catalog/category.rb
class Catalog::Category { 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 { 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
endSales Models
# app/models/sales/order.rb
class Sales::Order { 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
endBilling Models
# app/models/billing/payment.rb
class Billing::Payment { 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 { 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
endAdvanced 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
endUsage:
# app/models/catalog/product.rb
class Catalog::Product < ApplicationRecord
include CustomTableNaming
set_namespaced_table_name # Results in 'catalog_products'
# ... rest of model
endBase Classes for Namespaces
# app/models/catalog/base.rb
class Catalog::Base { 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
endHandling 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
endForeign 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'
endDatabase 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
endTesting Namespaced Models
RSpec Configuration
# spec/rails_helper.rb
RSpec.configure do |config|
# Configure factory paths for namespaced models
config.include FactoryBot::Syntax::Methods
endFactory 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
endModel 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
endController 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
endRoutes 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
endPerformance Considerations
Eager Loading
# app/models/sales/order.rb
class Sales::Order { includes(:line_items, :user, line_items: :product) }
def self.detailed_orders
with_details.limit(10)
end
endQuery 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)
endBest 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'
end2. 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'
end3. 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_id4. 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'
end2. 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
end3. Factory Naming
# Problem: Factory name conflicts
factory :product, class: 'Catalog::Product' do
# ...
end
# Solution: Namespaced factory names
factory :catalog_product, class: 'Catalog::Product' do
# ...
endConclusion
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.