online_multiplayer_game_lobby.md

šŸŽ® Online Multiplayer Game Lobby with Priority Rooms & Autoscaling — Low-Level Design & Multi-Threaded Simulation

Target Interview Level: SDE-1 / SDE-2 — Juspay Systems & Concurrency LLD
Topic: High-Concurrency Multiplayer Game Lobby with $N$ Default Priority Rooms & Dynamic On-Demand Autoscaling
Language: Modern, Clean C++17 with Complete Multi-Threaded Simulation (std::thread, std::mutex, std::condition_variable)
Evaluation Focus: OOP Principles (SOLID, Strategy, State, Observer, Factory), Thread-Safety Invariants, Zero-Deadlock Concurrency, Fine-Grained Locking, and Graceful Resource Reclamation.


🧭 Problem Statement & Requirements Clarification

Problem Statement

Design an Online Multiplayer Game Lobby System that:

  1. Initializes with $N$ Default Game Rooms, each assigned a distinct Priority Level (e.g., Priority 1 = Highest, Priority $N$ = Lowest).
  2. Incoming players are allocated to rooms based on Room Priority (higher priority rooms fill first).
  3. When all $N$ default rooms are fully occupied, the system automatically spawns extra Dynamic Game Rooms on demand to absorb player overflow.
  4. Each room has a fixed maximum player capacity (e.g., 4 players per match).
  5. When a room reaches full capacity, it transitions to IN_PROGRESS and starts a concurrent match simulation.
  6. When a match concludes:
    • Default Rooms: Reset back to WAITING state and re-enter the priority pool ready for new players.
    • Dynamic Rooms: Are decommissioned and garbage-collected to prevent memory leaks and server resource exhaustion.
  7. The entire system must run in a real live simulation with concurrent threads representing player arrivals, game matches, and lobby management.

Clarifying Scope & Invariants (Thinking Out Loud)

ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” │ GAME LOBBY ARCHITECTURE │ ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ [ Incoming Players (Concurrent Threads) ] │ ā–¼ ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” │ LOBBY MANAGER (Thread-Safe) │ │ - Allocation Strategy (Priority)│ │ - Room Registry │ ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ │ ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” ā–¼ ā–¼ [ N DEFAULT ROOMS (Permanent) ] [ DYNAMIC ROOMS (On-Demand) ] - Pre-allocated at startup - Spawned when defaults are full - Priority 1 (High) to N (Low) - Temporary lifecycle - Reset to WAITING on match end - Reclaimed & destroyed on match end
  1. Player Assignment Invariant:
    • An incoming player must be assigned to the highest-priority available room with open slots.
    • Only when all default rooms are either full or playing does the system spawn an extra dynamic room.
  2. Room Capacity & Race Condition Invariant:
    • Exactly $K$ players must enter a room of capacity $K$. Two concurrent threads must never be permitted to both claim the $K$-th slot (Over-subscription prevention).
  3. Lifecycle Invariant:
    • Default rooms are immortal (never deleted from memory).
    • Dynamic rooms are ephemeral (cleaned up when empty after match completion).
  4. Deadlock Freedom:
    • Strict hierarchical lock acquisition order: Lobby Lock $\to$ Room Lock. Never acquire the Lobby lock while holding a Room lock!

