Once in a while, SAAS companies find themselves wishing that their users would actually buy their products. JK, they always feel that way.
So say that you have an app, and you want to add a payment system to it, one that interacts with an external payment provider such as Stripe, BlueSnap, Adyen, etc. The system must be reliable: you can’t miss a payment, or charge a user more than they have authorized.
In my own experience with such systems, I have found them to be so delicate that I am afraid to make big changes or to do major refactoring. Instead I tend to use patchy code. You can call me a coward, but unfortunately I know I’m not the only one. A lot of people are scared to write good code when it comes to billing and payments - it's scary because big changes might come with big regressions. And that is why you need to start with a good design.
How you handle your payment system before reading this post
How do you design a good payment system?
Let’s begin by separating our areas of concern: we have the payment flow that should be reflected in the UI, and should provide good UX - fast, accurate and functional. Then we have the internal payment flows to manage the business logic and the updating of external tools. Some of my tips pertain to both areas, but my general focus is on the second part, the backend.
First of all, let’s not forget about common good practices:
Use queues. Queues make your data persistent, i.e., they prevent data loss, and it’s easy to build a retrying mechanism on top of them. Use them to guarantee that you won’t miss a single payment request.
Use proper error handling and descriptive error messages. Some errors are worth taking immediate action and some are not. For example, if you fail to update a subscription for an account, it’s an urgent error. But if you fail to delete an archived subscription, that’s ok.
Set alerts for every urgent error. While on some services an error is not a big deal and you just want to get an alert if you reach a couple dozen of them, on the payment service you probably want to be aware of each error as soon as possible.
Also related to monitoring: create audit tables, and plenty of logs. If you get an error or a complaint from a customer, you want to be able to understand exactly what happened.
Run Tests. Tests Tests Tests. I believe there’s no need to elaborate here.
Build a good testing environment. It is often complicated to build a good testing environment on the payment provider’s side since it requires different authentication tokens, duplicate plans and so on. At Oribi, we do not have a full end-to-end testing environment for payment flow. Therefore, manual tests are done locally and then on production against the provider’s production environment. But if you have the resources, building a proper staging environment and defining a sandbox environment on the provider’s side, is definitely a better practice.
Plan for multiple payment providers. Start with one, but think ahead. You’ll probably want to add another provider down the road.
Understand payment states. Payment states affect customers directly and are very error prone. Often a payment state will affect a customer’s ability to use the application. For example, they could be new, in grace period, paying, blocked, etc. The business logic of payment states should be carefully defined. It’s not usually done by the software engineers, but it’s their responsibility to verify that the states are well defined, and to make sure that the trigger to move from one state to another is clear and can be automated in a logical way. It’s also very likely that the payment states will change. For example, new states may appear: churned, resubscribed, etc.
Budget time for external integrations. Things like payment state, monthly spend amount and plan type should be accessible to other teams in the company. Usually, you will use a CRM system for these, so be prepared to support external integrations in your payment system. Since this is not a trivial thing to do, I wrote another post just on it.
Think about customization. The sales team will often want the ability to create a special price, a special plan, or a discount coupon. Most payment providers support coupons, but this is still something to consider.
Manage your plans in-house. Regardless of where you choose to manage your plans, you will have to define them on the provider’s side. But then you have a choice: where is your source of truth located? Is it at the plans you defined on the provider’s side, or is it at your own code and database?At Oribi we don't fully manage the plans in-house yet, and we plan an architecture change to support that.
If you choose to ignore the previous tip, at least keep the different types of plans separated in your payment service. In other words, don’t overgeneralize your code. At Oribi we have “regular” plans that we offer on the website, and we also have a “contact us” option for customers who have more traffic than offered in our “regular” plans. For these customers we create custom plans. The “regular” plans have tiers defined by traffic, and they are built with Stripe tiered pricing. However, the custom plans are created with a different structure on Stripe, so there must be a separation in the code.
How you will handle your payment system after reading this post
In conclusion, a payment system is a core business logic dynamic component, that gets more complicated over time, and that is absolutely not bug tolerant.
Many of the ideas I have suggested here might seem like overengineering, but in my experience, payment systems tend to suffer from exactly these pain points. I believe that the delicacy and importance of a payment system requires extra planning and careful attention. It’s probably true for every component, but trust me on this one - make sure you properly test and monitor your payment system, and verify that everything is flexible and extension ready.