Our New Payment Subsystem

Implementing a reusable payment subsystem that supports the upcoming 3D Secure protocol.

Jorge Rodriguez
Get on Board Dev
5 min readFeb 8, 2020

--

Unsplash

We have been using Stripe to charge our customers since 2014. How we integrated it back then wasn’t the most important stuff to care about, as we didn’t know whether we were onto something with our product. What was important in those days — and still is — was to be able to pivot quickly from one revenue model to another validating our hypotheses. It continues to be that way but today we are not under the radar anymore.

Among the things we discovered was that we could charge for multiple pay-as-you-go features like unlocking an individual hiring process, or packages of unlocks, or boost a job. Since integrating payments wasn’t meant to be reusable, each new paid feature had its own flow and code copied and pasted from another one already implemented.

Being honest, it wasn’t that bad, as it was easy to copy-and-paste code customizing the amount to be charged and some other logic. However, at this point, we had acquired a technical debt dealing with at least three payment controllers and views that were the same thing with subtle differences. All because of the trade-off of delivering fast measuring whether our customers were willing to pay for the new feature, deleting the code otherwise.

Paying the debt

By December 2019, we started to add a couple of new and not yet released features — one of which is automated subscriptions, the other still to be revealed — that needed payment with an uncommon characteristic, where customers will have to authorize the charges that the system will capture later on when they are offline (not present). This new capability means having the ability to store — with upfront authorization — the payment method to be charged in the future.

This time we felt motivated to do it the right way by taking time to design and rewrite everything from scratch. There were more reasons for doing it:

  • Migrate to the new Payment Intents API — so far we were relying on the old Charge API — meaning support for the upcoming Strong Customer Authentication European regulation to be fully enforced by December 31, 2020.
  • One code for managing the process: Payment is almost always the same: users select what service they want to pay for, enter or select a payment method and authorize the charge. There are custom parameters and custom business logic for each flow but they can ultimately be handled in one place and with one code.
  • Same UX and UI: As the flow is pretty similar on each payment, it is better to present the users with the same — and to some extent familiar — experience everywhere and every time they will pay for a feature.

Implementation

The first decision was not to rely on Stripe Checkout, even when it seemed the obvious way to go, as there was little to implement at our side. However, the downsides start to pile up when you want to have control of the user experience because using checkout means the experience is hosted-in and controlled-by Stripe, so we decided to take the long path and implement — as a tech company — everything on our own.

At the backend, we divided managing the payments in three modules or sets of endpoints, each with specific functions:

  • Payment methods controller: Handling the attaching, detaching or setting as default a payment method (i.e: credit card).
  • Intents controller: Handling the verification of a new payment method and/or the authorization of a charge.
  • Payments callback controller: Handling all the tasks that happen on the background once the payment is collected — or fails — , for instance notifying customers by email and executing the business logic related to the service the users paid for, for instance, increasing the unlocks quota after buying a package of unlocks or running the matching algorithm after boosting a job.

Then at the frontend, we created a UI component in React for managing — adding, deleting or setting as default — payment methods.

And a UI component for authorizing the payment.

The user is presented with the same interface across the site.

Considerations

I won’t dig into the specifics but will list instead some of the issues we considered while designing and implementing the subsystem:

  • The UI components were thought to be shared and so fully customized — through properties — even at the level of using them as modal or inline
  • There is heavy communication with the Stripe API at the backend (ruby) and frontend (Javascript) on each user interaction.
  • SetupIntent and PaymentIntent are concepts or models at Stripe mapped within our codebase too, so we keep a copy (reference) at our side in order to own the checkout flow experience and reduce the network communication with Stripe.
  • Never attempt to handle what happens next to a successful charge at the frontend (browser), because it is possible for customers to leave the page after the payment is complete but before the post-charge process initiates. The best practice hence is to monitor — by listening to webhooks events that Stripe sends to the application using a direct and secure channel — the status of a charge, executing the subsequent business logic related to the feature the customer is paying for.
  • Keep the customers aware of everything that is happening with their payments by notifying them, for instance, when a payment succeeds or fails (with the cause and action to take next) or a new credit card is added or deleted.
  • Track the failed attempts and notify the dev and sales teams with context info to offer help proactively to the customer 😉.

Handling payments is not easy — even when Stripe facilitates a lot — especially having to deal with the new transactional state of a charge via the new payment intents and setup intents support. Hopefully, this post gave you some insight into some of the things to bare in mind and take care of when going with your implementation.

I am pretty sure you have questions and we are more than happy to answer them, so leave us a comment or drop us a line to team at getonbrd dot com 🤓.

--

--

Jorge Rodriguez
Get on Board Dev

1. Software Engineer @fleetio.com 2. Co-Founder @getonbrd (500 SF)