šŸ›ļø System Architecture & Class Diagram

ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” │ <<enum>> │ │ RoomState │ ā”œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¤ │ WAITING │ │ FULL │ │ IN_PROGRESS │ │ FINISHED │ │ DESTROYED │ ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ ā–² │ ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” 1 * │ │ Player ā”œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” ā”œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¤ │ │ │ - id: string │ │ │ │ - name: string │ │ │ │ - skillRating: int │ │ │ ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ │ │ │ │ ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” │ │ GameRoom │ │ ā”œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¤ │ │ - id: string │ │ │ - roomType: RoomType (DEFAULT vs DYNAMIC) │ │ │ - priority: int │ │ │ - maxCapacity: int │ │ │ - currentPlayers: vector<shared_ptr<Player>> │ │ │ - state: RoomState │ │ │ - roomMutex: mutex │ │ ā”œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¤ │ │ + addPlayer(player): bool │ │ │ + removePlayer(playerId): bool │ │ │ + startMatch(): void │ │ │ + reset(): void │ │ ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ │ │ │ │ notifies │ ā–¼ │ ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” │ │ IRoomObserver │ │ ā”œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¤ │ │ + onMatchCompleted(roomId): void │ │ ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ │ │ implements │ ā–¼ │ ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” │ │ GameLobby │ │ ā”œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¤ │ │ - defaultRooms: vector<shared_ptr<GameRoom>> │ │ │ - dynamicRooms: vector<shared_ptr<GameRoom>> ā”‚ā”€ā”€ā”€ā”˜ │ - allocationStrategy: unique_ptr<IRoomAllocationStrategy> │ │ - lobbyMutex: mutex │ │ - cvMatchCompleted: condition_variable │ ā”œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¤ │ + addPlayerToLobby(player): shared_ptr<GameRoom> │ │ + onMatchCompleted(roomId): void │ │ + cleanupDynamicRooms(): void │ ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ │ uses ā–¼ ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” │ IRoomAllocationStrategy │ ā”œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¤ │ + findBestRoom(defaultRooms, dynamicRooms, cap): shared_ptr<Room>│ ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ │ implements ā–¼ ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” │ PriorityFirstAllocationStrategy │ ā”œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¤ │ - Scans Default rooms by Priority (1 to N) │ │ - Falls back to existing non-full Dynamic rooms │ │ - Returns nullptr if all full (signaling need to spawn new) │ ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜

šŸŽØ Design Patterns Applied

PatternComponentWhy It Was Chosen
Strategy PatternIRoomAllocationStrategy $\to$ PriorityFirstAllocationStrategyDecouples player-to-room matchmaking algorithms. In future, we can plug in SkillBasedAllocationStrategy or LatencyBasedAllocationStrategy without changing lobby code (Open/Closed Principle).
State PatternRoomState (WAITING, FULL, IN_PROGRESS, FINISHED)Explicitly governs valid room transitions. Prevents players from joining mid-game or joining rooms scheduled for destruction.
Observer PatternIRoomObserver $\to$ GameLobby::onMatchCompletedRooms notify the central Lobby asynchronously when games conclude, decoupling game execution threads from room reclamation logic.
Factory PatternRoomFactoryEncapsulates the instantiation of Default rooms (with predefined priority tiers) vs Dynamic rooms (with auto-generated IDs and lower priority).

šŸ”’ Concurrency & Thread-Safety Invariants

1. Two-Level Fine-Grained Locking

  • Lobby Mutex (lobbyMutex): Protects the room lists (defaultRooms, dynamicRooms). Held only for short durations when finding a room or appending a newly spawned dynamic room.
  • Room Mutex (roomMutex): Each GameRoom maintains its own dedicated mutex. When a player is being inserted into a room, only that room's lock is acquired.
  • Benefits: High concurrency! Players joining Room A do not block players joining Room B.

2. Eliminating Race Conditions During Room Allocation

Thread 1 (Player A) Thread 2 (Player B) │ │ ā”œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā–ŗ [ 1. Acquire Lobby Mutex ] ◄──┤ (Waits) │ [ 2. Strategy picks Room 1 ] │ [ 3. Releases Lobby Mutex] │ │ ā–¼ ā”œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā–ŗ [ Acquires Lobby Mutex ] [ Acquires Room 1 Mutex ] [ Strategy picks Room 1 ] [ Adds Player A ] [ Releases Lobby Mutex ] [ Releases Room 1 Mutex ] │ ā–¼ [ Acquires Room 1 Mutex ] [ If Room 1 became FULL in between: Falls back & retries allocation! ]
  • Optimistic Room Reservation: After picking a candidate room under the lobby lock, the lobby attempts room->tryAddPlayer(player). If another thread filled the room during the microsecond gap, the lobby smoothly advances to the next priority room or spawns an extra dynamic room.

