Bird
Raised Fist0

Design a Card Game Framework - Deck, Card, Hand, Rules Engine, Multiplayer

LLDmediumInterview freq: medium
Choose your preparation mode4 modes available
🏗️
Design a Card Game Framework - Deck, Card, Hand, Rules Engine, Multiplayer
A flexible, extensible card game framework supporting multiple games, players, and rule sets.
mediumGame DesignFreq: medium
Prerequisites:OOP FundamentalsJava Concurrency BasicsDesign Patterns (Factory, Strategy, Observer)
⚙️
Functional Requirements
  • Deck can be created with standard or custom cards
  • Deck supports shuffle and draw operations
  • Hand holds cards for a player and supports add/remove
  • Rules engine validates legality of moves and game progress
  • Support multiple players with isolated hands
  • Allow concurrent player actions without corrupting game state
  • Support extensible rules for different card games
👤
Use Cases
  • Create a deck of cards for a specific game
  • Shuffle and deal cards to players
  • Players hold cards in their hand and play cards
  • Rules engine validates moves and game state
  • Support multiple players playing concurrently
  • Allow different card games by plugging in different rules
🧑‍💻
Actors
  • Player
  • Game Engine
  • Rules Engine
  • Deck Manager
❓
Clarifying Questions to Ask
  • How many players can play concurrently? Are there thread pools or separate threads per player?
  • Should the framework support timed turns or asynchronous player actions?
  • Are there any constraints on deck size or card uniqueness?
  • How should invalid moves be handled by the rules engine?
  • Is there a pricing or monetization strategy affecting design (e.g., premium card sets)?
  • What edge cases should be handled, such as empty deck or simultaneous card draws?
Out of Scope
✗ Network communication for multiplayer
✗ UI or graphics rendering
✗ Persistent storage of game state
🔢
Enumerations
enum Suit
HEARTS | DIAMONDS | CLUBS | SPADES
Enum prevents typo bugs in card suits and enables exhaustive switch statements for game logic.
enum Rank
TWO | THREE | FOUR | FIVE | SIX | SEVEN | EIGHT | NINE | TEN | JACK | QUEEN | KING | ACE
Enum defines card ranks with fixed order and prevents invalid rank values.
enum GameState
NOT_STARTED | IN_PROGRESS | COMPLETED | PAUSED
Tracks game lifecycle states to control game flow and concurrency.
enum PlayerActionType
DRAW_CARD | PLAY_CARD | PASS | RESHUFFLE
Defines allowed player actions for rules validation and event handling.
📦
Card
2 fields
private final Suit suitprivate final Rank rank
Represents an immutable playing card with suit and rank. Provides identity and comparison.
Why thisImmutable objects ensure thread safety and consistency in concurrent games.
Why notMutable card objects risk state corruption in multiplayer scenarios.
public Suit getSuit()Returns the suit of the card.
public Rank getRank()Returns the rank of the card.
public String toString()Returns string representation of the card.
📦
Deck
2 fields
private final List<Card> cardsprivate volatile boolean isShuffled
Manages a collection of cards, supports shuffle and draw operations. Maintains deck state.
Why thisSynchronized methods ensure thread-safe operations on shared deck state.
Why notLock-free structures add complexity without clear benefit for deck operations.
public synchronized void shuffle()Randomizes the order of cards in the deck atomically.
public synchronized Card drawCard()Removes and returns the top card atomically; returns null if deck empty.
public synchronized int cardsRemaining()Returns the number of cards left in the deck.
📦
Hand
1 fields
private final List<Card> cards
Represents a player's hand holding cards. Supports adding and removing cards.
Why thisSynchronization protects hand state during concurrent player actions.
Why notUsing concurrent collections would complicate card order management.
public synchronized void addCard(Card card)Adds a card to the hand atomically.
public synchronized boolean removeCard(Card card)Removes specified card if present; returns true if removed.
public synchronized List<Card> getCards()Returns a copy of cards in hand to prevent external mutation.
📦
Player
2 fields
private final String playerIdprivate final Hand hand
Represents a player with unique ID and a hand of cards.
Why thisEncapsulating hand within player simplifies player state management.
Why notSeparating hand externally complicates player-hand association.
public String getPlayerId()Returns the unique player identifier.
public Hand getHand()Returns the player's hand.
📦
RulesEngine
1 fields
private final GameState gameState
Validates player actions and enforces game rules. Manages game state transitions.
Why thisSynchronized methods ensure consistent validation and state updates.
Why notEvent-driven or lock-free approaches add complexity without clear benefit.
public synchronized boolean validateAction(Player player, PlayerActionType action, Card card)Checks if the player's action with the given card is legal under current rules.
public synchronized void updateGameState(GameState newState)Updates the game state atomically.
public synchronized GameState getGameState()Returns the current game state.
📦
Game
4 fields
private final List<Player> playersprivate final Deck deckprivate final RulesEngine rulesEngineprivate volatile GameState currentState
Coordinates players, deck, and rules engine to run the card game lifecycle.
Why thisCentralized synchronization prevents race conditions in game flow.
Why notDecentralized control risks inconsistent game states.
public synchronized void startGame()Initializes game state and notifies players.
public synchronized boolean playTurn(Player player, PlayerActionType action, Card card)Processes a player's turn atomically, validating action and updating state.
public synchronized GameState getCurrentState()Returns current game state.
🔗
Relationships
Deck◆──►Card[1..*]
compositionDeck owns Cards; Cards do not exist independently
Hand◇──►Card[0..*]
aggregationHand aggregates Cards; Cards can exist independently in Deck or Hand
Player◆──►Hand[1]
compositionPlayer owns Hand; Hand cannot exist without Player
Game◇──►Player[1..*]
aggregationGame manages Players; Players can exist independently
Game◇──►Deck[1]
aggregationGame uses Deck; Deck can exist independently
Game◇──►RulesEngine[1]
aggregationGame uses RulesEngine; RulesEngine can exist independently
🗄️
Players
PK: playerId
playerId STRINGgameId STRINGplayerName STRINGhand BLOB
Stores player info and their current hand per game.
🗄️
Games
PK: gameId
gameId STRINGcurrentState STRINGdeckState BLOBrulesVersion STRING
Stores game state and deck info.
🔌
startGame
Request
{
  "gameId": "string",
  "playerIds": [
    "string"
  ],
  "rulesVersion": "string"
}
Response
{
  "success": "boolean",
  "message": "string"
}
Initializes a new game with players and rules.
🔌
playTurn
Request
{
  "gameId": "string",
  "playerId": "string",
  "action": "PlayerActionType",
  "card": "Card (optional)"
}
Response
{
  "success": "boolean",
  "message": "string",
  "updatedGameState": "GameState"
}
Processes a player's turn and returns updated game state.
🔌
getGameState
Request
{
  "gameId": "string"
}
Response
{
  "gameState": "GameState",
  "players": [
    "Player"
  ],
  "deck": "Deck"
}
Retrieves current state of the game.

