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).
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."
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.
Data model
Fit it onto items / column_values. Extend the existing schema; don't invent parallel tables. This is the #1 skill they test.
Write path
REST/GraphQL endpoint, validation, persistence, optimistic UI on the client.
Read path
How the board loads it. Index, pagination, virtualization for big boards.
Real-time
Write β publish to board:{id} Redis channel β websocket fan-out β clients patch the delta (never reload the board).
Concurrency
Two users at once. Optimistic locking (version), fractional index (order), or set-union delta ops (collections). Every E2E question ends here.
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.
- 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. positionis 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.
| Block | The move |
|---|---|
| Ordering | Fractional index on position. Insert between neighbors, no renumber. |
| Real-time sync | Write to DB β publish event to board:{id} Redis channel β all WS servers push to clients β clients patch local state. |
| Concurrency | Optimistic lock (version) for scalars; fractional index for reorder; set-union delta ops for collections. |
| Optimistic UI | Client applies the change immediately, sends the request, reverts on failure. |
| Permissions | Check board/column ACL on write; filter on read; re-verify on WS subscribe. |
| Large boards | Virtualized 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.
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.
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:
- AuthZ β can this user edit this board/column?
- Load column def (type + settings), cached.
- Validate via the type handler (status index exists in settings? people ids valid? number numeric?) β
422on fail. β the "different data type" payoff. - Concurrency check (step 6).
- Persist β upsert
column_values, bump version. - Emit event
cell_changed(async β history + automations + real-time). - 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).
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:
Current cell = [alice, bob]. Two users edit at the same time. Pick a strategy and run it.
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.
7 Β· Scale + tradeoffs (close strong)
column_valuesis enormous (items Γ columns Γ accounts) β sharded byaccount_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.
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 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_idin 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.
a/aa trap, stress test, rebalancing): fractional-indexing-tutorial.htmlPlausible 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.
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."
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)."
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."
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."
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
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.