Task manager with TTL expiry and time-travel snapshots

0
Remix

A four-level task manager: per-user task lists first, then TTL-based auto-deletion, then a snapshot read that returns a user's tasks as they stood at any past timestamp.

  • Level 1: add_task(ts, user, name, priority) returns "task1", ...; get_task, update_task, delete_task.
  • Level 2: list_tasks(ts, user): live task names, highest priority first, ties in creation order.
  • Level 3: add_task_with_ttl(ts, user, name, priority, ttl): alive for ts <= t < ts + ttl.
  • Level 4: tasks_at(ts, user, at): what list_tasks would have returned at the past time at.
class TaskManager:
    def add_task(self, ts, user, name, priority): ...
    def get_task(self, ts, task_id): ...
    def update_task(self, ts, task_id, name, priority): ...
    def delete_task(self, ts, task_id): ...

    def list_tasks(self, ts, user): ...

    def add_task_with_ttl(self, ts, user, name, priority, ttl): ...

    def tasks_at(self, ts, user, at): ...
AnthropicOnline assessmentMediumLast asked Aug 2026

What L5 looks like

  • Stores per-task version history from Level 1 so time travel is a binary search, and routes all reads through one "state at t" helper.
  • Treats expiry as part of the state, so it works for past reads with no extra history.
  • Handles same-timestamp writes and explains the memory and time trade-off against snapshot copies.
Interviewer expectations for every level ↓

Approach

The spec used here. Timestamps are integers and grow across calls.

Level Methods Rules
1 add_task(ts, user, name, priority), get_task(ts, task_id), update_task(ts, task_id, name, priority), delete_task(ts, task_id) add_task returns "task1", "task2", .... get_task returns {"name", "priority", "user"} or None. Update and delete return True/False.
2 list_tasks(ts, user) The user's live task names, highest priority first, ties in creation order.
3 add_task_with_ttl(ts, user, name, priority, ttl) The task is alive for ts <= t < ts + ttl, then behaves as deleted everywhere. Updating keeps its expiry.
4 tasks_at(ts, user, at) list_tasks as it would have answered at the past time at, counting updates, deletes and expiries.

Clarify first. Can an expired task be updated back to life (no: it is gone)? Does update_task change the TTL (no)? What does tasks_at return for at before the user existed ([])? Is at always at most ts (yes)?

Design for Level 4 from Level 1: store versions, not just the current state. Each task keeps a history list of (ts, state) where state is (name, priority, expires_at), or None after a delete. Current state is the last entry. Any past state is a binary search on the history. Expiry needs no history entry: "alive at t" is state is not None and t < expires_at, which works for the present and the past alike.

So every write goes through one _record(task, ts, state) helper, and every read through one _state_at(task, t) helper. Levels 1–3 call _state_at(task, ts); Level 4 calls _state_at(task, at). No separate snapshot code.

Why not copy a snapshot per write? A full copy per write is O(tasks) per write and O(writes × tasks) memory. Per-task version lists cost O(1) per write and O(writes) memory, and a time-travel read is O(k log v) for a user with k tasks and v versions each.

Same-timestamp writes. If two writes to one task happen at the same ts, replace the last history entry instead of appending, so the search sees the final state at that time.

Code

from bisect import bisect_right
from itertools import count


class Task:
    def __init__(self, task_id, user, seq):
        self.id, self.user, self.seq = task_id, user, seq
        self.times, self.states = [], []  # parallel lists, times ascending


class TaskManager:
    def __init__(self):
        self.tasks = {}       # task id -> Task
        self.by_user = {}     # user -> [Task] in creation order
        self._seq = count(1)

    # -- helpers -----------------------------------------------------------
    @staticmethod
    def _record(task, ts, state):
        if task.times and task.times[-1] == ts:
            task.states[-1] = state
        else:
            task.times.append(ts)
            task.states.append(state)

    @staticmethod
    def _state_at(task, t):
        i = bisect_right(task.times, t)
        if i == 0:
            return None
        state = task.states[i - 1]
        if state is None or (state[2] is not None and t >= state[2]):
            return None  # deleted or expired
        return state

    def _add(self, ts, user, name, priority, expires_at):
        seq = next(self._seq)
        task = Task(f"task{seq}", user, seq)
        self.tasks[task.id] = task
        self.by_user.setdefault(user, []).append(task)
        self._record(task, ts, (name, priority, expires_at))
        return task.id

    # -- level 1 -----------------------------------------------------------
    def add_task(self, ts, user, name, priority):
        return self._add(ts, user, name, priority, None)

    def get_task(self, ts, task_id):
        task = self.tasks.get(task_id)
        state = task and self._state_at(task, ts)
        if not state:
            return None
        return {"name": state[0], "priority": state[1], "user": task.user}

    def update_task(self, ts, task_id, name, priority):
        task = self.tasks.get(task_id)
        state = task and self._state_at(task, ts)
        if not state:
            return False
        self._record(task, ts, (name, priority, state[2]))
        return True

    def delete_task(self, ts, task_id):
        task = self.tasks.get(task_id)
        if not task or not self._state_at(task, ts):
            return False
        self._record(task, ts, None)
        return True

    # -- level 2 -----------------------------------------------------------
    def list_tasks(self, ts, user):
        return self.tasks_at(ts, user, ts)

    # -- level 3 -----------------------------------------------------------
    def add_task_with_ttl(self, ts, user, name, priority, ttl):
        return self._add(ts, user, name, priority, ts + ttl)

    # -- level 4 -----------------------------------------------------------
    def tasks_at(self, ts, user, at):
        live = []
        for task in self.by_user.get(user, []):
            state = self._state_at(task, at)
            if state:
                live.append((-state[1], task.seq, state[0]))
        return [name for _, _, name in sorted(live)]

