Design Patterns & LLD

Design a Deck of Cards, Blackjack and a Chat Server (Class Models)

Ace scoring, stacked-deck tests, and a chat server built with the observer pattern.

Lesson 0830 min read#blackjack#chat-server#class-design

On this page

  1. Words used in this lesson
  2. Part 1. Deck of cards
  3. Part 2. Design a chat server
  4. Part 3. The call center, briefly
  5. Interview questions
  6. Key takeaways
  7. What's next

In one line: these are the "class model" questions, where you mostly draw nouns, relationships and rules and the code is short. A deck of cards teaches inheritance versus composition, blackjack teaches rules that live in the right class (the ace!), and a chat server teaches who owns what (users, chats, messages, requests) and how to push messages to people who are online.

The system-design-primer lists three object-oriented design questions of this kind: a call center, a deck of cards, and a chat server. This lesson does the deck of cards (extended to blackjack) and the chat server in full, and the call center briefly. The skill being tested is the same each time: pick the right classes, give each rule to the class that has the data, and keep the design open for the follow-up question.

Read Lesson 0 first for how to run the interview and for SOLID. This lesson leans on the strategy and observer patterns from Lesson 3. The previous lesson is Lesson 7. For the large-scale chat design (WebSockets, many servers, storing history) see System Design, Lesson 63, Design a Chat System and Practical Design Problems, Lesson 5.

What I ran / checked. All code ran on Node 22.22 with built-in type stripping, tests with node:test. I read the three primer notebooks (deck_of_cards, online_chat, call_center) directly. Their stated assumptions are used below: a standard 52-card deck for card games, and for chat, text only, users and friends, friend requests, group chats and private 1-1 chats, with no scaling concerns at first. The notebooks are sketches and several lines would not run as written. For example, the deck notebook's deal_card() has no self, its remaining_cards uses deal_index without self., and it writes sys.MAXSIZE (Python's name is sys.maxsize). In the call center notebook, Supervisor and Director call super(Operator, self), and Operator leaves out the call_center argument that Employee needs. I mention these so you know not to copy them, not to criticise the author: the notebooks are interview outlines. The blackjack rules are the common ones; casino rules vary, and I list my choices.

Words used in this lesson

  • Inheritance is "B is a kind of A" (BlackjackHand extends Hand). Composition is "B has an A" (a Deck has Cards).
  • Abstract class is a base class that you do not create directly.
  • Ace soft/hard: a hand with an ace counted as 11 is soft, because the ace can drop to 1 if you would bust. Otherwise it is hard.
  • Bust means the hand is over 21. Push means a tie. Natural blackjack is an ace plus a ten-value card as the first two cards.
  • Fisher-Yates is the standard fair shuffle: go from the end, swap each card with a random card at or before it.
  • Observer pattern means: things that care about an event register a callback, and the source calls them when the event happens.
  • Aggregate is a group of objects handled as one unit. A chat with its messages and members is one.

Part 1. Deck of cards

Requirements (primer assumptions): a generic 52-card deck usable for poker and blackjack. 4 suits. Inputs are valid. I add: shuffle, deal, how many remain, and an injectable random source.

Nouns and rules:

NounClassDataRule it owns
CardCard (plain data)rank, suitnone: a card does not know it is in blackjack
DeckDeckthe cards, next positionshuffle, deal, remaining
HandHandcards heldadd a card
Blackjack handBlackjackHand extends Handsamescore, bust, natural

The key decision: the card has no blackjack value. In the primer sketch the value of a card is a property of BlackJackCard, so every card must be a blackjack card. But the value of an ace depends on the other cards in the hand (1 or 11), so a card alone cannot know it. The right place is the hand. That is the "give the rule to the class that has the data" idea.

I use plain strings for suits and ranks ("hearts", "K"), not enums, because Node's built-in type stripping does not run TypeScript enums. A union type like "hearts" | "spades" gives the same safety.

