End-to-End Feature Design

The E2E Round

Take a real board feature and build it client β†’ server β†’ DB, collaboratively. They feed you table info as you go. Grounded in research: only two E2E features appear in candidate reports β€” row ordering (you've seen it) and cell value change (your live target).

The round format confirmed

monday's culture: "engineers propose features and build them end-to-end β€” ideation to post-deploy, their own QA, talking to customers." The interview mirrors that.

  • It is NOT FAANG scale design. No "Twitter at 500M QPS." It's "here's our schema, build this feature on it."
  • Collaborative. They hand you table structure mid-conversation (exactly like the sorting question gave you the board table).
  • They grade thought process > the answer. Break the problem down, weigh tradeoffs, communicate. Their words.
  • Two axes they always probe: the data layer ("how does this fit the schema?") and the client contract ("how do other viewers see it live?" β†’ websockets).
Since you've already done sorting: they won't replay it. Cell value change is your #1. But the confirmed pool is tiny, so a reusable framework (next) is your real safety net if they pick something novel.

The reusable spine

Run this on ANY feature they name. Memorize the seven beats β€” it turns "I don't know this feature" into "I have a process."

1

Clarify

What exactly, who uses it, board size (10 items or 100k?), real-time needed, permissions, history in scope. Spend 2-3 min here β€” it's graded.

2

Data model

Fit it onto items / column_values. Extend the existing schema; don't invent parallel tables. This is the #1 skill they test.

3

Write path

REST/GraphQL endpoint, validation, persistence, optimistic UI on the client.

4

Read path

How the board loads it. Index, pagination, virtualization for big boards.

5

Real-time

Write β†’ publish to board:{id} Redis channel β†’ websocket fan-out β†’ clients patch the delta (never reload the board).

6

Concurrency

Two users at once. Optimistic locking (version), fractional index (order), or set-union delta ops (collections). Every E2E question ends here.

7

Scale & tradeoffs

Big boards, sharding by account, what you'd revisit. Name the option you rejected.

Data model reference card

Ground every answer here. Reciting this is what makes you sound like you understand their product.

accounts ──< boards ──< groups ──< items ──< column_values └──< columns β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ boards (id, account_id, name) groups (id, board_id, title, color, position) ← the colored sections items (id, board_id, group_id, name, position) ← the rows columns (id, board_id, type, title, settings JSON, position) ← column DEFINITIONS column_values (item_id, column_id, value JSON, version, updated_by, updated_at) ← the CELLS
  • Cells are EAV. Keyed by (item_id, column_id), value stored as JSON β€” because each of 30+ column types has a different shape. This flexibility is monday's whole product.
  • position is a fractional index on items (within a group) and columns. Insert between neighbors, no renumber.
  • Sharded by account_id β€” every query carries it; one account's data lives together.
  • Real-time = a websocket channel per board (board:{id}), backed by Redis pub/sub.
  • Value JSON shapes:
text:    {"text": "hello"}
number:  {"number": 42.5}
status:  {"index": 3}                       // references column.settings for label/color
date:    {"date": "2026-05-28"}
people:  {"persons": [{"id": 1}, {"id": 2}]}   // ← the multi-item list (the trap)

The recurring building blocks

Almost every feature is a combination of these six. Master them once, reuse everywhere.

BlockThe move
OrderingFractional index on position. Insert between neighbors, no renumber.
Real-time syncWrite to DB β†’ publish event to board:{id} Redis channel β†’ all WS servers push to clients β†’ clients patch local state.
ConcurrencyOptimistic lock (version) for scalars; fractional index for reorder; set-union delta ops for collections.
Optimistic UIClient applies the change immediately, sends the request, reverts on failure.
PermissionsCheck board/column ACL on write; filter on read; re-verify on WS subscribe.
Large boardsVirtualized list + windowed/paginated fetch + server-side filter on an index.

Cell value change β˜… your live target

The one confirmed E2E question left. There's a clear junior-vs-senior gap, and it lives almost entirely in how you handle the multi-item (people) column under concurrent edits.

Full walkthrough
Confirmed~35-45 min
β€Ί

The prompt (verbatim from reports)

"In the monday app you have boards. Each board has columns and cells. Design what should happen when a cell value is changed. Each column represents a different data type. Describe what the API should look like. Explain how to update a cell with a multi-item list, and what happens when multiple users make changes in parallel."

1 Β· Clarify (don't design yet)

  • "Which column types are in scope?" β€” text, number, status, date, people… each has a different value shape + validation. Asking this signals you see the core challenge.
  • "Single cell or batch?" β†’ start single.
  • "Do other viewers see it live?" β†’ yes, confirms websockets.
  • "Permissions β€” board-level, or column-level locks?"
  • "History/audit in scope?" β†’ monday has cell history; clarify now-or-follow-up.
  • "Scale β€” biggest board, concurrent editors?" β†’ 100k+ items; tens of concurrent editors.

2 Β· Data model β€” lead here

The cell is (item_id, column_id) β†’ value JSON, version. "Each column is a different type" β†’ the value shape varies by column.type, and validation differs per type.

The senior framing: "I'd model each column type as a handler β€” a ColumnTypeHandler interface with validate(value, settings) and serialize(). The write path stays type-agnostic; adding a new column type is a new handler, not a change to the update logic. Open/closed principle." Then: "Values are JSON so we get 30+ flexible types β€” the cost is we can't query values with plain SQL, so filtering is solved by a separate denormalized index."

3 Β· Write path (API)

PATCH /boards/{b}/items/{i}/columns/{c}
Body:  { "value": <type-specific>, "expected_version": 7 }

Server steps:

  1. AuthZ β€” can this user edit this board/column?
  2. Load column def (type + settings), cached.
  3. Validate via the type handler (status index exists in settings? people ids valid? number numeric?) β†’ 422 on fail. ← the "different data type" payoff.
  4. Concurrency check (step 6).
  5. Persist β€” upsert column_values, bump version.
  6. Emit event cell_changed (async β†’ history + automations + real-time).
  7. Return new { value, version }.

"monday's real public API is GraphQL (change_column_value); I'm using REST here for clarity β€” same shape."

4 Β· Read path

Board loads a page of items + their cells. PK index on (item_id, column_id); items indexed by (board_id, group_id, position). Virtualize the UI for big boards β€” fetch only visible rows.

5 Β· Real-time

After persist, publish to board:{id} Redis pub/sub:

{ "type": "cell_changed", item_id, column_id, value, version, updated_by }

Every WS server subscribed pushes to its connected clients β†’ clients patch the single cell. The editor already applied it optimistically and ignores their own echo (match a client op-id or updated_by).

"I broadcast the delta β€” one cell β€” never the whole board. The same event feeds the automations engine, which is the bridge to the system-design round."

6 Β· Concurrency β€” the heart of the question

Case A β€” two users edit the same scalar cell (text/number/status/date)

Last-write-wins is acceptable; the value is atomic. Optimistic locking via version lets you detect it: B wrote from v3 but it's now v4 β†’ 409 β†’ client retries or surfaces. monday leans LWW + fast sync for simple types.

Case B β€” the multi-item (people) column ⚠️ the trap

This is what the question is fishing for. The people value is a list. Whole-value LWW loses concurrent adds:

Live: whole-list LWW vs delta ops

Current cell = [alice, bob]. Two users edit at the same time. Pick a strategy and run it.

Strategy:
User A sends
User B sends
Final cell on server

The fix β€” model the operation as a delta, not a whole-value replace:

POST   /items/{i}/columns/{c}/people   { "add": ["carol"] }
DELETE /items/{i}/columns/{c}/people/{dave}

Server applies the delta to the current set β†’ current βˆͺ {carol} and current βˆͺ {dave} both land β†’ [alice, bob, carol, dave]. Nothing lost.

The answer they want: "For scalar columns LWW is fine. For collection columns like people, whole-list replacement loses concurrent adds β€” so I expose add/remove delta ops. That's set semantics: concurrent adds commute, they're order-independent. For a concurrent add+remove of the same person I'd pick add-wins. Essentially a CRDT OR-Set." The question literally asks about the multi-item list under parallel edits β€” this is the target, not LWW.

7 Β· Scale + tradeoffs (close strong)

  • column_values is enormous (items Γ— columns Γ— accounts) β†’ sharded by account_id, indexed on (item_id, column_id).
  • Filtering by value (e.g. "status = Done") can't use the JSON directly β†’ separate search/index service fed by the change events.
  • Tradeoffs to name: JSON value (flexible, 30+ types) vs normalized (queryable but rigid) β€” they chose flexible, solved querying separately. LWW (simple) vs delta ops (needed for collections). Optimistic UI (instant) vs authoritative server (correct) β€” do both.
The 3 things that separate you from a junior answer: (1) column-type handler, not a giant switch; (2) the people-column delta-ops insight β€” the centerpiece; (3) EAV tradeoff awareness β€” JSON value means querying is solved separately.

Row ordering you've seen it

They almost certainly won't replay this β€” but they might push a deeper variant ("now make it work for millions of rows") to see if you've grown. Have the sharper version ready.

The deeper version (in case they revisit)
Already asked
β€Ί

The 30-second core

Store items.position as a fractional index. Reorder = compute a position between the two neighbors at the drop point. Only the moved row is written β€” O(1), no renumbering. Broadcast the one item's new position on board:{id}; clients re-sort the affected group locally.

What "deeper" looks like β€” the concurrency + growth story

  • Two users drag different items into the same gap: both compute the same midpoint β†’ collision. Resolve with server-side jitter (random suffix), tie-break on item_id, or accept equal positions and break ties by id at sort time.
  • String growth: repeated inserts at the same spot grow the key (~log₆₂(2) chars/insert). Add a background rebalance job triggered when any key exceeds ~50 chars.
  • Cross-group moves: also update group_id in the same write.
  • Millions of rows: reorder is still O(1); reads use the (board_id, group_id, position) index; client virtualizes. The fractional index is exactly what makes "millions + moveable" tractable.
Full interactive deep-dive (midpoint calculator, the a/aa trap, stress test, rebalancing): fractional-indexing-tutorial.html

Plausible novel features speculation

None of these appear in any report β€” they're educated guesses about what monday might pick from their own product if they go off-script (likely, because you've seen sorting). Each tests the same muscles as cell-change. One key insight each.

Subitems (nested rows)
Speculative
β€Ί

Data model: subitems are just items on a hidden child board, linked via parent_item_id (a self-reference / connect). The interesting part: summary columns on the parent that roll up children (sum, count, % done) β€” do you compute on read (join, always fresh, slower) or denormalize onto the parent (fast read, must invalidate on every child change)? Real-time: a child cell change must also push a parent-summary update. Verbalize: "I'd denormalize the rollup with the child-change event recomputing it, since reads vastly outnumber writes."

Updates / comments + @mentions
Speculative
β€Ί

Data model: updates(id, item_id, author_id, body, created_at) β€” a feed per item, not a cell. The interesting part: parsing @mentions β†’ resolving users/teams β†’ fan-out notifications (this is literally your coding-round notification problem reused). Real-time: new update broadcasts to everyone viewing the item; notification goes to mentioned users even if offline (persisted inbox). Verbalize: "Mentions reuse the set-union dedup from the notification problem; delivery is at-least-once with an idempotency key per (update, user)."

Mirror / connect columns (cross-board)
Speculative Β· hardest
β€Ί

The hard one. A mirror column shows a value that lives on a different board's item. The interesting part: keeping the mirror fresh when the source changes β€” join on read (always correct, expensive, cross-shard) vs denormalize + invalidate via the source's change event (fast read, eventual consistency, fan-out cost). Cross-shard because boards shard by account but a connect can cross boards. Verbalize: "I'd subscribe the mirror to the source item's change events and denormalize, accepting eventual consistency β€” a stale mirror for a few hundred ms is fine; a cross-shard join on every board load is not."

Add a column / column types
Speculative
β€Ί

Data model: insert a row in columns with a fractional position. The interesting part: existing items have no cell for the new column β€” and that's fine. EAV means a missing column_values row = empty cell. You don't backfill millions of rows; you treat absent as default. Verbalize: "Adding a column is O(1) β€” one row in columns. The EAV model means existing items just have no value row yet; absent = empty. That's the payoff of the flexible schema."

Board filter / saved view
Speculative
β€Ί

The interesting part: client-side vs server-side by board size β€” <500 items filter on the client (instant); >10k filter server-side against the denormalized index (the JSON value isn't directly queryable). A saved view is just data: views(board_id, filter JSON, sort JSON). Verbalize: "Filtering on column values needs the search index, not the EAV table β€” same index that makes status/people queryable."

Phrases that signal senior

"Let me clarify scope before I design β€” which column types, board size, and do other viewers need this live?"
"This fits the existing column_values table β€” I'd extend the schema, not add a parallel one."
"Each column type is a handler with validate + serialize β€” adding a type is a new handler, not a change to the write path."
"I broadcast the delta on the board's channel, never reload the whole board."
"For scalar cells LWW is fine; for the people column I'd use add/remove delta ops so concurrent adds commute β€” that's an OR-Set."
"Optimistic UI locally, server validates, real-time corrects β€” the user feels instant, the server stays authoritative."
"JSON values give flexibility but aren't SQL-queryable, so filtering goes through a separate denormalized index fed by change events."

Common mistakes

  • Designing before clarifying. Jumping into boxes loses points. Ask the 5-6 scoping questions first.
  • Inventing parallel tables. Extend items/column_values. New tables for everything signals you don't get the EAV model.
  • Whole-list LWW on the people column. The single biggest trap. Concurrent adds get silently lost. Use delta ops.
  • A giant switch on column type in the write path. Use a handler per type.
  • Reloading the board on every change. Broadcast the one-cell delta.
  • Forgetting real-time entirely. It's monday β€” other viewers must see changes live. Websocket + Redis pub/sub.
  • Over-engineering scale. They want correctness + tradeoffs, not Cassandra. PostgreSQL + Redis + a search index is the right palette.
  • Not naming the rejected option. "I chose X over Y because Z" is the senior pattern in every tradeoff.