Complexity

Let k be a user's task count (ever created) and v the versions of one task.

Operation Time
add, update, delete, get O(log v) (the search), O(1) amortized append
list_tasks, tasks_at O(k log v + k log k)

Space is O(total writes). Long-lived systems compact: versions older than the oldest time anyone may query can be dropped, keeping the last one before that point.

Test cases

from solution import TaskManager


def test_add_get_update_delete():
    m = TaskManager()
    assert m.add_task(1, "ana", "write spec", 2) == "task1"
    assert m.get_task(2, "task1") == {"name": "write spec", "priority": 2, "user": "ana"}
    assert m.update_task(3, "task1", "write spec v2", 5) is True
    assert m.get_task(4, "task1")["priority"] == 5
    assert m.delete_task(5, "task1") is True
    assert m.delete_task(6, "task1") is False
    assert m.get_task(7, "task1") is None
    assert m.update_task(8, "nope", "x", 1) is False
from solution import TaskManager


def test_list_by_priority_then_creation():
    m = TaskManager()
    m.add_task(1, "ana", "a", 1)
    m.add_task(2, "ana", "b", 3)
    m.add_task(3, "ana", "c", 3)
    m.add_task(4, "bo", "z", 9)
    assert m.list_tasks(5, "ana") == ["b", "c", "a"]
    assert m.list_tasks(5, "nobody") == []
from solution import TaskManager


def test_ttl_expiry_boundary():
    m = TaskManager()
    m.add_task_with_ttl(10, "ana", "temp", 1, 5)
    assert m.get_task(14, "task1") is not None
    assert m.get_task(15, "task1") is None
    assert m.list_tasks(15, "ana") == []


def test_update_keeps_expiry_and_expired_cannot_update():
    m = TaskManager()
    m.add_task_with_ttl(10, "ana", "temp", 1, 5)
    assert m.update_task(12, "task1", "temp2", 4) is True
    assert m.get_task(16, "task1") is None
    assert m.update_task(16, "task1", "back", 1) is False
    assert m.delete_task(16, "task1") is False
from solution import TaskManager


def test_time_travel_sees_updates_deletes_and_expiry():
    m = TaskManager()
    m.add_task(1, "ana", "a", 1)
    m.add_task_with_ttl(2, "ana", "b", 2, 10)  # alive [2, 12)
    m.update_task(5, "task1", "a2", 9)
    m.delete_task(8, "task1")
    assert m.tasks_at(20, "ana", 0) == []
    assert m.tasks_at(20, "ana", 3) == ["b", "a"]
    assert m.tasks_at(20, "ana", 5) == ["a2", "b"]
    assert m.tasks_at(20, "ana", 8) == ["b"]
    assert m.tasks_at(20, "ana", 12) == []


def test_two_writes_at_one_timestamp_keep_the_last():
    m = TaskManager()
    m.add_task(1, "ana", "a", 1)
    m.update_task(4, "task1", "x", 1)
    m.update_task(4, "task1", "y", 1)
    assert m.tasks_at(9, "ana", 4) == ["y"]
    assert m.tasks_at(9, "ana", 3) == ["a"]

Follow-ups

  1. Quotas. "A user may have at most N live tasks." Count live tasks at ts before adding. Lazy expiry makes the count need a scan; keep an expiry heap per user, or a live counter decremented when the heap pops due tasks.
  2. Snapshots of the whole system at time t. The same per-task search over every task gives it in O(T log v). If such queries are frequent, a persistent balanced tree keyed by (priority, seq) per user makes each version share structure with the last.
  3. Undo. Undo of the last write is popping the last history entry, which the version list gives for free. Make it a new write (record(previous state)) if history must stay append-only.
  4. Out-of-order writes. If writes can arrive late, insert into the history with bisect.insort and decide whether a late update may change the past answer (usually yes for audit logs, no for user-facing reads).
  5. Storage. In a database this is a versioned table: rows of (task_id, valid_from, valid_to, fields), with an index on (user_id, valid_from). A time-travel query is valid_from <= at AND (valid_to IS NULL OR valid_to > at).

Interviewer expectations

Mid-level (L4)

  • Gets CRUD and sorting right, including tie order and failure returns.
  • Applies the TTL boundary consistently in every read, update and delete.
  • Reaches Level 4 with a working, if slower, approach (for example, replaying a write log up to at).

Senior (L5)

  • Stores per-task version history from Level 1 so time travel is a binary search, and routes all reads through one "state at t" helper.
  • Treats expiry as part of the state, so it works for past reads with no extra history.
  • Handles same-timestamp writes and explains the memory and time trade-off against snapshot copies.

Staff (L6+)

  • Explains compaction and how this maps to a temporal table in a real database.
  • Discusses quotas, undo and out-of-order writes as additions to the same model.
  • Finishes with time left and clear tests per level.

Make it yours

Remix it: write each part yourself, grade it against this answer, then let the coach ask you follow-ups. Practice it with the 4-level tests, run in your browser.