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:
- Single seat or a multi-seat table? One player vs. dealer, or several players each with their own bet acting in turn against one dealer?
- How many decks in the shoe? One, or the casino-standard 6–8 shuffled together?
- Dealer rule — S17 or H17? Does the dealer stand on all 17 (S17) or hit soft 17 (H17)? This single rule flips real hands (see below).
- Which player actions are in scope? Hit/stand always; but also double-down, split, re-split, split-aces, surrender, insurance?
- Blackjack payout and reshuffle policy? 3:2 vs 6:5; cut-card reshuffle vs continuous shuffler.
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:
- Start with the set
{0}. - For each card, add its game value to every total currently in the set. If the card is an Ace, ALSO add a branch that is 10 higher (the difference between counting it as 1 vs 11).
- At resolution, pick the maximum total
≤ 21. If every total busts, the hand is bust.
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.
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:
| Step | Card | faceValue | gameValue | Totals set after this card |
|---|---|---|---|---|
| init | — | — | — | {0} |
| 1 | A♠ | 1 | 1 | {1, 11} — Ace forks the +10 branch |
| 2 | 5♥ | 5 | 5 | {6, 16} — soft 16 / hard 6 |
| 3 | J♣ | 11 | 10 | {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
- Scoring with face value instead of game value. The single most common Blackjack-model bug. J/Q/K must score 10; only the rank distinction needs 11/12/13. Keep two fields and use
gameValueeverywhere scoring happens. - Hard-coding the Ace as 11 (or always 1). A hand with two Aces is 12 (one as 11, one as 1), never 22 or 2. The set-of-totals approach is the clean fix; the alternative (count Aces, then subtract 10 while busting) works but is easy to get wrong with multiple Aces.
- Biased shuffle. The original loop did
r.nextInt(cardCount - i - 1), which can never place the last card and skews the distribution — in a casino model that is a real fairness/exploit bug. Use Fisher-Yates (nextInt(i + 1)) orCollections.shuffle. - Shoe exhaustion mid-deal.
dealCard()must refill and reshuffle when empty, or a long session throws or deals a stale order. Real casinos insert a cut card and reshuffle before exhaustion; a minimal model at least guards the empty case. - Treating split hands as one. After a split, each hand has its own bet and is resolved independently against the dealer — iterate
player.getHands(), do not collapse them into a single score.
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 1Extension 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
- The Ace's 1/11 duality is the design's core; model a hand as a set of totals and collapse to the max
≤ 21only at resolution. - Separate
faceValue(identity, 1–13) fromgameValue(scoring, 1–10). Mixing them is a silent, never-crashing correctness bug. - Shoe = many decks flattened, shuffled with Fisher-Yates, dealt from the front, refilled when empty. Deck builds the 52; Shoe owns randomness and supply.
- Game orchestrates; Hand decides scores; Card/Deck/Shoe supply material. Push every total-comparison through
Hand.resolveScore()so the Ace logic lives in exactly one place.
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.
🤖 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.
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.
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.
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.
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.