Low-Level Design: Gym Membership and Access Control System — Plans, Check-In, and Class Booking

Core Entities

Member: member_id, user_id, first_name, last_name, email, phone, photo_url, join_date, status (ACTIVE, SUSPENDED, CANCELLED, FROZEN), emergency_contact. MembershipPlan: plan_id, name (BASIC, STANDARD, PREMIUM, STUDENT), price_monthly, price_annual, features (JSON: {guest_passes, classes_per_month, pool_access, personal_training_sessions}), billing_cycle (MONTHLY, ANNUAL). Subscription: sub_id, member_id, plan_id, status (ACTIVE, CANCELLED, EXPIRED, PAUSED), start_date, end_date, auto_renew, payment_method_id, next_billing_date. AccessLog: log_id, member_id, location_id, access_type (ENTRY, EXIT, CLASS_CHECKIN, GUEST_ENTRY), scanned_at, is_granted, denial_reason. FitnessClass: class_id, name, instructor_id, location_id, capacity, start_time, duration_minutes, type (YOGA, SPIN, PILATES, CROSSFIT), difficulty (BEGINNER, INTERMEDIATE, ADVANCED). ClassBooking: booking_id, class_id, member_id, status (CONFIRMED, WAITLISTED, CANCELLED, NO_SHOW), booked_at, checked_in_at.

Access Control

class AccessControlService:
    def check_access(self, member_id: int, location_id: int,
                     access_type: str) -> AccessDecision:
        member = self.repo.get_member(member_id)
        if not member or member.status != MemberStatus.ACTIVE:
            return AccessDecision(granted=False, reason='INACTIVE_MEMBER')

        subscription = self.repo.get_active_subscription(member_id)
        if not subscription or subscription.end_date  AccessDecision:
        booking = self.repo.get_booking(member_id, class_id)
        if not booking or booking.status != BookingStatus.CONFIRMED:
            return AccessDecision(granted=False, reason='NO_BOOKING')
        if booking.checked_in_at:
            return AccessDecision(granted=False, reason='ALREADY_CHECKED_IN')
        booking.checked_in_at = datetime.utcnow()
        booking.status = BookingStatus.CHECKED_IN
        self.repo.save(booking)
        return AccessDecision(granted=True)

Class Booking and Waitlist

Booking flow: (1) Member requests a class booking. (2) Check plan allowance: BASIC plan = 4 classes/month; count bookings in the current calendar month. (3) Check class capacity: if confirmed_bookings 2 hours before class: no penalty. Cancel < 2 hours before: mark as late cancellation (3 late cancellations per month = 1-week class booking suspension). No-show handling: if a member does not check in within 15 minutes of class start: mark as NO_SHOW. No-shows count toward late cancellations. Monthly class counter: track classes_used_this_month per member. Reset on billing cycle anniversary. Early access: PREMIUM members can book classes 7 days in advance; STANDARD 3 days; BASIC 1 day. Prevents premium spots from being captured immediately by basic members.

Billing and Freezing

Recurring billing: on next_billing_date, charge the payment method on file via Stripe. On success: extend end_date by one billing cycle. On failure: retry 3x over 5 days, then suspend the subscription. Payment failure email sequence: reminder on failure, warning after 2nd failure, suspension notice on 3rd. Subscription freezing: members can freeze their membership (vacation, injury) for up to 90 days per year. Freeze period: pause billing, pause end_date countdown, restrict access. Thaw: automatically on freeze_end_date or on member request. Freeze days_used tracking: cannot exceed 90 days per 12-month period. Pro-rating: if a member upgrades mid-cycle (BASIC → PREMIUM), charge the difference for the remaining days in the current cycle. Downgrade: takes effect at the next billing cycle (do not remove access mid-cycle). Plan change history: PlanChangeLog (member_id, old_plan_id, new_plan_id, changed_at, effective_date, reason).


{“@context”:”https://schema.org”,”@type”:”FAQPage”,”mainEntity”:[{“@type”:”Question”,”name”:”How do you enforce monthly class limits per membership plan?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”Store a ClassUsage record per (member_id, billing_month) with a classes_used counter. When a member books a class, check classes_used < plan.classes_per_month (use a SELECT … FOR UPDATE to prevent race conditions). On successful booking, increment classes_used atomically within the same transaction. The counter resets on each member's billing cycle anniversary, not calendar month, to align with their subscription period.”}},{“@type”:”Question”,”name”:”How do you manage the waitlist when a class spot opens up?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”On cancellation, query the waitlist ordered by booked_at ASC (first come, first served). Set the top waitlisted booking to PENDING_CONFIRM. Send push notification + email to the member with a 2-hour deadline. Use a Redis key with 2-hour TTL as the deadline timer. If the member confirms within 2 hours: set booking to CONFIRMED. If the TTL expires without confirmation: move to the next waitlisted member and repeat.”}},{“@type”:”Question”,”name”:”How does membership freeze work technically?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”Freeze stores freeze_start_date and freeze_end_date on the subscription. During the freeze period, access control denies entry (checking member.status == FROZEN). Billing is paused (skip the billing job for this member during freeze). The subscription end_date is extended by the freeze duration when the freeze ends (so the member does not lose paid time). Track total_frozen_days_this_year to enforce the 90-day annual limit.”}},{“@type”:”Question”,”name”:”How do you handle payment failures for recurring gym memberships?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”On billing date failure: retry 3 times over 5 days with exponential backoff (retry on days 1, 3, 5 after failure). After each failure, send an email to the member. After the 3rd failure with no payment, set subscription status to SUSPENDED and deny access. When the member updates their payment method and pays the outstanding balance, restore ACTIVE status and set the next billing date to one cycle from today.”}},{“@type”:”Question”,”name”:”How does early booking access work for premium members?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”Store booking_opens_at on each FitnessClass: booking_opens_at = class.start_time – N days, where N depends on plan tier (PREMIUM=7, STANDARD=3, BASIC=1). When a member requests a booking, check booking_opens_at <= now for their plan tier. Members cannot book classes before their tier's window even if they have classes remaining. This is enforced server-side — the client can show a countdown to when booking opens.”}}]}

See also: Shopify Interview Prep

See also: Stripe Interview Prep

See also: Airbnb Interview Prep

Scroll to Top