Migrations Are Not a Start Script

Article 106 / Architecture

Putting a migration in your API's start command feels convenient until the first time two things start at once. Give it its own job.

Putting a migration in your API's start command feels convenient until the first time two things start at once. Give it its own job.

The shortest path to a working database is a line in the start script: migrate, then serve. It works on the first day and fails in ways that are hard to name.

What the start script hides

When migration and serving share one process, three things get tangled. A failed migration looks like a crashing API. Two replicas race to migrate the same schema. And the log of what the migration did is interleaved with request logs, so nobody reads it.

None of this hurts on a one-service laptop project. It starts to hurt the first time a second service needs the same database.

A migration is a job with an exit code

A job has a clear contract: it runs, it succeeds or fails, and it ends. That is a much better thing to depend on than a long-running process that happens to also touch the schema.

In Flisk the rule is explicit. New services, examples and fixtures run migrations in a resource of type migrator. The API declares dependsOn on it. A dependant starts only after the migrator has exited 0, and when the job fails the services after it are not started. For local processes, a one-shot job with no port counts as ready when it exits 0 and is never restarted.

The API no longer needs to know a migration exists. It only needs the schema to be there when it starts.

What you get for free

  • A readable failure. The migrator's own log says what failed, and the API is simply not started, which is a clearer signal than a restart loop.
  • One runner per schema. Scaling the API to more replicas cannot start more migrations.
  • Honest ordering. Everything that needs the schema waits for the job, and everything that does not is free to start.

Scaffolding with --feature prisma on a backend creates the matching -migrator resource that runs prisma migrate deploy, with the API already depending on it, so the right shape is the default shape.

The objection, and the answer

The usual objection is that a separate resource is more ceremony than one extra line. It is, by about one small file. In exchange, the question "did the schema change apply" has a single place to look, and the answer is a process exit code rather than something you infer from whether the API came up.