Ledgering at Ivella - Part 1

Engineering
Written by 
Eric Wang
September 19, 2022

At Ivella, we’re building the best banking products for couples, and we started that journey with a new way for couples to automatically split expenses. With the Split Account, users maintain individual balances but are able to automatically split transactions by a pre-set ratio. At first glance, this problem may seem relatively straightforward (e.g. a $100.00 transaction comes in, and you charge two users $50.00). However, the reality is that there is much more complexity abstracted away from the user.

Consider a set of edge cases involved in splitting transactions: what happens when the amount changes between authorization and settlement? What happens when one party doesn’t have the balance to support their end of the transaction? What happens when a split transaction is reversed or refunded? What happens if an authorization is canceled after it has been split? Furthermore, how do you accomplish all of this without using some sort of pooled joint-custody account?

Because of this, and in order to ensure the validity of our money movement processes, one of the most critical tools we developed was our internal financial ledger. While our team is relatively new to payments, we borrowed from a long history of accounting principles and resources that provided the foundation for building our ledger. The goal of this series is to hopefully provide some insight into the intersection between accounting and software and highlight some of the processes we took to build out an auditable, maintainable, and scalable ledger.

Terminology

Before we dive into the content of this post, it is important to first lay out some of the terminology that will be used.

Transaction - A Transaction is any event coming in or out of Ivella’s system. Some examples of Transaction events that we process include purchases (e.g. a user purchasing a coffee at Starbucks),  ACH originations (e.g. a user depositing money from their external account), card reversals (e.g. a user returning a purchase), etc. Transactions are grouped under two categories: Active or Passive. Active Transactions are events which are split (e.g purchases, card reversals), while Passive Transactions are events that are not split (e.g ACH originations). There are many more event types, but all are examples of Transactions.

Double Entry Accounting - DEA is a form of accounting in which all Transactions are recorded twice in a ledger. The main principle behind DEA is that each transaction consists of a credit and debit pair. For example, consider a $50.00 purchase event at Trader Joe’s by John Doe. The first entry of the ledger would reflect a $50.00 debit for John, while the second entry of the ledger would reflect a $50.00 credit for Trader Joe’s. We can represent these events on a T-account where one side reflects debits and the other reflects credits. In double entry accounting, both sides of the T-account must always balance to 0. The previous T-account may look something like this:

Note: All amounts are recorded in cents to avoid floating point arithmetic.

Problem

Now that we've described some of the terminology, we can discuss the main problems we wanted to address when building our ledger. Generally speaking, these problems can be grouped into three buckets.

  1. In order to ensure that money is moving in its intended behavior, we needed to build an immutable and auditable ledgering system that tracks all events coming in and out of our system.
  2. In order to handle different Transaction states, we needed to be able to track modifications to the state of any given Transaction (e.g. a purchase at Trader Joe’s goes from authorized to settled, and the corresponding debit goes from $50.00 to $72.96).
  3. In order to support queries for our end-users,  we needed to be able to provide low latency reads and design a system that is highly scalable.


Solution

An early misconception we had while building our ledger was considering using only a few relational tables. However, we quickly realized that it was more advantageous to create multiple tables that each served a particular purpose. In order to demonstrate this, consider the previous example of a purchase Transaction at Trader Joe’s by John Doe. 

When a Transaction event reaches our system, it is processed independently and carried out in a series of steps configured via AWS Step Functions. The use of AWS Step Functions not only allows us to easily orchestrate and branch our transaction processing logic, but also allows us to break apart our code into reusable microservices that can be used in various areas of the business.

The first step of the Step Function records which accounts were debited and which accounts were credited. When the aforementioned $50.00 Trader Joe’s purchase reaches the first step, we need to record both a credit entry under Trader Joe’s merchant account and a debit entry under John Doe’s Ivella account. Additionally, we need to keep track of John Doe’s balance in their Ivella Account, both before and after the purchase. To address these requirements, and as well as problem one, we created two tables: a General Ledger (GL) and an Account Subledger (AS). The GL is an immutable table that tracks every event passing through our system using Double Entry Accounting; the General Ledger serves as a single source of truth for all Transactions. Additionally, the AS is a table that tracks a user’s available and current balance, and how different Transaction events affect a user’s balance over time. 

After a Transaction gets ledgered in the GL and AS, the next step of the Step Function is to ledger the Transaction on a more specific and individualized basis. For cases where a transaction’s state can be mutated, we created Event-Specific Ledgers that track state-changes for every event of a given type. For example, consider the scenario in which the purchase event at Trader Joe’s has settled and the corresponding debit changes from $50.00 to $72.96. For this scenario, the table would have one row describing the original authorization of $50.00 and another row describing the settled transaction of $72.96. All purchases and its changes are ledgered in the Purchase Events Ledger, while all ATM events and its changes are ledgered in the ATM Events Ledger. The use of Event-Specific Ledgers allows us to address problem two by keeping track of a Transaction’s lifecycle. 

The Step Function then inserts an entry into the Users Transactions table. In order to address problem three, we needed to design a table specifically intended for heavy read operations.  Thus, the Users Transactions table, which is configured via AWS Aurora,  stores the most up-to-date information for a specific transaction. In Trader Joe's example, there would be only one row for the transaction and we would simply update that row to contain the most recent information. As opposed to the GL and Event-Specific-Ledgers, which are immutable and track state-changes via additional rows, the rows in the Users Transactions are modified to keep the table relatively small in size.

To recap, in order to correctly ledger the purchase Transaction at Trader Joes, a series of steps is carried out through the use of AWS Step Functions:

  1. Insertion into the General Ledger using DEA and insertion into the Account Subledger 
  2. Insertion into the Event-Specific Ledgers to track state changes for Transactions
  3. Insertion into the Users Transactions table for our end users

Below is an image of an abstracted and generalized view of our Transaction Processing Step Function. There are additional steps involved depending on the specific workflow and edge case (e.g. Authorization Falloffs, ACH Returns, etc); however, for this post we will not describe those scenarios.


Summary

The ledgering system we created has allowed us to automate and handle the multitude of edge cases involved in splitting transactions, while ensuring the correctness of our money movement processes. Through relying on accounting principles and by creating multiple relational tables that each serve a specific purpose, we are now able to maintain an immutable and auditable ledger, track individual state changes for each Transaction, and provide low-latency queries for our end-users. We are still consistently learning and iterating on our ledger, but we hope this has provided a little insight into how we think about financial engineering problems at Ivella. In the next article, we will be doing a deeper dive into transactions subledgering from a relationship database perspective!

In the meanwhile, if you’re interested in building software solutions to payment-related problems like these, please email us here or apply here!