The 30 second answer
I clarify the rules first, then model only what they need: a card record, a deck with an injected random generator so tests are deterministic, players, and a game loop. I add one extension point where the interviewer says rules will vary, such as how a round is won. I write a fixed-deck test first. A candidate review of this exact prompt reports feedback that the design was more complex than needed.
A public candidate review of a senior Zalando coding hour describes "a simulation program for a card picking game" with a request to make the design extensible, and later feedback that the design had been made more complex than required at that point. The review does not give the full rules, so the rules below are an assumed example. In the interview, ask for the rules before writing anything.
Clarify. How many players, which deck, how cards are picked, how a round is won, how ties break, when the game ends, and what output is expected. Then ask which part is most likely to change; that is where the one extension point goes.
Assumed rules for this example. Two or more players, a standard 52-card deck, each round every player takes the top card, the highest rank wins and suit breaks ties, and the game ends when the deck cannot serve every player.
import java.util.*;
import java.util.random.RandomGenerator;
public class CardGame {
enum Suit { CLUBS, DIAMONDS, HEARTS, SPADES }
record Card(int rank, Suit suit) { // rank 2..14 (14 = ace)
Card {
if (rank < 2 || rank > 14) throw new IllegalArgumentException("rank " + rank);
Objects.requireNonNull(suit);
}
}
/** The one extension point the prompt asked for: how a round is won. */
interface RoundRule { int winner(List<Card> played); }
static final RoundRule HIGHEST_CARD = played -> {
Comparator<Card> byRankThenSuit =
Comparator.comparingInt(Card::rank).thenComparing(Card::suit);
int best = 0;
for (int i = 1; i < played.size(); i++) {
if (byRankThenSuit.compare(played.get(i), played.get(best)) > 0) best = i;
}
return best;
};
static Deque<Card> shuffledDeck(RandomGenerator rnd) {
List<Card> cards = new ArrayList<>();
for (Suit s : Suit.values()) for (int r = 2; r <= 14; r++) cards.add(new Card(r, s));
for (int i = cards.size() - 1; i > 0; i--) { // Fisher-Yates with an injected generator
Collections.swap(cards, i, rnd.nextInt(i + 1));
}
return new ArrayDeque<>(cards);
}
/** Plays until the deck cannot serve every player; returns rounds won per player. */
static int[] play(int players, Deque<Card> deck, RoundRule rule) {
if (players < 2) throw new IllegalArgumentException("need at least two players");
int[] wins = new int[players];
while (deck.size() >= players) {
List<Card> played = new ArrayList<>(players);
for (int p = 0; p < players; p++) played.add(deck.pop());
wins[rule.winner(played)]++;
}
return wins;
}
public static void main(String[] args) {
// Deterministic test: a fixed deck makes the outcome checkable.
Deque<Card> fixed = new ArrayDeque<>(List.of(
new Card(10, Suit.HEARTS), new Card(14, Suit.CLUBS), // round 1: player 1
new Card(5, Suit.SPADES), new Card(5, Suit.CLUBS), // round 2: tie on rank, spades wins -> player 0
new Card(2, Suit.HEARTS))); // leftover card, not dealt
System.out.println(Arrays.toString(play(2, fixed, HIGHEST_CARD))); // [1, 1]
int[] wins = play(4, shuffledDeck(RandomGenerator.of("L64X128MixRandom")), HIGHEST_CARD);
System.out.println(Arrays.stream(wins).sum()); // 13 rounds for 52 cards
try { play(1, fixed, HIGHEST_CARD); } catch (IllegalArgumentException e) { System.out.println(e.getMessage()); }
}
}Verified with OpenJDK 21.0.13: it prints [1, 1], 13 and need at least two players.
Design choices to explain.
- Records and enums for immutable values. A record (JEP 395) gives equality and a compact constructor for validation in a few lines.
- Injected randomness. The deck takes a
RandomGenerator, so a test can pass a fixed deck or a seeded generator and assert exact results. Hidden calls to a global random source make simulations untestable. - One extension point.
RoundRuleis the only interface, because "how a round is won" is the rule most likely to vary. Turn order, scoring and end conditions stay plain code until the interviewer asks for them to vary too. - Errors at the boundary. Invalid ranks and fewer than two players fail fast with clear messages, which a later Zalando candidate review describes interviewers looking for: requirement clarification, tests, and error handling as in production code.
Growing it on request. If the interviewer adds rules (jokers, a discard pile, players with strategies), add a Player type with a strategy interface at that point, or a sealed interface for card kinds (JEP 409) so a switch over them stays exhaustive. Each addition should answer a stated requirement.
Alternate interview scenarios
"The interviewer says: write it like production code, test first."
I would write the fixed-deck test before the game loop, name the rule being tested, then implement until it passes, and add tests for ties, an empty deck and invalid input. I would say why each check exists and keep methods small enough to test alone, instead of building a framework.
"Now make the winning rule configurable per game."
Pass a different RoundRule, for example lowest card wins or a trump suit that beats any other suit. Because the game loop already depends on the interface, no existing code changes, which is the open-closed principle in practice and the reason that single interface was worth adding.
Sources (4)
- Zalando Jobs blog: Technical interview for software engineersjobs.zalando.com
- Java SE 21 API: RandomGeneratordocs.oracle.com
- JEP 395: Recordsopenjdk.org
- JEP 409: Sealed Classesopenjdk.org