10. Distributed Job Scheduler Advanced + Distributed Systems

A. Realistic Interview Prompt

"Design a distributed job scheduler like cron, but for a large fleet: users register jobs that run on a schedule (or once, at a specific time), and the system guarantees each job actually runs, on time, exactly once, even as machines fail."

Plain cron works fine on one machine because that one machine is both the clock and the executor — there's nothing to coordinate. The moment scheduling has to survive a single machine's failure, "what time should this run" and "did this already run" become distributed-systems questions with no single obviously-correct answer, and the entire design is really about answering those two questions correctly under failure, not about scheduling logic itself.

B. Difficulty

Advanced, distributed-systems-heavy — the core mechanics (a schedule, a queue, workers) are simple; what's hard is guaranteeing a job runs despite crashes, network partitions, and clock skew, without either silently skipping it or running it twice in a way that matters (e.g., double-charging a customer, double-sending an email).

C. What This Problem Tests

D. Requirements

Functional

  1. Users can register a job with a schedule — a cron-style recurring expression, or a one-time future timestamp
  2. The system triggers each job's execution at (or very close to) its scheduled time
  3. Users can view a job's execution history and status (success, failure, retrying)
  4. A failed job execution is retried according to a configurable policy

Non-Functional

Out of scope: a full workflow/DAG orchestration UI, arbitrary job-code sandboxing/security model, cross-region job migration.

E. Clarifying Questions

#QuestionWhy It MattersDesign Decision Affected
1Is "exactly once" a hard, literal requirement, or is "effectively once, assuming idempotent job logic" acceptable?True exactly-once delivery/execution is not achievable over an unreliable network — the honest, correct answer reframes this immediatelyDeep Dive 3 (O)
2Can jobs depend on other jobs completing successfully first?Turns a flat list of independent triggers into a dependency-graph scheduling problemDeep Dive 5 (Q)
3How large can a single job's execution be — a lightweight HTTP callback, or an arbitrary long-running compute task?Long-running/heavy jobs need real resource isolation on the worker fleet; lightweight callbacks don'tDeep Dive 6 (R)
4What should happen if the whole system is down when a scheduled time passes (a missed run)?Determines whether missed runs are simply skipped, or backfilled/caught-up once the system recoversDeep Dive 7 (S)
5What's an acceptable timing tolerance — exact-second precision, or "within a few seconds/a minute" is fine?Directly sets the trigger-polling/evaluation interval and how tightly the leader must track wall-clock timeCapacity Estimation (G)

F. Assumptions

G. Capacity Estimation

