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...
Here are the APIs we have for this system:
This is an API used for browsing products available at a given vending machine:
GET v1/browse_products
{
user_id: UUID,
vending_machine_id: UUID
}
This is an API used for checkout out an item:
POST v1/checkout
{
user_id: UUID,
vending_machine_id: UUID,
item_id: String,
payment_method: String,
payment_amount: int
}
This is an API used for refund to user:
POST v1/refund {
user_id: UUID,
vending_machine_id: UUID,
item_id: String,
refund_method: String,
refund_amount: int
}
These are APIs used to maintain a machine:
GET v1/diagnostic {
maintainer_id: UUID,
}
POST v1/maintain {
maintainer_id: UUID,
maintain_type: String,
restock_type: String,
restock_inventory_count: int,
repair_type: String,
repair_ops: String
}
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.
For any request made by user, to either browse products, checkout or make payments, we need to go through the load balancer first, to balance traffic to different servers. We will be balancing requests based on a consistent hashing of the user_id, which will route user requests evenly to different servers. We then go through an API gateway, which implements authentication and rate limiting.
For browsing of the product in each vending machine, we access the vending machine feed service. The read QPS to this service is the highest. Within this service, we store the item metadata per vending machine in a relational database, we also store the item availability info. The data schemas look like:
table item_metadata {
vending_machine_id: UUID,
item_id: UUID,
item_name: String,
item_price: int,
}
table item_availability {
item_id: UUID,
vending_machine_id: UUID,
item_count: int
}
Both tables are cached in the redis cluster, with key of vending_machine_id + item_id. This will help us to speed up the huge read throughput. The redis cache will be write-through, so all updates to the inventory_count will be synchronously written to the database. This has a tradeoff of higher write latency, but we need this since inventory_count is required to be consistent in order for consumers to place orders.
Since we estimated the data storage is 1TB, which can easily fit in 1 modern database cluster, no sharding is needed. However, we can add read replicas to increase the throughput of the system.
For checking out, users will send requests to payment service, which triggers an external payment provider. Authentication will be established with the external payment provider. The payment service will look at the payment method. If it's cash or coins, the service will calculate the change, if there's any, and issue commands to the vending machine dispense controller, which directs the vending machine to issue refunds. If it's card, users will be charged the exact amount. The payment service will send authenticated requests to the external payment provider, and charge users' cards accordingly.
If payment is declined or user cancels it, we send a request to external payment provider to issue refund. To prevent double-charging or duplicate requests, we add idempotent key to each payment request, so duplicate requests are not processed twice.
Whenever payment is accepted, we issue a write to the redis cluster to update the inventory count. Whenever inventory count falls below a preset threshold, we trigger an event on the kafa message queue, upon consumption, issue an alert to notify that the inventory count for an item is too low on a vending machine.
For maintaining a machine, the user needs to be authenticated by the API gateway as a maintainer, and can then send requests to maintain machine, or update inventory count.
Deep dive into 2-3 key components. Explain how they work, how they scale, discuss tradeoffs, capacity, and any relevant algorithms or data structures.
For the inventory count update, it happens whenever an order is placed and payment is accepted. As we analyzed earlier, the peak QPS will be around 300. On redis side, for write requests, we will use a write-through cache, which writes to database synchronously everytime for a write request.
On the database side, we will have write replicas and read replicas. We will use a qurom consensus approach to achieve W + R > N. This ensures that every read gives the most updated inventory count, while we only need to populate write requests to W database replicas, and we only need to wait for read results from R database replicas.
The choice of a write-through cache and a qurom consensus approach guarantees strong consistency, with the tradeoff of higher read and write latency. However, in an inventory management use case, consistency is important for users to make purchases, so we will have to adopt this approach.
In the payment service, we will write each payment with its status in a relational database, with ACID properties. Payment is a critical event, so we need strong consistency. For replicas, we will write to all replicas synchronously. In case of any instance crash, other replicas can still store the payment event, and the crashed replica can replay from WAL upon recovery, and get updated data from other replicas.
All the payment requests submitted will be tied to a user payment session, and we will create an idempotent key for each request, such that retries or duplicate requests won't be processed twice.