CMD Guide
HomeOO & Low-Level DesignOO Design Problems

Design Blackjack and a Deck of Cards

The whole design turns on one fact: an Ace is worth either 1 or 11, so a hand has not one total but a set of possible totals — and you score it by keeping the largest total that is still ≤ 21. Everything else (Card, Deck, Shoe, Hand, Game) is plumbing that feeds cards into that one scoring decision.

Blackjack is a comparing-card game: each player races the dealer to get as close to 21 as possible without busting. One or more standard 52-card decks (13 ranks × 4 suits) are loaded into a shoe; the dealer deals two cards each, players act (hit / stand / double / split), then the dealer plays a fixed rule (hit on 16-or-less, stand on 17-or-more) and hands are resolved against the dealer.

Clarify first — the questions that pin down the model

Before writing a class, an interviewer expects you to bound the problem. The answers change the class model and the dealer logic, so surface them up front:

We commit to concrete answers here: a multi-seat-capable model, a 3-deck shoe, dealer stands on all 17 (S17), payouts 3:2, reshuffle when the shoe empties. Those choices are called out again where the code depends on them.

The mechanism: why a hand needs a set of totals

A single Ace gives a hand two readings. A + 5 is a soft 16 (Ace=11) or a hard 6 (Ace=1). With two Aces you get up to four readings. Rather than special-casing this, the clean trick is to carry all reachable totals and collapse them only at the end:

This is exactly why BlackjackCard has a gameValue separate from its faceValue: face value runs 1–13 (so we can tell an Ace from a King), but a Jack/Queen/King must score as 10, and an Ace's branch logic keys on face value 1. Conflating the two is the bug that breaks the whole page.

diagram
diagram

Worked trace: dealing and scoring one hand

Three decks go into a shoe (156 cards), it is shuffled, and cards are dealt off the front. Suppose the player is dealt A♠ then 5♥, then hits and draws J♣. Walk getScores() card by card:

StepCardfaceValuegameValueTotals set after this card
init{0}
1A♠11{1, 11} — Ace forks the +10 branch
25♥55{6, 16} — soft 16 / hard 6
3J♣1110{16, 26} — J adds 10, not 11

resolveScore() scans {16, 26}, drops 26 (over 21), and returns 16. Under the buggy faceValue() version, step 3 would add 11, give {17, 27}, and wrongly return 17 — a different, losing hand in many comparisons.

The class model at a glance

The recording structure is a composition chain from the table down to a single card, with the two player kinds sharing a base. Multiplicities are the load-bearing detail: a shoe holds many decks, a deck is exactly 52 cards, and a player can hold more than one hand once a split happens.

BasePlayer (the shared parent of Player and Dealer) is elided from the boxes for clarity — the "extends" edge for it is described in the code below; only the BlackjackCard → Card inheritance is drawn.

Correct, compiling code

Enums & Card. faceValue is the rank 1–13 (1 = Ace); the suit is kept for display and equality.

public enum Suit { HEART, SPADE, CLUB, DIAMOND }

public class Card {
    private final Suit suit;
    private final int faceValue; // 1..13, where 1 = Ace, 11=J, 12=Q, 13=K

    public Card(Suit suit, int faceValue) {
        this.suit = suit;
        this.faceValue = faceValue;
    }
    public Suit getSuit()    { return suit; }
    public int  getFaceValue() { return faceValue; }
}

BlackjackCard. Adds the scoring value: anything above 10 (J/Q/K) caps at 10. The Ace keeps faceValue == 1 so the hand logic can detect it; its 1-vs-11 choice is NOT baked into gameValue — it is decided per hand.

public class BlackjackCard extends Card {
    private final int gameValue;

    public BlackjackCard(Suit suit, int faceValue) {
        super(suit, faceValue);
        this.gameValue = Math.min(faceValue, 10); // J/Q/K -> 10, Ace -> 1 here
    }
    public int  getGameValue() { return gameValue; }
    public boolean isAce()     { return getFaceValue() == 1; }
}

Deck & Shoe. A deck builds 52 cards. The shoe holds N decks' worth of cards in one flat list, shuffles, and deals from the front. (The original nested Shoe inside Deck and tried cards.add(new Deck().getCards()) — adding a List into a List<BlackjackCard>, which does not compile. Use addAll, and make Shoe its own class.)

public class Deck {
    private final List<BlackjackCard> cards = new ArrayList<>();

    public Deck() {
        for (int value = 1; value <= 13; value++)
            for (Suit suit : Suit.values())
                cards.add(new BlackjackCard(suit, value));
    }
    public List<BlackjackCard> getCards() { return cards; }
}

public class Shoe {
    private final List<BlackjackCard> cards = new ArrayList<>();
    private final int numberOfDecks;

    public Shoe(int numberOfDecks) {
        this.numberOfDecks = numberOfDecks;
        createShoe();
        shuffle();
    }

    private void createShoe() {
        cards.clear();
        for (int d = 0; d < numberOfDecks; d++)
            cards.addAll(new Deck().getCards()); // addAll, not add
    }

    public void shuffle() {
        Random r = new Random();
        for (int i = cards.size() - 1; i > 0; i--) { // Fisher-Yates
            int j = r.nextInt(i + 1);
            Collections.swap(cards, i, j);
        }
    }

    public BlackjackCard dealCard() {
        if (cards.isEmpty()) { createShoe(); shuffle(); }
        return cards.remove(0);
    }
}

Hand. The corrected scoring engine — this is the heart. Note getGameValue() (not faceValue), the typo-free totals list, and the Ace branch keyed on isAce().

public class Hand {
    private final List<BlackjackCard> cards = new ArrayList<>();

    public Hand(BlackjackCard c1, BlackjackCard c2) {
        cards.add(c1);
        cards.add(c2);
    }
    public void addCard(BlackjackCard card) { cards.add(card); }
    public List<BlackjackCard> getCards()   { return cards; }

    /** All reachable totals, treating each Ace as 1 OR 11. */
    private List<Integer> getScores() {
        List<Integer> totals = new ArrayList<>();
        totals.add(0);
        for (BlackjackCard card : cards) {
            List<Integer> next = new ArrayList<>();
            for (int score : totals) {
                next.add(score + card.getGameValue());   // Ace=1, J/Q/K=10
                if (card.isAce())
                    next.add(score + 11);                // the +11 branch
            }
            totals = next;
        }
        return totals;
    }

    /** Highest total that is still <= 21; 0 means bust on every branch. */
    public int resolveScore() {
        int best = 0;
        for (int score : getScores())
            if (score <= 21 && score > best) best = score;
        return best;
    }

    public boolean isBust()      { return resolveScore() == 0; }
    public boolean isBlackjack() { return cards.size() == 2 && resolveScore() == 21; }
}

Players & Game. The original called dealer.getTotalScore() (never defined), wrote dealeer, used a never-created shoe field, and looped while(true) forever. Here scoring goes through Hand.resolveScore(), the shoe is stored on this, and the loop terminates.

public abstract class BasePlayer {
    protected final List<Hand> hands = new ArrayList<>();
    public List<Hand> getHands()      { return hands; }
    public void addHand(Hand h)       { hands.add(h); }
    public void removeHand(Hand h)    { hands.remove(h); }
    /** Best resolvable score across this player's first hand. */
    public int score(Hand h)          { return h.resolveScore(); }
}

public class Player extends BasePlayer {
    private int bet, totalCash;
    public void placeBet(int amount) { this.bet = amount; }
    public int  getBet()             { return bet; }
}

public class Dealer extends BasePlayer {
    /** Dealer rule: hit while best score <= 16, stand on 17+. */
    public boolean shouldHit(Hand h) { return h.resolveScore() <= 16; }
}

public class Game {
    private final Player player;
    private final Dealer dealer;
    private final Shoe shoe;             // stored, not a shadowed local
    private static final int MAX_DECKS = 3;

    public Game(Player player, Dealer dealer) {
        this.player = player;
        this.dealer = dealer;           // fixed typo
        this.shoe   = new Shoe(MAX_DECKS);
    }

    private void hit(Hand hand)   { hand.addCard(shoe.dealCard()); }

    private void split(Hand hand) {
        List<BlackjackCard> c = hand.getCards();   // List access, not c[0]
        player.addHand(new Hand(c.get(0), shoe.dealCard()));
        player.addHand(new Hand(c.get(1), shoe.dealCard()));
        player.removeHand(hand);
    }

    private void resolve() {
        while (dealer.shouldHit(dealer.getHands().get(0)))
            dealer.getHands().get(0).addCard(shoe.dealCard());
        int dealerScore = dealer.getHands().get(0).resolveScore();
        for (Hand hand : player.getHands()) {
            int p = hand.resolveScore();
            if (p == 0)                       { /* bust: collect bet */ }
            else if (dealerScore == 0)        { /* dealer bust: pay player */ }
            else if (hand.isBlackjack())      { /* pay 3:2 */ }
            else if (p > dealerScore)         { /* pay 1:1 */ }
            else if (p < dealerScore)         { /* collect bet */ }
            else                              { /* push: return bet */ }
        }
    }

