OOD / 05
Design a call center
Three agent tiers: Respondent → Manager → Director. A call arrives. The first available agent who can handle it takes it. If they can't, escalate. If nobody's free, queue. The interview tests how you model "who can take this" without nesting if statements ten levels deep.
1 · Clarifying questions
| Tiers? | Three: Respondent (handles routine), Manager (handles escalations), Director (handles VIP / unresolved). |
| How is escalation decided? | Either the call carries a "category" (handle-by-tier-2 from the start) or the respondent escalates manually after talking to the caller. |
| What if all agents in a tier are busy? | Try the next tier up. If everyone is busy, the call goes into a queue with a tier preference. |
| Languages, skills? | Yes — agents have skills (e.g., Spanish-speaker). Skill-based routing in extensions section. |
| SLA? | P90 answer time < 2 minutes. Drives the queue prioritisation. |
2 · Entities
Caller— name, language preference, customer tier (regular / VIP).Call— the in-flight unit. Caller, started_at, current handler, current tier.Employee(abstract) →Respondent,Manager,Director. Each hashandle(call)andescalate(call).CallCenter— the dispatcher. Knows agents per tier, owns the queue.
3 · Class sketch (Chain of Responsibility)
enum Tier: RESPONDENT, MANAGER, DIRECTOR
class Employee(ABC):
id: str
tier: Tier
available: bool = True
current_call: Call | None = None
def assign(call):
self.current_call = call
self.available = False
def release():
self.current_call = None
self.available = True
def escalate():
# Subclass overrides if it can escalate further.
...
class Respondent(Employee):
tier = RESPONDENT
def escalate(): self.current_call.center.route_to(MANAGER, self.current_call)
class Manager(Employee):
tier = MANAGER
def escalate(): self.current_call.center.route_to(DIRECTOR, self.current_call)
class Director(Employee):
tier = DIRECTOR
def escalate(): raise NoFurtherEscalation() # nothing above
class CallCenter:
agents_by_tier: dict[Tier, list[Employee]]
queue: PriorityQueue[Call] # priority by VIP + wait time
lock: Lock
def dispatch(call):
with lock:
for tier in [call.starting_tier, *upper_tiers(call.starting_tier)]:
agent = first_available(self.agents_by_tier[tier])
if agent:
agent.assign(call)
return
# nobody free
self.queue.put(call)
def route_to(tier, call):
with lock:
agent = first_available(self.agents_by_tier[tier])
if agent:
agent.assign(call)
else:
self.queue.put(call)
def on_release(agent):
with lock:
next_call = self.queue.get_if_matches_tier(agent.tier)
if next_call: agent.assign(next_call)The pattern: each tier knows how to escalate to the next. The CallCenter holds the dispatch logic and the queue. No nested if — the tiers form a chain.
4 · The call state machine
INCOMING ──► RINGING ──► IN_PROGRESS ──► ESCALATING ──► RINGING (next tier)
│ │ │
▼ ▼ ▼
ABANDONED COMPLETED (loop or COMPLETED)Edge cases land at the transitions. ABANDONED means the caller hung up while ringing. ESCALATING is a brief state — the call leaves one agent before reaching the next.
5 · Edge cases
- Director escalates. No tier above; either re-queue with high priority or hand off to an external system. Don't crash.
- Agent goes offline mid-call. Mark the call as needing a different agent; re-dispatch. Don't lose it.
- Queue full. Set a hard cap; over the cap, the caller hears "we'll call you back" and is logged for callback.
- Priority inversion. A VIP arrives while a long queue of regulars is waiting. Priority queue handles it — regulars wait longer; SLO measured per-priority.
- Skill mismatch. Spanish-only caller, no Spanish-speaking agent free. Queue with skill tag; ignore agents lacking the skill.
6 · Concurrency
A coarse lock around the dispatcher is fine for an interview. In real production:
- Per-tier queue + per-tier dispatcher thread; agents pull from the queue when they become free.
- An events bus (agent_released, call_arrived) drives the assignment, so the dispatcher is reactive.
- The queue's "pick the right one for this agent" needs index by skill + priority — a heap keyed by composite score.
7 · Extensions
- Skill-based routing. Agents have
skills: set[Skill]. Calls haverequired_skills. Match before dispatch; queue by skill bucket. - Outbound campaigns. The same dispatch infrastructure runs in reverse — pick a callable list, find an available agent, place the call.
- Predictive dialler. Estimate when the next agent will free up and pre-dial the next caller. Common in collections / sales.
- Quality monitoring. Recorded calls, supervisor whisper, post-call surveys. Hangs off the same call entity.
8 · What gets graded
- Did you recognise the Chain of Responsibility?
- Did the dispatcher logic stay in one place, not duplicated across tiers?
- Did you model the queue with priority/skill awareness rather than FIFO-of-everything?
- Did you handle "nobody free" and "Director escalates" cleanly?
Found this useful?