10. Distributed Job Scheduler Advanced + Distributed Systems
A. Realistic Interview Prompt
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
- Recognizing that "the scheduler" is a role that must itself be highly available — a single machine deciding "what's due to run right now" is a single point of failure for the entire system's core function
- Leader election as a concrete mechanism (not just a buzzword) for ensuring exactly one instance is actively making triggering decisions at a time, while the rest stand by ready to take over
- Honest engagement with "exactly-once" execution — recognizing it's unachievable in the strict distributed-systems sense over an unreliable network, and designing instead for effectively-once semantics via idempotency, which is the actual, correct engineering answer
- Designing retry and failure-handling policy (backoff, max attempts, dead-lettering) as a first-class feature, not an afterthought
- Comfort with job dependency graphs (job B should only run after job A succeeds) as a natural, expected extension of a scheduling system
D. Requirements
Functional
- Users can register a job with a schedule — a cron-style recurring expression, or a one-time future timestamp
- The system triggers each job's execution at (or very close to) its scheduled time
- Users can view a job's execution history and status (success, failure, retrying)
- A failed job execution is retried according to a configurable policy
Non-Functional
- High availability of the scheduling function itself — the system must keep triggering jobs on time even if individual machines (including whichever one was "in charge" of triggering) crash
- Effectively-once execution — a job should not be silently skipped, and should not run twice in a way that produces a duplicated real-world side effect (this document's standard illustrative example: a scheduled job that charges a customer's credit card must never double-charge them)
- Timing accuracy — triggering within a small tolerance window (illustratively, a few seconds) of the scheduled time, not "sometime that minute" or later
- Horizontal scalability — supporting a very large number of registered jobs (millions) and a large number of jobs due to fire at the same instant (e.g. everything scheduled for "midnight UTC")
Out of scope: a full workflow/DAG orchestration UI, arbitrary job-code sandboxing/security model, cross-region job migration.
E. Clarifying Questions
| # | Question | Why It Matters | Design Decision Affected |
|---|---|---|---|
| 1 | Is "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 immediately | Deep Dive 3 (O) |
| 2 | Can jobs depend on other jobs completing successfully first? | Turns a flat list of independent triggers into a dependency-graph scheduling problem | Deep Dive 5 (Q) |
| 3 | How 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't | Deep Dive 6 (R) |
| 4 | What 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 recovers | Deep Dive 7 (S) |
| 5 | What'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 time | Capacity Estimation (G) |
F. Assumptions
- Confirmed: cron-style recurring and one-time job registration, on-time triggering, execution history, configurable retries.
- Assumption: effectively-once semantics via idempotency (E's clarifying question 1) — the honest, achievable, and actually-expected answer to this problem, not literal exactly-once delivery.
- Assumption: job dependency graphs are in scope (E's clarifying question 2) — the more complete and commonly-discussed version of this problem.
- Assumption: jobs are lightweight triggers (an HTTP callback or a message enqueued to a downstream system), not arbitrary heavy compute (E's clarifying question 3) — keeps the design focused on scheduling/triggering correctness rather than a general compute-cluster scheduler.
- Assumption: a several-second timing tolerance (E's clarifying question 5) — realistic for the vast majority of real scheduled-job use cases (daily reports, periodic cleanups, reminder emails).
G. Capacity Estimation
| Metric | Value / Calculation | Why It Matters |
|---|---|---|
| Total registered jobs | Tens of millions, illustratively, across a large platform | The 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 instant | The 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 interval | Every 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 size | Schedule expression + target + metadata ≈ ~1KB | Trivially small in isolation — confirms this, like Ticketmaster, is a coordination/correctness problem, not a storage-volume problem |
| Execution history retention | Recent runs per job (e.g. last 30-90 days) for status/debugging visibility | An 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
| Entity | Purpose | Key Fields |
|---|---|---|
Job | A registered, schedulable unit of work | job_id, schedule_expr (cron or one-time timestamp), target (callback URL/queue), depends_on (Q), next_run_at |
Execution | One concrete attempt to run a job at a specific due time | execution_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.
(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
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.
| Mechanism | How | Trade-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 over | Simple, 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 system | More 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 leader | One instance is simply configured as "the" leader, with no election at all | Trivial 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.
- Idempotency keys on every execution — each execution carries a stable, unique key (derived from
job_id+scheduled_for); a job's target is expected to recognize a repeated key and treat a second delivery as a no-op — the exact same idempotent-retry principle as Chat System'sclient_msg_idand Ticketmaster's payment-confirmation key, here protecting against duplicate job side effects instead of duplicate messages or charges. - Leases on claimed executions (H), not permanent locks — a worker that claims an execution holds a short-TTL lease; if the worker crashes mid-execution, the lease simply expires and another worker can claim and retry it, at the cost of a possible duplicate attempt, which idempotency is specifically there to absorb.
- Atomically advancing
next_run_atat trigger time, not after execution completes — the leader marks a recurring job's next occurrence immediately upon triggering the current one, so a slow or failed execution never blocks or duplicates the next scheduled trigger; execution success/failure and scheduling the next run are deliberately decoupled.
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.
(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
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
- V1 — single scheduler process, triggers directly (J): a hard single point of failure with no redundancy possible without risking duplicate triggers.
- V2 — leader election among multiple scheduler instances: exactly one active triggering authority at a time, with automatic failover (M).
- V3 — decoupled trigger queue + separate worker fleet: absorbs the midnight-spike burst and lets execution scale independently of triggering (N, R).
- V4 — idempotency keys + leases instead of claimed literal exactly-once: the correctness fix that makes at-least-once delivery safe in practice (O).
- V5 — per-job configurable retry/backoff + dead-lettering: failures are handled as policy, not left to fail silently or retry forever (P).
- V6 — dependency-event-driven triggering for dependent jobs: extends the model beyond pure time-based scheduling (Q).
- V7 — sharded leader-per-partition scanning: scales the trigger-evaluation path itself for very large job counts (N).
- V8 — per-job missed-run/backfill policy: recovers correctly from extended downtime without a separate code path (S).
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
- The active leader crashes: its lease (M) expires unrenewed and a standby instance acquires it within one lease TTL — a brief gap in triggering, bounded and self-healing, never a permanently missed function.
- A worker crashes mid-execution: its lease on that execution (O) expires, and another worker claims and retries it — the idempotency key ensures a possible duplicate delivery doesn't produce a duplicate real-world side effect.
- A job's target is persistently broken: retries exhaust per its configured policy (P) and the execution is dead-lettered for investigation, rather than retried forever or silently dropped.
- The whole system is down across a scheduled time: the next active leader's scan naturally identifies the job as overdue (S) and applies that job's configured catch-up policy.
- A dependency job fails: the dependent job is never triggered (Q) — no wasted work is performed on data that was never successfully produced upstream.
X. Bottlenecks & Scaling Boundary
| Stage | Actual Bottleneck | Fix Applied |
|---|---|---|
| V1 single scheduler | Hard single point of failure for the entire triggering function | Leader election with automatic failover (M) |
| Single leader triggers everything directly | A midnight-scale trigger burst overwhelms one instance's ability to both scan and execute | Decoupled trigger queue + independently-scaled worker fleet (N, R) |
| One leader scanning the entire job table | Job-count growth eventually makes a single full-table scan too slow to meet the timing NFR | Sharded, partition-scoped leader-per-partition scanning (N) |
| Naive exactly-once assumption | Worker crashes between "action taken" and "recorded" create ambiguity that literal exactly-once can't resolve | At-least-once delivery + idempotency keys (O) |
Y. Design Trade-Offs
| Decision | Option A | Option B | Trade-off |
|---|---|---|---|
| High availability of triggering | Static, manually-designated leader | Lease-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 semantics | Attempt literal exactly-once | At-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 coupling | Leader executes jobs directly | Leader 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 handling | Always backfill every missed occurrence | Per-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 coordination | External 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
- A single always-on scheduler process with no redundancy. A hard single point of failure for the system's entire purpose — the most important mistake to avoid on this problem.
- Running multiple scheduler instances with no coordination between them. "Fixes" the single-point-of-failure problem by directly causing duplicate triggering instead — worse than the problem it was meant to solve.
- Claiming literal exactly-once execution without idempotency. Either quietly relies on the same idempotency mechanism under a different name, or is simply incorrect about what's achievable over a network.
- No retry policy, or unlimited retries with no backoff. Either silently loses failed jobs, or hammers an already-failing target at full speed indefinitely.
- Coupling job triggering and job execution into one tier. Forces the highly-available, low-throughput leader role and the high-throughput worker workload to scale together, when they have fundamentally different scaling needs.
AA. Interviewer Follow-Ups
| Follow-Up | Design Change | Components Changed | Components 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 effect | None — this is exactly what O already handles | Everything 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 succeed | None new — Q already generalizes to this | Triggering, 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 here | Worker 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 buffering | Trigger-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 skew | None structurally — a clock-source/ops concern | Everything else |
AB. Interviewer Evaluation Rubric
- Weak candidate: proposes a single always-on scheduler process with no discussion of what happens if it crashes; claims "exactly once" without any mechanism to actually achieve it.
- Mid-level: recognizes the single scheduler is a point of failure and proposes running more than one, but doesn't identify the duplicate-triggering risk that naively creates, or hasn't designed leader election concretely.
- Senior: proactively designs lease-based leader election; correctly reframes "exactly-once" as at-least-once-plus-idempotency unprompted; decouples triggering from execution via a queue and separate worker fleet.
- Staff+: reasons about scaling the trigger-evaluation path itself (sharded leaders) for very large job counts, not just the worker fleet; designs per-job retry and missed-run/backfill policy as configurable rather than one-size-fits-all; raises dependency graphs and clock-skew/jitter considerations unprompted.
AC. Interview Simulation
AD. Online Solution Comparison
| Source | Approach | Strength | Weakness | Worth Learning |
|---|---|---|---|---|
| Hello Interview | Leader-election-first framing, explicit at-least-once-plus-idempotency reasoning, decoupled trigger/execute tiers | Clearest 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 section | Lighter on dependency-graph (DAG) scheduling as its own deep dive | The "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 scale | Ground-truth confirmation that lease-based leader election for a scheduling controller is proven, widely-deployed production practice, not a hypothetical interview construct | Deeply tied to Kubernetes-specific abstractions (Pods, controllers) that need translating into this problem's more generic terms | Confirms 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 features | The 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 inventions | Airflow'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 here | Confirms per-job configurable catch-up/backfill policy (S) mirrors Airflow's own catchup setting almost exactly |
AE. Interview Cheat Sheet
| Problem | Distributed Job Scheduler |
| Difficulty | Advanced + Distributed Systems (simple mechanics, hard correctness-under-failure) |
| Core requirements | Cron-style/one-time job registration, on-time triggering, execution history, configurable retries, high availability of the triggering function itself |
| Central decision | Lease-based leader election for triggering + at-least-once delivery with idempotency for execution — not literal exactly-once, which isn't achievable |
| Scaling the trigger path | Decoupled trigger queue + independent worker fleet absorbs bursts (e.g. midnight spikes); sharded leader-per-partition scanning for very large job counts |
| Retries | Per-job configurable exponential backoff with a max attempt count, then dead-lettering — never unlimited retries, never silent drops |
| Dependencies | Event-driven triggering off an upstream job's success, not a separate time-based check; cycles rejected at registration |
| Missed runs | Naturally surfaced by the same next_run_at scan a recovering/new leader already performs; catch-up policy is per-job configurable |
| Common mistakes | Single 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-engineer | A full workflow-orchestration UI or arbitrary heavy-compute job sandboxing — name them as extensions/different-scope problems, not something to design from scratch here |