List functional requirements for the system (Ask the chat bot for hints if stuck.)...
List non-functional requirements for the system..
Estimate the scale of the system you are going to design...
Let's have 1 million DAU.
Every status has userId, a status value, and a timestamp.
UserId takes 8 bytes, status value takes 1 byte, and a timestamp takes 8 bytes. One status event takes 20 bytes.
Every user's status is changed 5 times daily.
It would give 100 million bytes every day of new statuses.
Let's keep this information for one year, and it takes 100 Gigabytes (two replications)
Let's use a key-value storage like AWS DynamoDB.
Define what APIs are expected from the system...
We use the REST API.
getUserStatus(userId,fromDate,toDate) returns the user status list for the given time range, where userId is the unique user identifier, fromDate and toDate are the timestamps that give the time range.
appendUserStatus(userId, status, currentTime) appends the new user status, where userId is the unique user identifier, status is the new user's status, and currentTime is the timestamp when it happens.
If multiple users would append the same userId simultaneously, we're going to handle the collision at the NoSQL level by using LWW( last writer wins)
appendNewStatus(newStatuses) returns HTTP 201 if new statuses have been appended, newStatus is the list of JSON objects representing the status object: { statusId: String, statusName: String}, the statusId should be unique.
deleteStatus(statusIds) returns 200 if the given statuses have been removed, statusIds is the list of the unique status ids.
Defining the system data model early on will clarify how data will flow among different components of the system. Also you could draw an ER diagram using the diagramming tool to enhance your design...
We would use a NOSQL solution like AWS DynamoDB.
It has concurrent write and read speeds.
The table UserStatus:
The key is userId (8 bytes)
The value is the timestamp ( 8 bytes)
The statusId is the unique key of status from the table StatusDefinition (8 bytes).
One row takes 24 bytes.
1 DAU takes 120 million bytes per day or 43,8 Gigabytes per year.
Let's count that we will have 2 copies of the table UserStatus, then
it takes 131.4 Gigabytes per year.
UserId would be the partition key, and timestamp and status are created as the secondary index.
The table StatusDefinition:
The key is statusId (8 bytes)
the statusName is string(20 bytes)
StatusDefinition contains till 100 statuses and we can neglect the required capacity for this table.
You should identify enough components that are needed to solve the actual problem from end to end. Also remember to draw a block diagram using the diagramming tool to augment your design. If you are unfamiliar with the tool, you can simply describe your design to the chat bot and ask it to generate a starter diagram for you to modify...
Loadbalancer provides DDoS protection and TLS termination, and forwards request to the right service nodes.
The User Presence Service handles the user status requests and it's a stateless service. AWS DynamoDB persists the user status information.
Most demanding users' statuses would be cached by the Cache.
The User Presence Service gets the user's status update from Kafka.
Explain how the request flows from end to end in your high level design. Also you could draw a sequence diagram using the diagramming tool to enhance your explanation...
getUserStatus(userId,fromDate,toDate):
The user sends a request to LoadBalancer to get the user's status for the given time range. The Load balancer distributes the load between the User Presence Service instances by using either round robin, weighted round robin, the lease connection number, or other strategies. The User Presence Service checks the cache if it has the user statuses; if so, it returns them to the client, otherwise it goes to the AWS DynamoDB to find user statuses by userId and the time ranges, after getting the user statuses, The User Presence Service puts the user's status to the cache. The response would return to the user with an HTTP code 200. If the user is unknown to the User Presence Service, it returns an HTTP code 404.
The cache uses LRU, LFU, or FIFO to store the user statuses efficiently.
Kafka connects to the external event source, and when the User Presence Service consumes a new user status update, it persists it to AWS DynamoDB.
appendUserStatus(userId, status, currentTime):
The user updates their status and sends the request to the Load Balancer.
The Load balancer distributes the load between the User Presence Service instances using either round robin, weighted round robin, the lease connection number, or other strategies. The User Presence Service makes the update user's status requests to AWS DynamoDB.
If the cache contains the user's status, it would be updated with the new one. If the user's status is updated, it returns an HTTP code 200, otherwise, it returns an HTTP code 404.
deleteStatus(statusIds):
The user deletes some users' statuses by passing the status list and sending it to the Load Balancer. The Load balancer distributes the load between the User Presence Service instances using either round robin, weighted round robin, the lease connection number, or other strategies. The User Presence Service removes the user's statuses from AWS DynamoDB and returns HTTP code 200.
Dig deeper into 2-3 components and explain in detail how they work. For example, how well does each component scale? Any relevant algorithm or data structure you like to use for a component? Also you could draw a diagram using the diagramming tool to enhance your design...
Performance and scalability of getUserStatus() and appendUserStatus() API are extremely important for this system. As such, we employ Redis cache to reduce the user's response time when the user is going to call getUserStatus().
The User Presence Service contains getUserStatus() and appendUserStatus() API
Indexing of the table UserStatus in AWS DynamoDB helps make getUserStatus() fast. Caching of the most requested user status in Redis minimizes the getUserStatus() latency. The User Presence Service would be deployed to Kubernetes and horizontally autoscaled automatically. Also, Kubernetes restarted failed instances of the User Presence Service.
Kafka connects to the User Presence Service by transmitting new user's status update events, the User Presence Service consumes it and persists the updated user statuses. Kaflka uses the userId as the partition key.
Redis uses as the cache to store the user's statuses. Redis updates the data on the primary and sends updated data to replica nodes. If the Redis primary fails, one of the replica node would be promoted to the primary.
AWS DynamoDB and the cache should be partitioned for improved scalability.
Explain any trade offs you have made and why you made certain tech choices...
We may change the user status with the REST endpoint or by sending events to Kafka. The external service may act as an event source.
The need to develop is how the Kafka consumers choose the User User Presence Service instance that handles Kafka events.
Try to discuss as many failure scenarios/bottlenecks as possible.
What are some future improvements you would make? How would you mitigate the failure scenario(s) you described above?