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...
The inventory management system is linked to an online selling platform.
Assuming 100 million sales happen on the selling platform each day, and peak QPS is twice of average QPS. Each sale would need an update on the inventory management system.
Assuming 100 thousand admins who see the inventory system, and each admin check inventory status 10 times per day.
Peak write QPS = 100 million / 24 / 3600 * 2 = 2314
Peak read QPS = 100000 * 10 / 24 / 3600 = 11
For storage, we need to store users data and inventory data.
Assuming 1 billion inventory items, each item's metadata (including current inventory count) takes 2KB to store. We need:
2kb * 1 billion = 2TB of storage.
Assuming 100 thousand users, and each user's role permissions and other metadata takes 5KB to store. We need
5kb * 100000 = 500MB of 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...
Here are the APIs we need:
To update inventory count after update from selling platform:
POST v1/purchase {
purchased_at: Timestamp,
purchased_by_user_id: UUID,
purchase_metadata: String,
merchant_id: UUID
}
To view current inventory information for a merchant:
GET v1/view_inventory_status {
user_id: UUID,
user_role: String,
merchant_id: UUID,
cursor: String (used for pagination)
}
POST v1/alert_inventory_status {
user_id: UUID,
merchant_id: UUID,
alert_metadata: String
}
To do stock replenishment for a merchant:
POST v1/stock_replenishment {
user_id: UUID,
user_role: String,
stock_replenish_metadata: String,
merchant_id: UUID,
}
To ingest new product to system:
POST v1/add_SKU {
user_id: UUID,
user_role: String,
merchant_id: UUID,
new_products_metadata: 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.
All requests to the system will go through API gateway, which does rate limiting and authentications, and load balancers, which uses consistent hashing to route requests to different servers.
The system is connected to the online sale system, so whenever an item is purchased/added/restocked/added, the online sale system sends request to the inventory system.
The inventory update system first reads user permission, to make sure the current user has the user role permission to update inventory. Then it triggers 2 flows:
For inventory updates, if 2 sales are updating the inventory concurrently, we could cause overselling when inventory goes below 0.
Here, there are 2 things we can do:
UPDATE inventory SET stock = stock - :quantity WHERE product_id = :id AND stock >= :quantity
If the query fails to update, we can tell the client that the purchase request failed.
For users to view inventory information, the request also goes through user permission service, to make sure that the user has the permission to do so.
Then, we send a request to redis cluster to read the inventory data, and if not available, we trigger a read to the underlying database. For large merchants, we pass in pagination parameters in the request, we deserialize the cursor, and read a portion of the SKU inventory information.
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...
For data storage and models, we need to store:
For the items on sale and their associated inventory count, we will have huge write throughput from real-time updates, and we need strong consistency. Under the circumstance, we will use a distributed relational database, for the below benefits:
The tradeoff is:
This is an acceptable tradeoff for our use case.
For the users table, we will also use a relational database. Here's sample data models for both:
table item {
item_id: UUID,
merchant_id: UUID,
item_count: Int,
item_metadata: String,
item_updated_at: Timestamp,
item_created_at: Timestamp,
item_status: String
}
For this table, we will shard the table by merchant_id, so requests for the same merchant will land on the same shard. If some merchants are mega merchants with a lot of SKUs, we will use virtual nodes to distribute them on multiple shards to prevent overwhelming a single shard. To facilitate read latency, we will create read replicas for each shard. This will be a leader-follower replication, where each update to the leader is synchronously propagated to all followers. This ensures strong consistency with tradeoff for higher write latency. If a leader goes down, a follower can be promoted to the leader.
table users {
user_id: UUID,
user_name: String,
hashed_user_password: String,
user_email: String,
user_phone_number: String,
user_metadata: String,
user_role_permission: String
}
The users table is a bit smaller. If we do need to shard it someday, we can shard by consistent hashing of user_id.
We will also have a caching layer with a redis cluster.
The redis cluster will have data models of:
key: merchant_id + item_id, value: inventory_count
key: user_id, value: user_role_permission
This will greatly speed up the read latency for item inventory count and user permissions. It will be a write-through cache, where any write is synchronously written to the redis cache and database cluster. This way, we ensure data consistency and durability when reading from and writing to cache.
NoSQL for unstructured data (major)
All stock mutations (purchase, restock, adjustment, return, transfer) write to 2 databases:
This keeps the OLTP store small and fast, and lets heavy analytical scans run on the columnar store without contending with the hot path. Kafka also gives a natural ordering guarantee per item, which matters for accurate stock lineage.
2. Indexing strategy (minor)
Since data is sharded bymerchant_id, each shard keeps local secondary indexes on frequently filtered columns —status,category_id, andwarehouse_id— to support low-stock lists and category filters without full shard scans. Cross-shard analytical queries route to the columnar store rather than the OLTP DB.
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 data storage and models, we need to store:
For the items on sale and their associated inventory count, we will have huge write throughput from real-time updates, and we need strong consistency. Under the circumstance, we will use a distributed relational database, for the below benefits:
The tradeoff is:
This is an acceptable tradeoff for our use case.
For the users table, we will also use a relational database. Here's sample data models for both:
table item {
item_id: UUID,
merchant_id: UUID,
item_count: Int,
item_metadata: String,
item_updated_at: Timestamp,
item_created_at: Timestamp,
item_status: String
}
For this table, we will shard the table by merchant_id, so requests for the same merchant will land on the same shard. If some merchants are mega merchants with a lot of SKUs, we will use virtual nodes to distribute them on multiple shards to prevent overwhelming a single shard. To facilitate read latency, we will create read replicas for each shard. This will be a leader-follower replication, where each update to the leader is synchronously propagated to all followers. This ensures strong consistency with tradeoff for higher write latency. If a leader goes down, a follower can be promoted to the leader.
table users {
user_id: UUID,
user_name: String,
hashed_user_password: String,
user_email: String,
user_phone_number: String,
user_metadata: String,
user_role_permission: String
}
The users table is a bit smaller. If we do need to shard it someday, we can shard by consistent hashing of user_id.
We will also have a caching layer with a redis cluster.
The redis cluster will have data models of:
key: merchant_id + item_id, value: inventory_count
key: user_id, value: user_role_permission
This will greatly speed up the read latency for item inventory count and user permissions. It will be a write-through cache, where any write is synchronously written to the redis cache and database cluster. This way, we ensure data consistency and durability when reading from and writing to cache.
NoSQL for unstructured data (major)
All stock mutations (purchase, restock, adjustment, return, transfer) write to 2 databases:
This keeps the OLTP store small and fast, and lets heavy analytical scans run on the columnar store without contending with the hot path. Kafka also gives a natural ordering guarantee per item, which matters for accurate stock lineage.
2. Indexing strategy (minor)
Since data is sharded bymerchant_id, each shard keeps local secondary indexes on frequently filtered columns —status,category_id, andwarehouse_id— to support low-stock lists and category filters without full shard scans. Cross-shard analytical queries route to the columnar store rather than the OLTP DB.
For data storage and models, we need to store:
For the items on sale and their associated inventory count, we will have huge write throughput from real-time updates, and we need strong consistency. Under the circumstance, we will use a distributed relational database, for the below benefits:
The tradeoff is:
This is an acceptable tradeoff for our use case.
For the users table, we will also use a relational database. Here's sample data models for both:
table item {
item_id: UUID,
merchant_id: UUID,
item_count: Int,
item_metadata: String,
item_updated_at: Timestamp,
item_created_at: Timestamp,
item_status: String
}
For this table, we will shard the table by merchant_id, so requests for the same merchant will land on the same shard. If some merchants are mega merchants with a lot of SKUs, we will use virtual nodes to distribute them on multiple shards to prevent overwhelming a single shard. To facilitate read latency, we will create read replicas for each shard. This will be a leader-follower replication, where each update to the leader is synchronously propagated to all followers. This ensures strong consistency with tradeoff for higher write latency. If a leader goes down, a follower can be promoted to the leader.
table users {
user_id: UUID,
user_name: String,
hashed_user_password: String,
user_email: String,
user_phone_number: String,
user_metadata: String,
user_role_permission: String
}
The users table is a bit smaller. If we do need to shard it someday, we can shard by consistent hashing of user_id.
We will also have a caching layer with a redis cluster.
The redis cluster will have data models of:
key: merchant_id + item_id, value: inventory_count
key: user_id, value: user_role_permission
This will greatly speed up the read latency for item inventory count and user permissions. It will be a write-through cache, where any write is synchronously written to the redis cache and database cluster. This way, we ensure data consistency and durability when reading from and writing to cache.
NoSQL for unstructured data (major)
All stock mutations (purchase, restock, adjustment, return, transfer) write to 2 databases:
This keeps the OLTP store small and fast, and lets heavy analytical scans run on the columnar store without contending with the hot path. Kafka also gives a natural ordering guarantee per item, which matters for accurate stock lineage.
2. Indexing strategy (minor)
Since data is sharded bymerchant_id, each shard keeps local secondary indexes on frequently filtered columns —status,category_id, andwarehouse_id— to support low-stock lists and category filters without full shard scans. Cross-shard analytical queries route to the columnar store rather than the OLTP DB.
For cache invalidation, we need to beware of thundering herd. This happens when item inventory count keys get expired at the same time for a huge merchant, and a huge number of requests hit the underlying database at the same time. To mitigate this, we do: