Skip to content
ExponaBLOG
RUBY ON RAILS · DAY 093 / 100

Rails seeds and sample data for Ruby on Rails developers

Realistic data for development.

Expona Admin7 min read

Introduction

Getting realistic data into your Rails development environment without manually creating records one by one is a problem every team faces. Rails seeds and sample data for Ruby on Rails developers are essential tools that let you populate your database consistently across the whole team, ensuring everyone works with the same baseline. Whether you're onboarding a new developer or resetting your local database after a schema change, a well-structured seed file can save hours of tedious setup.

In this article, we'll walk through building maintainable seed files that generate realistic data, stay idempotent, and separate what's required from what's optional for different environments.

Using db/seeds.rb for Initial Data

The db/seeds.rb file is your entry point for seeding. Rails gives you a clean interface: define what needs to exist, then run rails db:seed to populate it. The key is treating this file as a contract with your team about what baseline data should always be present.

Let's say you're building an e-commerce platform. You need core data like shipping zones, payment methods, and default tax rates. These aren't user-generated; they're configuration your system depends on.

ruby
# db/seeds.rb

# Shipping zones
SHIPPING_ZONES = [
  { name: 'Domestic', country_code: 'US', base_rate: 5.99 },
  { name: 'Europe', country_code: 'EU', base_rate: 15.99 },
  { name: 'Rest of World', country_code: 'RoW', base_rate: 25.99 }
].freeze

SHIPPING_ZONES.each do |zone_attrs|
  ShippingZone.find_or_create_by(name: zone_attrs[:name]) do |zone|
    zone.country_code = zone_attrs[:country_code]
    zone.base_rate = zone_attrs[:base_rate]
  end
end

# Payment methods
PAYMENT_METHODS = [
  { provider: 'stripe', name: 'Credit Card', active: true },
  { provider: 'paypal', name: 'PayPal', active: true }
].freeze

PAYMENT_METHODS.each do |method_attrs|
  PaymentMethod.find_or_create_by(provider: method_attrs[:provider]) do |m|
    m.name = method_attrs[:name]
    m.active = method_attrs[:active]
  end
end

puts "Seeded core configuration data"

This approach keeps your seeds deterministic. Running it multiple times doesn't create duplicates or fail—it just verifies the data exists. Team members clone the repo, run rails db:seed, and get an identical starting point.

One practical tip: include a summary at the end so developers can see what was actually created or updated. Add counts to your puts statements to give feedback.

Using Faker for Sample Content

Realistic sample data makes development and testing much more effective than Lorem Ipsum placeholders. The Faker gem generates convincing fake names, emails, addresses, product descriptions, and more. It's invaluable for creating bulk demo data that mimics your production workload.

Add Faker to your Gemfile in the development and test groups:

ruby
group :development, :test do
  gem 'faker'
end

Now you can generate realistic sample data. Imagine you need 50 products with descriptions, prices, and categories for your e-commerce site:

ruby
# db/seeds/sample_products.rb

require 'faker'

CATEGORIES = ['Electronics', 'Clothing', 'Home & Garden', 'Sports'].freeze

def seed_sample_products(count = 50)
  puts "Creating #{count} sample products..."
  
  count.times do |i|
    category = CATEGORIES.sample
    
    Product.find_or_create_by(sku: "SKU-#{1000 + i}") do |product|
      product.name = Faker::Commerce.product_name
      product.description = Faker::Lorem.paragraphs(number: 2).join("\n")
      product.category = category
      product.price = Faker::Commerce.price(range: 10.0..500.0)
      product.stock_quantity = rand(5..200)
      product.weight = Faker::Number.decimal(l_digits: 2, r_digits: 2)
    end
  end
  
  puts "Created #{count} sample products"
end

seed_sample_products(50) if ENV['SEED_SAMPLES'] == 'true'

Notice the SKU generation here—it's deterministic and unique, preventing duplicates across multiple seed runs. The `find_or_create_by` block ensures we only generate data that doesn't already exist.

You can layer different Faker calls to build realistic relationships too. If you're seeding customer orders, each order gets a real-looking timestamp and a realistic product selection from your existing catalog.

Idempotent Seeds with find_or_create_by

An idempotent seed file produces the same result no matter how many times you run it. This is critical because developers will reset their databases frequently during development, and you don't want the seed process to fail or create duplicates on the second run.

The pattern is straightforward: use find_or_create_by with a unique attribute. For records without a natural unique key, generate one deterministically.

ruby
# db/seeds.rb