Practice

(1/5)
1. In designing the Amazon Locker Service, should the Package entity be modeled as an abstract class or an interface, and why?
easy
A. Abstract class, because interfaces cannot have any methods related to package handling.
B. Abstract class, because Package has shared state like tracking info and common behavior.
C. Interface, because Package only defines behaviors and allows multiple inheritance of behavior.
D. Interface, because Package needs to hold mutable state for OTP and expiry.

Solution

  1. Step 1: Analyze Package state and behavior

    Package has shared mutable state such as OTP, expiry time, and tracking info, which requires fields and common method implementations.
  2. Step 2: Abstract class vs interface suitability

    Abstract classes can hold state and provide default behavior, interfaces cannot hold mutable state (prior to Java 8 default methods, but still no fields). Therefore, abstract class fits better.
  3. Final Answer:

    Option B -> Option B
  4. Quick Check:

    Shared mutable state -> abstract class needed [check]
Hint: Stateful entities -> abstract class; stateless behaviors -> interface [check]
Common Mistakes:
2. In designing a collection class that supports iteration, should the iterator interface be an abstract class or an interface in Java, and why?
easy
A. Interface, because it enforces immutability of the collection during iteration.
B. Abstract class, because it allows sharing state and default implementation for iteration methods.
C. Abstract class, because Java interfaces cannot have methods with parameters.
D. Interface, because it defines behavior without state, allowing multiple implementations and better flexibility.

Solution

  1. Step 1: Understand Java interface vs abstract class

    Interfaces define behavior contracts without state, allowing multiple implementations. Abstract classes can hold state but restrict multiple inheritance.
  2. Step 2: Apply to iterator design

    Iterator interface defines traversal methods (e.g., hasNext(), next()) without state, enabling different iterator implementations per collection type.
  3. Final Answer:

    Option D -> Option D
  4. Quick Check:

    Iterator as interface enables flexible, stateless traversal contracts [check]