    public void start() {
        player.placeBet(50);
        player.addHand(new Hand(shoe.dealCard(), shoe.dealCard()));
        dealer.addHand(new Hand(shoe.dealCard(), shoe.dealCard()));
        // ... player acts (hit/stand/double/split) via UI, then:
        resolve();
    }
}

Why the naive version was wrong: it scored with card.faceValue() so J/Q/K counted 11/12/13; referenced an undefined getTotalScore(); indexed a List with cards[i]; mis-typed total.add and dealeer; and shadowed the shoe field with a local. The page described the right algorithm but the code neither compiled nor computed it.

Pitfalls

Dealer rule (S17 vs H17), extension points & concurrency

The dealer rule is a chosen variant, and it moves real hands. Our Dealer.shouldHit returns resolveScore() <= 16, so the dealer stands on every 17 — this is the S17 rule. The common alternative is H17, where the dealer hits a soft 17 (a 17 that still counts an Ace as 11). Take a dealer holding A♠ + 6♥: getScores() yields {7, 17}, so resolveScore() is a soft 17. Under S17 the dealer stands on 17; under H17 the dealer must hit. To implement H17 you cannot key on resolveScore() alone — you must detect softness (a total of 17 that is only reachable by counting an Ace as 11) and hit in that case:

// H17 variant: hit hard 16-or-less AND soft 17.
public boolean shouldHit(Hand h) {
    int best = h.resolveScore();
    if (best <= 16) return true;
    return best == 17 && h.isSoft();   // isSoft(): getScores() also contains best-10, i.e. an Ace
}                                     // is being counted as 11 and could safely drop to 1

Extension points. Everything beyond hit/stand slots in without touching the scoring engine: double-down (deal one card, lock the hand, double the bet), split (already sketched — each split hand carries its own bet and resolves independently), re-split (recurse the split when a third matching rank appears), surrender (forfeit half the bet before acting), and insurance (a side bet offered when the dealer shows an Ace). Model each as a flag/action on the hand or a small strategy object; the S17/H17 choice itself is best expressed as a pluggable DealerStrategy.

Concurrency at a multi-seat table. Turn order is naturally single-threaded — only one seat acts at a time, so the per-hand state needs no locking. The shared mutable resource is the Shoe: every dealCard() mutates the same list and runs a check-then-act on the refill-when-empty path. If seats (or a networked table) could ever deal concurrently, two draws could return the same card or race on the reshuffle. Guard it by confining the shoe to a single dealer thread/session, or synchronize dealCard() (and make the empty-check-plus-reshuffle atomic) so deals are serialized.

Takeaways


Based on the classic Blackjack / Deck-of-Cards object-design exercise as presented in Grokking the Object Oriented Design Interview (DesignGurus / educative) and the widely-circulated “Designing a Deck of Cards” problem (Cracking the Coding Interview, Gayle Laakmann McDowell). Rules cross-checked against standard casino Blackjack (Bicycle / Wizard of Odds). Re-authored and deepened for this guide: the scoring code was corrected (gameValue vs faceValue, Fisher-Yates shuffle, List access, undefined-method and typo fixes) and a worked score-set trace and diagram were added.

🔨 Practice this hands-on — Design a Deck of Cards & Blackjack →
Attempt it from an empty file, break it to feel the failure, then defend it under pushback.
🤖 Don't fully get this? Learn it with Claude

Stuck on Design Blackjack and a Deck of Cards? Open Claude, copy a block below, and it'll teach you this exact concept — visually and interactively.

🎨 Explain it visually

Build the mental picture, not memorization.

I just read a lesson on **Design Blackjack and a Deck of Cards** (OO & Low-Level Design) and want to truly understand it. Explain Design Blackjack and a Deck of Cards from first principles using ONE vivid real-world analogy and a visual mental model — draw it as ASCII art or a clear step-by-step diagram — with a concrete example using real numbers. Then ask me one question to check I got the mental picture, and wait for my reply. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
🤔 Walk me through it (interactive)

Socratic — adapts to where you're stuck.

Teach me **Design Blackjack and a Deck of Cards** interactively. Ask me ONE guiding question at a time, wait for my answer, and adapt to my confusion — build the idea with me step by step instead of explaining it all at once. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
🧪 Quiz me & fix my gaps

Active recall exposes what you missed.

Quiz me on **Design Blackjack and a Deck of Cards** with 5 questions, easy to tricky, ONE at a time. Tell me if each answer is right; at the end, explain clearly what I got wrong and why. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
🧠 Make it stick

Intuition + hook + flashcards for long-term memory.

Help me remember **Design Blackjack and a Deck of Cards** for the long term: give the one-sentence intuition, a memorable hook/mnemonic, a tiny worked example, and 3 active-recall flashcards (Q -> A). If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.

📝 My notes