Company questionsGroundworkZomato

Low-level design · Games and state machines

Deck of cards

  • Reported once
  • SDE-1 (fresher)
  • Factory
  • Strategy
  • Iterator
  • 1 source

One candidate write-up. Treat it as a single data point.

The prompt
Model a deck of playing cards. Each card has one of four suits and a colour, and the deck must support popping the top card, shuffling, and dealing cards to every player. Expect a small, clean object design where the shuffle can be tested.

Restated in our own words, not a quote from Zomato or any page. The answer below is written for this site.

Try it first.The answer is covered until you pick. Talk it through against a 60 minute clock, or just read it.

Clarify first

Pin the scope down before you name a single class.

Questions to ask first:

  • Which deck: the usual 52 cards, with jokers, or a shorter deck? The report mentions only four suits and two colours. Assume 52 cards by default, with a factory that can build other variants.
  • What does popping an empty deck do? Assume it throws a clear error rather than returning a null.
  • What does dealing mean: a fixed number each, or the whole deck? Assume both: pass a count, or leave it out to deal evenly.
  • Does the deal go one card at a time round robin? Assume yes, as at a real table.
  • How random must the shuffle be? Assume unbiased, with the random source injected so tests are repeatable.

Out of scope: the rules of any particular card game, scoring and card comparison.

Entities and relationships

Who owns what, and who talks to whom.

  • Card is an immutable value with a rank, a suit and a colour. The colour comes from the suit, so a card can never be a red spade.
  • Suit data pairs a name with a colour: hearts and diamonds are red, clubs and spades are black.
  • Deck has many Cards in a stack and one random source. It offers pop(), shuffle(), size and an iterator that reads the cards from the top without removing them.
  • RandomSource is an interface with nextInt(bound). SeededRandom repeats for a given seed and StubRandom replays a fixed list.
  • createDeck is a factory that builds a deck for a named variant such as standard or short.
  • Player holds a name and a hand. Dealer pops cards from a deck and hands them to players.

The design

The class map, the main flow and the patterns that earn their place.

Deck of cards class mapA factory builds the deck. The deck owns cards and a random source, and a separate dealer fills hands.
createDeckvariant to cardsDeckpop, shuffle, iterateCardrank, suit, colourSuitname and colourRandomSourcenextInt(bound)Dealerround robin dealPlayername, handcreateshas manyhas oneshuffle usespop()receive(card)1234
  1. The factory loops suits by ranks for the chosen variant and returns a deck.
  2. Shuffle asks the injected random source for one index per position.
  3. The dealer pops the top card, after checking the deck has enough cards.
  4. The dealer gives the card to the next player in turn.

Open on the whiteboardAll 2 diagrams on one tidy board, with a step-by-step walkthrough.

Keep each class to one job. A Card is frozen after construction, so a card that has been dealt cannot change under a player. The suit supplies the colour, which removes an impossible state instead of validating it. The Deck stores cards in an array with the top at the end, so pop() is O(1) and the deck never shifts elements.

Shuffle is the Fisher-Yates algorithm: walk from the last position down to the second, pick an index from zero to the current index inclusive, and swap. It is O(n) and, given an unbiased random source, every ordering is equally likely. It works on a copy and swaps it in only at the end, and it rejects an index outside the range, so a faulty random source cannot leave a half-shuffled or corrupted deck. Sorting with a random comparator is not fair and should be avoided. The random source is a small Strategy passed to the deck, so the same code runs with a seeded generator for repeatable demos, a stub that returns scripted indices so a test can assert the exact order, or a secure source in production. The seeded generator here uses a modulo, which has a slight bias, and I would say so rather than hide it.

Construction. createDeck(variant, random) loops suits by ranks and builds the cards, so a 32 card short deck is a new entry in a table, not a new class. Jokers need a small change to the list the factory builds and a decision about their colour, but still no new class. That is the factory earning its place.

Dealing. The Dealer is separate from the Deck: the deck knows about cards, the dealer knows about tables. It first computes how many cards each player gets, throws before touching the deck if the deck cannot cover them, and then deals round robin, one card to each player per round. Checking first matters because a half-finished deal would leave some players with cards and the deck changed after a failure.

Iteration. The deck is iterable and yields a copy of the stack from the top, so printing or inspecting never changes it.

Trade-off: pop() throws on an empty deck instead of returning null. Callers must handle the error, but a forgotten check fails loudly instead of passing a null to a player.

The code

