Integration with Stripe subscriptions

Background: Our customer decided to use Stripe to manage subscriptions and payments for recurring subscriptions. In this case it was a subscription with two options: rolling monthly and annual. Pretty standard business customers issue to be honest, but that’s good news as it’s quite a common requirement so the solution we are sharing might be re-used by lots of potential businesses.  

A simple Stripe API integration

Naive approach. The bare minimum for our app was to know if:

  • Someone had just purchased a new subscription
  • Canceled an existing subscription
  • Subscription was renewed

Stripe supports developers thanks to accurate documentation. In my opinion the best one, at least from a developer point of view. We could find information we need straight in the Stripe API link shown below.

Basic approach to handle Stripe API events
Basic approach to handle Stripe API events

That would work, however it would introduce a couple of potential problems, where main is relying on third party services which would immediately impact our application. It would also mean that any changes in our business logic would be tightened closely with Stripe, so changing payment/subscriptions providers would be complicated.

Better approach - microservice and Stripe API Events

We decided to create a microservice that would be a middle layer between our Web App and Stripe API. We could source our service with Stripe Events, process them with our business logic, and finally expose a custom API which would tell our web application if a given customer has easy access to our service or not.

Introduce microservice to handle events from Stripe
Introduce microservice to handle events from Stripe

Command Query Responsibility Segregation

After reviewing various architectural approaches, we decided that we will make use of CQRS (Command Query Responsibility Segregation). This pattern separates the responsibilities of reading and writing data into two different models. The "Command" part handles writing operations and updates the data, while the "Query" part handles reading operations and retrieves the data.  

In our case, “Command” would be reacting to the events from Stripe. This also includes rendering status if the user has or hasn't an active subscription. This ready status can be then read by the “Query” part, which would be our internal API.

CQRS - implementation challenges

Although this approach seemed to be better, it introduced some issues we had to keep in mind while implementing it.

Respond as quick as possible for incoming Stripe events

We need to gather events from Stripe and return responses as quickly as possible. That's a requirement from Stripe that our listening service needs to be quick and always ready to reply with status code 200. That means that we should not process our business logic when receiving the event. Instead we need to queue information from Stripe to be processed later, but still as soon as possible.

To solve this, we decided to store incoming events into a queue and reply with 200 status code. Events will be processed by another process separately.

Don’t count on Stripe webhooks to work all the time

Stripe API may not work all the time. In particular we could stop receiving events from Stripe. We need an alternative way to be in sync with them.  

To solve that, we decided that we would have a ready tool (working on demand or manually)  which would call Stripe API to update subscription/user status in our database.

Don’t rely on our subscription service to work all the time

Our subscription service could be down as well.  

That might cause two issues:  

  • Stripe Events could not be delivered to our listening service
  • Web Application can’t check the current user subscription status

To mitigate the first one, we decided to create a cron job which would call Stripe API if there are any undelivered Stripe Events. If any, they would be added into the standard queue to be processed.

To mitigate the latter, we implemented a feature toggle in Web App that would allow signed users to see all premium content even if they don’t have an active subscription. We preferred to temporarily grant access to premium content for all users, rather than losing users who already paid for the service. Decisions would be made by the Product Owner and could be enabled/disabled at any time.

Stripe events order

Stripe events might be delivered to our systems in a random order (for various reasons). We might first receive an event for users subscribing to a product while we don’t yet know if such a customer exists.

This solution is a bit more complex.

As we couldn’t rely on the datetime of the event DELIVERED to our service, we decided to rely on the timestamp of the event object (property created).

Then we implemented our logic that way so it processes ALL events for a given user/subscription on every event received. So, coming back to our example of user events, if we receive a new subscription event for a user that does not exist in our database, we allow that call without doing anything. We expect to finally receive another event that a customer has created. Like how we process these events using stripe events date, we can process these two events in proper order which will not cause any constraint issues.  

Changing business logic layer

As we used an Agile approach while implementing Subscription Service, we assumed that our business logic might be not ideal. We need a way to re-apply our logic to Stripe Events.

To make it possible, we implemented our code in a way that doesn't rely on just the last event from Stripe. Instead, we process all events from history for a given user/subscription and apply our business logic on all of them. This way we change ie. the way how we react to ie. user creation events, even if that one was received a long time ago.

We also created a cron job which could be run against any user/subscription to re-play our business logic. It is handy in scenarios where we need to refresh our logic on items that don’t receive any new Stripe events (which would normally trigger our business logic).

Expect large volume of events from Stripe when migrating data

Our system was primarily designed to react to regular traffic where users purchase our product. This is quite a predictable chain of events.
There was another aspect of our Stripe integration we also had to keep in mind: migrate user and subscription data from the previous subscription system. We could prepare separate export/import tools which would do that work, but that would require some additional work to be run just once.  

Solution we used: Stripe offered some help with that to do once-time migration for us, where they would import user & subscription data from CSV files. They also confirmed that their import would trigger webhook events as these sets of data were created organically. It means that we would receive usual customer created or subscription created events, the events we are already familiar with. We just needed to make sure our system will be scalable enough to handle the high volume of traffic. As we decided all incoming data will be stored in the queue and processed later, we know that our architecture is ready for that out of the box!  

The final big picture – Stripe subscriptions handled by CQRS architecture

Keep scalability in mind - introduce queues to handle Stripe events
Keep scalability in mind - introduce queues to handle Stripe events

Immediately mark new subscription as active - possible improvement

As the system works pretty quick, there are potential bottlenecks that might cause delays impacting user experience. The obvious show-stopper is the queue and the queue processor. If for some reason the queue processor stops working, the API will not see new subscriptions or any updates to it.

Improvement we could do here is to update the Event Listener so it stores new subscription status directly in the database (marking it as “draft” or “to be confirmed”). This way the API Service would immediately know that the user has an active subscription, and we have more time for our queue processor to apply proper business logic.

Do you need to create high-performance web app?

Accelerate development, reduce costs and reach your goals faster.