We use analytics to understand how our website is used. No personal data is collected.

August 11, 2025 · Piyush Ranjan Mishra

Zero-Downtime Database Migrations with AWS Step Functions

AWSStep FunctionsDatabaseLoot Discount

At Loot Discount I used AWS Step Functions to design and execute data migration workflows moving data from development to production databases. It was part of the same push — CloudFormation templates and CI/CD pipelines alongside it — that took release cycles from weekly to daily. “Migration” here wasn’t a one-time schema change — it was an ongoing operational need as the platform evolved, and doing it safely, repeatedly, without downtime or manual babysitting, was the actual engineering problem.

Why Step Functions over a migration script

The naive approach — a script that reads from source, transforms, writes to destination, run it once by hand — works for a one-off migration on a quiet Sunday. It stops working as a repeatable operational tool the moment you need: resumability after a partial failure, visibility into progress for a migration touching millions of rows, and the ability to safely retry just the failed portion without redoing completed work. Step Functions’ state machine model gives you all three as structural properties of how the workflow is built, not as things you bolt on with custom code.

ExtractBatch → TransformBatch → ValidateBatch → LoadBatch → CheckpointProgress
     ↑______________________________________________________________|
                         (loop until source exhausted)

Batching and checkpointing: the core pattern

Every migration was structured as a loop over batches, not a single monolithic pass — extract a batch, transform it, validate it, load it, checkpoint progress, repeat. The checkpoint step recorded exactly which batch had been successfully completed, so a failure partway through a multi-hour migration meant resuming from the last successful checkpoint, not restarting from row zero. This is the same idempotent-and-resumable principle that matters for any long-running data process, and Step Functions’ native support for state persistence between steps made it far less error-prone than hand-rolling checkpoint tracking in application code.

Validation as a first-class step, not an afterthought

A dedicated ValidateBatch step, separate from transform and load, checked row counts, referential integrity, and business-rule sanity (e.g., no order records referencing a nonexistent user) before data was actually loaded into production. Catching a bad batch here — before it hits production — is a completely different incident severity than catching it after, when other systems and users may have already interacted with the corrupted data. It’s tempting to skip this step for “simple” migrations to save time; every case where I skipped it and regretted it, the migration wasn’t actually as simple as it looked.

Zero-downtime meant the destination stayed live and correct throughout

The “zero-downtime” part specifically meant the production database remained fully queryable and correct at every point during migration — not a maintenance window, not eventually-consistent intermediate states visible to users. This shaped how loads were structured: batches were loaded as complete, valid units (never a partially-written record visible mid-write), and for migrations touching data actively being read by the live application, load order was chosen to avoid ever exposing a foreign-key reference to data that hadn’t been loaded yet. Getting load ordering right for the specific data model in play mattered more than any generic “just batch it” advice — it required understanding the actual dependency graph of what’s being migrated.

Handling failures without losing what already succeeded

When a batch failed validation or a transient AWS service error interrupted a load, Step Functions’ built-in retry and catch semantics handled the transient case automatically (backoff and retry), while a validation failure routed to a distinct failure path that halted the specific batch without rolling back or blocking already-completed batches. This distinction mattered a lot operationally — a transient network blip and a genuine data-quality problem need different responses, and conflating them (retrying a batch that’s failing because the data is actually bad) just wastes time and obscures the real issue.

What actually made this repeatable, not just a one-time success

The migrations that went smoothly weren’t the ones where I got lucky — they were the ones where checkpointing, validation, and dependency-aware load ordering were designed in from the start, so each new migration was mostly reusing a proven pattern rather than reinventing the approach under time pressure. The single habit I’d recommend to anyone building this kind of pipeline: never let “this migration is small, I’ll just run a script” bypass checkpointing and validation — the migrations that hurt are rarely the ones you planned carefully for.