Brainly Payment Processing with XState

Marcin Cekiera
Brainly Technology Blog
6 min readApr 17, 2023

--

Payment processing is crucial for every monetized application. Still, due to the specific legal and security requirements for processing payment transactions, most companies will use a payment service provider. Luckily, there are numerous payment methods and payment service providers to choose from, all offering robust and constantly evolving platforms. However, even when integrating with an external service provider, you must ensure that the process is fully deterministic and durable. And if you have an international product, it has to scale well and be flexible enough to handle different configurations of services.

Brainly case

At Brainly, we process a lot of payments. We have multiple monetized markets worldwide, servicing millions of users. When considering our payment strategy, we decided to use battle-proven patterns and solutions. Based on lessons learned from previous payment integrations, we recently decided to improve our payment funnels using state machines and state charts with the XState library.

You can learn more about our implementation from the video above, where I share our experience with the prototyped system and some general conclusions from working with state machines. In this video, I detail how the solution we implemented uses multiple machines in a hierarchical structure to integrate with various external providers to unify how we work with payments. State machines were the right tool for the job! We simplified our code, making it less error-prone and easier to maintain. In this article, I will take a closer look at the technical details of our implementation.

Our solution consisted of three layers:

  • Backend systems communication — where we fetch market configuration and initialize transaction processing on the backend
  • Payment provider integration — integration of 3rd party libraries responsible for collecting sensitive user data for the transaction
  • UI Integration — the provider or method-specific sub-processes, such as UI management

Due to payment domain specificity, I will not share the actual implementation code, but you can look at the example app at the StackBlitz here, which is the source of all code examples in this article. Remember that it is a simplified and incomplete implementation, with mocks created for the demo due to security concerns. You can see how the state machines work and interact with them through the XState inspector — just make sure you enable displaying popups.

Building hierarchy

The first element of our structure is the primary state machine, which manages the common flows for all supported payment methods.

Here we are modeling a simplified machine that

  • Fetches the configuration of the environment from the backend
  • Spawns child machines responsible for payment service providers or payment methods based on that configuration
  • Waits for input from one of the spawned child machines to initialize processing on the backend side with provider-specific data.

Because we will reuse this process in many different markets and applications, we wanted to ensure that this machine will be agnostic regarding the specific payment providers and methods for any given market or application. To properly separate layers and achieve this polymorphic behavior, we introduced factories and a common interface for all child machines.

Child machine factory

Child machines, responsible for a particular payment method or provider, are created using a factory pattern, so the main machine doesn’t need to know the implementation details of a specific child actor.

The factory function is injected when the main machine is created and expects to be called with the config received from the backend. We keep the results in an object with the machine type as a key to easily access specific machine references.

Common interface

The main machine exposes the interface for child machine communication, such as initialization, transaction processing, or failures. The interface details the dispatched events and their corresponding payloads.

The actor model implemented by XState makes this relatively easy. The main machine creates child machines based on environment-specific configuration and waits until one of them dispatches an event required to continue its work.

It is worth mentioning that your child actor doesn’t need to be another state machine. The only requirement is that it implements the event dispatching based on the required interface. One added benefit to child machines is that you can be more agile in your approach. You can build the machine in stages, reusing existing implementations.

Method or provider-specific machines

The next level is for payment provider integration. These machines can be very diverse, so it’s important to define a common interface. For example, the ‘RESET’ event is used for clearing the machine’s state after backend failures and retries by the user.

This event interface allows for the payment provider internals to be abstracted away from consumers of the state machine. That makes introducing new features much easier because there is a clear separation between the public interface and private implementation details.

The example app includes machines with different integration methods that contain 3rd party code.

Additional layers

These two levels of machines are enough to cover most of our needs here, but you may always add more layers to solve other problems. For example, let’s say you want to implement a payment method that uses the input field in a form. Due to market-specific limitations, you use two different payment service providers offering two completely different ways of hosted input field integration. For example, one renders a hosted field with configuration on library initialization, and the second provides React components that use options passed as properties. You don’t know the exact content of the field, just the current state exposed by the library. One possible solution to this problem is to use separate dedicated forms, but the UI will be specific to the payment provider. An alternative that we decided to test in our prototype is to minimize the surface of 3rd party integration by using the simplest version of elements from the provider integration and building your custom UI around it. The state machine can be used to implement the facade, controlled by the vendor-specific library, that provides one unified interface for your presentational components.

It is enough to create a machine that models the behavior of your UI for that particular flow.

Then, utilizing the actor model again, you may use that machine and communicate with that machine in a way specific to a given third-party library. It may be another machine, callback function, UI component, or whatever.

Then, utilizing the actor model again, you may use that machine and communicate with that machine in a way specific to a given third-party library. It may be another machine, callback function, UI component, etc.

Then, you can use the state of that machine in your components, hiding integration implementation details.

This approach worked for that specific case, but it has significant limitations. Developing a universal solution for managing form states would be a daunting challenge, so we made the decision to abandon it for our current projects.

Integrating with react UI components

Machines already handle our business logic, so let’s add UI. @xstate/react library provides dedicated hooks for integration, so it is pretty straightforward. At that point, you just need to recreate the machines’ structure in the UI, using the machine state on the proper level. Besides the shared interface for communication between layers, you may model your machines in any way, which makes supporting diverse forms much more manageable. In effect, the business logic in the form of state charts and the corresponding UI elements can be developed separately and easily replaced.

Summary

State machines alone can significantly improve your implementation and guard against some common software engineering pitfalls. They force you to build your solution in a way that makes your code simpler and less error-prone. It is an excellent choice for workflows that require determinism and durability, and payment funnels are one of them. But XState gives you more than that! This superb combination of state charts and actor models allows you to model complex systems out of loosely coupled elements, giving you incredible flexibility. So when you start to work on some multi-step complex process, keep in mind that there are battle-proven ideas that can make your work much easier! We tried the XState at Brainly, and it’s a keeper!

--

--