Sitemap

The Hidden Complexity of “Just Change the SKU”

5 min readSep 14, 2026

--

Why subscription migrations are never as simple as they sound

A customer clicks “Upgrade Plan.” A few seconds later, they’re on a new plan. Feels like flipping a switch.

It isn’t. Underneath that click is a distributed transaction spanning multiple services, each owning a different piece of truth about the customer — and none of them can be allowed to disagree, even for a moment longer than necessary.

The illusion of simplicity

“Just change the SKU” sounds like a single database update: swap SKU A for SKU B in a subscriptions table, done.

But a SKU isn’t just an ID — it’s a pointer to a bundle of business rules: what products it unlocks, what price it charges, what billing cycle it follows, what it’s allowed to convert to or from. Changing it means re-deriving all of those downstream facts and pushing them out to every system that cares about them.

And critically: subscription platforms are rarely single services. A typical flow spans a billing/rating engine (calculates charges), a subscription record system (source of truth for plan state), an entitlement service (controls what the customer can actually use), and often a provisioning layer (spins up or tears down product access). These are usually owned by different teams, deployed independently, and updated asynchronously.

1. Validating the transition

Not all SKU-to-SKU moves are legal. Systems typically encode this as a transition rule matrix — a table or rules engine defining allowed source→target pairs, often gated by conditions like current billing status, contract type, or existing add-ons.

The technical trap here: this matrix grows combinatorially. With N products × M tiers × K billing cycles, the number of valid and invalid transitions explodes. Teams that hardcode transition logic in application code end up with sprawling if/else trees that become unmaintainable. The more mature pattern is a declarative rules engine — transitions defined as data, not code — so adding a new product doesn’t mean touching every existing code path.

2. Proration and billing math

Mid-cycle changes require prorating the old plan (credit unused time) and charging for the new one. This sounds like arithmetic, but it’s actually a set of edge cases:

  • What if the customer already used a discount or promo credit this cycle?
  • What if the new plan has a different billing frequency — monthly to annual?
  • What if the currency or tax jurisdiction changes mid-migration?
  • What if the proration math itself needs to be reversible, in case the migration is later rolled back?

Billing engines usually solve this with an immutable ledger model — instead of directly mutating a balance, every proration is recorded as a discrete, timestamped entry, a credit or a charge. This makes the system auditable and reversible, because you can always replay or reverse individual ledger entries instead of trying to “undo” a mutated number.

3. Entitlement propagation

Entitlements — what features or products the customer can actually access — often live in a separate system from billing, for good reason: billing changes should be able to happen independently of access changes, such as a grace period where access continues despite a billing failure.

Get Taniya Adil’s stories in your inbox

Join Medium for free to get updates from this writer.

This separation is exactly what makes entitlement propagation hard. The billing update and the entitlement update are not one atomic operation — they’re two separate writes to two separate systems, connected by a message or event. If the billing service publishes an event and the entitlement service’s consumer is behind, briefly, the two systems are in different states.

Most designs accept this as eventual consistency and instead focus on bounding the time window and detecting drift, rather than trying to force strict atomicity across service boundaries — which usually requires distributed transactions or sagas, expensive and fragile at scale.

4. The failure mode that matters most

Here’s the scenario that actually breaks systems: billing succeeds, the event is published, but the entitlement service fails to process it — maybe it’s down, maybe the message is malformed, maybe there’s a downstream timeout.

The naive fix is “just retry the whole migration.” This is dangerous for one reason: the billing step already succeeded. Retrying it means re-charging or re-crediting the customer. Now you have a duplicate financial transaction layered on top of a stuck entitlement.

This is why migrations need to be broken into idempotent, individually retryable steps, each protected by its own idempotency key — not one big irreversible operation. An idempotency key, typically a hash of the request payload plus a unique migration ID, lets the billing service recognize “I’ve already processed this exact request” and return the prior result instead of re-executing it. This turns a retry from a liability into a safe no-op.

5. Reconciliation as a safety net

Even with idempotent steps, drift happens — a message gets lost, a service crashes mid-processing, a network partition delays an event indefinitely. No design fully prevents this; the honest engineering answer is to detect and correct it after the fact.

This is where reconciliation jobs come in: scheduled processes that compare state across systems — does the subscription record match the entitlement record match the billing record — and automatically correct mismatches, or flag them for manual review if the correction is ambiguous. Mature subscription platforms treat reconciliation not as a bug-fix mechanism but as a first-class part of the architecture, because at scale, some percentage of migrations will always drift, and the system needs to be self-healing rather than relying on migrations being perfect.

6. Auditability

Every step above needs to leave a trace — not for debugging convenience, but because subscription changes are financial events. When a customer disputes a charge, or a reconciliation job flags a mismatch, someone needs to reconstruct exactly what state the system was in at each step of the migration. This usually means an append-only audit log per migration, keyed by a migration ID, capturing the before and after state of each system touched.

The takeaway

The SKU change itself is trivial — a single field update. The real engineering is everything built around it: rule engines to validate transitions, ledger-based billing to make proration reversible, idempotency keys to make retries safe, reconciliation jobs to catch drift, and audit trails to make the whole thing explainable after the fact.

Next time someone says “just change the SKU,” you’ll know exactly why that sentence hides an entire distributed systems problem.

--

--