Hint: Iterator interface defines behavior, no state needed [check]
Common Mistakes:
3. You want to add a WeekendFeeStrategy to calculate extra fees for weekend playback in the current music library design. Which approach requires the minimum change while respecting SOLID principles?
medium
A. Modify the existing FeeCalculator class to add weekend fee logic inside its method.
B. Create a new WeekendFeeStrategy class implementing the existing FeeStrategy interface and inject it where needed.
C. Add a boolean flag isWeekend to the Track class and calculate fees there.
D. Create a subclass WeekendFeeCalculator extending FeeCalculator and override the fee method.

Solution

  1. Step 1: Identify SOLID principle involved

    Open-Closed Principle (OCP) requires adding new behavior without modifying existing code.
  2. Step 2: Evaluate options against OCP

    Create a new WeekendFeeStrategy class implementing the existing FeeStrategy interface and inject it where needed. adds a new strategy class implementing FeeStrategy, no existing class modified, respecting OCP and Dependency Inversion Principle.
  3. Final Answer:

    Option B -> Option B
  4. Quick Check:

    New strategy class -> no existing code change -> OCP respected [check]
Hint: OCP: add new class, don't modify existing [check]
Common Mistakes:
4. You need to add support for ElectricCar that requires ChargingSpot compatibility in the current parking system. What is the minimum change to the design to support this without modifying existing classes?
hard
A. Modify Vehicle class to add a boolean needsCharging and update parking logic to check this flag.
B. Add ELECTRIC_CAR to VehicleType enum, CHARGING to SpotType, create ElectricCar subclass, and update SpotCompatibility.COMPATIBILITY_MAP accordingly.
C. Create a new ElectricParkingLot subclass that handles electric cars and charging spots separately.
D. Add a ChargingSpot field to Vehicle and modify all parking methods to handle it.

Solution

  1. Step 1: Identify extensibility requirements

    ElectricCar and ChargingSpot are new types requiring compatibility mapping without changing existing classes.
  2. Step 2: Apply minimal invasive change

    Adding new enum values and subclass plus updating compatibility map extends functionality without modifying existing class code, preserving OCP.
  3. Step 3: Avoid violating OCP

    Options B, C, and D modify existing classes or create parallel hierarchies, increasing complexity and violating OCP.
  4. Final Answer:

    Option B -> Option B
  5. Quick Check:

    Extend enums and map, subclass ElectricCar -> minimal change [check]
Hint: Extend enums and map, subclass new type -> minimal change [check]
Common Mistakes:
5. ElectricCar needs to park in ChargingSpots. What is the minimum change to the current design with VehicleType and SpotType enums, and SpotCompatibility map, to support this without modifying existing classes?
hard
A. Modify Vehicle class to include a boolean isElectric flag and add logic in ParkingLot to handle charging spots.
B. Add ELECTRIC_CAR to VehicleType, CHARGING to SpotType, create ElectricCar extending Vehicle, and update SpotCompatibility.COMPATIBILITY_MAP with new entries.
C. Create a new ElectricVehicle interface and modify all existing vehicle classes to implement it where applicable.
D. Add a ChargingSpot subclass of ParkingSpot and hardcode electric car checks in ParkingLot methods.

Solution

  1. Step 1: Identify extensibility requirements

    Adding ElectricCar and ChargingSpot requires new enum values and a new class without modifying existing classes to respect OCP.
  2. Step 2: Apply minimal change with enums and composition

    Adding new enum constants and extending Vehicle with ElectricCar allows reuse of existing logic. Updating SpotCompatibility.COMPATIBILITY_MAP with new mappings supports compatibility without changing existing code.
  3. Step 3: Eliminate options violating OCP

    Options B, C, and D modify existing classes or require widespread changes, violating OCP and risking regressions.
  4. Final Answer:

    Option B -> Option B
  5. Quick Check:

    New enums + subclass + compatibility map update -> minimal, OCP-compliant [check]
Hint: Add enums + subclass + map update, no existing class changes [check]
Common Mistakes:
Recommended

Ready to prove it?

Pass the test and this pattern moves unknown strong while earning +1 Grip toward your certificate.

10 questions~5 mineasy hard