Let Your Seeds Grow

Written by Dan Redington on December 16, 2025

If you've ever joined a Rails project and spent your first day hunting down Slack threads to figure out how to get your local environment into a workable state, you've experienced the cost of bad seed data.

Most teams treat seeds.rb as an afterthought, either ignoring it entirely or filling it with a random handful of records that don't reflect how the app actually works. This is a missed opportunity. A well-maintained seed file is one of the highest-leverage investments you can make in developer experience and velocity.

The Problem: Seeds as an Afterthought

In the wild, I've seen two common patterns:

Pattern 1: No seeds at all

The database is empty after running migrations. New developers have to manually create accounts, users, and all the prerequisite records just to see the app render a single page. This compounds over time as the data model grows.

Pattern 2: A random smattering of basic records

There's a User.create! and maybe a Company.create!, but they don't reflect real-world relationships or edge cases. You still end up in the Rails console 20 minutes into your workday, manually wiring more realistic data together.

Both approaches slow you down. Worse, they create inconsistency across environments. Your local setup looks nothing like staging, which looks nothing like the ephemeral environment spun up for your PR.

A Better Approach: Seeds as a Contract

Seed data should be a reasonable starting point for a new environment: local, staging, or otherwise. That means:

  • If you need to do a bunch of manual setup to get your app into a testable state, your seeds are insufficient.
  • You should always feel confident running rails db:reset and knowing you'll land in a functional, realistic state.

Reflect Common Use Cases

Your seed file should include the structures your app actually uses in production. Ask yourself:

  • Do your tests recreate the same set of records over and over? (e.g., to test a BlogPost you need a Topic, a User, some Tag records, an Account, etc.)
  • Are there specific record states that come up repeatedly in development or QA?

If yes, those belong in seeds.

Here's a minimal example for a hypothetical blogging platform:

# db/seeds.rb

# Core account structure
acme_account = Account.create!(name: "Acme Corporation", subdomain: "acme", plan: "business")

# Users with common roles
admin_user = User.create!(email: "admin@acme.test", name: "Alice Admin", role: "admin", account: acme_account)
author_user = User.create!(email: "author@acme.test", name: "Bob Writer", role: "author", account: acme_account)

# Realistic content structure
rails_topic = Topic.create!(name: "Ruby on Rails", slug: "ruby-on-rails", account: acme_account)

published_post = BlogPost.create!(title: "Getting Started with Rails 7", body: "Lorem ipsum dolor sit amet...", status: "published", author: author_user, topic: rails_topic, published_at: 2.days.ago)

draft_post = BlogPost.create!(title: "Advanced Active Record Patterns", body: "Draft content here...", status: "draft", author: author_user, topic: rails_topic)

# Common tags
Tag.create!([
{ name: "tutorial", account: acme_account },
{ name: "beginner", account: acme_account },
{ name: "advanced", account: acme_account }
])

This seed file gives you:

  • A complete account hierarchy
  • Users in different roles
  • Published and draft content
  • Associated records

Now when you run rails db:reset, you have a working app—not an empty shell.

Make It Referenceable

Use common, generic, or humorous names for seed records. "Acme Corporation" is a classic. "Alice Admin" and "Bob Writer" are easy to remember. This makes it trivial to reference seed data in tests or development:

# spec/models/blog_post_spec.rb

RSpec.describe BlogPost, type: :model do
  let(:account) { Account.find_by(name: "Acme Corporation") }
  let(:author) { User.find_by(email: "author@acme.test") }

  describe "validations" do
    let(:blog_post) { BlogPost.new(title: "Test", body: "Content", account:, author:) }

    it { is_expected.to be_valid }
  end
end

No need to create an account, user, and all the prerequisite records in a before block. They already exist. You're testing the edge case, not the scaffolding.

Tip
You may find that you quickly run into naming collisions with your tests. author is very generic and you're already using it in some specs. A functional alternative is to use personas. For example, Robert is our persona for our author so instead of author@acme.test use robert@acme.test and the corresponding let(:robert) variable name.

The Impact

When you treat seeds as a first-class tool, several things improve:

Lower Barrier for QA

Testing on a new ephemeral environment or a fresh local setup is the same every time. QA engineers don't need tribal knowledge to get started. They run db:reset and they're ready to test.

Faster, Focused Tests

You eliminate cumbersome setup in specs. Instead of recreating the same structure of records for every test, you reference what already exists and focus on edge cases:

# Instead of this:
before do
  @account = Account.create!(name: "Test Co")
  @user = User.create!(email: "test@example.com", account: @account)
  @topic = Topic.create!(name: "Tech", account: @account)
  # ... 15 more lines
end

# You write this:
let(:account) { Account.find_by(name: "Acme Corporation") }
let(:user) { User.find_by(email: "author@acme.test") }

Because you're not creating a mountain of records for every test, your test suite runs much faster. We've seen test execution time drop significantly on projects that adopt this pattern.

You can take this paradigm even further by defining a shared context that can be simply included and you now have access to the RSpec variables directly.

# spec/support/shared_contexts/seed_data.rb
RSpec.shared_context :default_context do
  let(:acme_account) { Account.find_by(name: "Acme Corporation") }
  let(:admin_user) { User.find_by(email: "admin@acme.test") }
  let(:author_user) { User.find_by(email: "author@acme.test") }
  let(:rails_topic) { Topic.find_by(slug: "ruby-on-rails") }
  let(:tutorial_tag) { Tag.find_by(name: "tutorial") }
end

And to use you simply include the context

RSpec.describe BlogPost, type: :model do
  include_context :default_context
end

Because our definition of seed data includes common use cases, a shared context is a perfect place to setup references to said use cases. Now with one include statement you're ready to start testing new features or edge cases.

More Complete Testing

I can't tell you how many issues I've found in testing due to the basic data not being available. Your test works fine until you have a blog post with tags included, then it blows up! It's really easy to miss edge cases when your base case is incomplete. By having a reliable set of data to work from you are less likely to fall into this insufficient data trap.

Better Onboarding

New developers joining your team will love you. Their first rails db:setup gives them a working app, not a puzzle to solve. Replacing your laptop? No worries, you can have your local environment setup in no time.

Wrapping Up

Rails seed files are underrated. When used intentionally, they're a force multiplier for developer experience, testing velocity, and environment consistency.

Start small:

  1. Identify the records you create over and over in tests or development.
  2. Move them into seeds.rb with clear, memorable names.
  3. Start a shared context with easy reference to your seed data.
  4. Work towards making rails db:reset a safe, fast operation you can run anytime.

Your future self and your team will thank you.

Your idea is ready. We're here to build it.

From prototype to production, we help you get there faster. We know how to balance speed, quality, and budget so your product starts delivering value right away.

Senior full stack engineersHIPAA experiencedRails and React specialists