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.
# 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:
group :development, :test do
gem 'faker'
endNow you can generate realistic sample data. Imagine you need 50 products with descriptions, prices, and categories for your e-commerce site:
# 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.
# 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 recordsA common refinement is to track which seed operations have run, especially if you have multiple developers adding seeds at different times:
# 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')
endThis 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:
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 fixturesThen load them conditionally in your main seed file:
# 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:
# 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
endThe demo_data file generates sample products, users, and orders for developers to work with:
# 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 workThis 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.rbas 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_bywith 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.






Comments
Loading comments…