List the key functional requirements for the system (Ask the AI for hints if stuck)...
List the key non-functional requirements (performance, scalability, reliability, etc.)...
Estimate the scale of the system. Consider daily active users, read/write ratio, storage requirements, bandwidth, and any relevant QPS calculations...
Assuming 100 million DAU, and each user on average makes 1 purchase/transfer per day and view/manage their account 5 times a day. Assuming peak QPS is twice of average QPS.
Peak read QPS = 100 million * 5 / 24 / 3600 * 2 = 11574
Peak write QPS = 2314
For storage, we need to store the account metadata for all 100 million users. Assuming each user's account metadata takes 5KB to store, we will need:
100 million * 5KB = 500GB of metadata storage.
We also need to store the payment transaction logs. Assuming 500 million payment events each day, and each log takes 1kb to store.
We will need 500 million * 1kb = 500GB of payment log storage EACH DAY. Once a payment is more than 6 months old, we move it to cold storage.
Define the APIs expected from the system. This is your chance to analyze and define the read and write paths so that you can come up with the high-level design...
For a user to view their account:
GET v1/view_account {
user_id: UUID,
account_id: UUID
}
For a user to transfer money to another account:
POST v1/transfer {
user_id: UUID,
account_id: UUID,
target_user_id: UUID,
target_account_id: UUID,
transferred_at: Timestamp,
transfer_currency: String,
transfer_amount: String,
idempotency_key: String,
}
For a user to switch money to a different currency:
POST v1/change_currency {
user_id: UUID,
account_id: UUID,
transferred_at: Timestamp,
current_currency: String,
transfer_currency: String,
transfer_amount: String
}
To notify a user of account balance change:
POST v1/notify {
user_id: UUID,
account_id: UUID,
notification_metadata: String
}
For a user to view transaction history:
GET v1/transactions {
user_id: UUID,
account_id: UUID,
}
Describe the overall system architecture. Identify the main components needed to solve the problem end-to-end. Use the diagramming tool to create a block diagram.
All requests to the service go through API gateway and load balancers. API gateway does rate limiting and authentication. Load balancers do request distribution through consistent hashing of user_id.
For any transfers between accounts, or deposits and credits:
For viewing account details:
We query the relational database, and retrieve account metadata for a user.
To ensure strict serializability, all transactions on the ledger are sorted by timestamp. To address clock skew, we use NTP servers to sync time across servers.
There is the problem of high request volume for corporate accounts:
A merchant receiving 10K TPS on one account → all traffic hits one shard, one row, one lock. Throughput collapses. Three standard solutions:
To achieve 99.9% availability, for non-critical services like notification service, we make them fail-open. If requests are overwhelming the database, instead of failing everything, we trigger load shedding on the database, as well as implement circuit breakers on the caller side.
Define the data model. Identify the main entities, their attributes, and relationships. Consider the choice of database type (SQL vs NoSQL) and justify your decision based on access patterns...
In our payment system, there are a few things we need to store:
To store these data, we want to use a relational database.
The tradeoff is higher horizontal scaling complexity, and we can't easily support high throughput. Also, the data schema is not as flexible as a document database. However, this is an acceptable tradeoff given the most critical feature is consistency.
Here's some sample data models:
table users {
user_id: UUID,
user_name: String,
user_email: String,
user_phone_number: String,
user_address: String,
user_mailing_address: String,
user_accounts: List
user_type: String,
}
table accounts {
account_id: UUID,
account_owner: UUID,
account_status: String,
account_opened_at: Timestamp,
account_balance: Double,
account_currency: String,
account_type: String,
account_metadata: String,
}
table transactions {
transaction_user: UUID,
transaction_target: UUID,
transaction_type: String,
transaction_amount: Double,
transaction_status: String,
transaction_time: Timestamp,
transaction_metadata: String,
tr
}
For any queries to deposit and transfer, we will have to make cross account updates an atomic operation. Either all succeed, or complete rollback. It will be like:
BEGIN TX; UPDATE accounts SET balance = balance - 100 WHERE id = sender; -- debit UPDATE accounts SET balance = balance + 100 WHERE id = recipient; -- credit INSERT INTO ledger (txn_id, from, to, amount, ts); -- the record COMMIT;
Once the data size becomes larger, we will need to shard the database. We will shard the users table by user_id, and shard the accounts table by account_owner_id, so accounts for the same user are in the same database shard. For transfer between each database shard, we will need to use 2 phase-commits between shards.
To ensure data durability and availability, we also need to create replicas for each shard. However, we do need to use synchronous replications across replicas, so we won't have different replicas returning different values.
Deep dive into 2-3 key components. Explain how they work, how they scale, discuss tradeoffs, capacity, and any relevant algorithms or data structures.
In our payment system, there are a few things we need to store:
To store these data, we want to use a relational database.
The tradeoff is higher horizontal scaling complexity, and we can't easily support high throughput. Also, the data schema is not as flexible as a document database. However, this is an acceptable tradeoff given the most critical feature is consistency.
Here's some sample data models:
table users {
user_id: UUID,
user_name: String,
user_email: String,
user_phone_number: String,
user_address: String,
user_mailing_address: String,
user_accounts: List
user_type: String,
}
table accounts {
account_id: UUID,
account_owner: UUID,
account_status: String,
account_opened_at: Timestamp,
account_balance: Double,
account_currency: String,
account_type: String,
account_metadata: String,
}
table transactions {
transaction_user: UUID,
transaction_target: UUID,
transaction_type: String,
transaction_amount: Double,
transaction_status: String,
transaction_time: Timestamp,
transaction_metadata: String,
tr
}
For any queries to deposit and transfer, we will have to make cross account updates an atomic operation. Either all succeed, or complete rollback. It will be like:
BEGIN TX; UPDATE accounts SET balance = balance - 100 WHERE id = sender; -- debit UPDATE accounts SET balance = balance + 100 WHERE id = recipient; -- credit INSERT INTO ledger (txn_id, from, to, amount, ts); -- the record COMMIT;
Once the data size becomes larger, we will need to shard the database. We will shard the users table by user_id, and shard the accounts table by account_owner_id, so accounts for the same user are in the same database shard. For transfer between each database shard, we will need to use 2 phase-commits between shards.
To ensure data durability and availability, we also need to create replicas for each shard. However, we do need to use synchronous replications across replicas, so we won't have different replicas returning different values.
All requests to the service go through API gateway and load balancers. API gateway does rate limiting and authentication. Load balancers do request distribution through consistent hashing of user_id.
For any transfers between accounts, or deposits and credits:
For viewing account details:
We query the relational database, and retrieve account metadata for a user.
To ensure strict serializability, all transactions on the ledger are sorted by timestamp. To address clock skew, we use NTP servers to sync time across servers.
There is the problem of high request volume for corporate accounts:
A merchant receiving 10K TPS on one account → all traffic hits one shard, one row, one lock. Throughput collapses. Three standard solutions:
To achieve 99.9% availability, for non-critical services like notification service, we make them fail-open. If requests are overwhelming the database, instead of failing everything, we trigger load shedding on the database, as well as implement circuit breakers on the caller side.