How we built Versions and Migrations

Ife Ajiboye

Every software team takes pride in shipping fast, but it’s too common to hear about features that still sit unlaunched because your homegrown billing setup will take months to factor them in. At Orb, we’ve built a first-in-class Versions and Migrations feature for our pricing data model to tackle this head-on. Our goal is that your billing system works with you when you need to roll out a new SKU, change how you measure usage, or modify an enterprise subscription.

On the surface, implementing a price change mechanic in a billing system sounds simple; you just need yet another long-awaited edit button! Alas, it’s no coincidence that other systems don’t offer this at all; the engineering team resorts to poorly audited migration scripts, data dumps in spreadsheets shared with finance, and the dreaded moment where the script is running overnight and you’re past the point of ‘undo’. 

In order to understand how we approached this problem at Orb and the principles we followed, we start first by looking at the foundational mechanic underlying Orb’s billing engine.

A brief aside: how Orb generates invoices

In Orb, each of your Customers has a Subscription to a Plan. For example, suppose Orb is tracking a customer Acuity Technologies which is subscribed to the Professional plan which consists of two Prices, Data Ingestion and Storage. Once the subscription is active, users might add or remove prices from the original plan (e.g. to enable add-ons not included as part of Professional).

An Orb subscription connects a customer to a “timeline” of price objects, where each price timeline has a starting point, billing cadence, and an optional ending point. If Acuity technologies has been a subscriber since January 1 and each price bills monthly, the subscription consists of two very simple timelines starting on January 1 without an end point. 

Given just a Subscription’s data model state, Orb can generate the full set of invoices that should exist for the subscription. In this case, that’s an infinite set of monthly invoices starting January 1. If a change is made to the subscription, it’s very simple to process. Suppose, for example, that as of April 16, a new monthly usage add-on is added to the subscription. The new set of price timelines will imply a slightly different set of invoices; our invoices now need to account for a price that should bill from April 16 - May 1, and then on each invoice thereafter. Orb takes the difference between the invoices that should exist and those that have already been generated, and simply re-creates any different invoices to accommodate the new timeline.

This basic concept – being able to construct and apply the “difference” between the invoices implied by two price timelines – is extremely powerful and it underlies Orb’s ability to power backdating, add-on modifications, and much more.

Building a versioned plan

Before we launched this feature, a Plan in Orb was intentionally immutable: in-place modifications should never be possible when you need a clean audit record to understand how each invoice was generated. In order to support safe rollouts of new pricing, we needed to layer in the concept of a version, where new versions could be made ‘default’ and then separately migrated between.

Because we want to allow any subscription to be moved from one version to the next, the relationship between prices across versions is extremely important – Orb keeps track of the difference between an addition, edit, and removal. This makes migrations safe: Orb understands that a price that’s been ad-hoc removed on an individual subscription (e.g. by a user’s action) shouldn’t be re-added to the subscription when a new plan version contains an edited version. In other words, there’s a tracked lineage of how a given price has evolved between plan versions.

Smart, scalable migrations

Once a plan version has been created and new customers are using the new prices, Orb allows you to migrate subscriptions from legacy versions to the newest version in bulk. This is simple in theory: we have a core primitive that can apply an edit to a subscription which will atomically re-generate the set of invoices that should exist given the new timeline of prices.

There were two tricky questions both centered around timing. 

First, you need to determine when the new pricing should apply (the “effective date” of the new version). It’s rare that you want your price change to apply immediately; you might want to have new pricing apply either at a specific scheduled date or even “whenever the next billing cycle for that specific customer rolls over”. Since Orb reflects changes to a given subscription based on its new timeline of prices, these technical primitives allow maximum flexibility – your new pricing can be effective any date of your choosing. Not only does that date not have to be the same across all your customers, Orb can use its knowledge of the subscription’s current timeline to align the change to the billing cycle for that individual customer.

The second question was when Orb – the system – should perform the migration across your subscriptions. All migrations need to propagate to subscriptions immediately, regardless of the effective date. This is key for a few reasons: any upcoming invoice needs to reflect your new version of pricing, and you should see the upcoming prices for the new version when you preview how the subscription will change over time. When the effective date arrives, perhaps months from triggering the migration, the correct invoice exists without any reliance on a “just in time” mutation. In most cases, this is a pattern that we’ve applied everywhere. We do not schedule work to happen at a future point in time, which would expose the system to an array of correctness edge cases. Instead, the future invoicing state is built atomically with the action that’s taken.

The final and perhaps most important consideration was scale: the process of propagating the new version’s prices can take some time, especially when you’re migrating millions of subscriptions. Orb guarantees that – even if you kicked off the migration at 11:59pm – all invoices have the correct pricing . To ensure this outcome, Orb checks for any pending migrations as a safety mechanism before issuing any invoice for a subscription, and applies them immediately if necessary.

Looking ahead

Plan versioning has enabled teams at companies like Replit and Pinecone to make changes confidently and save many engineering days of effort. 

And, we have a rich roadmap ahead of us. We’re building support to migrate subscriptions that were customized as a result of negotiations. We’re also more options so your migration takes effect exactly when you want it to. Soon you’ll be able to schedule a migration for the beginning of the current cycle, effectively backdating your change. 

Across our customer base, we see many large-scale migrations run every single week across thousands of subscriptions – a testament to how dynamic pricing can be when the tools are available.

Sound interesting? We’re hiring.

posted:
May 1, 2024
Category:
Eng Deep Dive

Ready to solve billing?

Contact us to learn how you can revamp your billing infrastructure today.

Let's talk.

Thank you! We'll be in touch shortly.
Oops! Something went wrong while submitting the form.