3. Safe Dynamic Room Reclamation

  • Dynamic rooms are stored as std::shared_ptr<GameRoom>.
  • When a game ends, onMatchCompleted(roomId) marks the dynamic room as FINISHED.
  • A dedicated or deferred cleanup pass locks lobbyMutex and erases finished dynamic rooms from dynamicRooms. Any active thread holding a shared_ptr retains a valid reference until its execution completes, preventing dangling pointer segfaults.

šŸ’» Complete, Runnable C++17 Implementation

Save this code as online_game_lobby.cpp and compile with:

g++ -std=c++17 -O2 -pthread online_game_lobby.cpp -o online_game_lobby ./online_game_lobby
#include <iostream> #include <string> #include <vector> #include <memory> #include <mutex> #include <shared_mutex> #include <condition_variable> #include <thread> #include <chrono> #include <algorithm> #include <iomanip> #include <sstream> #include <atomic> #include <random> // ============================================================================ // 1. DOMAIN ENUMS & LOGGING UTILITY // ============================================================================ enum class RoomType { DEFAULT, DYNAMIC }; enum class RoomState { WAITING, // Accepting players FULL, // Reached max capacity, preparing to launch IN_PROGRESS, // Game match is running FINISHED, // Game ended DESTROYED // Marked for deletion }; std::string roomTypeToString(RoomType type) { return (type == RoomType::DEFAULT) ? "DEFAULT" : "DYNAMIC"; } std::string roomStateToString(RoomState state) { switch (state) { case RoomState::WAITING: return "WAITING"; case RoomState::FULL: return "FULL"; case RoomState::IN_PROGRESS: return "IN_PROGRESS"; case RoomState::FINISHED: return "FINISHED"; case RoomState::DESTROYED: return "DESTROYED"; } return "UNKNOWN"; } // Thread-safe console logger static std::mutex g_logMutex; void logMessage(const std::string& msg) { std::lock_guard<std::mutex> lock(g_logMutex); auto now = std::chrono::system_clock::now(); auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch()) % 1000; std::cout << "[" << std::setw(3) << std::setfill('0') << ms.count() << "ms] " << "[Thread " << std::this_thread::get_id() << "] " << msg << std::endl; } // ============================================================================ // 2. PLAYER ENTITY // ============================================================================ class Player { private: std::string id; std::string name; int skillRating; public: Player(std::string id, std::string name, int skillRating = 1000) : id(std::move(id)), name(std::move(name)), skillRating(skillRating) {} const std::string& getId() const { return id; } const std::string& getName() const { return name; } int getSkillRating() const { return skillRating; } }; // ============================================================================ // 3. OBSERVER INTERFACE (Decouples GameRoom from GameLobby) // ============================================================================ class IRoomObserver { public: virtual ~IRoomObserver() = default; virtual void onMatchCompleted(const std::string& roomId) = 0; }; // ============================================================================ // 4. GAME ROOM ENTITY // ============================================================================ class GameRoom : public std::enable_shared_from_this<GameRoom> { private: std::string id; RoomType type; int priority; // Lower numerical value = Higher priority (1 is highest) size_t maxCapacity; RoomState state; std::vector<std::shared_ptr<Player>> players; IRoomObserver* observer; mutable std::mutex roomMutex; public: GameRoom(std::string id, RoomType type, int priority, size_t maxCapacity, IRoomObserver* observer) : id(std::move(id)), type(type), priority(priority), maxCapacity(maxCapacity), state(RoomState::WAITING), observer(observer) {} const std::string& getId() const { return id; } RoomType getType() const { return type; } int getPriority() const { return priority; } size_t getMaxCapacity() const { return maxCapacity; } RoomState getState() const { std::lock_guard<std::mutex> lock(roomMutex); return state; } size_t getPlayerCount() const { std::lock_guard<std::mutex> lock(roomMutex); return players.size(); } // Attempt to add player. Returns true if added, false if room is full or not in WAITING state bool tryAddPlayer(const std::shared_ptr<Player>& player, bool& outRoomBecameFull) { std::lock_guard<std::mutex> lock(roomMutex); outRoomBecameFull = false; if (state != RoomState::WAITING || players.size() >= maxCapacity) { return false; } players.push_back(player); std::ostringstream oss; oss << "Player " << player->getName() << " (" << player->getId() << ") entered Room " << id << " [" << roomTypeToString(type) << " | P" << priority << "] (" << players.size() << "/" << maxCapacity << ")"; logMessage(oss.str()); if (players.size() == maxCapacity) { state = RoomState::FULL; outRoomBecameFull = true; oss.str(""); oss << ">>> Room " << id << " is now FULL! Triggering match start..."; logMessage(oss.str()); } return true; } // Launches the game match in an asynchronous worker thread void launchMatchAsync() { std::shared_ptr<GameRoom> self = shared_from_this(); std::thread([self]() { self->runMatchSimulation(); }).detach(); } // Match Simulation logic void runMatchSimulation() { { std::lock_guard<std::mutex> lock(roomMutex); state = RoomState::IN_PROGRESS; } std::ostringstream oss; oss << "=== [MATCH STARTED] Room " << id << " with " << maxCapacity << " players! ==="; logMessage(oss.str()); // Simulate game duration (e.g., 200 - 400 milliseconds) std::this_thread::sleep_for(std::chrono::milliseconds(300)); { std::lock_guard<std::mutex> lock(roomMutex); state = RoomState::FINISHED; players.clear(); } oss.str(""); oss << "=== [MATCH FINISHED] Room " << id << " match concluded! ==="; logMessage(oss.str()); // Notify lobby observer if (observer) { observer->onMatchCompleted(id); } } // Reset default room back to WAITING state void reset() { std::lock_guard<std::mutex> lock(roomMutex); players.clear(); state = RoomState::WAITING; std::ostringstream oss; oss << "Room " << id << " [DEFAULT | P" << priority << "] reset to WAITING state."; logMessage(oss.str()); } // Mark dynamic room as destroyed void markDestroyed() { std::lock_guard<std::mutex> lock(roomMutex); state = RoomState::DESTROYED; } }; // ============================================================================ // 5. STRATEGY PATTERN: ROOM ALLOCATION // ============================================================================ class IRoomAllocationStrategy { public: virtual ~IRoomAllocationStrategy() = default; virtual std::shared_ptr<GameRoom> selectRoom( const std::vector<std::shared_ptr<GameRoom>>& defaultRooms, const std::vector<std::shared_ptr<GameRoom>>& dynamicRooms) = 0; }; // Allocation Strategy: // 1. Scans Default rooms ordered by highest priority (lowest numerical priority value). // 2. If all default rooms are full/playing, checks existing non-full dynamic rooms. // 3. If no room has space, returns nullptr (signaling the Lobby to spawn a new dynamic room). class PriorityFirstAllocationStrategy : public IRoomAllocationStrategy { public: std::shared_ptr<GameRoom> selectRoom( const std::vector<std::shared_ptr<GameRoom>>& defaultRooms, const std::vector<std::shared_ptr<GameRoom>>& dynamicRooms) override { // 1. Check Default Rooms sorted by Priority for (const auto& room : defaultRooms) { if (room->getState() == RoomState::WAITING) { return room; } } // 2. Check existing Dynamic Rooms for (const auto& room : dynamicRooms) { if (room->getState() == RoomState::WAITING) { return room; } } // 3. No room currently available return nullptr; } }; // ============================================================================ // 6. FACTORY PATTERN: ROOM FACTORY // ============================================================================ class RoomFactory { private: std::atomic<int> dynamicRoomCounter{1}; public: std::shared_ptr<GameRoom> createDefaultRoom(int priority, size_t capacity, IRoomObserver* observer) { std::string id = "DEF-ROOM-" + std::to_string(priority); return std::make_shared<GameRoom>(id, RoomType::DEFAULT, priority, capacity, observer); } std::shared_ptr<GameRoom> createDynamicRoom(size_t capacity, IRoomObserver* observer) { int seq = dynamicRoomCounter.fetch_add(1); std::string id = "DYN-ROOM-" + std::to_string(seq); // Dynamic rooms have lower priority (e.g., 999) return std::make_shared<GameRoom>(id, RoomType::DYNAMIC, 999, capacity, observer); } }; // ============================================================================ // 7. GAME LOBBY (Core Facade & Concurrency Orchestrator) // ============================================================================ class GameLobby : public IRoomObserver { private: size_t roomCapacity; std::unique_ptr<IRoomAllocationStrategy> allocationStrategy; RoomFactory roomFactory; std::vector<std::shared_ptr<GameRoom>> defaultRooms; std::vector<std::shared_ptr<GameRoom>> dynamicRooms; mutable std::mutex lobbyMutex; std::condition_variable cvCompletion; std::atomic<int> activeMatches{0}; public: GameLobby(int numDefaultRooms, size_t capacityPerRoom, std::unique_ptr<IRoomAllocationStrategy> strategy = std::make_unique<PriorityFirstAllocationStrategy>()) : roomCapacity(capacityPerRoom), allocationStrategy(std::move(strategy)) { // Initialize N Default Rooms with Priority (1 to N) for (int p = 1; p <= numDefaultRooms; ++p) { defaultRooms.push_back(roomFactory.createDefaultRoom(p, roomCapacity, this)); } // Sort default rooms by priority in ascending order (P1 > P2 > P3) std::sort(defaultRooms.begin(), defaultRooms.end(), [](const auto& a, const auto& b) { return a->getPriority() < b->getPriority(); }); std::ostringstream oss; oss << "Initialized Game Lobby with " << numDefaultRooms << " Default Rooms (Capacity: " << roomCapacity << " players per room)."; logMessage(oss.str()); } ~GameLobby() override = default; // Core Method: Add player concurrently to the best available room std::shared_ptr<GameRoom> addPlayer(const std::shared_ptr<Player>& player) { while (true) { std::shared_ptr<GameRoom> candidateRoom = nullptr; bool needSpawnDynamic = false; { std::lock_guard<std::mutex> lock(lobbyMutex); // Select best candidate via Strategy candidateRoom = allocationStrategy->selectRoom(defaultRooms, dynamicRooms); if (candidateRoom == nullptr) { // All default and existing dynamic rooms are full/busy! Auto-scale! needSpawnDynamic = true; candidateRoom = roomFactory.createDynamicRoom(roomCapacity, this); dynamicRooms.push_back(candidateRoom); std::ostringstream oss; oss << "⚔ [AUTOSCALING] All default rooms full! Spawned Dynamic Room " << candidateRoom->getId() << " (Active Dynamic Rooms: " << dynamicRooms.size() << ")"; logMessage(oss.str()); } } // Attempt to insert player into candidate room bool roomBecameFull = false; if (candidateRoom->tryAddPlayer(player, roomBecameFull)) { if (roomBecameFull) { activeMatches.fetch_add(1); candidateRoom->launchMatchAsync(); } return candidateRoom; } // If tryAddPlayer failed (another thread filled it first), loop retries! } } // Callback invoked when a match finishes void onMatchCompleted(const std::string& roomId) override { std::lock_guard<std::mutex> lock(lobbyMutex); // 1. Check if it's a default room for (auto& room : defaultRooms) { if (room->getId() == roomId) { room->reset(); activeMatches.fetch_sub(1); cvCompletion.notify_all(); return; } } // 2. Check if it's a dynamic room -> Reclaim & Decommission! auto it = std::find_if(dynamicRooms.begin(), dynamicRooms.end(), [&roomId](const auto& r) { return r->getId() == roomId; }); if (it != dynamicRooms.end()) { (*it)->markDestroyed(); std::ostringstream oss; oss << "šŸ—‘ļø [RECLAMATION] Dynamic Room " << roomId << " finished match. Decommissioned & removed from pool. Remaining dynamic rooms: " << (dynamicRooms.size() - 1); logMessage(oss.str()); dynamicRooms.erase(it); activeMatches.fetch_sub(1); cvCompletion.notify_all(); } } // Wait until all active matches have completed (for clean simulation shutdown) void waitForAllMatches() { std::unique_lock<std::mutex> lock(lobbyMutex); cvCompletion.wait(lock, [this]() { return activeMatches.load() == 0; }); } void printStatus() const { std::lock_guard<std::mutex> lock(lobbyMutex); std::cout << "\n================= LOBBY STATUS =================" << std::endl; std::cout << "DEFAULT ROOMS (" << defaultRooms.size() << "):" << std::endl; for (const auto& r : defaultRooms) { std::cout << " - " << r->getId() << " [P" << r->getPriority() << "]: " << roomStateToString(r->getState()) << " (" << r->getPlayerCount() << "/" << r->getMaxCapacity() << ")" << std::endl; } std::cout << "DYNAMIC ROOMS (" << dynamicRooms.size() << "):" << std::endl; for (const auto& r : dynamicRooms) { std::cout << " - " << r->getId() << " [P" << r->getPriority() << "]: " << roomStateToString(r->getState()) << " (" << r->getPlayerCount() << "/" << r->getMaxCapacity() << ")" << std::endl; } std::cout << "================================================\n" << std::endl; } }; // ============================================================================ // 8. LIVE CONCURRENT MULTI-THREADED SIMULATION // ============================================================================ int main() { std::cout << "========================================================\n"; std::cout << "šŸŽ® JUSPAY SDE LIVE MULTIPLAYER LOBBY SIMULATION\n"; std::cout << "========================================================\n\n"; // Setup: // 3 Default Rooms (Priority 1, 2, 3) // Capacity = 3 players per room // Total default capacity = 3 * 3 = 9 players const int NUM_DEFAULT_ROOMS = 3; const size_t ROOM_CAPACITY = 3; const int TOTAL_PLAYERS = 18; // 18 players will force autoscaling of 3 Dynamic Rooms! auto lobby = std::make_shared<GameLobby>(NUM_DEFAULT_ROOMS, ROOM_CAPACITY); lobby->printStatus(); std::vector<std::thread> playerThreads; playerThreads.reserve(TOTAL_PLAYERS); logMessage("Starting concurrent player arrival simulation (18 players)..."); // Spawn 18 concurrent threads simulating players arriving simultaneously for (int i = 1; i <= TOTAL_PLAYERS; ++i) { std::string pId = "P" + std::to_string(i); std::string pName = "Player_" + std::to_string(i); playerThreads.emplace_back([lobby, pId, pName, i]() { // Small random jitter between arrivals to simulate real-world arrival patterns std::this_thread::sleep_for(std::chrono::milliseconds((i % 5) * 20)); auto player = std::make_shared<Player>(pId, pName, 1000 + (i * 50)); lobby->addPlayer(player); }); } // Join all player dispatch threads for (auto& t : playerThreads) { if (t.joinable()) { t.join(); } } logMessage("All 18 players have been dispatched to rooms!"); lobby->printStatus(); logMessage("Waiting for all in-flight matches to complete and dynamic rooms to reclaim..."); lobby->waitForAllMatches(); logMessage("All matches concluded successfully!"); lobby->printStatus(); std::cout << "========================================================\n"; std::cout << "āœ… SIMULATION COMPLETED: ZERO DEADLOCKS, CLEAN RECLAMATION\n"; std::cout << "========================================================\n"; return 0; }

