
Developer staffing guide · 12 minute read ·
Build a Rails background-migration checklist for outsourced schema work
Separate deploy-safe schema changes, resumable backfills, verification, and cleanup across multiple releases.
Readiness check
Is the handoff ready?
Use this table before you send the first ticket. Fix the weak spots while access is still limited.
| Area | Ready | Needs work |
|---|---|---|
| Scope | Expand, compatibility, backfill, verify, enforce, switch, and cleanup. | A broad improvement request without a stopping point |
| Evidence | Ordered resumable batches with guarded writes. | A success claim without reproducible artifacts |
| Boundary | Race tests, measurements, semantic comparison, and error samples. | Production access or private data used as a shortcut |
| Authority | Owners approve pace, constraints, switch, and cleanup. | The contributor approves consequential changes alone |
The migration is a series of application states
Replacing a legacy status requires more than adding a column and filling it. Map separate releases: add a nullable destination, deploy code tolerant of both forms, begin new writes, backfill, compare meaning, enforce the invariant, switch reads, and remove the legacy field later. Mark which old and new application versions work at each state.
This map reveals rollback limits. Application code can often roll back while both fields exist, but not after a column disappears or a constraint rejects old writes. Owners choose the overlap window. The delegated engineer turns that decision into migration code, jobs, measurements, and stop conditions.
A resumable batch needs an explainable cursor
Build a table fixture with ordinary rows, nulls, malformed values, new rows, and primary-key gaps. Process an ordered batch and store its last safe key. Stop after a partial batch and restart. Repetition must not overwrite a newer value or trigger a user-facing callback twice. Offset pagination is fragile when the dataset changes during work.
Choose bulk updates or model saves deliberately. Bulk operations may bypass callbacks and validations; model saves may send mail, alter timestamps, or execute expensive hooks. The migration should invoke only effects required by the invariant. Keep rejected rows visible with reasons instead of letting one malformed value stall every later batch.
Let the normal application update a row mid-batch
Pause after the worker reads a row, change that row through candidate application code, then continue. Observe whether the backfill overwrites the fresh value. Use a guarded update, version check, or derivation from current state. Repeat with the previous deployable application if rolling releases overlap.
Temporary dual-write or fallback-read logic needs metrics and a removal ticket. Otherwise compatibility code becomes permanent and nobody knows which field is authoritative. Test retry after a lock timeout and verify the cursor does not skip the row. A visited counter is not proof that the destination holds the correct value.
Compare semantics before enforcing and deleting
Measure batch duration, locks, database load, replica lag, errors, and pause time on a representative staging shape. Provide ordinary pause and resume controls. Set production pace from observed headroom, not a sleep copied from another migration. Operators approve schedules and thresholds.
Query for disagreement between legacy meaning and normalized value. Sample every category and inspect all rejected records. Have a second reviewer check the comparison. Hand off the release map, fixture, cursor, race tests, measurements, comparison, errors, rollback notes, and later cleanup ticket. Destructive cleanup waits for separate approval after production verification.
Validate progress and coexistence against a production-shaped fixture
Generate a dataset with skew rather than evenly sized rows: a dense recent key range, large historical gaps, a few records with expensive associations, malformed legacy states, and rows updated constantly by the application. Run the candidate batcher with query logging and database statistics enabled. Record rows selected, rows changed, conflicts, rejected rows, duration, lock wait, replication lag, and cursor movement for every batch. Stop after selection but before update, deploy the previous compatible application, change one selected row, and resume. The guarded update must preserve the newer application write and report the conflict for later evaluation. Kill the worker after commit but before its progress acknowledgement, then restart from the last durable cursor. Revisited rows should be harmless, while skipped rows should be demonstrably absent. Test descending or sparse identifiers only if the real key space requires them; the cursor strategy should follow actual ordering, not a fashionable abstraction.
Verification must express business meaning. For a normalized status, compare each legacy state with the expected destination and count disagreements by category. For derived money, compare exact integer minor units and currency rather than formatted text. For ownership changes, detect orphaned or multiply assigned records. Run the comparison while normal writes continue, then again at a defined cutover point. A zero remaining-null count can still conceal wrong values. Add database constraints as not-valid or equivalent staged forms when the platform supports that approach, validate them separately, and record lock behavior before enforcement. Keep compatibility reads and dual writes instrumented so their use falls to zero before removal. The release package names the deploy that introduced each phase, its rollback boundary, pause command, resume command, owner, thresholds, and cleanup prerequisite. This turns a background migration from a long-running script into an observable multi-release product change whose destructive steps cannot occur simply because a worker reached the end of its queue.
Test the cleanup release as carefully as the backfill. Remove fallback reads in a candidate branch and use instrumentation to prove no supported request still touches the legacy column. Run old and new application versions against the pre-cleanup schema, then verify the declared rollback window closes only when removal is approved. Check schema dumps, factories, seeds, serializers, exports, and analytics queries for stale references. A database view or downstream report can outlive application code and still depend on the old representation. Archive the semantic comparison and rejected-row resolution before dropping data. After cleanup in the disposable environment, restore from backup and demonstrate the documented recovery process. The owner receives an explicit point-of-no-return decision instead of discovering during an incident that application rollback cannot reconstruct information already deleted.
Place capacity limits around the worker. Run with the configured connection pool and ordinary web traffic, then raise concurrency until a predefined stop condition appears in the disposable environment. Confirm the pause command prevents new batches while allowing an in-flight transaction to finish, and that resume continues from the durable cursor. A deploy or process restart must not reset throttling assumptions or spawn overlapping schedulers. Record estimated remaining rows and throughput as separate values rather than promising a completion time from one fast batch. Review dashboards and paging thresholds with the database operator before rollout. The safest migration is one that can slow down without losing its place or hiding its backlog.
Copy-ready brief
Paste this into your hiring request
Expand, compatibility, backfill, verify, enforce, switch, and cleanup.
Ordered resumable batches with guarded writes.
Race tests, measurements, semantic comparison, and error samples.
Owners approve pace, constraints, switch, and cleanup.
Buyer questions
Questions about planning the role
Why multiple releases?
Old and new code can overlap, while cleanup has a different rollback boundary.
What makes a batch resumable?
Stable order, durable cursor, guarded writes, and pause controls.
When can the old field go?
After production comparison passes and no deployable code needs it.
Sources
Planning references
These links explain the security, code review, and worker classification points used in this guide.
- Rails Active Record migrationsOfficial migration mechanics.
- GitLab batched migrationsOperational pattern for large backfills.