Auth token will be in the header.
userID: string
workoutType: enum # client chooses or detects walking/running/swimming
returns:
streamHandle: string # client holds onto this and pushes stream updates with to this streamHandle's ID
datetime: int # epoch time stream starts
code 200: stream started successfully (this pattern for all api calls)
code 5xx: stream failed to start
userID: string
data: float tuple # timestamp, acceleration, velocity, position
eventType: enum # update with new value or end stream with this call
userID: string
workoutType: enum # cardio
data: dict # {cardio_time: int # minutes, cardio_dist: float # miles, can be obtained by looking at gps position before and after workout}
datetime: int # epoch time
userID: string
strengthWorkoutType: string
sets: int
reps: int
datetime: int # epochtime
weight: int # in kg or lb based on user preference
userID: string
goalType: enum # daily steps, weekly distance, number of workout, streak
data: dict # additional parameters dependent on goalType like deadline, steps walked, etc
return: goalID: int
userID: string
goalID: int # goal to edit
goalType: enum
data: dict # relevant values
userID: string
goalID: int # goal to edit
userID: string
lookupDistance: int # days of data in the past from today to retrieve
userID: string
leaderboardType: enum # streak leaderboard, workouts in a week, steps taken
value: int # may cast down from float if a float is given
userID: string
leaderboardType: enum
returns: array of scores for the leaderboardType based on user's friends
userID: string
lookupDistance: int # days of data to export
returns: data: raw csv file
userID: string
param: string
val: string
userID: string
activityID: string
val: dict
userID: string
Clients connect to the app's backend via regional load balancers. Requests are then forwarded to an api gateway for authn and sanitation of user input (did they enter valid custom values?). Then requests are passed through a rate limiter; spikes in activity are absorbed here by a kafka queue.
We have 6 microservices: session manager, writer, reader, interaction, and export and feed and notifications.
Session manager: implemented by several worker replicas for horizontal scaling, these workers will create a session for streaming data when a user starts a new cardio workout that tracks their activity. A websockets connection will be created for that user (a user can have one tracked activity at a time). Updates will be sent periodically. When the activity ends then this service will gracefully end the session.
Writer: writes finished activity information to the activity DB primary, and session manager writes to this DB when a session ends. This design is write heavy so the stream alleviates some constant writes that would otherwise have to be done.
Reader: reads from DB replicas, which DB primary propagates to after writes for eventual consistency. This is accessed when clients want to view their activity history.
Interaction: reads from user DB replica and looks up friend IDs for a given user (the client) ID to get list of friends, and lookup activity scores by them in the activity DBs to show to the client locally how they're doing compared to their friends. Microservice also has workers that can generate a dated 'certificate' commemorating achieving goals against a user's goals table from goals DB.
Notification: when feed microservice reads updates of activity from other users (particularly, friends), notify the notification service. The user polls the /api/v1/notifications endpoint periodically to get notifications sent by the server: these can help to engage the user when their friends are doing activities.
Export: replica workers export chosen ranges of data to csv, store it long enough to send to client, cache for some time, then delete on success.
Feed: Read from and write to a feed of activity and goals updated by other users (that consent to sharing anonymized data). Read these from the activity db replica and goals db replicas; these can have loose consistency requirements as these are meant to encourage the user to do and log activities - getting this information as soon as possible is not at all required. the feed microservice's worker replicas read from these dbs and populate the client app's home page or feed page with the latest information to encourage engagement. Friend updates are given priority and are fetched before recent updates from everyone else are read.
On the client side, we can let the client create configurable reminders for a given workout type, how often the reminder should fire, and how soon before the workout it should fire. This can all be done locally on the client app since it does not require any service access, so it is just a local app feature and does not require some extra components.
We also want to ensure end-to-end encryption of activity data since it is sensitive (contains health and gps position data). Before client data is sent, it is encrypted with app logic via asymmetric key algorithm, (use the regional public key) and then sent with the http rest api request. The server decrypts it with their regional private key before exposing the raw values in the interaction microservice or export microservice. They are stored as encrypted values in the activity/userdata/goals databases.
To ensure that data can be safely recovered in the event of some major incident, take regular snapshots of each database and store them in cloud object storage (aws), in physical media (hard disks), and in a replica of the databases on a separate plane, so we have reliability and durability spread across 3 different sources.
Key thing is that this is a write-heavy system as opposed to read-heavy. We use streams so DBs arent hit with millions of writes per second, as often as the data is actually generated by phones/wearables.
For all DBs have a primary db that is written to, then propagates to all read replica dbs. Reads are done from the last read replica db in the chain. This is for availability and responsiveness.
For activity DB , have 1 table per user. Each row represents a completed activity. Columns include activityType, distance, time, reps, sets, weight. Negative value of -1 is default used when some columns are not applicable (as is the case for certain kidns of exercises)
For userdata DB, have 1 table for all users, but shard it across several replicas for better throughput and scaling. Each row is indexed by unique user ID, and has column for friends' IDs (a dynamic array).
For goals DB, 1 table per user. Each row is a goal ID that can be retrieved for that user along with goalType and goalProgress
All DBs are DynamoDB for easy, automatic horizontal scalability and monitoring.
Use load balancing algo least connections as not all requests are the same weight, export requests might be larger if requesting a longer history.
Rate limiter should enforce certain maximums on each service per unit of time, this acts as a buffer when spikes of activity happens like during a marathon or right after working hours.
Use caching in databases when activities are read from activity DB. Different operations in the app can use the same activity data (history, goals, compare with friends) so keep it in the cache, use LRU cache so when it's done being used recently it can be discarded. Placing more data in cache also helps relieve some storage in the DB before it's placed back.
The stream setup for writing activity data is resilient to lapses in communication from the apps to the service, as will often happen by not opening the app for a while or by outages. Streaming can pick up where the events left off using strict ordering of timestamps.
Use the primary/replica DB replication scheme to have high availability and the eventual consistency.
Use idempotency keys (unique request IDs) when sending activity completion data so that the same activity doesn't get logged twice (writer service should cache request IDs to avoid duplication)
Horizontal scaling is very important when users or data grows. Users can grow if the app gets more popular; if the number of users increases 10x for example, then we can provision more regional load balancers, api gateways and rate limiters to handle the increase in load. For each of the databases, let's use user ID as a partition key to shard the databases, splitting across several db resources, and use consistent hashing to ensure we can smoothly scale up/down as needed. We might also want to use redis instances as cache that can be hit before trying to read from databases.