Development Data That Resembles Production

I’ve spent twenty years looking at payment systems, and if there is one thing I’ve learned, it’s that tutorials are lying to you. They show you a pristine, three-line script that populates a table with “User A” and “User B” and call it a day. But they never show you what happens when your seed script triggers a cascading foreign key violation that locks your entire local instance, or how a poorly designed generator creates a dataset so massive it chokes your machine before you even finish your coffee. If you are looking for a sanitized guide on how to seed a database for development, you are in the wrong place; I’m more interested in the moment the terminal turns red.

I’m not here to teach you the happy path. My goal is to walk you through the actual, messy reality of building seeders that won’t break your workflow or your sanity. I’ll show you how to handle relational complexity, how to manage state without nuking your progress, and how to write scripts that actually survive a growing codebase. We aren’t just filling tables; we are building a reliable foundation that stays stable even when the edge cases start hitting.

Table of Contents

Why Your Mock Data for Development Environments Will Lie to You

Why Your Mock Data for Development Environments Will Lie to You

The problem with most mock data for development environments is that it’s too polite. When you use a library to generate a thousand rows of “John Doe” and “Jane Smith,” you aren’t testing your system; you’re testing a fantasy. These tools create perfectly formatted strings and predictable integers that follow the “happy path” every single time. They don’t account for the fact that a user’s last name might contain a single quote that breaks your string concatenation, or that a legacy payment record might have a null field where your new validation logic expects a non-empty string.

In my experience, the most dangerous part of populating relational databases is the illusion of coverage. You’ll build a beautiful schema, run your scripts, and see a green checkmark in your CI pipeline, feeling entirely confident. Then, you’ll hit production, and a single edge case—a timestamp from 1970 or a character set your ORM didn’t expect—will bring the whole service to its knees. Real data is messy, inconsistent, and occasionally nonsensical. If your seed scripts don’t reflect that chaos, they aren’t helping you develop; they’re just helping you stay comfortable.

Sql Seed Scripts Best Practices for When Everything Collapses

Sql Seed Scripts Best Practices for When Everything Collapses

The first rule of surviving a deployment is to stop treating your seed scripts like a static list of names and addresses. If you aren’t using automated test data generation to create edge cases—like null values, massive strings, or non-UTF8 characters—you aren’t actually testing anything. I’ve spent too many late nights fixing production outages caused by a “perfect” development environment that never once encountered a duplicate primary key or a broken foreign key constraint. Your scripts should be designed to fail in ways that mimic real-world entropy.

When you are populating relational databases, idempotency is your only real friend. I don’t care how elegant your ORM seeding techniques look on paper; if running that script a second time results in a “Unique Constraint Violation” error, your script is a liability. I write my SQL to check for existence before insertion, or better yet, I use upsert logic that ensures the state is predictable every single time. You want a script that can be run a thousand times without turning your local schema into a tangled mess of duplicate rows.

Five ways to stop lying to your future self

  • Stop using “Test User 1” and “Test User 2”. If your seed script doesn’t include edge cases like names with apostrophes, extremely long strings, or non-Latin characters, you aren’t testing your database; you’re just performing a ritual to satisfy a linter.
  • Build your seeds to be idempotent. If you can’t run your seed script five times in a row without it throwing a unique constraint violation or doubling your row count, your script is a liability, not a tool.
  • Respect the foreign key constraints. I’ve seen too many junior devs try to seed a `transactions` table before they’ve even populated the `users` table. Your seed script should mirror the actual dependency graph of your schema, or it will fail the moment you add a new mandatory relation.
  • Seed the “unhappy” state. A good developer seeds the data that makes the system work; a senior developer seeds the data that makes the system struggle. Include expired tokens, null values in nullable-but-tricky columns, and timestamps that sit right on the edge of a leap year.
  • Version your seed data alongside your migrations. If you change a column from `NULL` to `NOT NULL` in a migration but forget to update your seed script, your local environment is broken before you’ve even written your first line of business logic.

Lessons from the wreckage

Stop building “perfect” data; if your seed script doesn’t include edge cases like null values, massive strings, or duplicate IDs, you aren’t testing your code, you’re just stroking its ego.

Treat your seed scripts like production code—version them, test them, and for heaven’s sake, make them idempotent so you don’t end up with ten thousand “Test User” rows every time you restart your container.

The goal of seeding isn’t just to have a populated UI; it’s to create a controlled environment where you can actually trigger the errors that the tutorials promised would never happen.

## The Illusion of the Clean Slate

A perfect, sanitized seed script is just a lie you tell yourself to feel safe; real development begins when you realize your data needs to be just as messy, unpredictable, and broken as the production environment you’re actually trying to protect.

Pernille Askeland

Beyond the Happy Path

Testing software resilience Beyond the Happy Path.

At the end of the day, a good seeding strategy isn’t about making your local environment look pretty; it’s about making it resilient. If your scripts only generate perfect, clean strings and positive integers, you aren’t actually testing your system—you’re just performing a ritual. You need the null values, the massive strings that exceed column limits, and the orphaned foreign keys that mimic the chaos of a production environment. Remember that robust seed scripts are your first line of defense against the edge cases that will inevitably trigger a 3:00 AM incident report. Don’t just seed for the features you’re building; seed for the failures you haven’t anticipated yet.

I’ve spent two decades watching developers celebrate a successful deployment, only to see it crumble because their testing data was too sanitized to reveal the cracks in the logic. Building a reliable development environment is a bit like my bookbinding—it requires patience, an eye for structural integrity, and an understanding that the most important part is often the invisible binding holding everything together. Stop chasing the perfect tutorial output and start building for the breakage. When you design your data to fail, you finally gain the ability to write code that actually lasts.

Frequently Asked Questions

How do I handle foreign key constraints in my seed scripts without writing a massive, unmaintainable mess of ordered execution?

Stop trying to build a perfect hierarchy of execution. You’ll spend hours mapping out the exact order of every `INSERT` statement, only for a teammate to add a new mandatory column and break your entire sequence.

At what point does a custom seeding script become a liability that I’ll eventually have to refactor during a production migration?

The moment you start hardcoding business logic into your seeds, you’ve built a time bomb. If your script isn’t just injecting raw data, but is instead calculating tax rates or simulating complex user states using the same logic as your production services, you’re in trouble. When that logic changes in the app, your seeds will silently drift. You’ll eventually spend a frantic Monday morning refactoring a “simple” migration because your seed script and your schema are no longer speaking the same language.

How can I ensure my seed data actually mimics the edge cases—like null values or massive strings—that usually trigger the errors my happy-path tests miss?

Stop relying on Faker alone. It’s great for names and addresses, but it’s too polite. To actually find the cracks, you have to be malicious. I build my seeders with a “Chaos Profile”: one row with a 10MB string, one with a null where a non-nullable constraint should be, and one with a UTF-8 character that looks like a smudge. If your seed script doesn’t intentionally try to break your schema, it’s just lying to you.

About Pernille Askeland

I write the part the tutorial skipped: what it looks like when it breaks.

Scroll to Top