Back of the envelope estimations and assumptions:
Monthly active users: 1.000.000
Daily Active Users: 20-30% of monthly -> 200.000 - 300.000
Concurrent Users: at peak 5-10% of Daily active users -> 10.000 - 30.000
Read/Write Ratio: Considering the functional requirements, there should be a pretty balanced read/write ratio as users will both read and write a lot
API call estimation: avg. 5 API calls per session - 300.000 DAU -> 1.5 M API Requests Daily
Looking at our data model, we'll need API endpoints for 2 purposes
let's desing the User API first. We'll design a RESTful API with the following endpoints:
Now that we have the User API designed, let's focus on the Workout API
The API should accomodate all needs of our functional requirements now.
Let's start with the most easiest unit:
We assume that the user can enter new Workouts Types, so he can track his own workouts. The data model would look like:
This is pretty simple and should be enough to represent a Workout -> let's identify this model as workout_type
If we go next, then we should model a Workout now. Let's quickly recap on what we said as functional requirements for the workouts:
So let's model the data for this:
Here we're referencing the workout_type entity from above. We're also referencing the user id here, as a workout belongs to exactly one user (the one who created the workout). A date is also crucial for the application. Duration and calories burned can be optional, based on the user preferences. Let's identify this model as workout
So if we recap now, we can fully represent a workout by meeting the functional requirements. What is missing is the user and the association to his workouts.
Let's model the user:
Let's identify this model as user
So now we have our 3 entities:
These entities with their relationships as described above will help us to design the application layer in order to meet all specified requirements.
Let's talk about the Database choice. We could have the option of choosing a relational database (such as Postgres) or a document based (such as Dynamo or Mongo). We can see that we have a One-to-many relationship between a workout and the user, and for the use case of our application, this is key. Both databases are a good choice, when considering One-to-many relationships. When we'd have many-to-many relationships, I'd definitely be in favor of using a relational database. But coming back to our scenario, I think it depends on the expertise of the owner and maintainer of the system to choose the database. for our case, we'll use a relational database - Postgres.
flowchart TD
B["User App"];
n0["Backend Service"];
n3[("Database")];
n4[("Redis Cache")];
B --> n0;
n0 --> n3;
n0 --> n4;
The High level design is pretty simple and provides the necesary high level components. First we'll have a User Application that acts as a client, Reaching out to the Backend where both user data and workout data are retrieved from.
As users will have multiple reads per day, a caching layer is introduced. The backend first checks the cache for workout data on the given user and reaches out to the database, if not found.
The retrieved data is then returned back to the client.
flowchart TD
B["User App"];
n3[("User Database")];
n4[("Redis Cache")];
n5["API Gateway"];
n7["Workout Management Service"];
n8["User Management Service"];
n11(("Message Broker"));
n13[("Workout Database")];
n14[("Replica")];
n15[("Replica")];
B --> n5;
n5 --> n7;
n5 --> n8;
n8 --> n3;
n3 --> n11;
n11 -->|"notifies new user"| n7;
n7 --> n4;
n7 --> n13;
n13 --> n14;
n13 --> n15;
n7 --> n14;
n7 --> n15;
API Gateway and Separation of Backend Services
User Management Service
Keeping Workout Service informed about user creation / deletion
Database Replicas and Cache for Workout Mangement Service
The replication of databases always comes with tradeoffs. Single leader replication has the advantage of having no concurrent writes of the same data (as it happens only at one node), but it represents a single point of failure. So in this case we'd need deal with failover cases, where the leader fails. in worst case no writes would be possible anymore. So we'd need add the additional complexity to the system to detect a new leader.