Caching Stripe data for complete control of payment subscriptions

Piggy bank

Stripe subscriptions are a great way to offload a lot of complicated billing logic but it comes at a price. Stripe becomes the source of truth for all your billing and subscription data.

Problems with relying on Stripe subscriptions directly

When you use Stripe subscriptions directly, your billing data is no longer stored within your application. If you need to act on any of that data or even display it to users it’s a full round-trip request to the Stripe API.

For some applications, this may be okay. You may only be displaying subscription status on a single page. If that’s the case it may not be the end of the world if that page loads a bit slowly.

For other applications, immediate access to billing data is much more important. For example, when a user’s subscription is in a trial period you may want to display a banner at the top of the page. That would mean making a call to Stripe’s API on every single page load. Not ideal.

There’s another problem. What happens when Stripe’s API is having issues? What if it’s completely down? You have to handle those cases or your application is also down.

Rely less on the Stripe API

The most common solution to these problems is to duplicate bits of Stripe data in your database. First, you may find that you need a user’s subscription status (trialing, active, past due, etc.). So you add a stripe_subscription_status field to your users table. Then you need to know when the user’s trial will end. So you add a stripe_subscription_trial_ends field. Now, whenever you need either of them you query your local database.

For the most part, this will work. There are problems though. You now have two fields in your database storing subscription state. These fields are completely independent and you can update them independently. Because they can be updated independently, strange things can happen. What does it mean when stripe_subscription_status is trialing and stripe_subscription_trial_ends is NULL? How can a user be on a trial and yet have no trial end? This is an illegal state that Stripe does not allow but your application can easily end up in.

Cache all of the Stripe data

Instead of trying to store pieces of Stripe data, store all of the user’s stripe data. The customer object is perfect for this. It gives you all sorts of useful information. Subscription, plan and payment source are all there. In a lot of cases, that’s everything you’ll need.

This is easiest if you’re using a database that supports JSON. In the Ropig app, we store the customer object in Postgres using a JSONB field. This also allows us to write fast, indexed queries against the customer data.

SELECT * FROM users WHERE customer->>'id' = 'cus_1234';

Accessing data from the customer object is then easy. We write small functions to retrieve whatever data we need from the customer object (subscription status, trial end date, etc.).

#=> ~N[2017-12-05 19:10:04]

It’s important to note that you should treat this as read-only data.

Keep everything in sync

Now that you’ve cached all the customer data what happens when something changes?

There are times when you know an API call will change the customer object. In most of those cases, you need that change to be visible immediately. Luckily, most API calls that update the customer will also return the customer. All that you need to do is replace the cached customer with the new customer.

UPDATE users SET customer = '{"id": "cus_1234"}'::jsonb
WHERE customer->>'id' = 'cus_1234';

At other times things will happen that are out of control of the user. Stripe’s webhooks are great for keeping things in sync in those cases.

If a problem ever arises. You can pull down the customer objects for all your users from the API and replace them in your database.


Valid billing data is important. Storing your own version of the data is error-prone. Caching customers means you will always have valid data.

Not only is cached billing data always valid but it’s much more flexible. Flags and status fields derived from stripe data can encode all sorts of implicit state. What it means to be an active user with a valid subscription can change over time. Writing functions that act on cached data means changing the function if those definitions change. Storing your own state may mean a database migration or worse.

Relying on Stripe for subscriptions can make working with billing data more difficult. But caching customer data makes it much less painful.

Send this to a friend