Targeted prep covering the 3 previous rounds (coding, product, system design) plus high-probability new questions based on monday.com's interview patterns and product surface area.
monday.com's process typically runs 4–6 rounds over ~3 weeks. They optimize for thought process > perfect answer.
30 min. Background, motivation, role alignment, comp expectations.
Live coding via CoderPad. Real-world flavored — users, groups, notifications. Data structure choice matters.
Design + code a product feature. Often: board sorting, real-time updates, collaboration, permissions.
Whiteboard. Event-driven, failure tolerance, scaling. Past: automation worker pools.
Behavioral + experience deep dive. Leadership, conflict, technical decisions.
Culture, ambition, long-term fit. Less technical.
Polished solutions for the 3 questions you got before. Master these — they may repeat, or near-variants will come up.
Build a notification subscription system. Users belong to groups. You can subscribe a user or a group to a notification channel. When an event fires for that channel, deliver to every subscriber — exactly once, even if they're subscribed both directly and through multiple groups.
class NotificationSystem: def __init__(self): self.group_members = {} # group_id -> set[user_id] self.user_groups = {} # user_id -> set[group_id] (reverse index) self.channel_user_subs = {} # channel -> set[user_id] self.channel_group_subs = {} # channel -> set[group_id] def add_user_to_group(self, user, group): self.group_members.setdefault(group, set()).add(user) self.user_groups.setdefault(user, set()).add(group) def subscribe_user(self, user, channel): self.channel_user_subs.setdefault(channel, set()).add(user) def subscribe_group(self, group, channel): self.channel_group_subs.setdefault(channel, set()).add(group) def notify(self, channel, message): recipients = set(self.channel_user_subs.get(channel, set())) for group in self.channel_group_subs.get(channel, set()): recipients |= self.group_members.get(group, set()) for user in recipients: self._deliver(user, message) return len(recipients)
channel_user_blocks set per channel. Subtract during notify."A user shouldn't get the same notification twice within 5 minutes." Add an LRU + TTL per (user, message_hash) before dispatch.
If items are stored as position: 1, 2, 3, 4, 5 and a user moves item #5 to between #1 and #2, you'd have to renumber every item in between. With concurrent users, that's a write storm and a sync nightmare.
Store positions as strings (or arbitrary-precision fractions). To insert between A and B, generate a new value lexicographically between A and B. No rebalancing of neighbors needed.
def midpoint(a, b): # Returns a string strictly between a and b (lex order) # Both are lowercase a-z. Empty string = start of range. i = 0 result = [] while True: ca = ord(a[i]) if i < len(a) else ord('a') - 1 cb = ord(b[i]) if i < len(b) else ord('z') + 1 if ca == cb: result.append(chr(ca)) i += 1 continue if cb - ca > 1: mid = (ca + cb) // 2 result.append(chr(mid)) return "".join(result) # cb - ca == 1: must extend a's branch result.append(chr(ca)) i += 1 # Now extend with halfway char of a's next position to 'z'+1 while True: ca_next = ord(a[i]) if i < len(a) else ord('a') - 1 if ca_next < ord('z'): mid = (ca_next + ord('z') + 1) // 2 result.append(chr(mid)) return "".join(result) result.append(chr(ca_next)) i += 1
class Board: def insert(self, item, before=None, after=None): # before/after are item ids or None (start/end of board) a = self._pos_of(after) or "" b = self._pos_of(before) or "{" # char after 'z' item.position = midpoint(a, b) self.items.append(item) def sorted_items(self): return sorted(self.items, key=lambda x: x.position)
ORDER BY position + index on position string. O(log n) inserts, O(n log n) sorted reads (with index: O(n) sequential).midpoint() correctly handling the "no room" case.Users set up automations: "When status changes to Done, in 2 hours send a Slack message." Tens of millions of pending tasks. Each task has a fire-at timestamp. System must execute reliably, exactly once, with bounded latency.
| Component | Option | Why |
|---|---|---|
| Delayed queue | Redis ZSET keyed by ts | ZRANGEBYSCORE pulls due. Fast, simple. |
| Delayed queue | DB with index on fire_at | Durable. Slower polling. Easier idempotency. |
| Work queue | Kafka per action type | Isolate slow integrations from fast ones. |
| Worker | Stateless, horizontally scaled | Scale on queue depth. |
| Idempotency | Task id + dedup table | Workers may retry — must be safe. |
status=processing, leased_until=now+5m, worker_id=X. Other dispatchers skip. If lease expires (worker crashed), task becomes claimable again.def dispatch_loop(): while True: now = time.time() due = redis.zrangebyscore("tasks", 0, now, start=0, num=100) for task_id in due: # atomic lease: only one dispatcher wins if redis.set(f"lease:{task_id}", worker_id, nx=True, ex=300): task = db.get(task_id) if task.status == "pending": queue.publish(task.action_type, task) redis.zrem("tasks", task_id) sleep(0.5)
Patterns frequently seen at monday.com: data structure choice, real-world product framing, follow-ups that extend the problem.
Stream of events (user_id, action, item_id, timestamp). Group consecutive events from the same user on the same item within 5 minutes into one feed entry. "Vikas updated Status 3 times" instead of 3 lines.
(user_id, item_id) -> current_groupclass FeedCollapser: WINDOW = 300 def __init__(self): self.active = {} # key -> group def add(self, user, item, action, ts): key = (user, item) g = self.active.get(key) if g and ts - g.last_ts <= self.WINDOW: g.actions.append(action); g.last_ts = ts else: if g: self._emit(g) self.active[key] = Group(user, item, [action], ts, ts)
Workspaces contain boards. Boards contain items. Users have roles on workspaces and boards. Items can be private, restricted to a group, or inherit. Implement can_view(user, item).
def can_view(user, item): for node in [item, item.board, item.board.workspace]: decision = node.acl.decide(user) if decision in (ALLOW, DENY): return decision == ALLOW if not node.inherits: return False return False # default deny
Implement a sliding window rate limiter: max N requests per user per minute. In-memory first, then discuss distributed.
from collections import deque class RateLimiter: def __init__(self, limit, window): self.limit, self.window = limit, window self.hits = {} # user -> deque[ts] def allow(self, user, now): q = self.hits.setdefault(user, deque()) while q and q[0] <= now - self.window: q.popleft() if len(q) >= self.limit: return False q.append(now); return True
Distributed: Redis sorted set per user. ZADD on hit. ZREMRANGEBYSCORE old. ZCARD for count. All in a Lua script for atomicity.
O(1) get/put. Doubly linked list + hashmap. Move accessed nodes to head. Evict from tail.
class Node: def __init__(self, k, v): self.k, self.v = k, v self.prev = self.next = None class LRU: def __init__(self, cap): self.cap = cap; self.map = {} self.head, self.tail = Node(0,0), Node(0,0) self.head.next = self.tail; self.tail.prev = self.head def _remove(self, n): n.prev.next, n.next.prev = n.next, n.prev def _add_front(self, n): n.next = self.head.next; n.prev = self.head self.head.next.prev = n; self.head.next = n def get(self, k): if k not in self.map: return -1 n = self.map[k]; self._remove(n); self._add_front(n) return n.v def put(self, k, v): if k in self.map: self._remove(self.map[k]) n = Node(k, v); self.map[k] = n; self._add_front(n) if len(self.map) > self.cap: lru = self.tail.prev; self._remove(lru); del self.map[lru.k]
Formula columns can reference other columns. Detect cycles when a new formula is added. If no cycle, return evaluation order.
def topo(graph): # node -> [deps] state = {n: 0 for n in graph} # 0=new 1=visiting 2=done order = [] def dfs(n): if state[n] == 1: raise CycleError(n) if state[n] == 2: return state[n] = 1 for d in graph[n]: dfs(d) state[n] = 2; order.append(n) for n in graph: dfs(n) return order
Given comment text and a user/group directory, return distinct user_ids to notify. Watch for @group expanding to multiple users, and dedup.
This is a near-cousin of Q1. If they ask you something string-flavored about users/groups, default to: set union for resolution, set for dedup.
Stream of activity events. Periodically return top K items by event count in the last hour. Sliding window of counts per item + min-heap of size K.
Mention Count-Min Sketch if scale is huge — approximate counts with bounded error.
Design + code a user-facing feature. The fractional indexing question was here. Expect another collaboration-flavored or interaction-heavy problem.
Show other users' cursors/selections live as they navigate the same board. Sub-100ms latency, handle 50+ users.
{user_id, x, y, selection} every ~50ms when moving. Throttle.board:N:users. On disconnect → SREM. Heartbeat every 30s.User can filter by status, owner, date range; group by any column. Up to 10k items per board.
{
groups: [
{ key: "Done", count: 42, items: [...first 50...] },
{ key: "In Progress", count: 17, items: [...] },
],
total: 59
}
Server returns counts + first N per group. Expanding a group fetches more.
Cmd+Z reverts the last action. Cmd+Shift+Z redoes.
Command with do() and undo().undoStack, redoStack.Typeahead search across all items the user can see. Sub-200ms.
permissions: [user_ids, group_ids] for ACL filtering at query time.Two options: filter at query time (ES terms filter on user's group_ids) or filter at index time per-user (explosion). Pick query-time.
Sidebar shows pinned boards (user can reorder via drag) and recent boards (auto-updated by visit).
GET /sidebar returns both lists in one round-trip.Select 200 items, change status. Show optimistic UI + handle partial failures.
POST /items/bulk_update {ids: [...], changes}.[{id, ok, error}].Whiteboard. Focus on event-driven patterns, failure modes, scaling. monday.com's own guide says they want to see your thought process and tradeoff reasoning.
When an item changes, fire webhooks to subscribed third-party URLs. monday.com publicly says this is a typical interview question.
board:N.board:N.last_event_id. Server replays missed events from Redis stream (or DB).Email, push, in-app. Millions of users. Different preferences per user.
monday.com is shared with row-level isolation by account_id. Always include in queries and indexes.
Every change recorded: who, what, when, before/after. Massive write volume. Read patterns: per-item history, per-user activity, search.
Have STAR-format stories ready. monday.com values ownership, customer empathy, and being a builder.
STAR template:
For senior: show you considered organizational impact, not just code.
Show: respectful disagreement, data-driven argument, willingness to commit after the decision. Avoid stories where you "won" by force.
Walk through: detection, mitigation (rollback? feature flag?), root cause, postmortem, action items. Emphasize blameless culture.
Strong answers anchor on: (1) their work-OS vision & building flexible primitives, (2) the engineering challenges of real-time multi-tenant collaboration, (3) something specific about their public engineering blog or open-source.
Weak answer: "I want to work at a fast-growing company." Avoid.
Because you're a returning candidate, this is almost certain. Frame as growth:
Good ones:
Check items off as you go. Persists across reloads (localStorage).
midpoint() for fractional indexing and test edge cases