JavaScript
export const SUITS = ["hearts", "diamonds", "clubs", "spades"] as const;export const RANKS = ["A", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K"] as const;export type Suit = (typeof SUITS)[number];export type Rank = (typeof RANKS)[number];export interface Card {  readonly rank: Rank;  readonly suit: Suit;}export const show = (c: Card) => c.rank + c.suit[0].toUpperCase();export class Deck {  private cards: Card[];  private next = 0; // index of the next card to deal  constructor(cards?: Card[]) {    this.cards = cards ?? SUITS.flatMap((suit) => RANKS.map((rank) => ({ rank, suit })));  }  get remaining(): number {    return this.cards.length - this.next;  }  // Fisher-Yates. The random source is injected, so tests can control it.  shuffle(rand: () => number = Math.random): void {    for (let i = this.cards.length - 1; i > 0; i--) {      const j = Math.floor(rand() * (i + 1));      [this.cards[i], this.cards[j]] = [this.cards[j], this.cards[i]];    }    this.next = 0;  }  deal(): Card {    if (this.next >= this.cards.length) throw new Error("deck is empty");    return this.cards[this.next++];  }}export class Hand {  readonly cards: Card[] = [];  add(c: Card): void {    this.cards.push(c);  }}// Blackjack rules live in a subclass, so Hand stays usable for poker.export class BlackjackHand extends Hand {  // Count every ace as 1, then upgrade ONE ace to 11 if that does not bust.  get score(): number {    let total = 0;    let aces = 0;    for (const c of this.cards) {      if (c.rank === "A") { aces++; total += 1; }      else if (c.rank === "J" || c.rank === "Q" || c.rank === "K") total += 10;      else total += Number(c.rank);    }    return aces > 0 && total + 10 <= 21 ? total + 10 : total;  }  get isBust(): boolean { return this.score > 21; }  get isBlackjack(): boolean { return this.cards.length === 2 && this.score === 21; }}// ---- A round of blackjack. No bets, splits or doubling. ----export type Move = "hit" | "stand";export type PlayerStrategy = (hand: BlackjackHand, dealerUp: Card) => Move;export type Outcome = "player blackjack" | "player wins" | "dealer wins" | "push";export const standOn17: PlayerStrategy = (h) => (h.score >= 17 ? "stand" : "hit");export const standOn12: PlayerStrategy = (h) => (h.score >= 12 ? "stand" : "hit");export function playRound(deck: Deck, player: PlayerStrategy): { outcome: Outcome; player: BlackjackHand; dealer: BlackjackHand } {  const p = new BlackjackHand();  const d = new BlackjackHand();  p.add(deck.deal()); d.add(deck.deal()); p.add(deck.deal()); d.add(deck.deal());  const finish = (outcome: Outcome) => ({ outcome, player: p, dealer: d });  if (p.isBlackjack || d.isBlackjack) {    if (p.isBlackjack && d.isBlackjack) return finish("push");    return finish(p.isBlackjack ? "player blackjack" : "dealer wins");  }  while (!p.isBust && player(p, d.cards[0]) === "hit") p.add(deck.deal());  if (p.isBust) return finish("dealer wins");  while (d.score < 17) d.add(deck.deal()); // house rule used here: dealer stands on all 17s  if (d.isBust || p.score > d.score) return finish("player wins");  return finish(p.score === d.score ? "push" : "dealer wins");}

Points to say:

  • The shuffle takes rand. With Math.random it is real. In a test you pass a fixed function, so the order is repeatable. Never use "sort with a random comparator" for shuffling; it is not a fair shuffle (I am stating this from general knowledge, not from something I tested here).
  • The ace rule: count all aces as 1, then add 10 once if that stays at 21 or below. Only one ace can ever be 11, because two aces as 11 is already 22. That is why the code needs no list of "possible scores", which the primer notebook leaves as an exercise.
  • deal throws on an empty deck. An empty deck mid-round is a real problem (a casino reshuffles earlier), so a loud error is better than undefined.
  • Player behaviour is a function (PlayerStrategy). A human, a bot that stands on 17 and a bot that stands on 12 are all the same type. That is the strategy pattern in its smallest form.
  • House rules I chose: dealer stands on all 17s, a natural beats a normal 21, both naturals push. No bets, splits, doubling or insurance.

Running it (cards and chat together)

The chat half of this demo uses the ChatServer from Part 2 below.

JavaScript
import { BlackjackHand, Deck, playRound, show, standOn12, standOn17, type Card, type Rank, type Suit } from "./cards.ts";import { ChatServer } from "./chat.ts";const c = (rank: Rank, suit: Suit = "spades"): Card => ({ rank, suit });const hand = (...r: Rank[]) => { const h = new BlackjackHand(); r.forEach((x) => h.add(c(x))); return h.score; };console.log("A+K =", hand("A", "K"), "| A+A+9 =", hand("A", "A", "9"), "| A+6+10 =", hand("A", "6", "10"), "| K+Q+5 =", hand("K", "Q", "5"));// mulberry32: a tiny seeded random generator, so the output is the same every runfunction seeded(a: number) {  return () => { a = (a + 0x6d2b79f5) | 0; let t = Math.imul(a ^ (a >>> 15), 1 | a); t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; return ((t ^ (t >>> 14)) >>> 0) / 4294967296; };}const deck = new Deck();deck.shuffle(seeded(7));console.log("first five after shuffle:", [deck.deal(), deck.deal(), deck.deal(), deck.deal(), deck.deal()].map(show).join(" "), "| remaining", deck.remaining);const tally = (s: typeof standOn17) => {  const rand = seeded(2026), t: Record<string, number> = {};  for (let i = 0; i < 20000; i++) { const d = new Deck(); d.shuffle(rand); const o = playRound(d, s).outcome; t[o] = (t[o] ?? 0) + 1; }  return t;};console.log("stand on 17:", JSON.stringify(tally(standOn17)));console.log("stand on 12:", JSON.stringify(tally(standOn12)));const s = new ChatServer(() => 0);for (const n of ["ann", "bob", "cy"]) s.addUser(n, n);s.subscribe("bob", (m) => console.log(`  bob's screen <- [${m.chatId}] ${m.from}: ${m.text}`));const r1 = s.sendFriendRequest("ann", "bob"); s.respond(r1.id, "bob", true);const r2 = s.sendFriendRequest("ann", "cy"); s.respond(r2.id, "cy", true);s.messageUser("ann", "bob", "hi bob");const g = s.createGroup("ann", "trip", ["bob", "cy"]);s.post(g, "cy", "who is driving?");console.log("bob unread in group:", s.unread("bob", g)); s.markRead("bob", g);console.log("after markRead     :", s.unread("bob", g));try { s.messageUser("bob", "cy", "psst"); } catch (e) { console.log("bob -> cy:", (e as Error).message); }
TEXT
$ node demo.tsA+K = 21 | A+A+9 = 21 | A+6+10 = 17 | K+Q+5 = 25first five after shuffle: JD 10H QS 5D 7S | remaining 47stand on 17: {"player wins":7427,"dealer wins":9807,"push":1832,"player blackjack":934}stand on 12: {"player wins":7501,"dealer wins":10386,"push":1179,"player blackjack":934}  bob's screen <- [dm:ann|bob] ann: hi bob  bob's screen <- [group:trip] cy: who is driving?bob unread in group: 1after markRead     : 0bob -> cy: you can only message friends

The first output line checks the ace rule: A+K is 21, A+A+9 is 21 (11 + 1 + 9), A+6+10 is 17 (the ace dropped to 1) and K+Q+5 is 25, a bust. The two "stand on" lines are a simulation of 20,000 rounds each, with a freshly shuffled deck every round and a fixed random seed. It shows that the strategy is swappable, and that the numbers differ. It is not a claim about real casino odds: my model has one fresh deck per round, ignores the dealer's up-card and has no bets.

Tests

The scoring test and the stacked-deck round tests are the most useful. To test a round I do not shuffle: I create a Deck from a list in the order the cards will be dealt (player, dealer, player, dealer, then draws).

JavaScript
test("blackjack scoring: aces count as 11 only when that does not bust", () => {  assert.equal(score("A", "K").score, 21);  assert.equal(score("A", "A", "9").score, 21); // 11 + 1 + 9  assert.equal(score("A", "6", "10").score, 17); // the ace had to drop to 1  assert.equal(score("A", "A", "A", "A").score, 14);  assert.equal(score("K", "Q", "5").isBust, true);  assert.equal(score("A", "5", "5").isBlackjack, false); // 21 with three cards is not a natural});// deal order: player, dealer, player, dealer, then drawstest("round: natural blackjack, both naturals, dealer natural", () => {  assert.equal(playRound(stack("A", "5", "K", "6"), standOn17).outcome, "player blackjack");  assert.equal(playRound(stack("A", "A", "K", "K"), standOn17).outcome, "push");  assert.equal(playRound(stack("9", "A", "8", "K"), standOn17).outcome, "dealer wins");});test("round: player bust loses and the dealer does not draw", () => {  const r = playRound(stack("10", "2", "6", "3", "K"), standOn17);  assert.equal(r.outcome, "dealer wins");  assert.equal(r.dealer.cards.length, 2);});test("round: dealer draws to 17, busts, or pushes", () => {  const draws = playRound(stack("10", "5", "8", "2", "4", "6"), standOn17);  assert.equal(draws.dealer.score, 17);  assert.equal(draws.outcome, "player wins");  assert.equal(playRound(stack("10", "10", "8", "6", "9"), standOn17).outcome, "player wins"); // dealer bust  assert.equal(playRound(stack("10", "10", "8", "8"), standOn17).outcome, "push");});

Part 2. Design a chat server

Requirements (primer assumptions): text only. Add, remove and update users. Friends list with friend requests (send, approve, reject). Private 1-1 chats. Group chats where you can invite friends and post. No scaling at first.

Classes:

NounClassHoldsRules
UserUserid, name, friends, a listener (when online)none, it is data
Friend requestFriendRequestfrom, to, statuspending to accepted or rejected, once
ConversationChat (private or group)members, messages, owneronly members post
MessageMessageid, chat id, sender, text, timenone
The serviceChatServerusers, chats, requests, read positionsall the workflows

Notes on the choices:

  • One Chat class for both kinds. A private chat is a chat with two members and no owner. A group chat has an owner and can grow. The primer has PrivateChat and GroupChat subclasses. Subclasses are fine; I used a field because the only differences are the owner and "can invite". If the differences grow, subclasses become worth it.
  • One private chat per pair. The chat id is built from the two user ids sorted, so ann messaging bob and bob messaging ann land in the same chat.
  • Only friends can message and be invited. That is the rule from the requirements, kept in the server, where the friends lists are visible.
  • Request status is a tiny state machine (see Lesson 7). A pending request can be answered once. A second answer is an invalid transition, and it throws.
  • Delivery is an observer. subscribe(userId, callback) registers a function while the user is online. post calls it for every other member. Offline users read the history later; unread counts come from a read position per user per chat.
JavaScript
export type Status = "pending" | "accepted" | "rejected";export interface Message { id: number; chatId: string; from: string; text: string; at: number }export interface FriendRequest { id: number; from: string; to: string; status: Status }type Listener = (m: Message) => void;class User {  readonly friends = new Set<string>();  listener: Listener | null = null; // set while the user is online  readonly id: string;  readonly name: string;  constructor(id: string, name: string) { this.id = id; this.name = name; }}// One conversation. Private chats have 2 members; group chats have an owner and can grow.class Chat {  readonly messages: Message[] = [];  readonly members: Set<string>;  readonly id: string;  readonly owner: string | null;  constructor(id: string, members: string[], owner: string | null) {    this.id = id; this.members = new Set(members); this.owner = owner;  }}export class ChatServer {  private readonly users = new Map<string, User>();  private readonly chats = new Map<string, Chat>();  private readonly requests: FriendRequest[] = [];  private readonly readUpTo = new Map<string, number>(); // "user|chat" -> last message id seen  private nextId = 1;  private readonly now: () => number;  constructor(now: () => number = Date.now) { this.now = now; }  private user(id: string): User {    const u = this.users.get(id);    if (!u) throw new Error(`no such user: ${id}`);    return u;  }  addUser(id: string, name: string): void {    if (this.users.has(id)) throw new Error(`user exists: ${id}`);    this.users.set(id, new User(id, name));  }  // Observer: the user's connection registers a callback and gets messages pushed.  subscribe(id: string, fn: Listener | null): void { this.user(id).listener = fn; }  sendFriendRequest(from: string, to: string): FriendRequest {    this.user(from); this.user(to);    if (from === to) throw new Error("cannot befriend yourself");    if (this.user(from).friends.has(to)) throw new Error("already friends");    if (this.requests.some((r) => r.status === "pending" && ((r.from === from && r.to === to) || (r.from === to && r.to === from)))) throw new Error("request already pending");    const r: FriendRequest = { id: this.nextId++, from, to, status: "pending" };    this.requests.push(r);    return r;  }  respond(requestId: number, by: string, accept: boolean): void {    const r = this.requests.find((x) => x.id === requestId);    if (!r || r.to !== by) throw new Error("no such request for this user");    if (r.status !== "pending") throw new Error(`request already ${r.status}`); // invalid transition    r.status = accept ? "accepted" : "rejected";    if (accept) { this.user(r.from).friends.add(r.to); this.user(r.to).friends.add(r.from); }  }  messageUser(from: string, to: string, text: string): Message {    if (!this.user(from).friends.has(to)) throw new Error("you can only message friends");    const id = "dm:" + [from, to].sort().join("|"); // one private chat per pair, either direction    if (!this.chats.has(id)) this.chats.set(id, new Chat(id, [from, to], null));    return this.post(id, from, text);  }  createGroup(owner: string, name: string, memberIds: string[]): string {    for (const m of memberIds) if (!this.user(owner).friends.has(m)) throw new Error(`${m} is not your friend`);    const id = "group:" + name;    if (this.chats.has(id)) throw new Error("group exists");    this.chats.set(id, new Chat(id, [owner, ...memberIds], owner));    return id;  }  invite(by: string, chatId: string, who: string): void {    const c = this.chat(chatId);    if (c.owner === null) throw new Error("cannot invite to a private chat");    if (!c.members.has(by)) throw new Error("only members can invite");    if (!this.user(by).friends.has(who)) throw new Error(`${who} is not your friend`);    c.members.add(who);  }  private chat(id: string): Chat {    const c = this.chats.get(id);    if (!c) throw new Error(`no such chat: ${id}`);    return c;  }  post(chatId: string, from: string, text: string): Message {    const c = this.chat(chatId);    if (!c.members.has(from)) throw new Error("not a member of this chat");    const m: Message = { id: this.nextId++, chatId, from, text, at: this.now() };    c.messages.push(m);    for (const uid of c.members) if (uid !== from) this.users.get(uid)!.listener?.(m); // push to online users    return m;  }  unread(userId: string, chatId: string): number {    const seen = this.readUpTo.get(userId + "|" + chatId) ?? 0;    return this.chat(chatId).messages.filter((m) => m.id > seen && m.from !== userId).length;  }  markRead(userId: string, chatId: string): void {    const ms = this.chat(chatId).messages;    if (ms.length > 0) this.readUpTo.set(userId + "|" + chatId, ms[ms.length - 1].id);  }}

The chat output is the last six lines of the run above: bob's callback printed the direct message and the group message as they were posted, his unread count was 1 before markRead and 0 after, and the last line is the rejected message from bob to cy (they are not friends).

Tests

JavaScript
test("chat: only members post, only online members get pushes", () => {  const { s } = setup();  const got: string[] = [];  s.subscribe("bob", (m) => got.push(`bob:${m.text}`));  s.subscribe("cy", (m) => got.push(`cy:${m.text}`));  const g = s.createGroup("ann", "trip", ["bob"]);  assert.throws(() => s.post(g, "cy", "let me in"), /not a member/);  s.post(g, "ann", "hello");  assert.deepEqual(got, ["bob:hello"]); // cy is not in the group  s.invite("ann", g, "cy");  s.post(g, "bob", "welcome");  assert.deepEqual(got, ["bob:hello", "cy:welcome"]); // the sender gets no echo  assert.throws(() => s.createGroup("ann", "x", ["dee"]), /not your friend/);});

The whole run for deck, blackjack and chat:

TEXT
$ node --test --test-reporter=spec cards.test.ts | LC_ALL=C sed 's/\xE2\x9C\x94/pass/; s/\xE2\x84\xB9/#/'pass deck: 52 different cards, shuffle keeps them all, empty deck throws (2.133057ms)pass deck: same random source gives the same order (1.744312ms)pass blackjack scoring: aces count as 11 only when that does not bust (0.43314ms)pass round: natural blackjack, both naturals, dealer natural (0.426751ms)pass round: player bust loses and the dealer does not draw (0.241605ms)pass round: dealer draws to 17, busts, or pushes (0.26378ms)pass chat: friend request rules (2.589508ms)pass chat: one private chat per pair, in either direction (0.332719ms)pass chat: only members post, only online members get pushes (0.761834ms)pass chat: unread counts go down after markRead (0.69821ms)# tests 10# suites 0# pass 10# fail 0# cancelled 0# skipped 0# todo 0# duration_ms 237.781813

What this model leaves out

This is a single-process, in-memory model. These are not solved here, and each is a good follow-up:

They askShort answer
Remove a userRemove from friends lists, leave groups, keep old messages with a "deleted user" name. Decide it, then write it down.
Many serversThe chat state moves to a shared store, and a message must reach the server where the recipient is connected. See the system design lessons linked at the top.
Message orderPer-chat increasing ids, assigned by one writer (here a counter). In many servers, this is a real problem.
Delivery to offline usersKeep the message in history (done) and push a notification when they come online or via a push channel.

Part 3. The call center, briefly

The primer's call center has operators, supervisors and directors. A call goes to a free operator. If none is free, or the operator cannot handle it, it escalates to a supervisor, then to a director (the director can handle anything). If nobody is free, the call waits in a queue. VIP calls are out of scope.

The shape to remember is a chain of responsibility: a request is passed along a chain of handlers until one accepts it. A short design:

ClassRole
Employee (base, with Operator, Supervisor, Director)Has a rank and at most one current call. takeCall, completeCall, escalate
CallState: ready, in progress, complete. The rank needed
CallCenterdispatch(call): try operators, then supervisors, then directors, otherwise queue. When someone becomes free, take the next call from the queue

Two design questions to expect: where does the escalation rule live (in the employee, who knows "I cannot handle this", with the center doing the re-dispatch, which is what the primer does), and how do you add a new rank (a rank list instead of three hard-coded lists, so adding one is one new entry). I did not build or run this one.

Interview questions

Design a deck of cards. (Must know)

A Card is just a rank and a suit. A Deck holds 52 cards and a deal position, with shuffle, deal and remaining. A Hand holds cards. A game then adds rules: for blackjack, a BlackjackHand computes the score. I keep game rules out of Card and Deck so the same deck works for poker.

Where should the card's blackjack value live? (Senior signal)

Not in the card. An ace is 1 or 11 depending on the other cards, so only the hand can decide. The face cards are always 10, and that is also a blackjack rule, so it lives with the hand's scoring too. That keeps Card reusable.

How do you score a hand with aces? (Must know)

Count every ace as 1, add the other cards, then if there is at least one ace and adding 10 stays at 21 or below, add 10. Only one ace can ever count as 11. I tested A+A+9 (21), A+6+10 (17, ace dropped back) and A+5+5 (21, but not a natural because it has three cards).

How do you shuffle fairly? (Must know)

Fisher-Yates: walk from the last position down, and swap each card with a random card at or before it. It is O(n) and gives every order the same chance, if the random numbers are good. I inject the random function so tests can repeat the order.

How do you test a game with a random deck? (Must know)

I do not rely on random. I build a Deck from a list in the order cards will be dealt, so every round is a fixed script: natural blackjack, bust, dealer draws to 17, push. The shuffle gets its own small tests with a fixed random function.

How do you design the chat server's classes? (Must know)

Users, friend requests, chats, messages and a service. A chat has members and messages, and only members can post. Private and group chats share one class, and a private chat id is built from the two sorted user ids, so there is one per pair. The server owns the rules that need many objects, like "only friends can message".

How do you deliver a message to people who are online? (Must know)

Observer pattern. Each online user's connection registers a callback. When a message is posted, the server calls the callback of every other member. Offline members see it later in the history. In a real system the callback is a WebSocket write, and with many servers you need a shared channel between them (a topic on a message broker). I only ran the in-process version.

How do you track unread messages? (Senior signal)

A read position per user per chat: the id of the last message they have seen. Unread is the count of messages with a larger id that they did not send themselves. Marking as read just moves the position. That is one number per user per chat, not one flag per message.

A friend request is answered twice. What happens? (Trap)

The first answer moves it from pending to accepted or rejected. The second hits an invalid transition and throws, so a friend cannot be added twice or "un-rejected" by accident. Only the receiver may answer. My test covers both. Whether a rejected user may send a new request later is a product decision; I would ask.

What changes if there are millions of users? (Senior signal)

Then it is a system design problem: connections spread over many servers, messages stored in a database, delivery through a broker, and ordering per chat. The classes stay a useful picture of the domain, but each in-memory map becomes a table or a service. I would say this and point to the system design version rather than stretch the class model.

How would you do the call center? (Senior signal)

Chain of responsibility: operators first, then supervisors, then directors, with a queue when nobody is free. The employee knows whether it can handle a call and escalates; the center re-dispatches and queues. A rank list makes it easy to add a rank. I would also decide up front what happens to a call when an employee becomes free: take the oldest queued call that this rank can handle.

Key takeaways

  • Class-model questions test where you put rules. Give each rule to the class that has the data.
  • A card has no blackjack value. The hand scores, because an ace depends on the rest of the hand.
  • Ace rule: all aces as 1, then add 10 once if it does not bust.
  • Shuffle with Fisher-Yates and inject the random source. Test rounds with stacked decks.
  • Player behaviour is a strategy (a function is enough).
  • Chat: users, friend requests (a tiny state machine), chats with members and messages, and a service that enforces cross-object rules.
  • Delivery to online users is an observer. Unread is a read position per user per chat.
  • Call center is chain of responsibility with a queue.
  • Say what the model leaves out (many servers, ordering, offline delivery) and point to the system design version.

What's next

Next: Lesson 9, Design a Rate Limiter and a Notification Service. Practise: add split to blackjack (a player hand becomes two hands), and write the stacked-deck test first. Notice which classes you must change.

Published by Jainik · Last updated · How these lessons are made

Keep learning

Part of the study guide

Related series