šŸ”¬ Simulation Walkthrough & Expected Output

Phase 1: Default Priority Room Filling

When the first 9 players arrive concurrently:

  • Players are assigned strictly according to room priority:
    • Priority 1 (DEF-ROOM-1) is filled first (Players 1, 2, 3).
    • As soon as DEF-ROOM-1 reaches 3/3, it marks itself FULL and triggers launchMatchAsync().
    • Subsequent players spill over to Priority 2 (DEF-ROOM-2) (Players 4, 5, 6).
    • Then to Priority 3 (DEF-ROOM-3) (Players 7, 8, 9).

Phase 2: Dynamic Autoscaling on Overflow

When Players 10 through 18 arrive while default rooms are busy running matches:

  • allocationStrategy->selectRoom() detects that all default rooms are IN_PROGRESS or FULL.
  • The lobby triggers Autoscaling:
    • Spawns DYN-ROOM-1 $\to$ fills with Players 10, 11, 12 $\to$ starts match.
    • Spawns DYN-ROOM-2 $\to$ fills with Players 13, 14, 15 $\to$ starts match.
    • Spawns DYN-ROOM-3 $\to$ fills with Players 16, 17, 18 $\to$ starts match.

Phase 3: Match Completion & Garbage Collection

When simulated matches finish after 300ms:

  • Default Rooms (DEF-ROOM-1, DEF-ROOM-2, DEF-ROOM-3):
    • onMatchCompleted() invokes room->reset().
    • State transitions back to WAITING.
    • Players vector is cleared.
    • Room remains permanently in memory, ready for subsequent matches.
  • Dynamic Rooms (DYN-ROOM-1, DYN-ROOM-2, DYN-ROOM-3):
    • onMatchCompleted() marks the room as DESTROYED.
    • Removes the room pointer from dynamicRooms vector.
    • When the detached worker thread finishes, the std::shared_ptr ref count drops to zero, and the dynamic room is automatically deallocated from heap memory.
  • Final Lobby status shows 0 dynamic rooms, proving zero memory leaks!

