CASE STUDY

To-Do List Service (REST API and Real-Time Shared Lists)

4 min read·727 words·Beginner

How to use this case study

SDE-2 / Mid

Design clean REST APIs (resources, status codes, pagination), the data model, authentication and basic caching and rate limiting.

SDE-3 / Senior

Add API versioning, optimistic concurrency with ETags, and real-time sync of shared lists over WebSockets, including reordering.

Staff / Principal

Discuss conflict resolution for offline edits, scaling fan-out for shared lists, and evolving the API without breaking clients.


0) Problem Restatement

Design a to-do list service. Users create lists and tasks, then update, complete, delete and reorder them. Two versions of this appear in interviews:

  • Microsoft: a clean backend with CRUD APIs (Create, Read, Update, Delete), plus authentication, rate limiting, caching and API versioning.
  • Roblox: a shared list where several people edit at the same time and see changes quickly, with sensible behavior for conflicting edits and offline changes.

Asked at: Microsoft, Roblox — 2 candidate reports between Jun 2026 and Aug 2026.

1) Requirements

1.1 Functional

  • Lists: create, rename, delete and share with others.
  • Tasks: create, edit (title, due date, notes), complete or uncomplete, delete, and reorder.
  • See changes from collaborators in near real time.

1.2 Non-Functional

  • Simple, predictable APIs.
  • Secure: users see only their own or shared lists.
  • Fast (under 100 ms reads), highly available.
  • Shared edits converge. Nobody's change silently disappears.

1.3 Scale Estimates

  • 10M users, 100M tasks. Reads: 5K/sec, writes: 500/sec. This is modest scale, so correctness and API design matter most.


2) API Design (REST)

POST   /v1/lists                         { name }                    → 201 { list }
GET    /v1/lists?cursor=&limit=50                                    → 200 { items, next_cursor }
GET    /v1/lists/{list_id}/tasks?status=open&cursor=
POST   /v1/lists/{list_id}/tasks         { title, due_at?, after_task_id? } → 201 { task }
PATCH  /v1/tasks/{task_id}               { title?, completed?, due_at? }   (header If-Match: "v7")
POST   /v1/tasks/{task_id}/move          { after_task_id }
DELETE /v1/tasks/{task_id}                                           → 204
POST   /v1/lists/{list_id}/members       { user_id, role: editor|viewer }

Good practices to mention:

  • Nouns for resources and HTTP verbs for actions. Use PATCH for partial updates.
  • Status codes: 201 created, 204 no content, 400 bad input, 401 not logged in, 403 not allowed, 404 not found, 409 conflict, 412 precondition failed (stale ETag), 429 too many requests.
  • Cursor pagination instead of page numbers (stable when items are added).
  • Idempotency-Key header on POST, so a retried create doesn't make two tasks.
  • Versioning: put the version in the URL (/v1/), keep v1 working while v2 exists, only add fields in minor changes, and announce deprecations early.


3) High-Level Architecture

Architecture Diagram

flowchart LR
    C["Web / Mobile"] --> GW["API Gateway - auth, rate limit"]
    GW --> API["Todo Service"]
    API --> DB[("Postgres - lists, tasks")]
    API --> CA[("Redis cache")]
    API --> PS[("Pub/Sub - channel per list")]
    PS --> WS["WebSocket servers"]
    WS --> C
  • API Gateway: checks the auth token (OAuth/JWT) and applies rate limits per user (e.g., 100 requests/min).
  • Todo Service: stateless and horizontally scalable.
  • Postgres: lists, tasks and memberships. Every query filters by lists the user can access.
  • Redis: caches list contents (invalidated on writes).
  • Pub/Sub + WebSockets: push changes to everyone viewing a shared list.


4) Data Model

CREATE TABLE lists  (list_id UUID PRIMARY KEY, owner_id UUID, name TEXT, updated_at TIMESTAMP);
CREATE TABLE list_members (list_id UUID, user_id UUID, role TEXT, PRIMARY KEY (list_id, user_id));
CREATE TABLE tasks (
  task_id UUID PRIMARY KEY, list_id UUID, title TEXT, notes TEXT,
  completed BOOLEAN DEFAULT FALSE, due_at TIMESTAMP,
  rank TEXT,                 -- fractional index for ordering
  version INT DEFAULT 1,     -- for optimistic concurrency (ETag)
  updated_by UUID, updated_at TIMESTAMP, deleted BOOLEAN DEFAULT FALSE
);
CREATE INDEX ON tasks (list_id, rank);

5) Deep Dive A — Concurrent edits

  • Optimistic concurrency with ETags: a GET returns ETag: "v7". A PATCH sends If-Match: "v7". If someone else changed the task (now v8), the server returns 412, and the client refreshes and retries or shows a merge prompt.
  • Field-level merging is friendlier: if A changes the title and B marks it complete, both changes can apply, since they touch different fields. Only same-field edits conflict, and then last write wins with a notice.
  • Reordering: store a fractional rank (see the playlist design). A move updates only one row, so concurrent moves of different tasks never conflict.
  • Deletes: soft delete (deleted = true) for a short time, so undo works and offline clients can see that the task was deleted.


6) Deep Dive B — Real-time sync and offline

  • Every successful write publishes { list_id, task, version } to the list's channel. Clients viewing that list apply it. Clients ignore events older than the version they already have.
  • Reconnect: the client sends its last seen change timestamp or sequence number, and the server returns changes since then (GET /v1/lists/{id}/changes?since=...).
  • Offline: the app queues edits locally (with task IDs created on the client as UUIDs) and replays them on reconnect. Conflicts are handled as above.


7) Caching and Rate Limiting

  • Cache a list's tasks in Redis by list_id, and delete the cache key on any write to that list.
  • Use HTTP caching for GETs with ETags (If-None-Match → 304 Not Modified) to save bandwidth.
  • Rate limit per user and per IP at the gateway (token bucket). Return 429 with Retry-After.


8) Wrap-Up

Design resource-based REST APIs with proper status codes, cursor pagination, idempotency keys and URL versioning, behind a gateway that handles auth and rate limits. Store tasks in Postgres with a fractional rank for ordering and a version for optimistic concurrency (ETags), and cache per list. For shared lists, publish every change to a per-list channel pushed over WebSockets, and let clients catch up with a "changes since" API after reconnecting or working offline.

More Case Studies

Practice with a Mock Interview

Apply what you learned in a live system design mock interview with our AI interviewer.

Start System Design Interview →