system-design51 min read

Design a Gym & Fitness Tracking App: The Complete Guide — A Senior+ Guide | Ayodhyya

Design a Gym & Fitness Tracking App

Building a production-grade fitness platform: workout tracking, AI form correction, wearable integration, and social challenges at scale

Senior+ Guide 55+ min read 10,000+ words Ayodhyya

1. Introduction — The Fitness App Landscape

The global fitness app market was valued at approximately 1.3 billion dollars in 2024 and is projected to exceed 3.5 billion dollars by 2030. Applications like MyFitnessPal, Fitbod, Strong, JEFIT, and Strava have demonstrated that users are willing to pay premium subscriptions for intelligent workout tracking, personalized plans, and social accountability. Yet building a comprehensive gym and fitness tracking platform that handles real-time workout logging, wearable data ingestion, AI-powered form analysis, class scheduling, membership billing, and social challenges is a formidable systems-design challenge.

In this guide we design a full-featured gym and fitness tracking application from the ground up. We begin with requirements gathering and capacity estimation, walk through the data model and high-level architecture, implement core services in C#, and then dive deep into each feature domain. By the end you will have a production-grade blueprint that covers every major subsystem, complete with code examples, database schemas, Mermaid diagrams, and interview-ready answers.

Who this guide is for: Senior software engineers, solution architects, and system design interview candidates who want a thorough, end-to-end reference for building fitness platforms. Every code example is in C# targeting .NET 8 and ASP.NET Core.

Fitness apps differ from typical CRUD applications because they must handle high-frequency real-time data streams from wearables, compute aggregates over long historical windows, support offline-first mobile experiences, and integrate with third-party hardware ecosystems. The latency budget for logging a set during a workout is sub-second, while nightly analytics batch jobs may process millions of data points. This duality of real-time and batch workloads makes the architecture particularly interesting.

We will also address challenges that are unique to the fitness domain: handling timezone-aware class schedules across global gym chains, computing one-rep-max formulas in real time, managing recurring membership billing cycles with proration, and ensuring HIPAA-compliant storage of biometric health data. Each section provides both the conceptual reasoning and the production C# implementation so you can adapt the patterns to your own tech stack.

The fitness industry is undergoing a digital transformation where the physical gym experience is being augmented by intelligent software. Members expect their gym app to be as polished and responsive as the consumer apps they use daily. Personal trainers need data-driven insights to program effectively for their clients. Gym operators need operational dashboards to optimize class scheduling and equipment utilization. This guide addresses all three personas and shows how a unified platform can serve them all through well-designed microservices with clear domain boundaries.

We will examine real-world architectural decisions such as why MongoDB is preferred for workout session documents over traditional relational tables, how TimescaleDB handles billions of heart rate samples efficiently, and why Redis Sorted Sets are the perfect data structure for real-time leaderboards. Each technology choice is justified with concrete performance characteristics and cost analysis.

2. Functional & Non-Functional Requirements

Functional Requirements

  • Workout Logging: Users can log exercises with sets, reps, weight, duration, distance, and RPE (Rate of Perceived Exertion). Support for supersets, drop sets, and circuit training with real-time rest timer integration.
  • Exercise Library: Searchable database of 1000+ exercises with muscle group categorization, equipment tags, difficulty levels, instructional videos, and muscle activation heatmaps.
  • Personalized Plans: Algorithmically generated or trainer-authored workout plans that adapt based on progress, available equipment, training history, and user goals.
  • Progress Analytics: Charts showing volume load over time, estimated one-rep-max progression, body measurement trends, personal records, and muscle group balance analysis.
  • Social Features: Activity feed, leaderboards, workout challenges, friend lists, gym-wide competitions, and shared workout templates.
  • Wearable Integration: Sync heart rate, step count, sleep data, SpO2, and GPS routes from Apple Health, Google Fit, Garmin Connect, and Fitbit.
  • Class Booking: Browse, book, waitlist, and cancel group fitness classes with real-time seat availability and recurring schedule support.
  • Membership & Billing: Tiered membership plans, recurring Stripe subscriptions, freeze/pause, prorated upgrades, and annual payment options.
  • Trainer Dashboard: Trainers can view assigned clients, program assignments, attendance tracking, client progress, and communicate via in-app messaging.
  • Nutrition Tracking: Food logging with barcode scanning, macro breakdown, calorie targets based on TDEE, meal plan suggestions, and water intake tracking.
  • Gamification: XP points, achievement badges, streaks, level progression, and unlockable content to drive long-term retention.
  • Push Notifications: Workout reminders, class start alerts, friend activity, streak warnings, and personalized marketing messages.
  • Video Exercise Library: HD video demonstrations with slow-motion playback, angle switching, and frame-by-frame stepping.
  • AI Form Correction: Computer vision analysis of user-submitted exercise videos with real-time feedback on form deviations and injury risk scoring.
  • Multi-Gym Support: Franchise chains with location-specific schedules, equipment inventories, staff management, and cross-gym access tiers.

Non-Functional Requirements

AttributeTargetRationale
Availability99.95% uptimeFitness users expect 24/7 access; peak hours are early morning and evening
Latencyp99 < 200ms for API reads, < 500ms for writesWorkout logging must feel instantaneous during a session
Throughput50,000 concurrent users, 500 requests/sec sustainedMid-scale gym chain with 2 million registered users
Data Durability99.999999999% (11 nines)Workout history is irreplaceable; users rage-quit over lost data
Offline SupportFull workout logging offline, sync on reconnectGyms often have poor connectivity in basements and parking garages
PrivacyHIPAA-compliant for health data, GDPR for EU usersBiometric data requires regulatory compliance
ScalabilityLinear horizontal scaling to 10M usersTargeting growth from 2M to 10M over 3 years
Media SupportVideo uploads up to 100 MB, streaming at 1080pAI form correction requires high-quality video input

3. Capacity Estimation & Back-of-Envelope

User Activity Model

Assume 2 million registered users with 400,000 daily active users. Each active user performs an average of 1 workout per day, with each workout containing roughly 6 exercises, each exercise having 3 sets. This yields approximately 400,000 workouts per day or about 4.6 workouts per second on average. During peak hours (6 AM to 9 AM and 5 PM to 8 PM), traffic spikes to roughly 3x the average, reaching approximately 14 workouts per second.

Each workout session generates approximately 18 set log entries (6 exercises × 3 sets), resulting in 7.2 million set logs per day. Each set log requires a write operation, bringing our peak write throughput to approximately 42 set logs per second. This is well within the capacity of a single PostgreSQL instance with proper indexing, but we plan for multi-region deployment from the start.

Storage Estimation

Data TypeSize per RecordDaily VolumeDaily Storage
Workout Session~2 KB (JSON payload)400,000~800 MB
Set Log Entry~200 bytes7,200,000~1.4 GB
Heart Rate Sample~32 bytes345,600,000 (1/sec per active user for 1 hr)~11 GB
Exercise Video (AI)~5 MB10,000 submissions~50 GB
User Profile~1 KBIncremental~Negligible
Nutrition Log~300 bytes800,000 (2 meals avg)~240 MB
Class Booking~150 bytes50,000~7.5 MB

Annual storage for structured data (excluding video) is approximately 4.5 TB. Video storage adds roughly 18 TB per year. We should budget for 25 TB total annual storage growth and implement tiered storage policies that move data older than 90 days to colder storage tiers.

Bandwidth Estimation

Assuming average API response size of 5 KB and 500 requests per second sustained, outbound bandwidth is approximately 2.5 MB/s or 216 GB/day. During peak, this reaches 7.5 MB/s. The heartbeat streaming endpoint from wearables adds approximately 128 Kbps of continuous inbound traffic per active session, which at 10,000 concurrent sessions totals about 1.28 Gbps — a significant but manageable load that benefits from dedicated streaming infrastructure with horizontal partitioning.

Cache Estimation

The exercise library (1,000 exercises) is read-heavy and changes rarely. At 2 KB per exercise document, the entire library fits in 2 MB of cache. User profiles and recent workout history should be cached with a TTL of 5 minutes, requiring approximately 800 MB of cache for 400,000 daily active users at 2 KB each. Leaderboards require approximately 200 KB per active challenge. Total Redis cache cluster should be provisioned at 4 GB minimum with 8 GB recommended for headroom.

4. Data Model & Database Design

We adopt a polyglot persistence strategy: PostgreSQL for transactional relational data, MongoDB for semi-structured workout session documents, Redis for caching and real-time leaderboards, TimescaleDB for time-series heart rate data, and S3-compatible object storage for media files. Each store is chosen for its strengths relative to the access patterns of the data it holds.

Entity-Relationship Overview

