High-Level Design
At a high level, the payment flow is broken down into two steps to reflect how money flows:
-
Pay-in flow
-
Pay-out flow
Take the e-commerce site, Amazon, as an example. After a buyer places an order, the money flows into Amazon’s bank account, which is the pay-in flow. Although the money is in Amazon's bank account, Amazon does not own all of the money. The seller owns a substantial part of it and Amazon only works as the money custodian for a fee. Later, when the products are delivered and money is released, the balance after fees then flows from Amazon’s bank account to the seller's bank account. This is the pay-out flow. The simplified pay-in and pay-out flows are shown in Figure 1.
Pay-in flow
The high-level design diagram for the pay-in flow is shown in Figure 2. Let’s take a look at each component of the system.
Payment service
The payment service accepts payment events from users and coordinates the payment process. The first thing it usually does is a risk check, assessing for compliance with regulations such as AML/CFT 2, and for evidence of criminal activity such as money laundering or financing of terrorism. The payment service only processes payments that pass this risk check. Usually, the risk check service uses a third-party provider because it is very complicated and highly specialized.
Payment executor
The payment executor executes a single payment order via a Payment Service Provider (PSP). A payment event may contain several payment orders.
Payment Service Provider (PSP)
A PSP moves money from account A to account B. In this simplified example, the PSP moves the money out of the buyer’s credit card account.
Card schemes
Card schemes are the organizations that process credit card operations. Well known card schemes are Visa, MasterCard, Discovery, etc. The card scheme ecosystem is very complex 3.
Ledger
The ledger keeps a financial record of the payment transaction. For example, when a user pays the seller $1, we record it as debit $1 from a user and credit $1 to the seller. The ledger system is very important in post-payment analysis, such as calculating the total revenue of the e-commerce website or forecasting future revenue.
Wallet
The wallet keeps the account balance of the merchant. It may also record how much a given user has paid in total.
As shown in Figure 2, a typical pay-in flow works like this:
-
When a user clicks the “place order” button, a payment event is generated and sent to the payment service.
-
The payment service stores the payment event in the database.
-
Sometimes, a single payment event may contain several payment orders. For example, you may select products from multiple sellers in a single checkout process. If the e-commerce website splits the checkout into multiple payment orders, the payment service calls the payment executor for each payment order.
-
The payment executor stores the payment order in the database.
-
The payment executor calls an external PSP to process the credit card payment.
-
After the payment executor has successfully processed the payment, the payment service updates the wallet to record how much money a given seller has.
-
The wallet server stores the updated balance information in the database.
-
After the wallet service has successfully updated the seller’s balance information, the payment service calls the ledger to update it.
-
The ledger service appends the new ledger information to the database.
APIs for payment service
We use the RESTful API design convention for the payment service.
POST /v1/payments
This endpoint executes a payment event. As mentioned above, a single payment event may contain multiple payment orders. The request parameters are listed below:
| Field | Description | Type |
|---|---|---|
| buyer_info | The information of the buyer | json |
| checkout_id | A globally unique ID for this checkout | string |
| credit_card_info | This could be encrypted credit card information or a payment token. The value is PSP-specific. | json |
| payment_orders | A list of the payment orders | list |
Table 1 API request parameters (execute a payment event)
The payment_orders look like this:
| Field | Description | Type |
|---|---|---|
| seller_account | Which seller will receive the money | string |
| amount | The transaction amount for the order | string |
| currency | The currency for the order | string (ISO 4217 [4]) |
| payment_order_id | A globally unique ID for this payment | string |
Table 2 payment_orders
Note that the payment_order_id is globally unique. When the payment executor sends a payment request to a third-party PSP, the payment_order_id is used by the PSP as the deduplication ID, also called the idempotency key.
You may have noticed that the data type of the “amount” field is “string,” rather than “double”. Double is not a good choice because:
-
Different protocols, software, and hardware may support different numeric precisions in serialization and deserialization. This difference might cause unintended rounding errors.
-
The number could be extremely big (for example, Japan’s GDP is around 5x1014 yen for the calendar year 2020), or extremely small (for example, a satoshi of Bitcoin is 10-8).
It is recommended to keep numbers in string format during transmission and storage. They are only parsed to numbers when used for display or calculation.
GET /v1/payments/{:id}
This endpoint returns the execution status of a single payment order based on payment_order_id.
The payment API mentioned above is similar to the API of some well-known PSPs. If you are interested in a more comprehensive view of payment APIs, check out Stripe’s API documentation 5.
The data model for payment service
We need two tables for the payment service: payment event and payment order. When we select a storage solution for a payment system, performance is usually not the most important factor. Instead, we focus on the following:
-
Proven stability. Whether the storage system has been used by other big financial firms for many years (for example more than 5 years) with positive feedback.
-
The richness of supporting tools, such as monitoring and investigation tools.
-
Maturity of the database administrator (DBA) job market. Whether we can recruit experienced DBAs is a very important factor to consider.
Usually, we prefer a traditional relational database with ACID transaction support over NoSQL/NewSQL.
The payment event table contains detailed payment event information. This is what it looks like:
| Name | Type |
|---|---|
| checkout_id | string PK |
| buyer_info | string |
| seller_info | string |
| credit_card_info | depends on the card provider |
| is_payment_done | boolean |
Table 3 Payment event
The payment order table stores the execution status of each payment order. This is what it looks like:
| Name | Type |
|---|---|
| payment_order_id | String PK |
| buyer_account | string |
| amount | string |
| currency | string |
| checkout_id | string FK |
| payment_order_status | string |
| ledger_updated | boolean |
| wallet_updated | boolean |
Table 4 Payment order
Before we dive into the tables, let’s take a look at some background information.
-
The checkout_id is the foreign key. A single checkout creates a payment event that may contain several payment orders.
-
When we call a third-party PSP to deduct money from the buyer's credit card, the money is not directly transferred to the seller. Instead, the money is transferred to the e-commerce website’s bank account. This process is called pay-in. When the pay-out condition is satisfied, such as when the products are delivered, the seller initiates a pay-out. Only then is the money transferred from the e-commerce website’s bank account to the seller's bank account. Therefore, during the pay-in flow, we only need the buyer’s card information, not the seller’s bank account information.
In the payment order table (Table 4), payment_order_status is an enumerated type (enum) that keeps the execution status of the payment order. Execution status includes NOT_STARTED, EXECUTING, SUCCESS, FAILED. The update logic is:
-
The initial status of payment_order_status is NOT_STARTED.
-
When the payment service sends the payment order to the payment executor, the payment_order_status is EXECUTING.
-
The payment service updates the payment_order_status to SUCCESS or FAILED depending on the response of the payment executor.
Once the payment_order_status is SUCCESS, the payment service calls the wallet service to update the seller balance and update the wallet_updated field to TRUE. Here we simplify the design by assuming wallet updates always succeed.
Once it is done, the next step for the payment service is to call the ledger service to update the ledger database by updating the ledger_updated field to TRUE.
When all payment orders under the same checkout_id are processed successfully, the payment service updates the is_payment_done to TRUE in the payment event table. A scheduled job usually runs at a fixed interval to monitor the status of the in-flight payment orders. It sends an alert when a payment order does not finish within a threshold so that engineers can investigate it.
Double-entry ledger system
There is a very important design principle in the ledger system: the double-entry principle (also called double-entry accounting/bookkeeping 6). Double-entry system is fundamental to any payment system and is key to accurate bookkeeping. It records every payment transaction into two separate ledger accounts with the same amount. One account is debited and the other is credited with the same amount (Table 5).
| Account | Debit | Credit |
|---|---|---|
| buyer | $1 | |
| seller | $1 |
Table 5 Double-entry system
The double-entry system states that the sum of all the transaction entries must be 0. One cent lost means someone else gains a cent. It provides end-to-end traceability and ensures consistency throughout the payment cycle. To find out more about implementing the double-entry system, see Square’s engineering blog about immutable double-entry accounting database service 7.
Hosted payment page
Most companies prefer not to store credit card information internally because if they do, they have to deal with complex regulations such as Payment Card Industry Data Security Standard (PCI DSS) 8 in the United States. To avoid handling credit card information, companies use hosted credit card pages provided by PSPs. For websites, it is a widget or an iframe, while for mobile applications, it may be a pre-built page from the payment SDK. Figure 3 illustrates an example of the checkout experience with PayPal integration. The key point here is that the PSP provides a hosted payment page that captures the customer card information directly, rather than relying on our payment service.
Pay-out flow
The components of the pay-out flow are very similar to the pay-in flow. One difference is that instead of using PSP to move money from the buyer’s credit card to the e-commerce website’s bank account, the pay-out flow uses a third-party pay-out provider to move money from the e-commerce website’s bank account to the seller’s bank account.
Usually, the payment system uses third-party account payable providers like Tipalti 9 to handle pay-outs. There are a lot of bookkeeping and regulatory requirements with pay-outs as well.
Finished reading?
Mark it complete to track your progress.