The Schema, and Why Tasks Live in a Database
In one line: the schema is the design in compressed form. Every column exists because some later lesson needs it, and reading it that way tells you what the scheduler can and cannot do.
The schema
| Column | Type | Description |
|---|---|---|
| TaskID | Integer | Uniquely identifies each task |
| UserID | Integer | The ID of the task owner |
| SchedulingType | VarChar | Once, daily, weekly, monthly, or annually |
| TotalAttempts | Integer | The maximum number of retries in case a task execution fails |
| ResourceRequirements | VarChar | Clients specify a resource category — Basic, Regular, or Premium — saved as a string |
| ExecutionCap | Time | The maximum time allowed for task execution. This time starts when a resource is allocated to the task |
| Status | VarChar | Waiting, in progress, done, or failed |
| DelayTolerance | Time | How much delay we can sustain before starting a task |
| ScriptPath | VarChar | The path of the script to execute. The script is a file placed in a file system, made accessible so it can be executed |
Read the columns as answers to scheduling questions
Each column exists because the scheduler has a decision to make:
| Question the scheduler asks | Column |
|---|---|
| Who owns this, for fairness accounting? | UserID |
| Is this a one-off or recurring? | SchedulingType |
| Which machine can hold it? | ResourceRequirements |
| How urgently must it start? | DelayTolerance |
| When do I kill it? | ExecutionCap |
| How many times do I retry before giving up? | TotalAttempts |
| Where is it in its lifecycle? | Status |
| What do I actually run? | ScriptPath |
Two of these carry the most weight. DelayTolerance drives ordering (Lesson 7) and ResourceRequirements drives placement — the two separable jobs from Lesson 3, each with its own column.
Notice also what is absent: no priority field. Priority is derived from delay tolerance rather than stated directly, which is a better design — a user asked to pick "high/medium/low" always picks high, whereas "how long can this wait?" has an answer grounded in the actual requirement.
The execution cap clock starts at allocation, not submission — and that distinction matters
This time starts when a resource is allocated to the task.
Easy to skim, genuinely important. The cap measures execution time, not wall-clock time since submission.
That is correct because the two failures are different. Waiting a long time in a queue is a scheduling problem, covered by DelayTolerance and the bounded-waiting requirement. Running a long time once started is an execution problem — usually a bug, as Lesson 7 notes: an infinite loop.
If the cap ran from submission, a task that queued for an hour would be killed the instant it started, punishing it for the scheduler's own backlog. Two separate clocks for two separate failure modes.
So DelayTolerance bounds waiting and ExecutionCap bounds running, and together they bound total time — which is what the user actually cares about.
SchedulingType makes this a cron system too
Once, daily, weekly, monthly, annually means the scheduler handles recurring tasks, not just one-offs. That is a meaningful expansion of scope.
A recurring task is not one row that runs repeatedly — it is a template that spawns an execution each period, and each execution needs its own status, attempt count, and outcome. So the design implicitly needs a distinction between the task definition and a task run, which this single-table schema does not model.
That gap is worth naming, and it opens the questions interviewers like: what if a daily task is still running when the next one is due — skip, queue, or run concurrently? What if the system was down over a scheduled time — backfill or drop? Those have no universally right answer, and having an opinion is what separates a considered design from a transcribed one.
ScriptPath is the untrusted-code entry point
"The path of a script placed in a file system, made accessible so it can be executed" is the most security-relevant field in the schema, and it is easy to read as mundane plumbing.
The scheduler is being handed arbitrary code to run on shared infrastructure. Everything in Lesson 10 — sandboxing, authentication, performance isolation — exists because of this column.
Two attack surfaces follow immediately. The script content is arbitrary and may be malicious. And the path itself is a reference the scheduler resolves, so path traversal, symlink games, or swapping the file between validation and execution are all live concerns — the classic time-of-check-to-time-of-use problem.
Notice the indirection is deliberate: the schema stores a pointer, not the code. That keeps task rows small and lets large payloads live in a file system or blob store — the same claim check pattern object storage described for oversized messages.
claimed_by and claimed_at are the two fields doing the recovery work. Without them a crashed worker's task is indistinguishable from one that is legitimately still running, and the scheduler has no safe basis for re-dispatching it.
Why store tasks at all?
"Why do we store tasks in a database? Why not push them directly to the queue?"
Four reasons, and durability is only the first
Durability at submission. Lesson 2 required that submitted tasks are never lost, and Lesson 1 established the client was already told the work was accepted. Writing to a persistent store immediately on admission is what makes that promise real. A queue can be durable too, but the database is where the task lives for its whole life, not just until execution.
Reprioritization. Priority depends on conditions that change — time passing shrinks the remaining delay tolerance, load shifts between peak and off-peak. A task in the database can be re-evaluated on every batching pass. Once in the queue, its position is essentially fixed.
Scheduled and recurring tasks. SchedulingType allows a task to be due next month. Holding a month of future tasks in a queue is absurd; holding them in a database and promoting them near execution time is exactly right. Lesson 11 says this directly: tasks "remain in storage until they are pushed to the queue near their execution time."
Cancellation and status. Lesson 2 requires both "remove tasks" and "show task status." Against a database these are a delete and a select. Against a queue, cancelling an arbitrary queued message is awkward at best.
The unifying idea: the queue is a short-term execution buffer; the database is the system of record. Lesson 4's top-K batching is the bridge between them.
Key takeaway
Nine columns, each answering one scheduling question — with DelayTolerance driving ordering and ResourceRequirements driving placement. The execution cap clock starts at allocation, so waiting and running are bounded separately. And tasks live in a database rather than a queue because the database is the system of record — supporting durability, reprioritization, future scheduling, cancellation, and status.
Interview signal by level
| Level | What a strong answer sounds like |
|---|---|
| L4 | "Store task ID, owner, status, and what to run." |
| L5 | Explains the database-before-queue choice: "we persist on submission for durability, and keep tasks there until near execution time so we can reprioritize, cancel, and handle scheduled tasks that aren't due yet." |
| Staff+ | Reads the schema as decisions and spots the gap: "delay tolerance drives ordering and resource requirements drive placement — two separate columns for two separable jobs. Note the execution cap starts at allocation, not submission, so a long queue wait doesn't get the task killed the moment it starts. And SchedulingType implies recurrence, which this single table doesn't really model — you need task definition versus task run, and then decisions about what happens when a daily task is still running as the next is due." |
Next: how tasks are ordered.