Skip to main content

Migrating 600k users to a new billing service

Back in 2021, we helped our friends at Steady Media to design, plan and build an in-house alternative billing system that they could migrate their users to. The migration of their 600k+ users to the new system had to be seamless and preserve data integrity.

· 5 min read
Agathe Lenclen

Back in 2021, we helped our friends at Steady Media to design, plan and build an in-house alternative billing system that they could migrate their users to. This process and the implementation happened in multiple iterations and spanned over years. In 2025, we finally operated the migration of their 600k+ users to the new system. It had to be seamless and preserve data integrity. This blog post explores the strategy that was used to achieve this challenging task.

The backstory

Steady supports media makers in building an audience and driving revenue via subscriptions to newsletters, blogs etc. While most of the tools and features for media makers live in their main application, they outsourced the subscriptions & billing management to a third-party service (Chargebee). The media makers app was very tightly coupled to Chargebee as it relied on it for core business logic within the app, as well as accounting.

Graph illustrating the relationship between Steady Media Makers app and Chargebee via an API Layer

Introducing: Charger, the fraternal twin

Although Chargebee was useful initially, it became increasingly painful to work around their standard processes over time. This ultimately led to the development of a custom billing tool.

For this we had to reverse-engineer all of what Chargebee was doing. We started by building the required schemas, and exposing the API endpoints and webhook notifications that were needed by the Media Makers app. Any changes in the Media Makers app usage of the API/Webhooks, or to Chargebee itself, needed to be propagated to Charger. Both billing systems had to behave identical from the outside.

The Media Makers app now needed to forward calls to Charger and/or Chargebee. A “multiplexer“ abstraction close to the API & Webhook layer was added, so that for developers & users of the app, the fact that Charger or Chargebee was used for the calls would be transparent.

Graph illustrating the new billing system Charger, exposing the same API as Chargebee

Charger needed to process payments and refunds, with all payment providers supported by the Media Makers app. The first flow of the application was created: subscribe to a publication, generate an invoice, create a payment, and settle that payment 🎉!

A bird watching experience 🔭

At this stage, no real users were migrated to Charger. No real users were even created on Charger! The system just worked™️... Subscription statuses, invoices, payments processing being crucial to the system meant that we had to come up with a transition plan, that allowed the developers to observe and watch the behaviour of the system for just a handful of users. If any problem arose, it would need to affect only such a small amount of users that their support team could resolve it manually. We handpicked 10 users with rather simple data: a single recent subscription, no cancellations, and various payment providers / currencies to cover as much ground as possible.

Graph illustrating the migration job between the Media Makers app and Charger

Using our beloved Oban for job processing, we built a migration job, that would, for a given user:

  1. Fetch all their data from Chargebee
  2. Format it to please Chargerʼs data model
  3. Send it to a dedicated endpoint in Charger
  4. Charger creates all the given data for the user (subscriptions, invoices, payments, etc.)
  5. Cancel the user subscriptions on Chargebee (so that they donʼt get billed twice!)

Of course, all of this must happen in a transaction on both systems, fail gracefully and report meaningful errors to the devs. We leveraged Oban configuration to optimise the parallelisation and retry mechanism of the jobs, as we could be rate-limited by Chargebee. At some point, we would want to migrate users in batches of 100-10k users at a time, so the migration mechanism had to be robust and auto-heal.

Silent bugs

A main challenge in this process was to validate the integrity of the migrated data. The Media Makers app and Charger run on different databases, and have their own representation of what is an invoice, a subscription, a user, etc. Forgetting to migrate a field, or mapping the wrong timestamp from one system to the other, would happen silently because a part of the migration code lives in the Media Makers app, and the other part lives in Charger. The data could be silently missing or corrupted, and we would only realise down the road that all users were migrated with errors. Luckily, we had the chance of having access to a Chargebee staging instance with users in all kinds of configuration. This helped us a lot in finding migration issues by comparing the user data on both systems before and after the migration, and strengthening our tests to cover all the edge cases we could find. The developers at Steady also added ERPC, which enabled us to run integration tests evaluating the effects of Charger on the Media Makers app.

Conclusion

Once we were confident that our 10 users were rolling, we migrated 10 more users with more complex data. And so on, for weeks, increasing the batch size, and deciding to create new users on Charger with a rand() choice. This allowed Steady to steer the future of their billing strategy for their users, integrating with other payment providers and simplifying some of the user flows for refunds etc. While it looks simple on paper, this project required a lot of patience, collaboration and planning ahead as the stakes were really high and we had to build up confidence in the new system and in the migration approach.

Agathe Lenclen

Agathe Lenclen

Pattern Matching Sandwich Artist

We’re hiring

Work with our great team, apply for one of the open positions at bitcrowd