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).

See also: Shopify Interview Prep

See also: Stripe Interview Prep

See also: Airbnb Interview Prep

Scroll to Top