Runnable JavaScript, with the checks it passes.

zomato-deck-of-cards.js
const SUITS = [
  { name: "hearts", color: "red" },
  { name: "diamonds", color: "red" },
  { name: "clubs", color: "black" },
  { name: "spades", color: "black" },
];
const RANKS = ["2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K", "A"];
const VARIANTS = { standard: RANKS, short: RANKS.slice(5) };

class Card {
  constructor(rank, suit) {
    this.rank = rank;
    this.suit = suit.name;
    this.color = suit.color;
    Object.freeze(this);
  }
  toString() {
    return this.rank + " of " + this.suit;
  }
}

class SeededRandom {
  constructor(seed) {
    if (!Number.isInteger(seed)) throw new Error("the seed must be an integer");
    this.state = seed % 2147483647;
    if (this.state <= 0) this.state += 2147483646;
  }
  nextInt(bound) {
    this.state = (this.state * 48271) % 2147483647;
    return this.state % bound;
  }
}

class StubRandom {
  constructor(values) {
    this.values = values.slice();
  }
  nextInt() {
    return this.values.shift();
  }
}

class Deck {
  constructor(cards, random) {
    this.stack = cards.slice();
    this.random = random;
  }
  get size() {
    return this.stack.length;
  }
  pop() {
    if (this.stack.length === 0) throw new Error("deck is empty");
    return this.stack.pop();
  }
  shuffle() {
    if (!this.random) throw new Error("this deck has no random source");
    const next = this.stack.slice();
    for (let i = next.length - 1; i > 0; i -= 1) {
      const j = this.random.nextInt(i + 1);
      if (!Number.isInteger(j) || j < 0 || j > i) throw new Error("the random source returned " + j);
      const held = next[i];
      next[i] = next[j];
      next[j] = held;
    }
    this.stack = next;
    return this;
  }
  [Symbol.iterator]() {
    return this.stack.slice().reverse()[Symbol.iterator]();
  }
}

function createDeck(variant, random) {
  const ranks = VARIANTS[variant];
  if (!ranks) throw new Error("unknown variant " + variant);
  const cards = [];
  for (const suit of SUITS) {
    for (const rank of ranks) cards.push(new Card(rank, suit));
  }
  return new Deck(cards, random);
}

class Player {
  constructor(name) {
    this.name = name;
    this.hand = [];
  }
  receive(card) {
    this.hand.push(card);
  }
}

class Dealer {
  deal(deck, players, cardsEach) {
    if (players.length === 0) throw new Error("no players");
    if (cardsEach !== undefined && (!Number.isInteger(cardsEach) || cardsEach < 0)) {
      throw new Error("cardsEach must be a whole number");
    }
    const each = cardsEach === undefined ? Math.floor(deck.size / players.length) : cardsEach;
    if (each * players.length > deck.size) throw new Error("not enough cards");
    for (let round = 0; round < each; round += 1) {
      for (const player of players) player.receive(deck.pop());
    }
    return players;
  }
}

function names(cards) {
  return cards.map((card) => card.toString());
}

Runs here in your browser, in a sandboxed worker. Nothing is sent anywhere.

Try your own input

Write JavaScript that calls the code above and prints with console.log. It runs after the solution.

Extend it

The change requests an interviewer throws next.

  1. How would you add jokers, and how would the colour rule work for them?
  2. How would you compare two cards for a game where ace is high in one game and low in another?
  3. How would you build a shoe of several decks with a cut card, and what changes in the factory?
  4. Several threads deal from one deck at a table. What must be atomic, and how would you protect it?
  5. How would you store a shuffle so that a game can be replayed later?

Where candidates slip

The mistakes this problem punishes.

This problem punishes these slips:

  • Calling Math.random inside the deck, so a shuffle cannot be tested.
  • Shuffling with a sort and a random comparator, which gives a biased order, or swapping with an index chosen from the whole array each time.
  • Removing the top card with shift or splice on the front of the array when a stack is the natural fit.
  • Storing the colour separately from the suit, so the two can disagree.
  • Dealing cards one by one and failing halfway, which leaves a half-dealt deck.
  • Hard-coding 52 everywhere so a short deck needs edits in several places.

Have a go before you read.

Interviews reward the sketch you make cold. Clarify out loud, draw it, then compare with the answer.

Now make it stick

Where this was reported

Public pages only. Companies do not confirm these, and they rotate their questions, so treat each link as one person's account or one list's tag.