Covered (all majors):
run_at) — majorOne gap:
I'd still write the explicit list in the panel — an interviewer expects to hear it stated up front. Paste-ready:
Functional: schedule one-time tasks; recurring tasks (interval/cron); view/edit/cancel tasks; notify on completion/failure; task priority. Non-Functional: high availability & fault tolerance; low latency & minimal jitter; horizontal scalability; idempotency/dedup.
List the key non-functional requirements (performance, scalability, reliability, etc.)...
Assumptions (state these — they drive everything)
Storage
Read/Query load
Recurring tasks
Cache / Queue sizing
Create one-time task (major)
POST /tasks { "target": { "type": "http", "url": "...", "method": "POST" }, "payload": {...}, "run_at": "2025-06-01T14:00:00Z" } → 201 { "task_id": "...", "status": "PENDING" }
Create recurring task (major)
POST /recurring-tasks { "target": {...}, "interval": "300s" | "cron": "0 9 * * *", "start_at": "...", "max_runs": 100 } → 201 { "recurring_task_id": "...", "next_run_at": "..." }
Get task status (minor)
GET /tasks/{id} → { "status": "PENDING|RUNNING|SUCCESS|FAILED|CANCELLED", "attempts": 2, "error": "...", "timestamps": {...} }
Cancel / pause (minor)
POST /tasks/{id}/cancel POST /recurring-tasks/{id}/pause
Health & metrics (minor) — GET /health, GET /metrics (queue depth, exec rate, failure rate).
The end-to-end flow the interviewer wants you to narrate:
run_at is near-future, pushes it into a Redis sorted set.next_run_at for recurring tasks.score <= now()), atomically claims each one (mark RUNNING), and hands it to a worker via a queue.Core entities
id, type, payload, status (pending/running/done/failed), run_at, attempts, max_retries, created_atid, interval/cron, next_run_at, last_run_at, max_runs. Each firing generates a concrete Tasks row.task_id, started_at, finished_at, result, error, attempt1. The Dispatcher / Task Claiming (most important)
UPDATE tasks SET status='RUNNING' WHERE id=? AND status='PENDING' compare-and-swap pattern (or SELECT ... FOR UPDATE, or an advisory lock)max_attempts2. The Timing Wheel / Redis Sorted Set
run_atscore <= now() for O(log N) lookup → sub-second dispatch without hammering the DB3. Recurring Task Engine
next_run_at + interval (not now() + interval) to avoid driftnext_run_at so two schedulers don't create duplicate runs