Your Billing Migration is Really a State Machine

Your Billing Migration is Really a State Machine

A customer opens their inbox and finds two invoices for the same subscription. Another customer shows up in the retention report as a churn, except they never cancelled. Their payment failed, the subscription lapsed in the billing system, and the lapse got recorded as a lost customer. I led the architecture on a migration that produced both of these bugs. A large file-sharing company was replacing its homegrown quoting and billing system with Salesforce CPQ and Salesforce Billing, integrated with Oracle ERP, across hundreds of thousands of customer accounts. The data moved fine. State was the problem.First, A Little About Me I’m Naresh Goyal, a Salesforce and GTM systems architect with 20+ years in CPQ, billing, and revenue systems. I hold the Revenue Cloud Advanced certification and led the architecture on the migration in this story. Alongside revenue systems work, I've been focused on bringing AI into go-to-market teams. I led AI deployment across a GTM organization, built an AI orchestration platform for revenue workflows, and set up a Salesforce MCP integration with Claude so agents can work directly with CRM data. That work keeps reinforcing the same lesson as this migration: AI agents are only as reliable as the state they read.I’m sharing these lessons because billing migrations usually fail quietly, and most fixes only surface in postmortems.Your subscription state lives in four places at once. A subscription does not live in one system. Quoting knows what was sold. Billing knows what was invoiced and paid. Provisioning knows what the customer can access. The ERP knows what revenue was recognized. Each system holds a partial copy of the truth, and each one updates on its own schedule. When those copies disagree, the bug lands on a customer. Billing treats a subscription as active and keeps invoicing it after provisioning has shut off access. Or the reverse. The failures we kept hitting were duplicate invoices for one subscription, churn records for customers whose only problem was a failed payment, and provisioning that fired at the wrong moment, granting access before payment was confirmed or withholding it after.None of these are data movement bugs. The records arrived. They arrived carrying state that the receiving system read differently.Write the state machine down before you configure anything. Our fix started on a whiteboard. Every subscription got an explicit state machine: named states and the allowed transitions between them. Draft, quoted, ordered, activated, suspended, amended, expired, cancelled. A subscription could move from ordered to activated. It could not jump from quoted to invoiced, and any process that tried got rejected instead of quietly writing a bad state. Subscription state machine: every allowed transition and the one system that owns itThis sounds basic, and most billing systems never do it. State ends up implied by a status field here, a checkbox there, a date that may or may not be set. When state is implied, every integration invents its own reading of it, and you ship the disagreements to your customers.Here is a simplified version of the transition table. The important column is the last one. From To Trigger Owning system Draft Quoted Quote generated Quoting (CPQ) Quoted Ordered Quote accepted Quoting (CPQ) Ordered Activated Order recognized by finance Billing Activated Suspended Payment failed Billing Suspended Activated Payment recovered Billing Suspended Cancelled Dunning exhausted Billing Activated Amended Upsell or downgrade quoted Quoting (CPQ) Amended Activated Amendment order recognized Billing Activated Expired Term ends without renewal Billing Activated Cancelled Customer cancels Quoting (CPQ) Notice that "payment failed" moves a subscription to Suspended, not Cancelled. That row addresses the false churn problem from the opening. Churn reporting counts Cancelled and Expired, never Suspended, so a failed card no longer looks like a lost customer. Do this on your own migration: list the states, list the transitions, and name the one system allowed to make each transition. If two systems can both push a subscription into the same state, you have found a future incident.Provisioning should wait for the financial event. The sharpest disagreement sat between billing and provisioning. The legacy setup triggered provisioning off the sales process: a deal closed, a webhook fired, access followed. A closed deal is a sales opinion. The financial system had not yet recognized the order, and sometimes it never did. We rebuilt the handshake around an event. Provisioning fires only when the order product reaches the Activated state, the point where the financial system officially recognizes the order. The trigger is the state change itself. No timer. No scheduled job guessing. No sales stage.Access granted before payment and access denied after payment look like opposite bugs. They have the same root cause: provisioning listening to the wrong signal. Bind it to the financial state and both failure modes close.Profile the source data before you touch configuration. Before any configuration began, we profiled the full source data, and it was humbling. Thousands of accounts were ghosts. Some had no opportunities at all. Some held closed deals with no products attached. Account hierarchies pointed at parent accounts that no longer existed. Migrating that as-is would have carried every one of those problems into the new platform, where each would have surfaced as a migration defect and burned the team's credibility in the first week. We stopped, cleaned the accounts and repaired the hierarchies with the revenue operations team, and only then started loading.Your source data is worse than you think. The time to find out is before the new system exists, while cleanup is still a data exercise instead of a production incident. If you want a checklist for that first pass, the three questions to ask before any data migration is a good place to start.Cut over in phases, and check before every switch. We did not flip the whole company in one weekend. During the cutover window, the legacy system stayed read-only while both systems stayed in sync. Downstream consumers then switched one at a time: finance first, then provisioning, then the customer success tools. Before each switch, a validation checkpoint compared both systems on the records that the consumer cared about. A phased cutover turns one giant risk into a series of small, checkable ones. When a checkpoint fails, you know which consumer and which record class broke, and the legacy system is still there to compare against. For the checkpoints themselves, this account of proving a 12-million-record migration correct shows what real source-to-target reconciliation looks likeKeep migrated history out of your new pricing rules. Historical records carry commercial terms from deals signed years ago. New pricing rules know nothing about those deals. Run a migrated contract through a new pricing engine, and it can reprice a renewal under rules that did not exist when the customer signed. We put a bypass flag on every migrated quote, opportunity, and order. Pricing and product rules check the flag and skip those records, which keep their original terms through a separate legacy price book. New business runs through the live pricing engine. History stays history. It is one more case of a lesson CPQ teams learned long ago: the pricing engine is only as good as the catalog and data underneath it.Run revenue schedules in parallel until finance signs off. Revenue schedules were the last piece, because finance owns them and finance does not accept assurances. We migrated the in-progress schedules into rules-based recognition, then ran the old and new systems side by side. The final cutover happened only after consecutive clean cycles, with finance signing off on each one. If your migration touches revenue recognition, build the parallel run into the plan from the start, with a named sign-off at the end of it. It is slower. It is also the only version of that cutover that survives an audit conversation.State first, data second. If you are planning a billing or subscription migration, spend your design time on state. Write the state machine. Decide which system owns each transition. Bind provisioning to the financial event. Profile and clean the source before you configure anything. Cut over in phases with checkpoints, wall history off from new pricing logic, and let finance call the revenue cutover. Once those decisions are made, the data load is close to a weekend job. The decisions are the migration.

Original Source

Read the full article at Hackernoon →

KhanList aggregates and links to publicly available news content. We do not host full articles from third-party sources. Always verify important information with original sources.