Assume 100 000 users, logging up to five activities per day with 5 goals set.
Let's also consider concurrent users, since that's what actually drives server sizing. Of 100k registered users, assume ~20% are daily active users (DAU) = 20k. At peak hours (evening after work), maybe 10% of DAU are active at the same moment = ~2,000 concurrent users. Each active user makes roughly one request every few seconds while using the app, so peak load is about 2,000 × 0.2 ≈ 400 req/s — not the 10 req/s average. That's a 40× spike at peak, and a single standard server would still handle it, but it shows the difference between average and peak traffic.
Also worth noting: if the user base grows 5% per month, in 12 months we'd have ~180k users and peak load would climb to ~700 req/s. Still manageable, but it's why we design for scaling out rather than one beefy server.
Fitness apps are dominated by time-series sensor data. Let's estimate per entity:
| Entity | Record size | Volume/day (100k logs) |
| Activity log (text) | ~100 B | 10 MB |
| GPS track (30-min run, sample every 5s ≈ 360 pts × 50B) | ~20 KB | 2 GB |
| Heart-rate samples (1/sec for 30 min) | ~10 KB | 1 GB |
Total ≈ 3 GB/day → ~1 TB/year → ~5 TB over 5 years.
Even at 5 TB, this fits on a single modern database — but the high-frequency sensor data is what matters, and it motivates storing it separately (time-series DB) with cold archival of old workouts.
Network consumed is 10 requests per second * 100 bytes = 1kb/s; easy to handle
User management and login endpoints:
POST /users — create profile (name, email, age, weight, height) PUT /users/{id} — update profile (e.g., new weight after a cut)In all endpoints I am assuming user-id is derivable from some sort of authentication token or request header.
Goal setting and progress endpoints:
Activity tracing endpoints (where the mobile app sends near-live data about an ongoing activity):
We will split the system into two parts:
1) managing users, their goals, and activities - this is attached to SQL DB and has a lesser load than the second part
2) ingesting activity tracking data - this one doesn't do much more than saving the data to a TSDB, but it has to be able to handle larger load, and scale independently than the first part of the system
To lessen the load on the TSDB and speed up reporting on goals and activities, we could have a summary of an activity created after the activity is done. This summary can then be saved in the SQL DB in the activities table and be quickly available together with the other activity related data. The raw data in TSDB can be archived in case we need a detailed representation, because once the activity is over, the timeseries is over as well, and we can move it out of the TSDB. The summary should be enough for a quick display. We don't have a business need to leave them there and the TSDB will in fact only serve to aggregate data until a timeseries is over. The only period where we need live tracking of the timeseries is during the activity: a user might want to observe their heart rate as they run - in this period we would query the TSDB for the timeseries. This leads to having a third part of the system:
3) summary service - triggered by an event emitted at the end of an activity summary service reads the activity data from TSDB, and saves it in SQL in an easily parsable format so that it can be used for display purposes, together with some aggregate numbers to be used for higher level reporting (like total kilometers run, average speed...)
Integrate with third-party fitness APIs (Apple Health, Garmin) — wearable data flows through the same ingestion → TSDB → summary pipeline.
We will need two separate databases for this system: 1) SQL database for users, goals and activity details (description, maybe photos, other metadata) - because this is highly structured data with clear relations and 2) a timeseries database to save tracking data about activities - this data fits naturally into a timeseries database as it is a set of data points attached to timestamps.
SQL DB structure:
Activity type is used to connect activities to goals. To make it simpler we will have a predefined set of activity types.
Index on activities(user_id, start_time) for the activity feed, and goals(user_id) for goal queries.
Timeseries database:
Measurement name is there to enable us to have multiple timeseries per activity to measure, for example, heart rate in one, and progress in the other, maybe location in the third to track a running route. Archive timeseries older than one year to keep the DB fast.
Let's consider most common user actions.
1) Creating a goal
Is pretty straight forward. A user would log in, click create a goal and fill out a form with main fields being activity-type, unit of measurement, desired achievement and desired period. The Goal is saved in the SQL DB. It is a simple thing.
Setting a goal also needs to schedule reminders. When the goal is created the users service will insert into the DB "notifications" table one row per each scheduled reminder - the row will contain: timestamp, goal_id, a message and a status initially "pending". A separate process will poll the DB every configured number of minutes to find all reminders which are due to be fired. For each due notification the status will be changed to "queued" and the notification put in a queue. Two important mechanism need to be implemented here:
Another service, call it "notification service" will poll the outbox table and actually push the notifications to the end user.
2) Performing an activity
This is the most demanding part of the whole system. The user would choose an activity and click start. The front end app would hit the /activity/start endpoint on the users service. The activity would be saved in the activities table of SQLDB with the ID being returned to the front end app with a status "started"
The front end app will poll the wearable fitness app every few seconds to get the data it needs, like location and heart rate, for example, it will then send this data together with the activity id and the timestamp to the activity service. The object sent to the activity update endpoint could look something like this:
{activity-id; timestamp; location; heart-rate}
The activity service will save it in the TSDB.
When the user finishes the activity, they will press the "end activity" button which will trigger the following actions by the /activity/end endpoint handler
Summary service will consume the activity-over events and perform the following actions for each:
Saving the summary is a single update query, so we need no transactions involved. In case the summary service fails during a summary creation, the event would be re-consumed by another instance of the service. Accidental re-delivery of the event can be ignored given that the summary creation is idempotent and that there are multiple instances of the service which will prevent a single repeating event to block processing of other services. If the event turns out to be unprocessable (for example the activity id can't be found in the TSDB) it should be sent to a dead letter queue.
In case the TSDB breaks down, the summary service should back-off, so there needs to be a circuit breaker on the calls from the summary service to TSDB. The queue would grow but presumably it would survive until the return to service of the TSDB. A circuit breaker also needs to be implemented when calling the SQLDB, not to overwhelm it if it is broken.
Adding to the goals aggregates is not an idempotent activity. It needs to be made idempotent, possibly by saving the ids of all the activities already taken into account. The query does not need to be part of transaction with the query updating the activities table assuming that both queries are idempotent and will eventually end up in the DB (given the retry mechanisms involved).
3) Observing accomplishments
After they have ended their activity and pressed "end' the user can be notified that the summary will "appear very soon". In case they click on activity details the front end app could poll the activity table and once it finds that the activity is marked as "over" it will get the JSON BLOB with the summary and render the necessary graphs, maps and statistics.
TBD
TBD
TBD
TBD