erDiagram USER ||--o{ WORKOUT_SESSION : logs USER ||--o{ MEMBERSHIP : holds USER ||--o{ FRIENDSHIP : has USER ||--o{ ACHIEVEMENT : earns USER }o--|| GYM : belongs_to GYM ||--o{ CLASS_SCHEDULE : offers GYM ||--o{ EQUIPMENT : contains WORKOUT_SESSION ||--o{ EXERCISE_LOG : contains EXERCISE_LOG ||--o{ SET_LOG : has EXERCISE_LOG }o--|| EXERCISE : references CLASS_SCHEDULE ||--o{ CLASS_BOOKING : has CLASS_BOOKING }o--|| USER : booked_by CLASS_SCHEDULE }o--|| TRAINER : led_by TRAINER ||--o{ CLIENT_ASSIGNMENT : manages CLIENT_ASSIGNMENT }o--|| USER : assigned_to USER ||--o{ NUTRITION_LOG : tracks USER ||--o{ BODY_MEASUREMENT : records WORKOUT_SESSION ||--o{ HEART_RATE_SAMPLE : streams

PostgreSQL Schema — Core Tables

SQL
CREATE TABLE users (
    id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    email           VARCHAR(255) UNIQUE NOT NULL,
    password_hash   VARCHAR(512) NOT NULL,
    display_name    VARCHAR(100) NOT NULL,
    avatar_url      TEXT,
    date_of_birth   DATE,
    gender          VARCHAR(20),
    height_cm       DECIMAL(5,1),
    weight_kg       DECIMAL(5,1),
    fitness_level   VARCHAR(20) CHECK (fitness_level IN ('beginner','intermediate','advanced','elite')),
    timezone        VARCHAR(50) DEFAULT 'UTC',
    created_at      TIMESTAMPTZ DEFAULT NOW(),
    updated_at      TIMESTAMPTZ DEFAULT NOW()
);

CREATE TABLE gyms (
    id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    name            VARCHAR(200) NOT NULL,
    address         TEXT,
    latitude        DECIMAL(9,6),
    longitude       DECIMAL(9,6),
    phone           VARCHAR(30),
    timezone        VARCHAR(50) DEFAULT 'UTC',
    max_capacity    INT,
    chain_id        UUID REFERENCES gym_chains(id),
    created_at      TIMESTAMPTZ DEFAULT NOW()
);

CREATE TABLE exercises (
    id                UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    name              VARCHAR(200) NOT NULL,
    description       TEXT,
    muscle_group      VARCHAR(50) NOT NULL,
    secondary_muscles TEXT[],
    equipment         VARCHAR(50),
    difficulty        VARCHAR(20) CHECK (difficulty IN ('beginner','intermediate','advanced')),
    exercise_type     VARCHAR(30) CHECK (exercise_type IN ('strength','cardio','flexibility','balance','plyometric')),
    video_url         TEXT,
    thumbnail_url     TEXT,
    met_value         DECIMAL(4,2),
    is_compound       BOOLEAN DEFAULT FALSE,
    created_at        TIMESTAMPTZ DEFAULT NOW()
);

CREATE TABLE workout_templates (
    id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    name            VARCHAR(200) NOT NULL,
    description     TEXT,
    created_by      UUID REFERENCES users(id),
    difficulty      VARCHAR(20),
    duration_weeks  INT,
    goal            VARCHAR(50),
    is_public       BOOLEAN DEFAULT FALSE,
    created_at      TIMESTAMPTZ DEFAULT NOW()
);

CREATE TABLE workout_template_exercises (
    id               UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    template_id      UUID REFERENCES workout_templates(id) ON DELETE CASCADE,
    exercise_id      UUID REFERENCES exercises(id),
    day_of_week      INT CHECK (day_of_week BETWEEN 0 AND 6),
    order_index      INT NOT NULL,
    target_sets      INT,
    target_reps      VARCHAR(20),
    target_weight_kg DECIMAL(6,2),
    rest_seconds     INT,
    notes            TEXT
);

CREATE TABLE memberships (
    id                       UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id                  UUID REFERENCES users(id),
    gym_id                   UUID REFERENCES gyms(id),
    plan_type                VARCHAR(30) CHECK (plan_type IN ('basic','standard','premium','vip')),
    status                   VARCHAR(20) CHECK (status IN ('active','paused','cancelled','expired')),
    start_date               DATE NOT NULL,
    end_date                 DATE,
    freeze_until             DATE,
    stripe_subscription_id   VARCHAR(200),
    monthly_price            DECIMAL(8,2),
    created_at               TIMESTAMPTZ DEFAULT NOW()
);

CREATE TABLE class_schedules (
    id                    UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    gym_id                UUID REFERENCES gyms(id),
    class_name            VARCHAR(200) NOT NULL,
    instructor_name       VARCHAR(200),
    trainer_id            UUID REFERENCES trainers(id),
    start_time            TIMESTAMPTZ NOT NULL,
    end_time              TIMESTAMPTZ NOT NULL,
    max_participants      INT DEFAULT 30,
    current_participants  INT DEFAULT 0,
    room                  VARCHAR(50),
    difficulty            VARCHAR(20),
    recurrence            VARCHAR(20),
    created_at            TIMESTAMPTZ DEFAULT NOW()
);

CREATE TABLE class_bookings (
    id         UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id    UUID REFERENCES users(id),
    class_id   UUID REFERENCES class_schedules(id),
    status     VARCHAR(20) CHECK (status IN ('confirmed','waitlisted','cancelled','attended')),
    booked_at  TIMESTAMPTZ DEFAULT NOW(),
    UNIQUE(user_id, class_id)
);

CREATE TABLE body_measurements (
    id             UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id        UUID REFERENCES users(id),
    measured_at    DATE NOT NULL,
    weight_kg      DECIMAL(5,1),
    body_fat_pct   DECIMAL(4,1),
    muscle_mass_kg DECIMAL(5,1),
    chest_cm       DECIMAL(5,1),
    waist_cm       DECIMAL(5,1),
    hips_cm        DECIMAL(5,1),
    bicep_cm       DECIMAL(5,1),
    thigh_cm       DECIMAL(5,1),
    notes          TEXT,
    created_at     TIMESTAMPTZ DEFAULT NOW()
);

CREATE TABLE nutrition_logs (
    id          UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id     UUID REFERENCES users(id),
    logged_at   TIMESTAMPTZ DEFAULT NOW(),
    meal_type   VARCHAR(20) CHECK (meal_type IN ('breakfast','lunch','dinner','snack')),
    food_name   VARCHAR(200),
    calories    INT,
    protein_g   DECIMAL(5,1),
    carbs_g     DECIMAL(5,1),
    fat_g       DECIMAL(5,1),
    fiber_g     DECIMAL(5,1),
    barcode     VARCHAR(50),
    serving_size VARCHAR(50)
);

CREATE TABLE achievements (
    id          UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id     UUID REFERENCES users(id),
    badge_code  VARCHAR(50) NOT NULL,
    badge_name  VARCHAR(100) NOT NULL,
    description TEXT,
    xp_value    INT DEFAULT 0,
    earned_at   TIMESTAMPTZ DEFAULT NOW()
);

CREATE TABLE notifications (
    id         UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id    UUID REFERENCES users(id),
    type       VARCHAR(50) NOT NULL,
    title      VARCHAR(200) NOT NULL,
    body       TEXT NOT NULL,
    data       JSONB,
    is_read    BOOLEAN DEFAULT FALSE,
    created_at TIMESTAMPTZ DEFAULT NOW()
);

MongoDB — Workout Session Document

JSON
{
    "_id": "ObjectId('66a1b2c3d4e5f6a7b8c9d0e1')",
    "userId": "UUID('...')",
    "gymId": "UUID('...')",
    "startedAt": "2026-07-13T06:30:00Z",
    "endedAt": "2026-07-13T07:45:00Z",
    "durationMinutes": 75,
    "sessionType": "strength",
    "templateId": "UUID('...')",
    "mood": "energized",
    "energyLevel": 8,
    "notes": "Felt strong today, hit a new PR on bench",
    "totalVolumeLoad": 4875.5,
    "estimatedCaloriesBurned": 620,
    "exercises": [
        {
            "exerciseId": "UUID('...')",
            "exerciseName": "Barbell Bench Press",
            "order": 1,
            "isSuperset": false,
            "sets": [
                { "setNumber": 1, "reps": 12, "weightKg": 60, "rpe": 6, "isWarmup": false, "restSeconds": 90 },
                { "setNumber": 2, "reps": 10, "weightKg": 80, "rpe": 7, "isWarmup": false, "restSeconds": 120 },
                { "setNumber": 3, "reps": 8, "weightKg": 95, "rpe": 8.5, "isWarmup": false, "restSeconds": 150 },
                { "setNumber": 4, "reps": 6, "weightKg": 105, "rpe": 9.5, "isWarmup": false, "restSeconds": 180 }
            ],
            "notes": "Last set was a grinder, might deload next week"
        }
    ],
    "wearableData": {
        "avgHeartRate": 132,
        "maxHeartRate": 168,
        "heartRateZones": { "zone1": 12, "zone2": 25, "zone3": 28, "zone4": 8, "zone5": 2 },
        "caloriesBurned": 620,
        "stepsDuringWorkout": 2400
    }
}

TimescaleDB — Heart Rate Time Series

SQL
CREATE TABLE heart_rate_samples (
    time        TIMESTAMPTZ NOT NULL,
    user_id     UUID NOT NULL,
    session_id  UUID,
    heart_rate  SMALLINT NOT NULL,
    source      VARCHAR(30) DEFAULT 'apple_watch'
);

SELECT create_hypertable('heart_rate_samples', 'time');

CREATE INDEX idx_hr_user_time ON heart_rate_samples (user_id, time DESC);
CREATE INDEX idx_hr_session ON heart_rate_samples (session_id, time);

5. High-Level Architecture

The system follows a microservices architecture with an API Gateway pattern. Mobile clients communicate through a central gateway that handles authentication, rate limiting, and request routing. Individual services own their data stores and communicate asynchronously via RabbitMQ for event-driven workflows and synchronously via gRPC for low-latency internal calls. The architecture is designed for independent deployment and scaling of each service based on its specific load characteristics.

graph TB subgraph Clients iOS[iOS App] Android[Android App] Web[Web Dashboard] TrainerApp[Trainer Tablet App] end subgraph Edge Layer CDN[CloudFront CDN] LB[Application Load Balancer] end subgraph API Gateway GW[Ocelot API Gateway] end subgraph Core Services AuthSvc[Auth Service] UserSvc[User Service] WorkoutSvc[Workout Service] ExerciseSvc[Exercise Service] PlanSvc[Plan Generator Service] AnalyticsSvc[Analytics Service] SocialSvc[Social Service] WearableSvc[Wearable Ingestion] BookingSvc[Class Booking Service] BillingSvc[Billing Service] TrainerSvc[Trainer Service] NutritionSvc[Nutrition Service] NotificationSvc[Notification Service] VideoSvc[Video Processing Service] AISvc[AI Form Correction] end subgraph Data Stores PG[(PostgreSQL + TimescaleDB)] Mongo[(MongoDB)] Redis[(Redis Cluster)] S3[(S3 Object Storage)] ElasticSearch[(Elasticsearch)] end subgraph Message Broker RabbitMQ[RabbitMQ] Kafka[Kafka - Heart Rate Streams] end subgraph External Stripe[Stripe API] AppleHealth[Apple HealthKit] GoogleFit[Google Fit API] PushAPNS[APNs / FCM] OpenAI[OpenAI Vision API] end iOS --> CDN --> LB Android --> LB Web --> LB TrainerApp --> LB LB --> GW GW --> AuthSvc GW --> UserSvc GW --> WorkoutSvc GW --> ExerciseSvc GW --> PlanSvc GW --> AnalyticsSvc GW --> SocialSvc GW --> BookingSvc GW --> BillingSvc GW --> NutritionSvc WearableSvc --> Kafka WorkoutSvc --> RabbitMQ AnalyticsSvc --> RabbitMQ NotificationSvc --> RabbitMQ AuthSvc --> PG UserSvc --> PG WorkoutSvc --> Mongo ExerciseSvc --> PG AnalyticsSvc --> PG SocialSvc --> Redis BookingSvc --> PG BillingSvc --> PG NutritionSvc --> Mongo VideoSvc --> S3 AISvc --> S3 BillingSvc --> Stripe WearableSvc --> AppleHealth WearableSvc --> GoogleFit NotificationSvc --> PushAPNS AISvc --> OpenAI

Service Responsibilities

ServiceResponsibilityPrimary StoreLanguage
Auth ServiceJWT issuance, refresh tokens, OAuth2, MFAPostgreSQLC#
User ServiceProfile management, preferences, body measurementsPostgreSQLC#
Workout ServiceSession CRUD, real-time set logging, PR detectionMongoDBC#
Exercise ServiceExercise catalog, search, filtering, admin CRUDPostgreSQL + ElasticsearchC#
Plan GeneratorML-powered plan generation, adaptation algorithmsPostgreSQLC# + Python ML
Analytics ServiceVolume load calculations, trend analysis, reportsTimescaleDBC#
Social ServiceActivity feed, leaderboards, friend graphRedis + PostgreSQLC#
Wearable IngestionHealthKit/Fit sync, heart rate stream processingTimescaleDB + S3C#
Class BookingSchedule management, reservations, waitlistsPostgreSQLC#
Billing ServiceStripe integration, subscriptions, invoicingPostgreSQLC#
Trainer ServiceClient management, program assignment, attendancePostgreSQLC#
Nutrition ServiceFood logging, barcode lookup, macro computationMongoDBC#
Notification ServicePush notifications, in-app alerts, schedulingPostgreSQL + RedisC#
Video ProcessingTranscoding, thumbnail generation, CDN uploadS3C#
AI Form CorrectionPose estimation, deviation scoring, feedback generationS3 + PostgreSQLC# + Python

6. API Design & RESTful Endpoints

All endpoints are versioned under /api/v1/. Authentication is handled via Bearer JWT tokens. The API Gateway handles rate limiting (1000 requests per minute per user), request validation via JSON Schema, and request/response logging for observability. All timestamps use ISO 8601 format in UTC.

Workout Endpoints

MethodEndpointDescription
POST/api/v1/workouts/sessionsCreate a new workout session
GET/api/v1/workouts/sessions/{id}Get workout session details with all exercises and sets
PATCH/api/v1/workouts/sessions/{id}Update session metadata (end time, notes, mood)
DELETE/api/v1/workouts/sessions/{id}Delete an entire workout session
POST/api/v1/workouts/sessions/{id}/exercisesAdd exercise to session
PATCH/api/v1/workouts/sessions/{id}/exercises/{eid}Update exercise order or superset grouping
POST/api/v1/workouts/sessions/{id}/exercises/{eid}/setsLog a set with reps, weight, RPE
PATCH/api/v1/workouts/sessions/{id}/exercises/{eid}/sets/{sid}Update an existing set
DELETE/api/v1/workouts/sessions/{id}/exercises/{eid}/sets/{sid}Delete a set
GET/api/v1/workouts/historyGet paginated workout history with date filtering
GET/api/v1/workouts/personal-recordsGet all personal records across exercises
POST/api/v1/workouts/syncBulk sync offline mutations

Exercise Library Endpoints

MethodEndpointDescription
GET/api/v1/exercisesList exercises with filtering by muscle, equipment, type
GET/api/v1/exercises/{id}Get exercise details with videos and muscle diagrams
GET/api/v1/exercises/search?q=benchFull-text search with autocomplete suggestions
GET/api/v1/exercises/muscle-groupsList all muscle groups with exercise counts
POST/api/v1/exercises/{id}/videoUpload exercise demonstration video (admin only)

Plan & Analytics Endpoints

MethodEndpointDescription
POST/api/v1/plans/generateGenerate AI workout plan based on goals and history
GET/api/v1/plans/{id}Get plan with daily schedules and exercise prescriptions
PATCH/api/v1/plans/{id}/adaptTrigger plan re-adaptation based on recent performance
GET/api/v1/analytics/volume-loadVolume load over date range with granularity param
GET/api/v1/analytics/estimations/{exerciseId}Estimated 1RM progression chart data
GET/api/v1/analytics/body-trendsBody measurement trend data with regression analysis
GET/api/v1/analytics/muscle-balanceMuscle group volume distribution and balance score

C# API Controller Example

C#
[ApiController]
[Route("api/v1/[controller]")]
[Authorize]
public class WorkoutSessionsController : ControllerBase
{
    private readonly IWorkoutSessionService _sessionService;
    private readonly IEventBus _eventBus;
    private readonly ILogger<WorkoutSessionsController> _logger;

    public WorkoutSessionsController(
        IWorkoutSessionService sessionService,
        IEventBus eventBus,
        ILogger<WorkoutSessionsController> logger)
    {
        _sessionService = sessionService;
        _eventBus = eventBus;
        _logger = logger;
    }

    [HttpPost]
    [ProducesResponseType(typeof(WorkoutSessionResponse), StatusCodes.Status201Created)]
    public async Task<IActionResult> CreateSession(
        [FromBody] CreateWorkoutSessionRequest request,
        CancellationToken ct)
    {
        var userId = GetUserId();

        var session = await _sessionService.CreateSessionAsync(new CreateSessionCommand
        {
            UserId = userId,
            GymId = request.GymId,
            SessionType = request.SessionType,
            TemplateId = request.TemplateId,
            StartedAt = DateTime.UtcNow
        }, ct);

        await _eventBus.PublishAsync(new WorkoutSessionStartedEvent
        {
            UserId = userId,
            SessionId = session.Id,
            StartedAt = session.StartedAt
        });

        _logger.LogInformation("User {UserId} started workout session {SessionId}", userId, session.Id);
        return CreatedAtAction(nameof(GetSession), new { id = session.Id }, session);
    }

    [HttpPost("{id:guid}/exercises/{exerciseId:guid}/sets")]
    [ProducesResponseType(typeof(SetLogResponse), StatusCodes.Status201Created)]
    public async Task<IActionResult> LogSet(
        Guid id, Guid exerciseId,
        [FromBody] LogSetRequest request,
        CancellationToken ct)
    {
        var userId = GetUserId();

        var setLog = await _sessionService.LogSetAsync(new LogSetCommand
        {
            SessionId = id,
            ExerciseId = exerciseId,
            UserId = userId,
            SetNumber = request.SetNumber,
            Reps = request.Reps,
            WeightKg = request.WeightKg,
            Rpe = request.Rpe,
            DurationSeconds = request.DurationSeconds,
            DistanceMeters = request.DistanceMeters,
            IsWarmup = request.IsWarmup,
            RestSeconds = request.RestSeconds,
            LoggedAt = DateTime.UtcNow
        }, ct);

        var prCheck = await _sessionService.CheckPersonalRecordAsync(
            userId, exerciseId, request.WeightKg, request.Reps, ct);

        if (prCheck.IsNewPR)
        {
            await _eventBus.PublishAsync(new PersonalRecordAchievedEvent
            {
                UserId = userId,
                ExerciseId = exerciseId,
                PRType = prCheck.PRType,
                PreviousValue = prCheck.PreviousValue,
                NewValue = prCheck.NewValue
            });
        }

        return CreatedAtAction(nameof(LogSet), new { id, exerciseId }, setLog);
    }

    [HttpGet("history")]
    [ProducesResponseType(typeof(PaginatedResponse<WorkoutSessionSummary>), StatusCodes.Status200OK)]
    public async Task<IActionResult> GetHistory(
        [FromQuery] int page = 1,
        [FromQuery] int pageSize = 20,
        [FromQuery] DateTime? fromDate = null,
        [FromQuery] DateTime? toDate = null,
        CancellationToken ct = default)
    {
        var userId = GetUserId();
        var result = await _sessionService.GetHistoryAsync(userId, page, pageSize, fromDate, toDate, ct);
        return Ok(result);
    }

    private Guid GetUserId() => Guid.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!);
}

7. Workout Tracking & Logging

Workout logging is the core feature that defines the user experience. It must be fast, reliable, and forgiving of poor network conditions. The key architectural decision is to use an offline-first approach where the mobile app maintains a local SQLite database of the current workout session and syncs to the server when connectivity is available. This is critical because gym environments often have thick concrete walls that block cellular signals, especially in basements and parking garage workout areas.

Offline Sync Strategy

Each set logged offline is assigned a monotonically increasing local sequence number. When the app reconnects, it performs a bulk sync that sends all pending mutations in order. The server applies these idempotently using the sequence number and a client-generated UUID for each operation. This guarantees exactly-once semantics even if the client retries due to network instability.

C#
public class OfflineSyncService
{
    private readonly IWorkoutSessionRepository _repository;
    private readonly ISyncQueueRepository _syncQueue;
    private readonly IEventBus _eventBus;

    public async Task<SyncResult> SyncPendingMutationsAsync(
        Guid userId, List<PendingMutation> mutations, CancellationToken ct)
    {
        var result = new SyncResult
        {
            ProcessedCount = 0,
            Conflicts = new List<SyncConflict>()
        };

        var orderedMutations = mutations.OrderBy(m => m.SequenceNumber).ToList();
        await using var transaction = await _repository.BeginTransactionAsync(ct);

        try
        {
            foreach (var mutation in orderedMutations)
            {
                var isApplied = await _syncQueue.IsAlreadyAppliedAsync(
                    mutation.IdempotencyKey, ct);

                if (isApplied)
                {
                    result.ProcessedCount++;
                    continue;
                }

                switch (mutation.Type)
                {
                    case MutationType.LogSet:
                        var setCmd = JsonSerializer.Deserialize<LogSetCommand>(mutation.Payload)!;
                        await _repository.LogSetAsync(setCmd, ct);
                        break;
                    case MutationType.UpdateSet:
                        var updateCmd = JsonSerializer.Deserialize<UpdateSetCommand>(mutation.Payload)!;
                        await _repository.UpdateSetAsync(updateCmd, ct);
                        break;
                    case MutationType.DeleteSet:
                        var deleteCmd = JsonSerializer.Deserialize<DeleteSetCommand>(mutation.Payload)!;
                        await _repository.DeleteSetAsync(deleteCmd, ct);
                        break;
                    case MutationType.EndSession:
                        var endCmd = JsonSerializer.Deserialize<EndSessionCommand>(mutation.Payload)!;
                        await _repository.EndSessionAsync(endCmd, ct);
                        break;
                }

                await _syncQueue.MarkAppliedAsync(mutation.IdempotencyKey, ct);
                result.ProcessedCount++;
            }

            await transaction.CommitAsync(ct);
        }
        catch (Exception ex)
        {
            await transaction.RollbackAsync(ct);
            throw new SyncFailedException("Bulk sync failed; client should retry", ex);
        }

        return result;
    }
}

Real-Time Personal Record Detection

When a user logs a new set, we compute the estimated one-rep-max using the Epley formula: e1rm = weight * (1 + reps / 30). We compare against cached PRs in Redis and emit an event if a new record is set. The PR is persisted asynchronously to avoid blocking the logging path. The event triggers a notification to the user, updates the social activity feed, and may trigger achievement checks in the gamification engine.

C#
public class PersonalRecordService
{
    private readonly IRedisCache _cache;
    private readonly IPersonalRecordRepository _repository;

    public decimal CalculateEstimatedOneRepMax(decimal weightKg, int reps)
    {
        if (reps == 1) return weightKg;
        return Math.Round(weightKg * (1 + reps / 30m), 1);
    }

    public async Task<PRCheckResult> CheckAndRecordPRAsync(
        Guid userId, Guid exerciseId, decimal weightKg, int reps,
        CancellationToken ct)
    {
        var e1rm = CalculateEstimatedOneRepMax(weightKg, reps);
        var cacheKey = $"pr:user:{userId}:exercise:{exerciseId}";

        var currentPR = await _cache.GetAsync<PersonalRecordDto>(cacheKey, ct);
        var result = new PRCheckResult();

        if (currentPR == null || e1rm > currentPR.EstimatedOneRepMax)
        {
            var newPR = new PersonalRecord
            {
                UserId = userId,
                ExerciseId = exerciseId,
                WeightKg = weightKg,
                Reps = reps,
                EstimatedOneRepMax = e1rm,
                AchievedAt = DateTime.UtcNow
            };

            await _repository.UpsertAsync(newPR, ct);
            await _cache.SetAsync(cacheKey, new PersonalRecordDto
            {
                WeightKg = weightKg,
                Reps = reps,
                EstimatedOneRepMax = e1rm,
                AchievedAt = DateTime.UtcNow
            }, TimeSpan.FromDays(365), ct);

            result.IsNewPR = true;
            result.PRType = currentPR == null
                ? PRType.FirstRecord
                : PRType.BestE1RM;
            result.PreviousValue = currentPR?.EstimatedOneRepMax;
            result.NewValue = e1rm;
        }

        return result;
    }
}

Volume Load Computation

Volume load for a workout session is the sum of sets * reps * weight for every exercise. This metric is critical for tracking progressive overload — the foundational principle of strength training adaptation. We compute it both in real time (for the session summary displayed on the completion screen) and in batch (for weekly and monthly aggregate charts in the analytics dashboard).

C#
public static class VolumeLoadCalculator
{
    public static decimal ComputeSessionVolume(WorkoutSession session)
    {
        return session.Exercises.Sum(exercise =>
            exercise.Sets
                .Where(s => !s.IsWarmup)
                .Sum(set => set.Reps * set.WeightKg)
        );
    }

    public static decimal ComputeExerciseVolume(ExerciseLog exercise)
    {
        return exercise.Sets
            .Where(s => !s.IsWarmup)
            .Sum(set => set.Reps * set.WeightKg);
    }

    public static decimal ComputeWeeklyVolume(IEnumerable<WorkoutSession> sessions)
    {
        return sessions.Sum(ComputeSessionVolume);
    }

    public static decimal ComputeAverageDailyVolume(
        IEnumerable<WorkoutSession> weekSessions, int daysInPeriod)
    {
        var total = ComputeWeeklyVolume(weekSessions);
        return Math.Round(total / daysInPeriod, 1);
    }

    public static Dictionary<string, decimal> ComputeMuscleGroupVolume(
        IEnumerable<WorkoutSession> sessions,
        Dictionary<Guid, Exercise> exerciseLookup)
    {
        return sessions
            .SelectMany(s => s.Exercises)
            .GroupBy(e =>
                exerciseLookup.TryGetValue(e.ExerciseId, out var ex)
                    ? ex.MuscleGroup : "Unknown")
            .ToDictionary(
                g => g.Key,
                g => g.Sum(e => ComputeExerciseVolume(e))
            );
    }
}

8. Exercise Library

The exercise library is the foundation upon which all workout features are built. It contains 1,000+ exercises, each tagged with primary and secondary muscle groups, required equipment, difficulty level, exercise type (strength, cardio, flexibility, balance, plyometric), MET value for calorie estimation, and optional video demonstrations. The library is read-heavy (99.5% reads) and relatively static, making it ideal for aggressive caching with Redis and CDN-level caching for media assets.

Elasticsearch Index Mapping

JSON
{
    "mappings": {
        "properties": {
            "name": { "type": "text", "analyzer": "english" },
            "name_suggest": { "type": "completion" },
            "muscle_group": { "type": "keyword" },
            "secondary_muscles": { "type": "keyword" },
            "equipment": { "type": "keyword" },
            "difficulty": { "type": "keyword" },
            "exercise_type": { "type": "keyword" },
            "is_compound": { "type": "boolean" },
            "met_value": { "type": "float" },
            "description": { "type": "text", "analyzer": "english" },
            "tags": { "type": "keyword" }
        }
    }
}

C# Exercise Search Service

C#
public class ExerciseSearchService : IExerciseSearchService
{
    private readonly IElasticClient _elastic;
    private readonly IDistributedCache _cache;

    public async Task<PagedResult<ExerciseDto>> SearchAsync(
        ExerciseSearchCriteria criteria, CancellationToken ct)
    {
        var cacheKey = BuildCacheKey(criteria);
        var cached = await _cache.GetAsync<PagedResult<ExerciseDto>>(cacheKey, ct);
        if (cached != null) return cached;

        var searchDescriptor = new SearchDescriptor<ExerciseDocument>()
            .Index("exercises")
            .Size(criteria.PageSize)
            .From((criteria.Page - 1) * criteria.PageSize);

        searchDescriptor.Query(q => q
            .Bool(b => b
                .Must(must =>
                {
                    var queries = new List<Func<
                        QueryContainerDescriptor<ExerciseDocument>,
                        QueryContainer>>();

                    if (!string.IsNullOrWhiteSpace(criteria.Query))
                    {
                        queries.Add(qq => qq.Match(mm => mm
                            .Field(f => f.Name)
                            .Query(criteria.Query)
                            .Fuzziness(Fuzziness.Auto)
                        ));
                    }

                    if (!string.IsNullOrWhiteSpace(criteria.MuscleGroup))
                    {
                        queries.Add(qq => qq.Term(t => t
                            .Field(f => f.MuscleGroup)
                            .Value(criteria.MuscleGroup)
                        ));
                    }

                    if (!string.IsNullOrWhiteSpace(criteria.Equipment))
                    {
                        queries.Add(qq => qq.Term(t => t
                            .Field(f => f.Equipment)
                            .Value(criteria.Equipment)
                        ));
                    }

                    if (!string.IsNullOrWhiteSpace(criteria.Difficulty))
                    {
                        queries.Add(qq => qq.Term(t => t
                            .Field(f => f.Difficulty)
                            .Value(criteria.Difficulty)
                        ));
                    }

                    if (criteria.IsCompound.HasValue)
                    {
                        queries.Add(qq => qq.Term(t => t
                            .Field(f => f.IsCompound)
                            .Value(criteria.IsCompound.Value)
                        ));
                    }

                    return queries.Any()
                        ? b.Must(queries.ToArray())
                        : null;
                })
            )
        ).Sort(s => s.Ascending(a => a.Name));

        var response = await _elastic.SearchAsync<ExerciseDocument>(
            searchDescriptor, ct);

        var result = new PagedResult<ExerciseDto>
        {
            Items = response.Documents.Select(MapToDto).ToList(),
            TotalCount = (int)response.Total,
            Page = criteria.Page,
            PageSize = criteria.PageSize
        };

        await _cache.SetAsync(cacheKey, result,
            TimeSpan.FromMinutes(10), ct);

        return result;
    }
}

9. Personalized Workout Plans

Personalized workout plans are generated using a hybrid approach: a rule-based engine handles the structural scaffolding (split type, exercise selection, volume prescription) while a lightweight ML model predicts optimal sets, reps, and weight targets based on the user's training history and recovery metrics. Plans are regenerated weekly as new data arrives from completed workouts and wearable sleep data.

Plan Generation Algorithm

C#
public class WorkoutPlanGenerator : IWorkoutPlanGenerator
{
    private readonly ITrainingHistoryAnalyzer _historyAnalyzer;
    private readonly IExerciseRepository _exerciseRepo;
    private readonly IRecoveryPredictor _recoveryPredictor;

    public async Task<WorkoutPlan> GeneratePlanAsync(
        PlanGenerationRequest request, CancellationToken ct)
    {
        var userProfile = request.UserProfile;
        var history = await _historyAnalyzer
            .GetRecentHistoryAsync(userProfile.UserId, weeks: 12, ct);

        var split = DetermineOptimalSplit(userProfile, history);
        var exercisesPerDay = SelectExercises(
            split, userProfile.Goal,
            userProfile.AvailableEquipment, ct);
        var volumeTargets = ComputeVolumeTargets(
            exercisesPerDay, userProfile.FitnessLevel, history);

        var plan = new WorkoutPlan
        {
            UserId = userProfile.UserId,
            Name = $"{split.Name} — {request.Goal}",
            Goal = request.Goal,
            DurationWeeks = 8,
            Difficulty = userProfile.FitnessLevel,
            Days = new List<PlanDay>()
        };

        foreach (var dayTemplate in split.Days)
        {
            var planDay = new PlanDay
            {
                DayOfWeek = dayTemplate.DayOfWeek,
                Focus = dayTemplate.Focus,
                Exercises = new List<PlannedExercise>()
            };

            foreach (var exerciseSlot in dayTemplate.Exercises)
            {
                var prescription = volumeTargets[exerciseSlot.ExerciseId];
                var lastPerf = history.GetLastPerformance(exerciseSlot.ExerciseId);

                var predictedWeight = PredictStartingWeight(
                    lastPerf, prescription, userProfile);

                planDay.Exercises.Add(new PlannedExercise
                {
                    ExerciseId = exerciseSlot.ExerciseId,
                    ExerciseName = exerciseSlot.ExerciseName,
                    TargetSets = prescription.TargetSets,
                    TargetReps = prescription.TargetReps,
                    TargetWeightKg = predictedWeight,
                    RestSeconds = prescription.RestSeconds,
                    RPE = prescription.TargetRPE,
                    Notes = GenerateCoachNotes(
                        exerciseSlot, prescription, lastPerf)
                });
            }

            plan.Days.Add(planDay);
        }

        plan.WeeklyVolumeLoad = plan.Days
            .SelectMany(d => d.Exercises)
            .Sum(e => e.TargetSets * e.TargetReps * e.TargetWeightKg);

        return plan;
    }

    private TrainingSplit DetermineOptimalSplit(
        UserProfile profile, TrainingHistory history)
    {
        var daysPerWeek = profile.AvailableDaysPerWeek ?? 4;
        var level = profile.FitnessLevel;

        return (daysPerWeek, level) switch
        {
            (3, _) => TrainingSplit.UpperLower3Day,
            (4, "beginner") => TrainingSplit.FullBody4Day,
            (4, "intermediate") or (4, "advanced")
                => TrainingSplit.UpperLower4Day,
            (5, _) => TrainingSplit.PushPullLegsUpperLower,
            (6, _) => TrainingSplit.PushPullLegs2x,
            _ => TrainingSplit.FullBody3Day
        };
    }

    private decimal PredictStartingWeight(
        ExercisePerformance? lastPerf,
        VolumePrescription prescription,
        UserProfile profile)
    {
        if (lastPerf == null)
        {
            return profile.FitnessLevel switch
            {
                "beginner" => 20m,
                "intermediate" => 40m,
                "advanced" => 60m,
                _ => 80m
            };
        }

        var estimated1RM = lastPerf.EstimatedOneRepMax;
        var targetPct = prescription.TargetPercentageOf1RM / 100m;
        var predicted1RM = estimated1RM * 1.02m;

        return Math.Round(predicted1RM * targetPct / 2.5m) * 2.5m;
    }
}

Plan Adaptation Engine

Every Sunday night, the adaptation engine analyzes the past week's performance data. If a user completed all prescribed sets and reps with RPE below the target threshold, the plan auto-progresses by increasing weight by 2.5 to 5 percent. If the user failed to complete sets or reported consistently high RPE values, the plan auto-regresses by reducing volume by 10 to 20 percent and may substitute exercises to address weak points. This closed-loop system ensures the user is always training within an optimal stimulus range without requiring manual plan adjustments from a trainer.

Adaptation Logic Summary: If completion rate > 90% and avg RPE < target - 1, increase load by 2.5-5%. If completion rate < 70% or avg RPE > target + 1, reduce volume by 10-20%. If completion rate is between 70-90%, maintain current load. If sleep quality is below 60% for 3+ consecutive days, reduce intensity by 10% as a recovery precaution.

10. Progress Analytics & Charts

Progress analytics transform raw workout data into actionable insights. The analytics service runs nightly batch jobs to pre-compute aggregates and materializes them into summary tables for fast dashboard queries. Real-time queries for current-session metrics are served directly from the operational stores. The key design principle is to never compute expensive aggregations on the hot path — all historical analytics should be pre-computed and cached.

Key Metrics Computed

MetricFormula / MethodComputation Frequency
Estimated 1RMEpley: weight * (1 + reps/30)Real-time on each set log
Volume LoadSUM(sets * reps * weight)Real-time per session, nightly aggregate
Training FrequencyCount of sessions per weekNightly batch
Personal RecordsMax weight * reps combination per exerciseReal-time detection, persisted nightly
Body Weight Trend7-day moving average of daily weigh-insNightly batch
Body Fat TrendLinear regression of monthly measurementsOn new measurement
Muscle Group VolumeSUM(volume_load) grouped by muscle_group per weekNightly batch
Strength ScoreWeighted sum of top-5 exercise e1RMsNightly batch

C# Analytics Service

C#
public class AnalyticsService : IAnalyticsService
{
    private readonly IWorkoutSessionRepository _sessionRepo;
    private readonly IBodyMeasurementRepository _measurementRepo;
    private readonly IPersonalRecordRepository _prRepo;
    private readonly IExerciseRepository _exerciseRepo;

    public async Task<VolumeLoadChartDto> GetVolumeLoadOverTimeAsync(
        Guid userId, DateTime fromDate, DateTime toDate,
        string granularity, CancellationToken ct)
    {
        var sessions = await _sessionRepo.GetSessionsInRangeAsync(
            userId, fromDate, toDate, ct);

        var groupedData = granularity.ToLower() switch
        {
            "daily" => sessions.GroupBy(s => s.StartedAt.Date)
                .Select(g => new VolumeDataPoint
                {
                    Date = g.Key,
                    VolumeLoad = VolumeLoadCalculator
                        .ComputeWeeklyVolume(g),
                    SessionCount = g.Count()
                }).OrderBy(d => d.Date).ToList(),

            "weekly" => sessions
                .GroupBy(s => ISOWeek.GetWeekOfMonth(s.StartedAt))
                .Select(g => new VolumeDataPoint
                {
                    Date = g.First().StartedAt,
                    VolumeLoad = VolumeLoadCalculator
                        .ComputeWeeklyVolume(g),
                    SessionCount = g.Count()
                }).OrderBy(d => d.Date).ToList(),

            "monthly" => sessions
                .GroupBy(s => new { s.StartedAt.Year, s.StartedAt.Month })
                .Select(g => new VolumeDataPoint
                {
                    Date = new DateTime(g.Key.Year, g.Key.Month, 1),
                    VolumeLoad = VolumeLoadCalculator
                        .ComputeWeeklyVolume(g),
                    SessionCount = g.Count()
                }).OrderBy(d => d.Date).ToList(),

            _ => throw new ArgumentException(
                $"Unsupported granularity: {granularity}")
        };

        return new VolumeLoadChartDto
        {
            DataPoints = groupedData,
            TotalVolume = groupedData.Sum(d => d.VolumeLoad),
            AveragePerSession = groupedData.Average(d =>
                d.VolumeLoad / Math.Max(d.SessionCount, 1)),
            Trend = CalculateTrend(
                groupedData.Select(d => d.VolumeLoad).ToList())
        };
    }

    public async Task<E1RMProgressionDto> GetE1RMProgressionAsync(
        Guid userId, Guid exerciseId, CancellationToken ct)
    {
        var sessions = await _sessionRepo
            .GetSessionsContainingExerciseAsync(userId, exerciseId, ct);

        var progressionPoints = sessions
            .SelectMany(s => s.Exercises
                .Where(e => e.ExerciseId == exerciseId))
            .SelectMany(e => e.Sets
                .Where(s => !s.IsWarmup && s.Reps > 0))
            .GroupBy(s => s.LoggedAt.Date)
            .Select(g => new E1RMDataPoint
            {
                Date = g.Key,
                EstimatedOneRepMax = g.Max(s =>
                    PersonalRecordService
                        .CalculateEstimatedOneRepMax(
                            s.WeightKg, s.Reps)),
                BestSet = g.OrderByDescending(s =>
                    PersonalRecordService
                        .CalculateEstimatedOneRepMax(
                            s.WeightKg, s.Reps)).First()
            })
            .OrderBy(p => p.Date)
            .ToList();

        return new E1RMProgressionDto
        {
            ExerciseId = exerciseId,
            Points = progressionPoints,
            CurrentE1RM = progressionPoints
                .LastOrDefault()?.EstimatedOneRepMax ?? 0,
            AllTimeBest = progressionPoints.Any()
                ? progressionPoints.Max(p => p.EstimatedOneRepMax)
                : 0,
            Improvement = progressionPoints.Count >= 2
                ? progressionPoints.Last().EstimatedOneRepMax
                  - progressionPoints.First().EstimatedOneRepMax
                : 0
        };
    }
}

11. Social Features & Challenges

Social features drive retention by creating accountability loops. Research shows that users who connect with friends are 3.5x more likely to maintain their workout streak beyond 90 days. The social service manages the friend graph, activity feed, leaderboards, and challenge system. It uses a combination of PostgreSQL for the relational friend graph and Redis for real-time leaderboard rankings and feed caching.

Challenge System

Challenges are time-bound competitions with measurable goals. Types include total volume load, most workouts completed, longest streak, highest average RPE, and most calories burned. Leaderboards are computed in real time using Redis Sorted Sets for sub-millisecond ranking queries. Challenges can be global (open to all users), gym-specific, or friend-group only.

C#
public class ChallengeService : IChallengeService
{
    private readonly IChallengeRepository _challengeRepo;
    private readonly IRedisCache _redis;
    private readonly INotificationService _notificationService;

    public async Task JoinChallengeAsync(
        Guid userId, Guid challengeId, CancellationToken ct)
    {
        var challenge = await _challengeRepo
            .GetByIdAsync(challengeId, ct);

        if (challenge.Status != ChallengeStatus.Active)
            throw new ChallengeNotActiveException(challengeId);

        if (challenge.EndDate < DateTime.UtcNow)
            throw new ChallengeExpiredException(challengeId);

        if (challenge.MaxParticipants.HasValue
            && challenge.CurrentParticipants
               >= challenge.MaxParticipants.Value)
            throw new ChallengeFullException(challengeId);

        var participant = new ChallengeParticipant
        {
            ChallengeId = challengeId,
            UserId = userId,
            JoinedAt = DateTime.UtcNow,
            CurrentScore = 0
        };

        await _challengeRepo.AddParticipantAsync(participant, ct);

        var leaderboardKey =
            $"challenge:leaderboard:{challengeId}";
        await _redis.SortedSetAddAsync(
            leaderboardKey, userId.ToString(), 0);

        await _notificationService.SendAsync(new Notification
        {
            UserId = userId,
            Type = "challenge_joined",
            Title = "Challenge Joined!",
            Body = $"You joined \"{challenge.Title}\". Let's go!",
            Data = JsonSerializer.Serialize(new
                { ChallengeId = challengeId })
        }, ct);
    }

    public async Task UpdateChallengeScoreAsync(
        Guid userId, Guid challengeId,
        decimal scoreDelta, CancellationToken ct)
    {
        var leaderboardKey =
            $"challenge:leaderboard:{challengeId}";
        await _redis.SortedSetIncrementAsync(
            leaderboardKey, userId.ToString(),
            (double)scoreDelta);

        var rank = await _redis.SortedSetRankAsync(
            leaderboardKey, userId.ToString());
        var challenge = await _challengeRepo
            .GetByIdAsync(challengeId, ct);

        if (rank.HasValue && rank.Value == 0
            && challenge.NotifyOnFirstPlace)
        {
            await _notificationService
                .BroadcastToParticipantsAsync(challengeId,
                    new Notification
            {
                Type = "challenge_leader_changed",
                Title = "Leaderboard Update!",
                Body = $"New leader in \"{challenge.Title}\"!"
            }, ct);
        }
    }

    public async Task<LeaderboardDto> GetLeaderboardAsync(
        Guid challengeId, int topN = 50,
        CancellationToken ct)
    {
        var leaderboardKey =
            $"challenge:leaderboard:{challengeId}";
        var entries = await _redis
            .SortedSetRangeByRankWithScoresAsync(
                leaderboardKey, 0, topN - 1,
                Order.Descending);

        var userIds = entries
            .Select(e => Guid.Parse(e.Element)).ToList();
        var profiles = await _userRepository
            .GetByIdsAsync(userIds, ct);

        return new LeaderboardDto
        {
            ChallengeId = challengeId,
            Entries = entries
                .Select((entry, index) =>
                    new LeaderboardEntry
            {
                Rank = index + 1,
                UserId = Guid.Parse(entry.Element),
                DisplayName = profiles
                    .GetValueOrDefault(Guid.Parse(entry.Element))
                    ?.DisplayName ?? "Unknown",
                Score = (decimal)entry.Score
            }).ToList()
        };
    }
}

Activity Feed Design

The activity feed uses a fan-out-on-write approach for users with fewer than 500 followers and fan-out-on-read for users with more than 500 followers. This hybrid approach balances write amplification against read latency. Activities include workout completed, PR achieved, challenge milestone, achievement earned, and class attended. The feed is stored as a Redis list per user with a 30-day TTL, backed by PostgreSQL for long-term persistence. Users can filter their feed by activity type, date range, or specific friends.

12. Wearable Device Integration (Apple Health / Google Fit)

Wearable integration is essential for automatic data collection and removing friction from the tracking experience. The system supports Apple HealthKit (iOS), Google Health Connect (Android), Garmin Connect API, and Fitbit Web API. The wearable ingestion service handles OAuth2 authorization flows, data normalization across different units and formats, duplicate detection, and conflict resolution when the same data arrives from multiple sources.

Data Flow Architecture

sequenceDiagram participant App as Mobile App participant Wearable as Apple HealthKit participant API as Wearable Ingestion API participant Kafka as Kafka Stream participant Worker as Stream Processor participant DB as TimescaleDB App->>Wearable: Request health data authorization Wearable-->>App: Authorized App->>Wearable: Read workout and heart rate data Wearable-->>App: HKSampleQuery results App->>API: POST /api/v1/wearable/sync API->>Kafka: Publish WearableDataSyncedEvent API-->>App: 202 Accepted Kafka->>Worker: Consume event Worker->>Worker: Normalize units, deduplicate Worker->>DB: Upsert heart rate samples Worker->>DB: Update session wearable metrics Worker->>Worker: Compute HR zones, calories

C# Wearable Sync Service

C#
public class WearableSyncService : IWearableSyncService
{
    private readonly IHeartRateRepository _hrRepo;
    private readonly ISleepDataRepository _sleepRepo;
    private readonly IEventBus _eventBus;

    public async Task<SyncResult> SyncHealthKitDataAsync(
        Guid userId, HealthKitSyncPayload payload,
        CancellationToken ct)
    {
        var result = new SyncResult();

        if (payload.HeartRateSamples?.Any() == true)
        {
            var normalized = payload.HeartRateSamples
                .Select(s => new HeartRateSample
                {
                    UserId = userId,
                    Time = s.Timestamp,
                    HeartRate = (short)s.Value,
                    Source = s.SourceName ?? "apple_watch",
                    SessionId = await MatchToSessionAsync(
                        userId, s.Timestamp, ct)
                })
                .GroupBy(s => new { s.UserId, s.Time })
                .Select(g => g.First())
                .ToList();

            await _hrRepo.BulkUpsertAsync(normalized, ct);
            result.HeartRateSamplesProcessed =
                normalized.Count;
        }

        if (payload.SleepSamples?.Any() == true)
        {
            var sleepRecords = payload.SleepSamples
                .Select(s => new SleepRecord
            {
                UserId = userId,
                Date = s.StartTime.Date,
                BedTime = s.StartTime,
                WakeTime = s.EndTime,
                TotalMinutes =
                    (int)(s.EndTime - s.StartTime)
                        .TotalMinutes,
                DeepMinutes = s.SleepStages?
                    .Where(st => st.Type == "deep")
                    .Sum(st => (int)st.Duration
                        .TotalMinutes) ?? 0,
                REMMinutes = s.SleepStages?
                    .Where(st => st.Type == "rem")
                    .Sum(st => (int)st.Duration
                        .TotalMinutes) ?? 0,
                Quality = CalculateSleepQuality(s)
            }).ToList();

            await _sleepRepo.BulkUpsertAsync(
                sleepRecords, ct);
            result.SleepRecordsProcessed =
                sleepRecords.Count;
        }

        await _eventBus.PublishAsync(
            new WearableDataSyncedEvent
        {
            UserId = userId,
            HeartRateCount =
                result.HeartRateSamplesProcessed,
            SyncedAt = DateTime.UtcNow
        });

        return result;
    }

    private async Task<Guid?> MatchToSessionAsync(
        Guid userId, DateTime timestamp,
        CancellationToken ct)
    {
        var session = await _sessionRepo
            .FindActiveSessionAtTimeAsync(
                userId, timestamp, ct);
        return session?.Id;
    }
}

13. Class Booking & Scheduling

Class booking handles group fitness classes like yoga, spin, CrossFit, Zumba, HIIT, and pilates. The key challenge is handling concurrent booking requests for popular classes with limited capacity. We use optimistic concurrency control with a version column to prevent overbooking without the performance penalty of pessimistic database locks. The system also handles recurring class schedules, instructor availability, and room assignments.

Booking with Concurrency Control

C#
public class ClassBookingService : IClassBookingService
{
    private readonly IClassScheduleRepository _classRepo;
    private readonly IBookingRepository _bookingRepo;
    private readonly INotificationService _notifService;

    private const int MaxRetryAttempts = 3;

    public async Task<BookingResult> BookClassAsync(
        Guid userId, Guid classId, CancellationToken ct)
    {
        for (int attempt = 0;
            attempt < MaxRetryAttempts; attempt++)
        {
            var schedule = await _classRepo
                .GetByIdAsync(classId, ct);

            if (schedule == null)
                throw new ClassNotFoundException(classId);
            if (schedule.Status == ClassStatus.Cancelled)
                throw new ClassCancelledException(classId);
            if (schedule.StartTime <= DateTime.UtcNow)
                throw new ClassAlreadyStartedException(classId);

            var existing = await _bookingRepo
                .GetByUserAndClassAsync(userId, classId, ct);

            if (existing != null)
                throw new DuplicateBookingException(
                    userId, classId);

            if (schedule.CurrentParticipants
                >= schedule.MaxParticipants)
            {
                var waitlist = new ClassBooking
                {
                    UserId = userId,
                    ClassId = classId,
                    Status = BookingStatus.Waitlisted,
                    BookedAt = DateTime.UtcNow
                };
                await _bookingRepo.AddAsync(waitlist, ct);
                var position = await _bookingRepo
                    .GetWaitlistPositionAsync(classId, ct);

                return new BookingResult
                {
                    Status = BookingStatus.Waitlisted,
                    Position = position
                };
            }

            try
            {
                schedule.CurrentParticipants++;
                await _classRepo.UpdateAsync(
                    schedule, ct);

                var booking = new ClassBooking
                {
                    UserId = userId,
                    ClassId = classId,
                    Status = BookingStatus.Confirmed,
                    BookedAt = DateTime.UtcNow
                };
                await _bookingRepo.AddAsync(booking, ct);

                return new BookingResult
                {
                    Status = BookingStatus.Confirmed,
                    Position = null
                };
            }
            catch (DbUpdateConcurrencyException)
            {
                if (attempt == MaxRetryAttempts - 1)
                    throw;
                await Task.Delay(
                    50 * (attempt + 1), ct);
            }
        }

        throw new BookingFailedException(
            "Unable to complete booking after retries");
    }
}

14. Membership & Billing

The billing service integrates with Stripe for payment processing and subscription management. It handles tiered membership plans (Basic at 29 dollars/month, Standard at 49 dollars/month, Premium at 79 dollars/month, VIP at 129 dollars/month), prorated upgrades and downgrades, membership freezes, and annual payment discounts. The service must handle webhook callbacks from Stripe for payment succeeded, payment failed, subscription cancelled, and dispute events.

C# Stripe Integration

C#
public class BillingService : IBillingService
{
    private readonly StripeClient _stripe;
    private readonly IMembershipRepository _membershipRepo;
    private readonly IEventBus _eventBus;

    public async Task<MembershipResult> CreateSubscriptionAsync(
        CreateSubscriptionCommand command,
        CancellationToken ct)
    {
        var customer = await EnsureStripeCustomerAsync(
            command.UserId, command.Email, ct);

        var priceId = command.PlanType switch
        {
            PlanType.Basic => "price_basic_monthly",
            PlanType.Standard => "price_standard_monthly",
            PlanType.Premium => "price_premium_monthly",
            PlanType.Vip => "price_vip_monthly",
            _ => throw new ArgumentException(
                $"Unknown plan: {command.PlanType}")
        };

        var options = new SubscriptionCreateOptions
        {
            Customer = customer.Id,
            Items = new List<SubscriptionItemOptions>
            {
                new SubscriptionItemOptions
                {
                    Price = priceId, Quantity = 1
                }
            },
            PaymentBehavior = "default_incomplete",
            Expand = new List<string>
            {
                "latest_invoice.payment_intent"
            },
            Metadata = new Dictionary<string, string>
            {
                { "user_id",
                    command.UserId.ToString() },
                { "gym_id",
                    command.GymId.ToString() },
                { "plan_type",
                    command.PlanType.ToString() }
            }
        };

        var subscription = await _stripe.Subscriptions
            .CreateAsync(options);

        var membership = new Membership
        {
            UserId = command.UserId,
            GymId = command.GymId,
            PlanType = command.PlanType,
            Status = MembershipStatus.Pending,
            StartDate = DateTime.UtcNow.Date,
            StripeSubscriptionId = subscription.Id,
            MonthlyPrice =
                GetMonthlyPrice(command.PlanType)
        };

        await _membershipRepo.AddAsync(membership, ct);

        return new MembershipResult
        {
            MembershipId = membership.Id,
            ClientSecret =
                ((Invoice)subscription.LatestInvoice)
                    .PaymentIntent.ClientSecret,
            Status = membership.Status
        };
    }

    public async Task FreezeMembershipAsync(
        Guid userId, DateTime untilDate,
        CancellationToken ct)
    {
        var membership = await _membershipRepo
            .GetActiveByUserAsync(userId, ct)
            ?? throw new NoActiveMembershipException(userId);

        var freezeDays = (untilDate - DateTime.UtcNow).Days;
        if (freezeDays > 90)
            throw new FreezeTooLongException(90);
        if (freezeDays < 7)
            throw new FreezeTooShortException(7);

        membership.Status = MembershipStatus.Paused;
        membership.FreezeUntil = untilDate;
        await _membershipRepo.UpdateAsync(membership, ct);

        await _stripe.Subscriptions.UpdateAsync(
            membership.StripeSubscriptionId,
            new SubscriptionUpdateOptions
            {
                PauseCollection =
                    new SubscriptionPauseCollectionOptions
                    {
                        Behavior = "void"
                    }
            });
    }

    private decimal GetMonthlyPrice(PlanType plan) =>
        plan switch
        {
            PlanType.Basic => 29m,
            PlanType.Standard => 49m,
            PlanType.Premium => 79m,
            PlanType.Vip => 129m,
            _ => throw new ArgumentException(
                $"Unknown plan: {plan}")
        };
}

15. Trainer Dashboard

The trainer dashboard gives fitness professionals a comprehensive view of their assigned clients. Trainers can see client workout adherence rates, progress trends, upcoming class schedules, flag at-risk clients who are dropping off, and can assign or modify workout plans directly from the dashboard. The dashboard is optimized for tablet use with a responsive grid layout and supports real-time updates via SignalR websockets for live client activity monitoring.

Trainer Service Implementation

C#
public class TrainerService : ITrainerService
{
    private readonly IClientAssignmentRepository
        _assignmentRepo;
    private readonly IWorkoutSessionRepository
        _sessionRepo;
    private readonly IWorkoutPlanRepository _planRepo;
    private readonly IAnalyticsService _analyticsService;

    public async Task<TrainerDashboardDto> GetDashboardAsync(
        Guid trainerId, CancellationToken ct)
    {
        var assignments = await _assignmentRepo
            .GetByTrainerAsync(trainerId, ct);

        var clientSummaries =
            new List<ClientSummaryDto>();

        foreach (var assignment in assignments)
        {
            var recentSessions = await _sessionRepo
                .GetRecentSessionsAsync(
                    assignment.UserId, days: 30, ct);

            var lastSession = recentSessions.FirstOrDefault();
            var thisWeekStart =
                DateTime.UtcNow.StartOfWeek(DayOfWeek.Monday);
            var sessionsThisWeek = recentSessions
                .Count(s => s.StartedAt >= thisWeekStart);

            var analytics = await _analyticsService
                .GetVolumeLoadOverTimeAsync(
                    assignment.UserId,
                    DateTime.UtcNow.AddDays(-30),
                    DateTime.UtcNow,
                    "weekly", ct);

            clientSummaries.Add(new ClientSummaryDto
            {
                UserId = assignment.UserId,
                ClientName = assignment.UserName,
                AssignedPlan =
                    assignment.CurrentPlanName,
                SessionsThisWeek = sessionsThisWeek,
                TargetSessionsPerWeek =
                    assignment.TargetSessionsPerWeek,
                AdherenceRate =
                    assignment.TargetSessionsPerWeek > 0
                    ? Math.Round(
                        (decimal)sessionsThisWeek
                        / assignment.TargetSessionsPerWeek
                        * 100)
                    : 0,
                LastWorkoutAt =
                    lastSession?.StartedAt,
                WeeklyVolumeLoad =
                    analytics.TotalVolumeLoad,
                VolumeTrend = analytics.Trend,
                FlaggedIssues =
                    await IdentifyIssuesAsync(
                        assignment, recentSessions, ct)
            });
        }

        return new TrainerDashboardDto
        {
            TrainerId = trainerId,
            TotalClients = assignments.Count,
            ActiveClients = clientSummaries
                .Count(c => c.SessionsThisWeek > 0),
            AtRiskClients = clientSummaries
                .Count(c => c.AdherenceRate < 50),
            Clients = clientSummaries
                .OrderByDescending(c => c.AdherenceRate)
                .ToList(),
            UpcomingClasses =
                await GetTrainerScheduleAsync(
                    trainerId, ct)
        };
    }

    public async Task AssignPlanAsync(
        Guid trainerId, Guid userId,
        Guid planId, string notes,
        CancellationToken ct)
    {
        var assignment = await _assignmentRepo
            .GetByTrainerAndUserAsync(
                trainerId, userId, ct)
            ?? throw new ClientNotAssignedException(
                trainerId, userId);

        var plan = await _planRepo.GetByIdAsync(planId, ct)
            ?? throw new PlanNotFoundException(planId);

        assignment.CurrentPlanId = planId;
        assignment.CurrentPlanName = plan.Name;
        assignment.PlanAssignedAt = DateTime.UtcNow;
        assignment.TrainerNotes = notes;

        await _assignmentRepo.UpdateAsync(
            assignment, ct);

        await _notifService.SendAsync(new Notification
        {
            UserId = userId,
            Type = "plan_assigned",
            Title = "New Workout Plan!",
            Body = $"Your trainer assigned you \"{plan.Name}\".",
            Data = JsonSerializer.Serialize(new
                { PlanId = planId })
        }, ct);
    }
}

16. Nutrition Tracking

Nutrition tracking is essential for users with body composition goals. The system provides food logging via manual entry and barcode scanning, integrates with the Nutritionix and USDA FoodData Central APIs for comprehensive nutritional data, computes daily macro targets based on TDEE (Total Daily Energy Expenditure) using the Mifflin-St Jeor equation, and tracks adherence to personalized meal plans. Water intake tracking and supplement logging are also supported.

Calorie and Macro Calculation

C#
public static class NutritionCalculator
{
    public static MacroTarget CalculateDailyTarget(
        UserProfile profile, FitnessGoal goal)
    {
        var bmr = CalculateBMR(profile);
        var tdee = bmr *
            GetActivityMultiplier(profile.ActivityLevel);

        var calories = goal switch
        {
            FitnessGoal.WeightLoss => tdee - 500,
            FitnessGoal.MuscleGain => tdee + 300,
            FitnessGoal.Maintenance => tdee,
            FitnessGoal.Endurance => tdee + 200,
            _ => tdee
        };

        var proteinPerKg = goal switch
        {
            FitnessGoal.MuscleGain => 2.2m,
            FitnessGoal.WeightLoss => 2.0m,
            FitnessGoal.Endurance => 1.6m,
            _ => 1.8m
        };

        var proteinG = profile.WeightKg * proteinPerKg;
        var fatPct = 0.25m;
        var fatG = calories * fatPct / 9m;
        var remaining =
            calories - (proteinG * 4) - (fatG * 9);
        var carbsG = remaining / 4m;

        return new MacroTarget
        {
            Calories = (int)calories,
            ProteinG = Math.Round(proteinG),
            CarbsG = Math.Round(carbsG),
            FatG = Math.Round(fatG),
            FiberG = profile.Gender == "male" ? 38 : 25
        };
    }

    private static decimal CalculateBMR(
        UserProfile profile)
    {
        var age = profile.DateOfBirth.HasValue
            ? (decimal)(DateTime.Today
                - profile.DateOfBirth.Value)
                .TotalDays / 365.25m
            : 30m;

        return profile.Gender?.ToLower() switch
        {
            "male" => 10m * profile.WeightKg
                + 6.25m * (profile.Height_cm ?? 175m)
                - 5m * age + 5m,
            "female" => 10m * profile.WeightKg
                + 6.25m * (profile.Height_cm ?? 162m)
                - 5m * age - 161m,
            _ => 10m * profile.WeightKg
                + 6.25m * (profile.Height_cm ?? 170m)
                - 5m * age + 5m
        };
    }

    private static decimal GetActivityMultiplier(
        string? level) => level?.ToLower() switch
    {
        "sedentary" => 1.2m,
        "light" => 1.375m,
        "moderate" => 1.55m,
        "active" => 1.725m,
        "very_active" => 1.9m,
        _ => 1.55m
    };
}

17. Gamification & Achievements

Gamification elements dramatically improve user retention. The system awards XP (experience points) for completing workouts, logging meals, attending classes, and maintaining streaks. Users level up as they accumulate XP, unlocking new badge tiers and cosmetic profile features. The achievement engine runs after every significant user action and evaluates all applicable achievement conditions.

Achievement Definitions

BadgeConditionXP ValueTier
First WorkoutComplete your first workout session50Bronze
Week WarriorWork out 5 days in a single week200Silver
Iron MonthWork out 20+ days in a month500Gold
100 Day StreakMaintain a 100-day workout streak2000Platinum
PR CrusherSet 10 personal records in a month300Gold
Marathon MindRun 42 km total distance250Silver
Social ButterflyJoin 5 challenges and complete them all400Gold
Nutrition GuruLog meals for 30 consecutive days600Gold
Early BirdComplete 50 workouts before 7 AM350Silver
Volume KingAccumulate 100,000 kg total volume load1000Platinum

C# Achievement Engine

C#
public class AchievementEngine : IAchievementEngine
{
    private readonly IAchievementRepository _achievementRepo;
    private readonly IStreakService _streakService;
    private readonly IXpService _xpService;
    private readonly INotificationService _notifService;

    private static readonly List<AchievementDefinition>
        Definitions = new()
    {
        new AchievementDefinition
        {
            Code = "FIRST_WORKOUT",
            Name = "First Workout", XP = 50,
            Tier = "bronze"
        },
        new AchievementDefinition
        {
            Code = "WEEK_WARRIOR",
            Name = "Week Warrior", XP = 200,
            Tier = "silver"
        },
        new AchievementDefinition
        {
            Code = "IRON_MONTH",
            Name = "Iron Month", XP = 500,
            Tier = "gold"
        },
        new AchievementDefinition
        {
            Code = "STREAK_100",
            Name = "100 Day Streak", XP = 2000,
            Tier = "platinum"
        },
        new AchievementDefinition
        {
            Code = "PR_CRUSHER",
            Name = "PR Crusher", XP = 300,
            Tier = "gold"
        },
        new AchievementDefinition
        {
            Code = "VOLUME_KING",
            Name = "Volume King", XP = 1000,
            Tier = "platinum"
        },
    };

    public async Task<List<AchievementDto>>
        EvaluateAchievementsAsync(
            Guid userId,
            WorkoutSession completedSession,
            CancellationToken ct)
    {
        var earned = new List<AchievementDto>();
        var existing =
            await _achievementRepo
                .GetUserBadgesAsync(userId, ct);
        var existingCodes =
            existing.Select(a => a.BadgeCode).ToHashSet();

        var totalWorkouts =
            await _achievementRepo
                .GetTotalWorkoutCountAsync(userId, ct);
        var currentStreak =
            await _streakService
                .GetCurrentStreakAsync(userId, ct);
        var prCount =
            await _achievementRepo
                .GetPRCountAsync(userId,
                    DateTime.UtcNow.Month, ct);
        var totalVolume =
            await _achievementRepo
                .GetTotalVolumeLoadAsync(userId, ct);

        var checks = new List<
            Func<AchievementDefinition, bool>>
        {
            d => d.Code == "FIRST_WORKOUT"
                && totalWorkouts >= 1,
            d => d.Code == "WEEK_WARRIOR"
                && GetSessionsThisWeek(totalWorkouts) >= 5,
            d => d.Code == "STREAK_100"
                && currentStreak >= 100,
            d => d.Code == "PR_CRUSHER"
                && prCount >= 10,
            d => d.Code == "VOLUME_KING"
                && totalVolume >= 100_000,
        };

        foreach (var def in Definitions)
        {
            if (existingCodes.Contains(def.Code))
                continue;

            var matcher = checks.FirstOrDefault(
                c => c(def));
            if (matcher != null)
            {
                var achievement = new Achievement
                {
                    UserId = userId,
                    BadgeCode = def.Code,
                    BadgeName = def.Name,
                    XPValue = def.XP,
                    EarnedAt = DateTime.UtcNow
                };

                await _achievementRepo.AddAsync(
                    achievement, ct);
                await _xpService.AddXPAsync(
                    userId, def.XP, ct);

                await _notifService.SendAsync(
                    new Notification
                {
                    UserId = userId,
                    Type = "achievement_earned",
                    Title = $"Badge: {def.Name}!",
                    Body = $"+{def.XP} XP earned!",
                    Data = JsonSerializer.Serialize(
                        new { def.Code, def.XP })
                }, ct);

                earned.Add(MapToDto(achievement));
            }
        }

        return earned;
    }
}

18. Push Notifications & Reminders

Push notifications are critical for re-engagement and retention. The notification service supports scheduled reminders (daily workout at 6 AM), event-triggered alerts (friend completed a challenge, PR achieved, class starting in 30 minutes), and marketing campaigns (new features, promotions). It integrates with Apple Push Notification Service (APNs) and Firebase Cloud Messaging (FCM) with intelligent delivery scheduling based on user timezone and quiet hours preferences.

C# Notification Service

C#
public class NotificationService : INotificationService
{
    private readonly IPushNotificationClient _pushClient;
    private readonly INotificationRepository _notifRepo;
    private readonly IUserPreferencesRepository _prefsRepo;
    private readonly IBackgroundJobScheduler _jobScheduler;

    public async Task SendAsync(
        Notification notification,
        CancellationToken ct)
    {
        var prefs = await _prefsRepo
            .GetAsync(notification.UserId, ct);

        if (prefs != null && !prefs.PushEnabled)
            return;

        if (prefs?.QuietHoursEnabled == true
            && IsQuietHours(prefs))
        {
            await ScheduleForEndOfQuietHoursAsync(
                notification, prefs, ct);
            return;
        }

        await _notifRepo.AddAsync(notification, ct);

        var tokens = await _notifRepo
            .GetDeviceTokensAsync(
                notification.UserId, ct);

        var payload = new PushPayload
        {
            Title = notification.Title,
            Body = notification.Body,
            Data = notification.Data,
            Badge = await _notifRepo
                .GetUnreadCountAsync(
                    notification.UserId, ct),
            Sound = "default"
        };

        var targets = tokens
            .Select(t => new PushTarget
            {
                Token = t.Token,
                Platform = t.Platform
            }).ToList();

        var results = await _pushClient
            .SendBatchAsync(payload, targets, ct);

        var failed = results
            .Where(r => !r.Success)
            .Select(r => r.Token).ToList();
        if (failed.Any())
            await _notifRepo
                .DeactivateTokensAsync(failed, ct);
    }

    public async Task ScheduleWorkoutRemindersAsync(
        Guid userId, CancellationToken ct)
    {
        var prefs = await _prefsRepo
            .GetAsync(userId, ct);
        if (prefs?.WorkoutReminderEnabled != true)
            return;

        var reminderTime =
            prefs.WorkoutReminderTime
            ?? new TimeOnly(18, 0);
        var tz = TimeZoneInfo
            .FindSystemTimeZoneById(
                prefs.Timezone ?? "UTC");
        var now = TimeZoneInfo
            .ConvertTimeFromUtc(
                DateTime.UtcNow, tz);

        var scheduledTime =
            now.Date.Add(reminderTime.Value.ToTimeSpan());
        if (scheduledTime < now)
            scheduledTime = scheduledTime.AddDays(1);

        var utcScheduled = TimeZoneInfo
            .ConvertTimeToUtc(scheduledTime, tz);

        var activePlan = await _planRepo
            .GetActivePlanAsync(userId, ct);
        var tomorrow = now.AddDays(1);
        var dayOfWeek = tomorrow.DayOfWeek;
        var hasWorkout = activePlan?.Days
            .Any(d => d.DayOfWeek == (int)dayOfWeek)
            ?? false;

        if (!hasWorkout) return;

        var exerciseCount = activePlan!.Days
            .First(d => d.DayOfWeek == (int)dayOfWeek)
            .Exercises.Count;

        await _jobScheduler.ScheduleAsync(
            new ScheduledNotification
        {
            UserId = userId,
            Title = "Time to Crush It!",
            Body = $"Tomorrow: {exerciseCount} exercises. See you at the gym!",
            ScheduledAt = utcScheduled,
            Type = "workout_reminder"
        }, ct);
    }

    private bool IsQuietHours(UserPreferences prefs)
    {
        var now = TimeOnly.FromDateTime(
            DateTime.UtcNow);
        var start = prefs.QuietHoursStart
            ?? new TimeOnly(22, 0);
        var end = prefs.QuietHoursEnd
            ?? new TimeOnly(7, 0);

        if (start > end)
            return now >= start || now <= end;
        return now >= start && now <= end;
    }
}

19. Video Exercise Library

The video exercise library provides high-definition demonstrations for every exercise in the catalog. Videos are stored in S3, transcoded to multiple resolutions (360p, 720p, 1080p) via a background worker using FFmpeg, and served through CloudFront CDN for low-latency global delivery. The system supports slow-motion playback, angle switching (front, side, 45-degree), and frame-by-frame stepping for detailed form study.

Video Processing Pipeline

graph LR Upload[Upload Video] --> Validate[Validate and Virus Scan] Validate --> Store[Store Original in S3] Store --> Queue[Send to Processing Queue] Queue --> Worker1[Worker: 1080p Transcode] Queue --> Worker2[Worker: 720p Transcode] Queue --> Worker3[Worker: 360p Transcode] Queue --> Worker4[Worker: Thumbnail Extract] Queue --> Worker5[Worker: GIF Preview Gen] Worker1 --> CDN[Upload to CloudFront] Worker2 --> CDN Worker3 --> CDN Worker4 --> CDN Worker5 --> CDN CDN --> Manifest[Update Video Manifest]
C#
public class VideoProcessingService
    : IVideoProcessingService
{
    private readonly IS3Client _s3;
    private readonly IFFmpegRunner _ffmpeg;
    private readonly IVideoRepository _videoRepo;

    public async Task ProcessVideoAsync(
        VideoUploadEvent uploadEvent,
        CancellationToken ct)
    {
        var originalKey =
            $"exercise-videos/{uploadEvent.ExerciseId}"
            + $"/{uploadEvent.VideoId}/original.mp4";
        var tempPath = Path.Combine(
            Path.GetTempPath(),
            $"{uploadEvent.VideoId}.mp4");

        try
        {
            await _s3.DownloadAsync(
                uploadEvent.BucketName,
                originalKey, tempPath, ct);

            var metadata = await _ffmpeg
                .GetMetadataAsync(tempPath, ct);
            if (metadata.Duration.TotalMinutes > 10)
                throw new VideoTooLongException(10);

            var resolutions = new[]
            {
                ("360p", 640, 360),
                ("720p", 1280, 720),
                ("1080p", 1920, 1080)
            };

            foreach (var (label, w, h) in resolutions)
            {
                var outputPath = Path.Combine(
                    Path.GetTempPath(),
                    $"{uploadEvent.VideoId}_{label}.mp4");
                await _ffmpeg.TranscodeAsync(
                    tempPath, outputPath, w, h, ct);

                var s3Key =
                    $"exercise-videos/"
                    + $"{uploadEvent.ExerciseId}/"
                    + $"{uploadEvent.VideoId}/{label}.mp4";
                await _s3.UploadAsync(
                    uploadEvent.BucketName,
                    s3Key, outputPath, ct);
                File.Delete(outputPath);
            }

            var thumbPath = Path.Combine(
                Path.GetTempPath(),
                $"{uploadEvent.VideoId}_thumb.jpg");
            await _ffmpeg.ExtractFrameAsync(
                tempPath, thumbPath,
                TimeSpan.FromSeconds(2), ct);
            var thumbKey =
                $"exercise-videos/"
                + $"{uploadEvent.ExerciseId}/"
                + $"{uploadEvent.VideoId}/thumbnail.jpg";
            await _s3.UploadAsync(
                uploadEvent.BucketName,
                thumbKey, thumbPath, ct);

            var gifPath = Path.Combine(
                Path.GetTempPath(),
                $"{uploadEvent.VideoId}_preview.gif");
            await _ffmpeg.CreateGifAsync(
                tempPath, gifPath,
                TimeSpan.FromSeconds(3),
                TimeSpan.FromSeconds(5), ct);
            var gifKey =
                $"exercise-videos/"
                + $"{uploadEvent.ExerciseId}/"
                + $"{uploadEvent.VideoId}/preview.gif";
            await _s3.UploadAsync(
                uploadEvent.BucketName,
                gifKey, gifPath, ct);

            var manifest = new VideoManifest
            {
                VideoId = uploadEvent.VideoId,
                ExerciseId = uploadEvent.ExerciseId,
                ThumbnailUrl =
                    $"https://cdn.ayodhyya.com/{thumbKey}",
                PreviewGifUrl =
                    $"https://cdn.ayodhyya.com/{gifKey}",
                Resolutions = resolutions
                    .Select(r => new VideoResolution
                {
                    Label = r.Item1,
                    Url = $"https://cdn.ayodhyya.com/"
                        + $"exercise-videos/"
                        + $"{uploadEvent.ExerciseId}/"
                        + $"{uploadEvent.VideoId}/"
                        + $"{r.Item1}.mp4",
                    Width = r.Item2,
                    Height = r.Item3
                }).ToList(),
                Duration = metadata.Duration,
                Status = VideoStatus.Ready,
                ProcessedAt = DateTime.UtcNow
            };

            await _videoRepo.UpdateManifestAsync(
                manifest, ct);
        }
        finally
        {
            if (File.Exists(tempPath))
                File.Delete(tempPath);
        }
    }
}

20. AI Form Correction

AI form correction is a differentiating feature that sets premium fitness apps apart. Users record a video of their exercise, and the system analyzes body joint angles, movement path, tempo, and range of motion to provide real-time feedback on form deviations. The system uses MediaPipe Pose for landmark detection and a fine-tuned classification model for deviation scoring. This feature requires significant compute resources and is processed asynchronously via GPU-accelerated workers.

Processing Pipeline

graph TB UserVideo[User Records Video] --> Upload[Upload to API] Upload --> Queue[Processing Queue] Queue --> PoseExtraction[MediaPipe Pose Landmark Extraction] PoseExtraction --> FrameAnalysis[Per-Frame Angle Computation] FrameAnalysis --> TemporalAnalysis[Temporal Pattern Analysis] TemporalAnalysis --> DeviationScoring[Deviation Scoring Model] DeviationScoring --> FeedbackGen[Feedback Text Generation] FeedbackGen --> Result[Return Annotated Video + Feedback] Result --> Notify[Push Notification: Analysis Ready]

C# AI Form Analysis Service

C#
public class FormAnalysisService : IFormAnalysisService
{
    private readonly IPoseEstimationClient _poseClient;
    private readonly IDeviationClassifier _classifier;
    private readonly IFeedbackGenerator _feedbackGen;
    private readonly IS3Client _s3;
    private readonly IFormAnalysisRepository _repo;

    public async Task<FormAnalysisResult>
        AnalyzeExerciseVideoAsync(
            FormAnalysisRequest request,
            CancellationToken ct)
    {
        var videoUrl = await _s3.GetPreSignedUrlAsync(
            request.VideoKey,
            TimeSpan.FromMinutes(30), ct);

        var landmarks = await _poseClient
            .ExtractLandmarksAsync(videoUrl, ct);

        var frameAngles = new List<FrameAngles>();
        foreach (var frame in landmarks.Frames)
        {
            var angles = ComputeJointAngles(
                frame, request.ExerciseType);
            frameAngles.Add(angles);
        }

        var temporalMetrics = new TemporalMetrics
        {
            Tempo = ComputeTempo(frameAngles),
            RepCount = CountReps(
                frameAngles, request.ExerciseType),
            RangeOfMotion = ComputeROM(
                frameAngles, request.ExerciseType),
            TimeUnderTension = ComputeTUT(frameAngles),
            DescentToAscentRatio =
                ComputeTempoRatio(frameAngles)
        };

        var deviations = await _classifier
            .ClassifyDeviationsAsync(
                frameAngles, temporalMetrics,
                request.ExerciseType, ct);

        var feedback = await _feedbackGen
            .GenerateAsync(new FeedbackContext
        {
            ExerciseType = request.ExerciseType,
            Deviations = deviations,
            Metrics = temporalMetrics,
            UserLevel = request.UserFitnessLevel
        }, ct);

        var analysis = new FormAnalysis
        {
            Id = Guid.NewGuid(),
            UserId = request.UserId,
            ExerciseId = request.ExerciseId,
            VideoKey = request.VideoKey,
            OverallScore =
                CalculateOverallScore(deviations),
            Deviations = deviations,
            Metrics = temporalMetrics,
            Feedback = feedback,
            AnalyzedAt = DateTime.UtcNow
        };

        await _repo.SaveAsync(analysis, ct);

        return new FormAnalysisResult
        {
            AnalysisId = analysis.Id,
            OverallScore = analysis.OverallScore,
            Deviations = deviations
                .Select(d => new DeviationDto
            {
                Type = d.Type,
                Severity = d.Severity,
                Description = d.Description,
                Timestamp = d.Timestamp,
                JointInvolved = d.JointInvolved,
                Recommendation = d.Recommendation
            }).ToList(),
            Metrics = temporalMetrics,
            Feedback = feedback
        };
    }

    private Dictionary<string, double>
        ComputeJointAngles(
            PoseFrame frame, string exerciseType)
    {
        var angles =
            new Dictionary<string, double>();

        angles["knee_left"] = ComputeAngle(
            frame.LeftHip, frame.LeftKnee,
            frame.LeftAnkle);
        angles["knee_right"] = ComputeAngle(
            frame.RightHip, frame.RightKnee,
            frame.RightAnkle);
        angles["hip_left"] = ComputeAngle(
            frame.LeftShoulder, frame.LeftHip,
            frame.LeftKnee);
        angles["hip_right"] = ComputeAngle(
            frame.RightShoulder, frame.RightHip,
            frame.RightKnee);
        angles["shoulder_left"] = ComputeAngle(
            frame.LeftElbow, frame.LeftShoulder,
            frame.LeftHip);
        angles["shoulder_right"] = ComputeAngle(
            frame.RightElbow, frame.RightShoulder,
            frame.RightHip);
        angles["elbow_left"] = ComputeAngle(
            frame.LeftShoulder, frame.LeftElbow,
            frame.LeftWrist);
        angles["elbow_right"] = ComputeAngle(
            frame.RightShoulder, frame.RightElbow,
            frame.RightWrist);
        angles["spine_angle"] = ComputeAngle(
            frame.Neck, frame.MidHip, frame.MidKnee);

        return angles;
    }

    private double ComputeAngle(
        Point3D a, Point3D b, Point3D c)
    {
        var ba = new Point3D(
            a.X - b.X, a.Y - b.Y, a.Z - b.Z);
        var bc = new Point3D(
            c.X - b.X, c.Y - b.Y, c.Z - b.Z);

        var dot = ba.X * bc.X
            + ba.Y * bc.Y + ba.Z * bc.Z;
        var magBA = Math.Sqrt(
            ba.X * ba.X + ba.Y * ba.Y + ba.Z * ba.Z);
        var magBC = Math.Sqrt(
            bc.X * bc.X + bc.Y * bc.Y + bc.Z * bc.Z);

        if (magBA == 0 || magBC == 0) return 0;

        var cosine = dot / (magBA * magBC);
        cosine = Math.Clamp(cosine, -1.0, 1.0);

        return Math.Acos(cosine) * (180.0 / Math.PI);
    }
}

Common Form Deviations Detected

ExerciseDeviationThresholdFeedback
SquatKnee valgusKnee angle < 160 at bottomPush knees outward over toes
SquatForward leanSpine angle < 45 from verticalKeep chest up, engage core
Bench PressFlared elbowsElbow angle > 80 from torsoTuck elbows at 45 degree angle
DeadliftRound backSpine deviation > 15 degreesBrace core, maintain neutral spine
Overhead PressHyperextensionSpine > 10 past verticalSqueeze glutes, avoid arching
RowExcessive momentumTorso swing > 20 degreesReduce weight, control the movement

21. Multi-Gym Support

Franchise gym chains require multi-tenancy at the gym level. Each gym location has its own equipment inventory, class schedule, staff roster, and operational hours, while sharing a global user database and exercise library. The architecture uses a gym_id tenant discriminator across all tables with row-level security in PostgreSQL to enforce data isolation. This allows a single deployment to serve hundreds of gym locations with complete data separation.

Gym Scoping in C#

C#
public class GymScopedRepository<T>
    where T : class, IGymScoped
{
    private readonly ApplicationDbContext _context;
    private readonly IGymContext _gymContext;

    public GymScopedRepository(
        ApplicationDbContext context,
        IGymContext gymContext)
    {
        _context = context;
        _gymContext = gymContext;
    }

    public IQueryable<T> Query()
    {
        return _context.Set<T>()
            .Where(e =>
                e.GymId == _gymContext.CurrentGymId);
    }

    public async Task<T?> GetByIdAsync(
        Guid id, CancellationToken ct)
    {
        return await Query()
            .FirstOrDefaultAsync(
                e => e.Id == id, ct);
    }

    public async Task AddAsync(
        T entity, CancellationToken ct)
    {
        entity.GymId =
            _gymContext.CurrentGymId;
        await _context.Set<T>()
            .AddAsync(entity, ct);
    }
}

public class GymContext : IGymContext
{
    private readonly IHttpContextAccessor
        _httpContextAccessor;

    public Guid CurrentGymId
    {
        get
        {
            var claim =
                _httpContextAccessor.HttpContext
                    ?.User?.FindFirst("gym_id");
            if (claim == null
                || !Guid.TryParse(
                    claim.Value, out var gymId))
                throw new GymContextNotSetException();
            return gymId;
        }
    }
}

Row-Level Security Policy

SQL
ALTER TABLE class_schedules
    ENABLE ROW LEVEL SECURITY;
ALTER TABLE equipment
    ENABLE ROW LEVEL SECURITY;
ALTER TABLE trainers
    ENABLE ROW LEVEL SECURITY;

CREATE POLICY gym_isolation ON class_schedules
    USING (gym_id =
        current_setting('app.current_gym_id')::uuid);

CREATE POLICY gym_isolation ON equipment
    USING (gym_id =
        current_setting('app.current_gym_id')::uuid);

CREATE POLICY gym_isolation ON trainers
    USING (gym_id =
        current_setting('app.current_gym_id')::uuid);

22. Security & Compliance

Fitness apps handle sensitive health data including body measurements, heart rate, sleep patterns, and potentially medical information. Security must be defense-in-depth with encryption at rest and in transit, strict access controls, audit logging, and compliance with HIPAA (for US health data), GDPR (for EU users), and CCPA (for California residents). The security architecture follows zero-trust principles where every service-to-service call is authenticated and authorized.

Security Architecture

LayerMechanismDetails
TransportTLS 1.3All API traffic encrypted; HSTS enabled
AuthenticationJWT + Refresh Tokens15-min access tokens, 30-day refresh, rotation
AuthorizationRBAC + ABACRoles: user, trainer, gym_admin, platform_admin
Data at RestAES-256Database encryption, S3 SSE-KMS, Redis AUTH
PII HandlingField-level encryptionEmail, body measurements with per-user keys
AuditImmutable logAll data access logged, 7-year retention
API SecurityRate limiting + WAFAWS WAF rules, per-user limits, DDoS protection
MobileCertificate pinningSSL pinning, root/jailbreak detection
ComplianceHIPAA + GDPRBAA with providers, DPO appointed, DPA templates

C# Health Data Encryption Service

C#
public class HealthDataEncryptionService
    : IHealthDataEncryptionService
{
    private readonly IKeyVaultClient _keyVault;

    public async Task<EncryptedPayload>
        EncryptHealthDataAsync(
            Guid userId, object healthData,
            CancellationToken ct)
    {
        var key = await _keyVault.GetKeyAsync(
            $"health-data-key-{userId}", ct);
        var plainBytes = JsonSerializer
            .SerializeToUtf8Bytes(healthData);

        using var aes = Aes.Create();
        aes.Key = key.Key;
        aes.GenerateIV();

        using var encryptor =
            aes.CreateEncryptor();
        var encrypted =
            encryptor.TransformFinalBlock(
                plainBytes, 0, plainBytes.Length);

        return new EncryptedPayload
        {
            EncryptedData =
                Convert.ToBase64String(encrypted),
            IV = Convert.ToBase64String(aes.IV),
            KeyVersion = key.Version,
            Algorithm = "AES-256-CBC",
            EncryptedAt = DateTime.UtcNow
        };
    }

    public async Task<T> DecryptHealthDataAsync<T>(
        Guid userId, EncryptedPayload payload,
        CancellationToken ct)
    {
        var key = await _keyVault.GetKeyAsync(
            $"health-data-key-{userId}",
            payload.KeyVersion, ct);

        using var aes = Aes.Create();
        aes.Key = key.Key;
        aes.IV =
            Convert.FromBase64String(payload.IV);

        using var decryptor =
            aes.CreateDecryptor();
        var encrypted =
            Convert.FromBase64String(
                payload.EncryptedData);
        var plain =
            decryptor.TransformFinalBlock(
                encrypted, 0, encrypted.Length);

        return JsonSerializer
            .Deserialize<T>(plain)!;
    }
}

23. Cost Estimation

Cost estimation for a mid-scale fitness platform (2 million registered users, 400,000 daily active users) running on AWS. We assume a microservices architecture with moderate over-provisioning for reliability. All costs are estimated for the US East region and include data transfer charges.

ServiceSpecificationMonthly Cost (USD)
EKS Cluster (3 nodes)m5.xlarge (4 vCPU, 16 GB)
RDS PostgreSQLdb.r5.xlarge, Multi-AZ, 500 GB
MongoDB AtlasM40 cluster, 100 GB
ElastiCache Redisr5.large, cluster mode, 3 shards
Amazon MSK (Kafka)kafka.m5.large, 3 brokers
S3 Storage25 TB (video + images)
CloudFront CDN5 TB transfer/month
ALB2 ALBs, moderate traffic
Elasticsearcht3.large, 3 nodes
Lambda (async jobs)Video processing, analytics
SQS + SNSNotification queues
CloudWatchLogs, metrics, alarms
WAFAPI protection
KMSKey management
Data TransferInter-region + internet
Total,015
Cost Optimization Tips: Use Spot Instances for non-critical workloads (video processing, batch analytics) to save 60-70%. Reserved Instances for databases save 30-40%. Consider Cloudflare R2 over S3 for egress-heavy workloads to eliminate egress fees. Use Graviton instances for 20% cost reduction on compatible workloads.

Revenue Model

PlanPrice/MonthTarget UsersMonthly Revenue
Free Tier600,000 (30%)
Basic.99600,000 (30%),994,000
Premium.99500,000 (25%),995,000
Trainer Pro.99100,000 (5%),999,000
EnterpriseCustomGym Chains,000
Total,488,000

With infrastructure costs of approximately 6,000 dollars per month and projected revenue exceeding 20 million dollars per month at scale, the gross margin is exceptionally healthy. Even at 10% of projected scale, the platform is profitable with infrastructure costs remaining under 3,000 dollars per month thanks to auto-scaling capabilities.

24. Testing & Deployment

Testing Strategy

The testing pyramid for the fitness platform consists of unit tests at the base (target: 80% code coverage), integration tests for database and external API interactions, contract tests for inter-service communication via Pact, and end-to-end tests using Playwright for web and Appium for mobile. Performance tests use k6 to simulate peak workout hours with realistic user behavior patterns.

C# Integration Test Example

C#
public class WorkoutSessionIntegrationTests
    : IClassFixture<WebApplicationFactory<Program>>
{
    private readonly WebApplicationFactory<Program>
        _factory;
    private readonly HttpClient _client;

    public WorkoutSessionIntegrationTests(
        WebApplicationFactory<Program> factory)
    {
        _factory = factory.WithWebHostBuilder(
            builder =>
        {
            builder.ConfigureTestServices(services =>
            {
                services.RemoveAll<
                    IDbContextFactory<
                        ApplicationDbContext>>();
                services.AddDbContextFactory<
                    ApplicationDbContext>(options =>
                    options.UseNpgsql(
                        "Host=localhost;"
                        + "Database=fitness_test"));
            });
        });

        _client = _factory.CreateClient();
        _client.DefaultRequestHeaders
            .Authorization =
            new AuthenticationHeaderValue(
                "Bearer",
                TestAuthTokenGenerator.Generate());
    }

    [Fact]
    public async Task
        CreateSession_ThenLogSets_ComputesVolume()
    {
        var createResp = await _client
            .PostAsJsonAsync(
                "/api/v1/workout-sessions",
                new
        {
            SessionType = "strength",
            GymId = Guid.NewGuid()
        });

        createResp.EnsureSuccessStatusCode();
        var session = await createResp.Content
            .ReadFromJsonAsync
                <WorkoutSessionResponse>();

        var exerciseId =
            await SeedExerciseAsync(
                "Barbell Bench Press");

        await _client.PostAsJsonAsync(
            $"/api/v1/workout-sessions/"
            + $"{session!.Id}/exercises",
            new
        {
            ExerciseId = exerciseId,
            Order = 1
        });

        var sets = new[]
        {
            new { SetNumber = 1, Reps = 12,
                  WeightKg = 60m, Rpe = 6m },
            new { SetNumber = 2, Reps = 10,
                  WeightKg = 80m, Rpe = 7m },
            new { SetNumber = 3, Reps = 8,
                  WeightKg = 95m, Rpe = 8.5m }
        };

        foreach (var set in sets)
        {
            var resp = await _client.PostAsJsonAsync(
                $"/api/v1/workout-sessions/"
                + $"{session.Id}/exercises/"
                + $"{exerciseId}/sets",
                set);
            resp.EnsureSuccessStatusCode();
        }

        var detail = await _client.GetAsync(
            $"/api/v1/workout-sessions/"
            + $"{session.Id}");
        detail.EnsureSuccessStatusCode();
        var d = await detail.Content
            .ReadFromJsonAsync
                <WorkoutSessionResponse>();

        var expected = (12 * 60) + (10 * 80)
            + (8 * 95);
        Assert.Equal(
            (decimal)expected,
            d!.TotalVolumeLoad);
    }
}

CI/CD Pipeline

graph LR Push[Git Push] --> Lint[Lint and Format] Lint --> Build[Build and Compile] Build --> UnitTest[Unit Tests] UnitTest --> IntegrationTest[Integration Tests] IntegrationTest --> SecurityScan[Security Scan] SecurityScan --> DockerBuild[Docker Image Build] DockerBuild --> ECR[Push to ECR] ECR --> StagingDeploy[Deploy to Staging] StagingDeploy --> E2E[End-to-End Tests] E2E --> ManualApproval{Manual Approval} ManualApproval -->|Approved| ProdDeploy[Blue-Green Deploy] ProdDeploy --> CanaryAnalysis[Canary 10%] CanaryAnalysis --> FullRollout[Full Rollout]

25. Interview Q&A

Q1: How would you handle workout logging when the user has no internet connection?

Answer: Use an offline-first architecture. The mobile app maintains a local SQLite database that mirrors the server schema for the active workout session. All mutations are written locally first and queued in an outbox table with monotonically increasing sequence numbers. When connectivity resumes, a bulk sync endpoint accepts the entire mutation list. The server applies operations idempotently using client-generated UUIDs and sequence numbers, guaranteeing exactly-once semantics. Conflict resolution is straightforward because workout data is user-partitioned. Last-writer-wins per set based on the sequence number is acceptable for this domain.

Q2: How do you design the real-time leaderboard for a fitness challenge?

Answer: Redis Sorted Sets are the ideal data structure. The challenge ID becomes the key, user IDs are members, and scores represent the challenge metric. When a user logs a workout, an event triggers a score update via ZINCRBY which is O(log N). Ranking queries use ZREVRANK for O(log N) lookups. For the top-N display, ZREVRANGE gives O(log N + M) where M is the returned count. For millions of users, this handles sub-millisecond latency. To prevent stale data, we set a 30-second TTL on cached leaderboard snapshots. The backing store is PostgreSQL for durability with Redis as the fast-path cache.

Q3: How would you scale heart rate data ingestion from 10,000 concurrent wearables?

Answer: Each wearable sends approximately one heart rate sample per second. With 10,000 concurrent users, that is 10,000 samples per second or approximately 864 million per day. We use Kafka as the ingestion buffer with a partitioned topic keyed on user ID. This ensures ordered processing per user while distributing load across consumers. Each partition handles approximately 1,000 samples per second, well within a single consumer's capacity. The stream processor computes rolling averages, detects anomalies, and batches writes to TimescaleDB. TimescaleDB compression policies automatically compress data older than 7 days, reducing storage by 90%.

Q4: How would you handle the AI form correction feature at scale with GPU requirements?

Answer: AI form correction is inherently asynchronous and compute-intensive. We use a dedicated GPU worker pool (AWS g4dn instances) behind an SQS queue. The mobile app uploads the video to S3 and publishes a processing request. The queue decouples upload from processing, allowing us to scale GPU instances independently. We implement priority queuing so premium users get faster processing. The average video is 30 seconds long and takes approximately 15 seconds to process on a T4 GPU. With 10 g4dn.xlarge instances, we can process approximately 40 videos per minute, which handles 10,000 daily submissions with room for burst traffic.

Q5: How do you ensure data consistency across microservices when a workout is completed?

Answer: We use the Saga pattern with compensating transactions. When a workout session ends, the Workout Service publishes a WorkoutCompleted event. The Analytics Service updates aggregates, the Gamification Service checks for new achievements, the Social Service broadcasts to the activity feed, and the Notification Service sends a summary. If any step fails, we use a dead letter queue for manual retry. We accept eventual consistency for analytics and social features (acceptable delay of seconds to minutes) while keeping the workout data itself strongly consistent within the Workout Service's MongoDB transactions.

Q6: How would you design the system to support both a single gym and a 500-location franchise?

Answer: Multi-gym support uses a gym_id tenant discriminator across all tables with row-level security in PostgreSQL. Each API request carries the gym context from the JWT token. The data model is designed with gym_id as a foreign key on location-specific tables (classes, equipment, trainers, staff) but not on global tables (exercises, workout templates, user profiles). For franchise-wide analytics, we query across gym_ids with appropriate aggregation. The architecture supports a single-tenant deployment (one gym) by simply not configuring multi-tenancy, keeping the operational complexity proportional to the deployment size.

Q7: How do you handle prorated membership upgrades and downgrades?

Answer: When a user upgrades, we calculate the remaining days in their current billing cycle and apply a prorated credit. For example, if a user on a 49 dollar/month plan upgrades to a 79 dollar/month plan with 10 days remaining, they receive a credit of 49 * 10/30 = 16.33 dollars and are immediately charged the new rate. Downgrades take effect at the next billing cycle to avoid disruption. Stripe handles the proration calculation natively via SubscriptionUpdateOptions with ProrationBehavior set to create_prorations. We also support mid-cycle plan changes for annual subscriptions with proportional adjustments.

© 2026 Ayodhyya. All rights reserved.

Design a Gym & Fitness Tracking App: The Complete Guide — A Senior+ Guide