We will have a charity event that will last for 3 days(let’s say from Friday to Sunday). We want to collect $100M and there will be 10 charities so each user of our system may decide where he/she wants to donate the money. We may assume that the front-end part is ready and also we have an agreement with some 3d party payment system so there is no need to handle money transfers from one account to another. Also, we need to collect all money in a single bank account and after the event will be finished we need somehow to spread the collected money among all 10 charities. The last thing to mention is that we need to handle any failure we may face while processing the payment.
Since we are going to work with payments our system should be highly consistent, we need to handle stale data.
Also since will use some 3d party service that will handle our transactions we need to provide some reconciliation process (a process that will ensure that our stored data is consistent). Our system should be reliable and fault-tolerance.
Usually, a plan for a system design interview looks like this:
But in this article, we will focus on the high-level design to discuss probable issues with it and follow-up questions we might face during the interview.
Issues with that design:
We can handle this issue in several ways:
Let’s go ahead with the second approach. First, we provide a page with a dropdown of 10 charities, the amount user wants to donate, and the button “next”:
After that, we will generate UUID from PSP and store it in our DB, after data was saved we redirect the user to the payment page:
The whole flow will look like this:
UUID was used to make our request idempotent. In other words, if we will call our PSP with a method that proceeds transactions several times for a single UUID, the result will remain the same.
Storing credit card information is not an easy task since we need to follow complex regulations like Payment Card Industry Data Security Standard(PCI DSS) in the United States. Most companies avoid storing credit card information and instead host payment pages provided by PSP. Here is an example of Stripe checkout:
PSP systems like Stripe got internal mechanisms of retry in case the first attempt to transfer money failed for some reason. Keeping that in mind and the fact that we will not store any sensitive data in our storage we can say that if for some reason the payment will fail we cannot automatically retry to do it once again, what we can do is check the result of payment and depending on the result make a record about this transaction consistent. For that purpose, we need a reconciliation mechanism.
Here is the whole flow:
In step 4 our record in the transactions table will look like this:
id(pk) |
charityID(fk) |
amount |
status |
updated |
---|---|---|---|---|
uuid123 |
1 |
500(String) |
in progress |
timestamp |
We will have a status field that indicates the current status of the transaction.
Notice that the amount field got type String, not double. The reason for this is that double is usually not a good choice because either software, protocols, etc. may support different numeric precisions in serialization or the number could be extremely big or extremely small.
On step 8 PSP calls our webhook and provides an acknowledgment that the transaction was completed successfully. After that, we may change the status to “completed”.
id(pk) |
charityID(fk) |
amount |
status |
updated |
---|---|---|---|---|
uuid123 |
1 |
500(String) |
completed |
timestamp |
If something went wrong between steps 1-4, then we just not saving any data to the DB and our DB remains consistent.
If something went wrong between steps 4-8, then we may provide a reconciliation process that will periodically read the transactions table and check all transactions with status in progress and timestamp older than X and push such records to a dedicated queue so the finance team may handle it manually. Or if PSP provides some API to check transactions by ID we can make it automated.
The last thing worth mentioning here is a double-entry bookkeeping system. Great approach working with accounting, the idea behind this is that you got 2 records for each transaction, with debit and credit respectively. But in our design, this might be overkill.