DEV Community

Cover image for A whole new world behind the scenes! - Part 3
Akarsh Agarwal
Akarsh Agarwal

Posted on • Originally published at Medium

A whole new world behind the scenes! - Part 3

The primary goal was to process the transaction when we started building our payment engine in the previous blog. The preliminary checks about whether a transaction should happen took a back seat. Moreover, the system’s resiliency was never questionable in our last design. But is that how our current systems work? I believe everyone knows the answer to that question.

First, we’ll understand the numerous preliminary checks a payment processor performs before the addition and subtraction of balances. Then, post that, we’ll analyze different failure scenarios and improve our design to account for them. Hence, this blog is a bit of both theory and coding.

So, let’s get started!

N number of steps

Ideally, a transaction is the transfer of funds between two entities. Those two entities could be individuals, businesses, systems, or a mix-and-match. However, it’s the 21st Century, and our transactions involve much more than the addition and subtraction of balances.

Today, it is a series of processes that enables the transfer. For starters, KYC (Know Your Customer) is one such process. Validation of who you are is a crucial part of the transaction. Hence, we’ll look into some critical processes that perform checks before a transaction can proceed. However, due to my limited knowledge, the below is not an exhaustive list, and the sequence below is irrelevant. The sequence of checks is highly dependent on the payment processor and its rationale.

Identity Check

Today, KYE or Know Your Entity, to generalize, is as vital as transferring funds. KYE of individuals (KYC: Know Your Customer), businesses (KYB: Know Your Businesses), and even our interacting systems/partners defines whether a transaction should proceed or not.

Knowing who’s the sender and the receiver to avoid any illicit transactions is fundamental to today’s payment systems. It’s primarily to prevent fraud of any kind. What does that mean? If they represent themselves as Bob and Alice, the engine should ensure the fact, and the transfer should happen only between the two. Erroneous funds debiting or crediting can lead to losses and user experience.

I’m not oblivious to the disruption brought by Crypto in the last few years, where Anonymity has been one of the most extensive offerings by the network. However, this series focuses on the non-crypto Payment Engines that fuel our day-to-day transactions. I’ll take up Crypto a few blogs down the line.

Authorization Check

Let’s not confuse Authorization with Authentication. Authorization checks if the sender and the receiver are allowed to transact. For example, if I’ve blocked international usage on my account, it will not allow me to make international transactions. It is because I’m not authorized to perform it. Another example is if the receiver’s account is blocked due to suspicious activity. In that scenario, too, the transaction is not authorized.

Risk Check

When transacting, the payment processors need to ensure that the risk of the transaction is rightly managed. A famous example is OTP or One Time Password. When paying online, they usually redirect you to the OTP verification page to ensure additional authentication and avoid unwanted results.

Recently, the minimum threshold transaction amount for OTP has become configurable. Earlier, as you might recall, we received OTP for every transaction. Nowadays, we receive OTP for high-value transactions.

Compliance Check

Does your transaction comply with the rules and regulations of the region? It’s an essential check because the payment processors are answerable to their rules and regulations. Compliance checks are dependent on the transacting entities. It may differ for individuals and businesses.

For example, MAS (Monetary Authority of Singapore) allows only a S$30,000 yearly transaction limit for e-wallets.

Balance Check

Finally, last but not least, balance check. It is one of the most important checks to perform and needs no introduction or explanation. Balance check asks: Do you have the amount to transact?

Don’t worry! We’ll retry it for you!

We created our first payment engine in the last blog. The engine was capable of processing transactions in an all-good-nothing-fails scenario. However, failures are a part of our systems every day! So, how do we enhance our previous design to accommodate for errors? Let’s check that out.

We’re not working out anymore!

Our last design had the depositing and deducting steps in the same function. While we might know which step of the transaction failed, it’s pretty impossible to re-run the same function to process the second part of the transaction. It would deduct the receiver more than once. Hence, we’ll update our design to support retries and better failure management for erroneous scenarios.

Let’s separate the two states to become independent functions. Here’s how it would look:

func deductSender(tx Transaction) {
    tx.Sender.Balance -= amount
    tx.State = "deducted"
    save(tx)

    depositReceiver(tx)
}

func depositReceiver(tx Transaction) {
    tx.Receiver.Balance += amount
    tx.State = "deposited"
    save(tx)
}
Enter fullscreen mode Exit fullscreen mode

One could run the depositReceiver function to complete the transaction if the transaction errored out after the deducted State. Does this look complete? Are we done?

Not really! The deducted state is tightly coupled with the deposited state as the deductSender calls the depositReceiver. Consider a scenario where we want to hold the money in escrow before depositing the receiver. The above logic doesn’t work, as we’ll need to update the deductSender function to add a step. Is this the best we can do?

Power up your SM!

Well, we’re using State Machines! Our states should define the execution and not the code. What do I mean by that? Our function should trigger events to process the transaction. For example, we can define that once a transaction reaches deducted state and an event next is called; it automatically executes the function depositReceiver.

Overall, it reduces the dependency on the code and moves it to the states. Now your functions become stateless, and you can add or remove states before or after the current function without affecting it.

I’ve added some sudo code for the State Machines to keep the details out. Here’s how it would look:

var sm *sm.StateMachine

func init() {
    sm := sm.NewSM()

    sm.Trigger("deducted", "next", depositReceiver) 
}

func executeTx() {
    tx := Transaction{
        ...
    }

    sm.Execute("deductSender", "next", tx)
}

func deductSender(tx Transaction) {
    tx.Sender.Balance -= amount
    tx.State = "deducted"
    save(tx)
}

func depositReceiver(tx Transaction) {
    tx.Receiver.Balance += amount
    tx.State = "deposited"
    save(tx)
}
Enter fullscreen mode Exit fullscreen mode

The above is a short sudo code. So let’s dive into what I’m trying to achieve here. Please ignore the nuances, as it’s not a correct Go code, so it’s not usable.

The init function initializes the SM. Then, it creates a new SM object and defines a state transition rule. The rule says that any transaction in the deducted state triggering the next event should call the depositReceiver function.

Then, the sm.Execute function executes the SM by calling the deductSender function with the argument tx. Moreover, it defines the event name to trigger once the function completes. Hence, it would call the depositReceiver automatically post that.

One of the long-term benefits of the above approach is the maintainability and the possibility of branching when new flows are added. What do I mean by that? When we add an escrow in the middle, we don’t need to touch the deductSender function. We can update the SM’s Trigger function to call despositEscrow instead of the depositReceiver function. The functional logic remains the same. However, managing and adding functionality becomes easier without modifying the previous code.

Summary

That’s about it, folks!

In the first part, we covered a few behind the scene checks and understood the different steps (not necessarily in the same order as above) that define our transactions. In the second part, we improved our SM to define our execution flow and provide partial error handling. Finally, we improved the design and removed the dependency on code for executing the next step.

The following blog will target formalizing sudo code with appropriate libraries, steps, and function executions. We’d target to develop a CLI tool for executing a transaction and see through the state transition. I’ll add the code + the Github repo link for easier access.

See you next weekend!

Top comments (0)