Migrating live subscription billing without your customers noticing
One of the highest-stakes projects I’ve led was migrating live e-commerce orders and subscriptions from WooCommerce onto Stripe billing — for a healthcare product where a missed renewal means a patient’s medication subscription lapses. Nobody could see an interruption. Not one customer.
This post is the playbook I wish I’d had at the start. The patterns generalize to any live billing migration, whatever the systems involved.
Rule zero: the old system stays authoritative until proven otherwise
The single biggest mistake in billing migrations is flipping authority too early. Until the moment of cutover, the legacy system remains the source of truth, and the new system is a shadow being validated against it.
That means running both in parallel and comparing outputs — not for a day, but for full billing cycles. Subscriptions have temporal behavior that only shows up when a renewal actually fires. A migration validated only against static data will pass every test and then fail on the first renewal that hits an edge case: a paused subscription, a mid-cycle plan change, a card that expired during the migration window.
Backfill history, don’t just port state
It’s tempting to migrate only current state — active subscriptions, current plans, next billing dates. Resist it. Support teams, refund flows, and dispute processes all reach into history. If a customer disputes a charge from four months before the migration, someone needs to trace it in the new system.
Our approach: backfill historical orders as first-class records in the new system, linked to their originals with a stable cross-reference ID. That last part matters more than it looks — during the messy weeks after cutover, the ability to hop from a record in the new system to its ancestor in the old one is what turns a support nightmare into a lookup.
Active-order linking is the hard part
Historical backfills are mechanical. The genuinely hard problem is active subscriptions: an order that exists in the old system, has a renewal scheduled, and must come alive in the new system at the right moment with the right state.
The pattern that worked:
- Freeze windows, per cohort. Never migrate everything at once. Slice by renewal date and migrate each cohort during its quietest window — right after a renewal fires, when the subscription has the longest runway before the next event.
- Idempotent creation. Every migration script must be safe to run twice. Partial failures will happen, and the recovery path should be “run it again,” not an archaeological dig.
- Suppress, then verify, then release. After creating the subscription in the new system, the old one doesn’t get deleted — it gets suppressed (renewals disabled) and retained as evidence. Deleting the fallback before validating the replacement is how you turn a bug into an outage.
Reconciliation is a permanent fixture, not a phase
We built a reconciliation job that compared the two systems continuously: every active subscription, its next billing date, its amount, its payment method status. Any divergence raised a flag before the customer’s renewal date arrived — which converts silent billing failures into tickets you handle proactively.
My rule of thumb: the reconciliation tooling should outlive the migration by at least one full billing cycle of every plan you offer. Annual plans mean a year.
The cutover should be boring
If the parallel run is validated, the backfills are linked, and reconciliation is green, then cutover day is a non-event — a configuration change, not a launch. That’s the goal. The drama budget for a billing migration should be spent entirely in the preparation, so the moment itself has none left.
Ours was boring. The patients never knew. That’s what success looks like in this kind of work: nothing visible happened at all.