MetricValue / CalculationWhy It Matters
Total registered jobsTens of millions, illustratively, across a large platformThe job-definition store must scale to large counts, though each individual definition is tiny (schedule expression + metadata)
Peak simultaneous triggers ("midnight spike")Hundreds of thousands of jobs can share a common schedule (e.g. "run at 00:00 UTC daily") and become due in the same instantThe real scale challenge — an enormous burst of near-simultaneous triggers, not steady-state throughput, echoing Ticketmaster's flash-sale-shaped load rather than a smooth-traffic problem
Trigger evaluation intervalEvery few seconds (per F's timing-tolerance assumption)Sets how frequently the leader (M) must scan for due jobs — too infrequent misses the timing NFR, too frequent wastes evaluation work on jobs nowhere near due
Job definition sizeSchedule expression + target + metadata ≈ ~1KBTrivially small in isolation — confirms this, like Ticketmaster, is a coordination/correctness problem, not a storage-volume problem
Execution history retentionRecent runs per job (e.g. last 30-90 days) for status/debugging visibilityAn append-heavy, time-ordered log — naturally suited to a wide-column or time-series-style store, separate from the job-definition store

H. Core Entities

EntityPurposeKey Fields
JobA registered, schedulable unit of workjob_id, schedule_expr (cron or one-time timestamp), target (callback URL/queue), depends_on (Q), next_run_at
ExecutionOne concrete attempt to run a job at a specific due timeexecution_id, job_id, scheduled_for, status (pending/running/succeeded/failed/retrying), attempt_count
Lease (ephemeral)A worker's claim on one execution, preventing two workers from running the same execution concurrently (O)execution_id, worker_id, lease_expires_at
LeaderLock (ephemeral)The single active leader's claim on the triggering role itself (M)leader_id, term, lease_expires_at

I. API Design

POST   /v1/jobs                    { schedule_expr, target, depends_on? } -> { job_id }
GET    /v1/jobs/{id}/executions    -> [{ execution_id, scheduled_for, status, attempt_count }]
DELETE /v1/jobs/{id}                                                       (unregister)
-- internal, not client-facing:
(leader-only) scan Job store for next_run_at <= now()                      (M, N)
(worker-facing) POST /internal/executions/claim  -> { execution_id, lease } (O)
(worker-facing) POST /internal/executions/{id}/complete { status }         (O, P)

Why the trigger/scan step has no client-facing endpoint at all: "decide what's due to run" is a purely internal system responsibility performed by whichever instance currently holds the leader role (M) — exposing it as an API would imply any caller should be able to trigger job evaluation, which is meaningless; only the internal claim/complete lifecycle needs to be a well-defined contract, since that's what the worker fleet (R) actually interacts with.

J. Basic / Naive High-Level Design (V1)

The simplest thing that satisfies the functional requirement in isolation: one server holds all job definitions in memory (or polls a database), loops every few seconds checking which jobs are due, and directly executes each due job itself.

flowchart LR Loop["Single Scheduler Server
(poll loop)"] -- "every N seconds,
scan for due jobs" --> DB[("Job Store")] Loop -- "execute directly" --> Target["Job's target
(HTTP callback / queue)"]

V1 — a single always-on process. Works fine until that one process crashes.

Why this breaks immediately, not eventually: the single scheduler server is a hard single point of failure for the entire system's core purpose — if it crashes, restarts, or is mid-deploy at the exact moment a job is due, that job simply doesn't run, with no other instance positioned to notice or take over. Worse, if a naive fix is "just run two scheduler instances for redundancy," both instances would independently see the same due job and both trigger it — turning one failure mode (missed jobs) into another (duplicate execution), unless something coordinates which instance is actually allowed to act.

K. Data Model & Store Choice

A relational or strongly-consistent store for job definitions and the leader lock: both next_run_at updates and leader-lock acquisition (M) need atomic conditional writes — the exact same "reach for the storage layer's native atomicity primitive" principle used for Ticketmaster's seat holds and Rate Limiter's counter, here applied to "claim the leader role" and "claim this due job for a worker" instead.

A separate, append-heavy store for execution history: executions are written far more frequently than job definitions change, and are queried mostly by recent time range per job — a natural fit for a wide-column or time-series-oriented store, kept apart from the comparatively small, slowly-changing job-definition table for the same reason Chat System separates its message store from user/conversation metadata.

Indexing jobs by next_run_at: the leader's core operation is "find all jobs due now," which is fundamentally a range scan on next_run_at — this index is the single most performance-critical structure in the whole data model, directly enabling N's scale-out approach.

L. End-to-End Flow — One Scheduled Job, Triggered and Executed

sequenceDiagram participant Leader as Leader Instance (M) participant DB as Job Store (K) participant Queue as Trigger Queue participant W as Worker (R) Leader->>DB: scan for next_run_at <= now() DB-->>Leader: due job(s) Leader->>DB: atomically advance next_run_at (idempotency key, O) Leader->>Queue: enqueue execution W->>Queue: claim execution (lease, O) W->>W: run job / call target alt success W->>DB: mark execution succeeded else failure W->>DB: mark failed, schedule retry (P) end

M. Deep Dive 1 — Leader Election for the Triggering Role

Requirement this addresses: directly fixes V1's fatal flaw (J) — the single idea that makes the triggering function itself highly available without causing duplicate triggers.

MechanismHowTrade-off
Lease-based leader election (chosen default)Multiple scheduler instances race to atomically write a leader-lock record with a short TTL (via the database's conditional-update primitive, or a dedicated coordination service like ZooKeeper/etcd); whoever succeeds is the leader until the lease expires, and must periodically renew it or another instance takes overSimple, uses the same atomic-conditional-write primitive already established for other problems in this document; a brief gap with no active leader is possible right after a crash, bounded by the lease TTL
External consensus service (ZooKeeper, etcd, Consul)Delegate leader election entirely to a purpose-built consensus systemMore robust for very large fleets of candidate leaders, but introduces an additional operational dependency for a coordination problem a simple lease can already solve at this scale
Static, manually-designated leaderOne instance is simply configured as "the" leader, with no election at allTrivial to implement, but reintroduces V1's exact single point of failure — a manual failover process is not high availability

Why only one instance is ever allowed to actively trigger, rather than letting all standby instances also scan-and-trigger "just in case": exactly one active triggering authority is what prevents the duplicate-trigger failure mode named in J — every non-leader instance still runs, watching the lease and ready to take over the instant it expires unrenewed, but deliberately does nothing else while standing by.

N. Deep Dive 2 — Scaling the Trigger Path (The Midnight Spike)

Requirement this addresses: the horizontal-scalability NFR (D) and G's "midnight spike" — hundreds of thousands of jobs sharing a common due time is a fundamentally different load shape than a smooth, evenly-distributed trigger rate.

Why a single leader triggering everything doesn't mean a single leader executing everything: the leader's job is narrowly just to identify due jobs and enqueue their executions (L) — a comparatively cheap operation per job — while the actual execution work (calling each job's target) is handled by an entirely separate, horizontally-scaled worker fleet (R) pulling from a queue. This split is what lets the trigger burst be absorbed: the leader can enqueue hundreds of thousands of executions quickly, and the queue naturally smooths the burst into whatever rate the worker fleet can sustainably process, the same buffering role a message queue plays in Video Streaming's transcoding pipeline.

Sharding the job-scanning work itself, if a single leader's scan becomes the bottleneck: jobs can be partitioned (e.g. by job_id hash range) across multiple concurrently-active leader-per-partition roles, each independently elected (M) and each responsible only for scanning and triggering its own partition — turning one large scan into several smaller, parallel ones without reintroducing any duplicate-trigger risk, since each partition still has exactly one active leader.

O. Deep Dive 3 — Exactly-Once-Ish Execution

Requirement this addresses: the effectively-once NFR (D) — the central technical decision of this entire problem, in the same way overselling-prevention was Ticketmaster's and OT-vs-CRDT was Google Docs's.

Why literal exactly-once is not actually achievable: at some point a worker calls a job's target (an HTTP request, a queue publish) and then must record that it succeeded — if the worker crashes between "the call went through" and "the success was recorded," there is no way to distinguish that from "the call never went through" without asking the target system itself, which most targets can't answer. The honest engineering answer is at-least-once delivery of the trigger, combined with idempotent execution, producing an effectively-once outcome even though the underlying delivery mechanism can occasionally deliver twice.

Why this framing is the strong, senior-level answer rather than a weakness to hide: claiming literal exactly-once execution over a network is either wrong or hiding the same idempotency mechanism under a different name — stating plainly that true exactly-once isn't achievable, and that idempotent effectively-once is the correct and sufficient goal, is a mark of real distributed-systems judgment.

P. Deep Dive 4 — Retries & Failure Handling

Requirement this addresses: the configurable-retry functional requirement (D) — a job execution can fail for reasons ranging from a transient network blip to a permanently broken target, and the two need different handling.

Exponential backoff with a bounded maximum attempt count: a failed execution is retried after a delay that grows with each successive attempt (avoiding a failing target being hammered repeatedly at full speed) up to a configurable attempt limit — standard, well-precedented retry policy, reused conceptually rather than invented fresh.

Dead-lettering after exhausting retries: an execution that fails every allowed attempt moves to a distinct "failed permanently" state, surfaced in execution history (D) for a human to investigate, rather than being retried forever or silently dropped — the same dead-letter-queue pattern common to message-queue-based systems generally.

Why retry policy is per-job configurable, not a single global default: a job that sends a best-effort notification email has very different acceptable-failure tolerance than a job that reconciles financial records — letting each job's owner set its own retry count/backoff respects that difference instead of forcing one policy onto fundamentally different risk profiles.

Q. Deep Dive 5 — Job Dependency Graphs

Requirement this addresses: dependency support (E's clarifying question 2, confirmed in F) — turns a flat set of independently-triggered jobs into a directed-acyclic-graph scheduling problem.

flowchart LR A["Job A
(extract data)"] -- "on success" --> B["Job B
(transform)"] B -- "on success" --> C["Job C
(load)"] A -.->|"on failure"| Skip["B, C not triggered"]

A dependent job's real trigger condition is "upstream succeeded," not just "the clock reached a time."

Why a dependent job is triggered by an upstream completion event, not by its own independent schedule check: a job with a depends_on field isn't evaluated against next_run_at at all during the leader's normal time-based scan (L) — instead, a successful execution completion (O) for its dependency emits an event that makes the dependent job immediately eligible to run, decoupling "is it time" from "is it ready."

Cycle detection at registration time, not at run time: validating the dependency graph is acyclic when a job is created (I) catches a configuration mistake immediately with a clear error, rather than allowing a cycle to silently deadlock a chain of jobs that never actually become eligible to run.

R. Deep Dive 6 — Worker Fleet & Isolation

Requirement this addresses: the actual execution half of the system, decoupled from triggering (N) — a horizontally-scaled fleet of stateless workers pulling executions off the trigger queue.

Why workers are stateless and horizontally scaled independently from the leader/triggering tier: triggering (deciding what's due) and executing (actually calling a job's target) have entirely different scaling shapes — triggering needs exactly one active authority (M) per partition, while execution benefits from as many parallel workers as the queue's backlog demands, with no coordination needed between workers beyond the per-execution lease (O).

Resource isolation between jobs: since F assumes lightweight callback-style jobs rather than arbitrary heavy compute, isolation is comparatively simple (a request timeout per execution, rate limits per job or per target host) — a fundamentally different and harder problem if E's clarifying question 3 had instead confirmed arbitrary long-running compute jobs, which would push this toward a full container-orchestration-style worker design instead.

S. Deep Dive 7 — Missed Runs & Backfill

Requirement this addresses: E's clarifying question 4 — what happens if the whole system (or a given partition's leader lineage) is down when a scheduled time passes.

Why detecting a missed run is just a variant of the normal scan, not a special code path: when a new leader takes over (M) or a recovering system resumes scanning (L), any job whose next_run_at is now in the past is, by definition, both due and late — the same range-scan-on-next_run_at query (K) naturally surfaces it without needing separate missed-run detection logic.

Configurable catch-up policy per job: some jobs should run exactly once to "catch up" (a daily report — running it once late is fine, running it 10 times for 10 missed days is wasteful and wrong), while others genuinely need every missed occurrence backfilled (a per-hour data-aggregation job where each hour's data matters individually) — this is a per-job policy choice (analogous to P's per-job retry policy), not a single global rule.

T. Full Solution at Scale — Final Architecture

flowchart TB subgraph LEADERS [" Scheduler Instances (M) "] direction LR L1["Leader (active)"] L2["Standby"] L3["Standby"] end L1 -- "renews lease" --> LockStore[("Leader Lock (K)")] L1 -- "scan next_run_at <= now (N)" --> JobDB[("Job Store, sharded (K)")] L1 -- "advance next_run_at,
enqueue (O)" --> TrigQ["Trigger Queue"] TrigQ --> W1["Worker"] TrigQ --> W2["Worker"] TrigQ --> W3["Worker"] W1 -- "lease + idempotency key (O)" --> ExecDB[("Execution History (K)")] W1 -- "on failure" --> Retry["Backoff / Dead-letter (P)"] W1 -- "on success" --> DepEvent["Dependency-completion event (Q)"] DepEvent -.-> JobDB

Everything above is the composition of M through S — no new component introduced here.

U. Design Evolution

What stays stable throughout: the register-once, trigger-many-times job contract from I — every later refinement changes how triggering is coordinated, delivered, or recovered, never the basic shape of what a client registers.

V. Consistency Model

The leader-election and job-triggering path (M) requires genuine strong consistency — exactly one instance must be recognized as the active leader at a time, enforced via the same atomic conditional-write primitive used for Ticketmaster's seat holds. Execution delivery itself is deliberately at-least-once, not exactly-once, with idempotency (O) doing the work of making that safe — a close relative of Chat System's "deliver reliably, dedupe on the client" philosophy, here deduping on the job target instead of a chat client. Execution history and job-status visibility (D) can tolerate brief staleness (a status view lagging the true state by a second or two) without any correctness impact, the same "narrow strong-consistency island, eventually-consistent everywhere else" shape used throughout this document.

W. Failure Scenarios

X. Bottlenecks & Scaling Boundary

StageActual BottleneckFix Applied
V1 single schedulerHard single point of failure for the entire triggering functionLeader election with automatic failover (M)
Single leader triggers everything directlyA midnight-scale trigger burst overwhelms one instance's ability to both scan and executeDecoupled trigger queue + independently-scaled worker fleet (N, R)
One leader scanning the entire job tableJob-count growth eventually makes a single full-table scan too slow to meet the timing NFRSharded, partition-scoped leader-per-partition scanning (N)
Naive exactly-once assumptionWorker crashes between "action taken" and "recorded" create ambiguity that literal exactly-once can't resolveAt-least-once delivery + idempotency keys (O)

Y. Design Trade-Offs

DecisionOption AOption BTrade-off
High availability of triggeringStatic, manually-designated leaderLease-based leader election (chosen)A static leader is simple but reintroduces the exact single point of failure this design exists to remove; lease-based election gives automatic, bounded-time failover
Execution delivery semanticsAttempt literal exactly-onceAt-least-once + idempotency (chosen)Literal exactly-once isn't achievable over an unreliable network without asking the target itself; idempotent at-least-once is honest, achievable, and sufficient
Trigger vs. execute couplingLeader executes jobs directlyLeader enqueues, separate worker fleet executes (chosen)Direct execution ties the highly-available, low-throughput leader role to a high-throughput, horizontally-scaled workload it's poorly suited for; decoupling lets each scale on its own terms
Missed-run handlingAlways backfill every missed occurrencePer-job configurable catch-up policy (chosen)Always backfilling is simple but wastes work and can produce wrong results for jobs where only the latest state matters; per-job policy respects that different jobs have different correctness needs
Job coordinationExternal consensus service (ZooKeeper/etcd)Database-native atomic conditional writes (chosen default)A dedicated consensus service is more battle-tested at extreme fleet sizes, but adds an operational dependency this scale doesn't yet require — the same "reach for the storage layer's own primitive first" principle used in Ticketmaster and Rate Limiter

Z. Common Bad Designs

AA. Interviewer Follow-Ups

Follow-UpDesign ChangeComponents ChangedComponents Unchanged
"Two workers somehow both claim the same execution."Directly exercises O's lease mechanism plus idempotency at the target — at most a brief overlap window, absorbed safely rather than causing a real duplicate side effectNone — this is exactly what O already handlesEverything else
"Support job chains where B depends on A, and A depends on nothing — plus a diamond dependency (D depends on both B and C)."Directly exercises Q's DAG model — a job simply lists multiple entries in depends_on, becoming eligible only once all of them succeedNone new — Q already generalizes to thisTriggering, execution, retry logic
"A job needs to run a heavy, long-running batch computation, not a lightweight callback."Revisits F's assumption directly — pushes the worker fleet (R) toward real container-orchestration-style resource isolation and scheduling, a materially harder problem than the lightweight-callback case assumed hereWorker fleet design (R)Leader election, triggering, dependency model
"How do you avoid the midnight spike overwhelming downstream systems that jobs call into?"Add jitter — deliberately spreading otherwise-identical trigger times across a small random window — at the trigger-enqueue step, alongside N's queue-based bufferingTrigger-enqueue step (N)Leader election, execution, retries
"What if the leader's clock is wrong?"Directly the clock-skew concern named in Chat System's ordering deep dive and Rate Limiter's atomicity deep dive — mitigated by using a monotonic, NTP-synced clock source for the leader and treating the timing NFR's tolerance window as accounting for realistic skew, not zero skewNone structurally — a clock-source/ops concernEverything else

AB. Interviewer Evaluation Rubric

AC. Interview Simulation

Interviewer: Design a distributed job scheduler — basically cron, but for a large fleet, with guarantees.
Candidate: The core problem is that a single always-on scheduler process is a single point of failure for the entire triggering function — if it crashes right when a job is due, that job just doesn't run. But you can't naively fix that by running several scheduler instances, because then all of them would see the same due job and all trigger it. I'd use lease-based leader election: instances race to claim a leader lock with a short TTL, and only the current leader is allowed to actually trigger jobs. If it crashes, the lease expires and another instance takes over.
Interviewer: You said "guarantees" — can you guarantee exactly-once execution?
Candidate: Honestly, no — not in the literal sense. At some point a worker calls the job's target and then has to record that it succeeded, and if it crashes in between, you can't tell "it succeeded but wasn't recorded" apart from "it never happened" without asking the target system itself. The correct goal is at-least-once delivery combined with idempotency — every execution carries a stable idempotency key, and the job's target is expected to treat a repeated key as a no-op. That gets you an effectively-once outcome even though the delivery mechanism can occasionally deliver twice.
Interviewer: What happens at midnight when a huge number of jobs are all scheduled for the same instant?
Candidate: The leader's job is just to identify what's due and enqueue it — actually calling each job's target happens on a separate, horizontally-scaled worker fleet pulling off that queue. That split is what absorbs the burst: the leader can enqueue a huge batch quickly, and the queue smooths it into whatever rate the workers can sustainably handle. If the scan itself becomes the bottleneck at very large job counts, I'd shard the jobs by ID and have a separately-elected leader per partition.
Interviewer: How would you support job B only running after job A succeeds?
Candidate: A dependent job wouldn't be evaluated by the normal time-based scan at all — instead, when job A's execution completes successfully, that emits a completion event, and job B becomes eligible to run because of that event rather than because a clock reached a certain time. I'd validate the dependency graph is acyclic at registration time so a bad configuration fails immediately with a clear error, instead of silently deadlocking later.

AD. Online Solution Comparison

SourceApproachStrengthWeaknessWorth Learning
Hello InterviewLeader-election-first framing, explicit at-least-once-plus-idempotency reasoning, decoupled trigger/execute tiersClearest articulation of why literal exactly-once is the wrong goal and idempotent at-least-once is the right one — the core structure adopted for this sectionLighter on dependency-graph (DAG) scheduling as its own deep diveThe "leader only triggers, workers only execute" split reused directly as the spine of M, N, and R
Kubernetes CronJob controller (GitHub)Real, production leader-elected controller pattern (via the Kubernetes API server's own lease objects) driving scheduled job creation at genuine large-fleet scaleGround-truth confirmation that lease-based leader election for a scheduling controller is proven, widely-deployed production practice, not a hypothetical interview constructDeeply tied to Kubernetes-specific abstractions (Pods, controllers) that need translating into this problem's more generic termsConfirms the missed-run/catch-up policy discussion (S) directly — Kubernetes CronJob exposes a real, near-identical concurrencyPolicy/missed-schedule configuration
Apache Airflow (GitHub)Production DAG-based workflow scheduler with dependency graphs, retries, and backfill as first-class, real-world-battle-tested featuresThe strongest real-world precedent specifically for Q's dependency-graph model and S's backfill policy — both are core, well-documented Airflow concepts, not interview inventionsAirflow's scope (a full workflow orchestration platform) is considerably larger than this problem's assumed scope (F) — most of it is deliberately out of scope hereConfirms per-job configurable catch-up/backfill policy (S) mirrors Airflow's own catchup setting almost exactly

AE. Interview Cheat Sheet

ProblemDistributed Job Scheduler
DifficultyAdvanced + Distributed Systems (simple mechanics, hard correctness-under-failure)
Core requirementsCron-style/one-time job registration, on-time triggering, execution history, configurable retries, high availability of the triggering function itself
Central decisionLease-based leader election for triggering + at-least-once delivery with idempotency for execution — not literal exactly-once, which isn't achievable
Scaling the trigger pathDecoupled trigger queue + independent worker fleet absorbs bursts (e.g. midnight spikes); sharded leader-per-partition scanning for very large job counts
RetriesPer-job configurable exponential backoff with a max attempt count, then dead-lettering — never unlimited retries, never silent drops
DependenciesEvent-driven triggering off an upstream job's success, not a separate time-based check; cycles rejected at registration
Missed runsNaturally surfaced by the same next_run_at scan a recovering/new leader already performs; catch-up policy is per-job configurable
Common mistakesSingle scheduler with no redundancy; multiple uncoordinated schedulers causing duplicate triggers; claiming literal exactly-once; coupling triggering and execution into one scaling tier
What NOT to over-engineerA full workflow-orchestration UI or arbitrary heavy-compute job sandboxing — name them as extensions/different-scope problems, not something to design from scratch here