# Good: find_or_create_by with natural unique identifier
AdminUser.find_or_create_by(email: 'admin@example.com') do |user|
  user.password = 'securepassword123'
  user.role = 'administrator'
  user.created_at = Time.current
end

# Also good: compound unique key
Address.find_or_create_by(
  user_id: user.id,
  address_type: 'billing'
) do |addr|
  addr.street = Faker::Address.street_address
  addr.city = Faker::Address.city
  addr.postal_code = Faker::Address.postcode
  addr.country = 'US'
end

# Avoid: always creating new records
user = User.create!(email: Faker::Internet.email) # Bad! Creates duplicates

# Avoid: update without checking existence
User.update_all(active: true) # Could affect unrelated records

A common refinement is to track which seed operations have run, especially if you have multiple developers adding seeds at different times:

ruby
# db/seeds.rb

class SeedLog
  def self.mark(key)
    File.write(".seed_log/#{key}.txt", Time.current.to_s) unless File.exist?(".seed_log/#{key}.txt")
  end
  
  def self.run?(key)
    !File.exist?(".seed_log/#{key}.txt")
  end
end

if SeedLog.run?('core_payment_methods')
  # ... seed payment methods
  SeedLog.mark('core_payment_methods')
end

This is more sophisticated than you might initially need, but it scales well as your seed file grows and different team members contribute different pieces.

Separating Demo and Required Data

Not all seed data is equal. Some data is required for your application to function (payment methods, roles, default settings), while other data is purely for demonstration during development. Mixing them creates confusion and bloat.

Structure your seeds directory to separate concerns:

ruby
db/
  seeds.rb
  seeds/
    01_core_data.rb       # Required for all environments
    02_demo_data.rb       # Optional sample content
    03_test_data.rb       # Test-specific fixtures

Then load them conditionally in your main seed file:

ruby
# db/seeds.rb

require_relative 'seeds/01_core_data'

# Only seed demo data in development
if Rails.env.development?
  require_relative 'seeds/02_demo_data'
end

# Never seed test data from seeds.rb—use test factories instead
# require_relative 'seeds/03_test_data'

The core_data file holds shipping zones, payment methods, tax rates—anything production needs:

ruby
# db/seeds/01_core_data.rb

# These must exist in every environment
TaxRate.find_or_create_by(region: 'US', rate: 0.08)
TaxRate.find_or_create_by(region: 'CA', rate: 0.10)

PaymentMethod.find_or_create_by(code: 'credit_card') do |m|
  m.name = 'Credit Card'
  m.active = true
end

The demo_data file generates sample products, users, and orders for developers to work with:

ruby
# db/seeds/02_demo_data.rb

require 'faker'

puts "Seeding demo data..."

# Create demo users
5.times do |i|
  User.find_or_create_by(email: "user#{i}@example.dev") do |user|
    user.password = 'password'
    user.name = Faker::Name.name
  end
end

# Create sample products and orders
# ... generate realistic data for feature work

This split makes it clear what's infrastructure versus what's scaffolding. When a junior developer joins, they run rails db:seed and get everything. When you deploy to staging, you can choose to skip demo data if you prefer production-like conditions.

Common Mistakes

Forgetting idempotency: The most frequent error is writing seeds that assume a clean database. Using create! without checking existence will fail on the second run. Always use find_or_create_by or find_by with fallback.

Hardcoding environment-specific logic: Avoid checking Rails.env inside seed blocks. Move entire seed files between development and production concerns, and let your db:seed invocation decide what runs.

Storing test fixtures in seeds: Your db/seeds.rb should never create FactoryBot factories or test-only data. Use proper test factories or fixtures for that. Seeds are for development baseline data.

Massive single seed file: If your seed file grows beyond a few hundred lines, split it. The modular approach with seeds/ subdirectories becomes essential for team collaboration and maintenance.

Not handling dependencies: If a Product belongs to a Category, ensure categories are seeded first. Order your seed files or use explicit checks to create missing dependencies on demand.

Key Takeaways

  • Use db/seeds.rb as the contract defining baseline data every developer should have in their local environment.
  • Leverage Faker to generate realistic sample content for products, users, and test scenarios without manual data entry.
  • Build idempotent seeds using find_or_create_by with unique identifiers so running them multiple times produces identical, conflict-free results.
  • Separate required infrastructure data (payment methods, roles, tax rates) from optional demo data (sample products, sample orders) using subdirectories and conditional loading.
  • Keep seed files modular and clearly ordered so team members can locate, review, and update data generation logic easily.
  • Avoid mixing seeds with test fixtures—use factories and test-specific setup for unit and integration tests instead.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles