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)
}
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)
}
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)