⚔ Edge Cases & Juspay Interview Gauntlet

1. The Thundering Herd & Double-Checked Reservation

  • Scenario: 100 players arrive at the exact same millisecond when only 1 slot is free in DEF-ROOM-1.
  • Solution: The lobby picks the candidate room under lobbyMutex, releases the lobby lock, and calls room->tryAddPlayer(). Only ONE player acquires roomMutex and successfully increments the count. The other 99 threads receive false, loop back, re-evaluate the lobby state, and fall back to the next priority room or trigger dynamic room creation.

2. Player Disconnects While Waiting in Room

  • Scenario: A player loses internet connectivity while waiting for the room to fill (3/4 players).
  • Implementation:
    bool removePlayer(const std::string& playerId) { std::lock_guard<std::mutex> lock(roomMutex); if (state != RoomState::WAITING) return false; // Cannot leave mid-match auto it = std::remove_if(players.begin(), players.end(), [&](const auto& p) { return p->getId() == playerId; }); if (it != players.end()) { players.erase(it, players.end()); return true; } return false; }

3. Starvation of Lower Priority Default Rooms

  • Interview Question: "If high-priority rooms finish matches and reset quickly, will low-priority rooms ever get players?"
  • Answer: Under high arrival traffic, Priority 1 fills instantly. While Priority 1 is in-progress (300ms), incoming players naturally flow to Priority 2 and Priority 3. If arrival rate is low, Priority 1 indeed absorbs all matches, which is desirable (concentrating players into optimal servers). If fair distribution is needed, replace PriorityFirstAllocationStrategy with a RoundRobinAllocationStrategy via the Strategy Pattern!

4. Memory Leak Prevention for Detached Threads

  • When launchMatchAsync() detaches a thread:
    • We capture self = shared_from_this().
    • Even if GameLobby erases the dynamic room from its vector, self keeps the object alive until the thread exits its function scope.
    • The destructor of GameRoom is called safely when the thread terminates.