The System Design Round
Exactly one prompt is confirmed in reports: "How can external apps be triggered by an action on the table?" β the automations engine, graded on event-driven architecture + failure tolerance. Below it: three plausible systems (my inference from monday's product, clearly flagged) as insurance if they go off-script.
The round format confirmed
From Blind + Glassdoor reports: round 3 is system design about their system β "how external apps can be triggered by an action on the table, with emphasis on event-driven architecture and failure tolerance."
- Domain-specific, not generic. It's monday's automations/integrations ("when status changes β notify Slack / call a webhook / move item"), not "design YouTube."
- They grade tradeoffs & thought process, not a perfect diagram. Break it down, weigh options, communicate.
- Failure tolerance is the recurring theme. Expect: "what if a worker dies mid-task? what if the external API returns 500? what if the queue is down?" Have an answer for each.
Design framework
Verbalize these beats so the interviewer can follow. Score 2Γ for leading with failure modes before they ask.
| Beat | What to cover |
|---|---|
| 1 Β· Clarify | What triggers? (cell change, status, date) What actions? (notify, webhook, move, create) Scale β automations/sec at peak? Latency SLA? |
| 2 Β· Trigger source | Every table action already emits a change event (from the E2E round). Automations subscribe to it. |
| 3 Β· Event bus | Kafka. Partition by account_id (isolation) or item_id (ordering). Decouples producers from consumers. |
| 4 Β· Condition eval | Match the event against subscribed automation rules. "When status = Done." |
| 5 Β· Action workers | Stateless pool, autoscaled on queue depth. One per action type so a slow integration can't block others. |
| 6 Β· Failure tolerance | Idempotency, retry+backoff, DLQ, Saga for multi-step. The section they care about most. |
| 7 Β· Scale numbers | Throughput estimate, worker count, shard count. Show you can size it. |
Automations / external triggers β confirmed
The prompt: "How can external apps be triggered by an action on the table?" Build the engine that runs "when X happens, do Y" reliably.
1 Β· Clarify
- What triggers an automation? (cell change, status change, date arrives, item created, sub-item added)
- What actions? (send notification, call external webhook, create/move item, send email)
- Is there a delay? ("in 2 hours, do X" β scheduled execution)
- Scale β how many automations fire/sec at peak? Latency expectation?
- Exactly-once or at-least-once acceptable? (Spoiler: at-least-once + idempotent.)
2 Β· Architecture
3 Β· Scheduled automations ("in 2 hours, do X")
For delayed actions, add a delayed queue between the matcher and the action queue:
- Redis ZSET scored by fire-at timestamp; a dispatcher polls
ZRANGEBYSCORE 0 nowfor due jobs. - Dispatcher claims a due job with an atomic lease (
SET NX EX) so two dispatchers don't double-fire. Crashed worker β lease expires β job becomes claimable again. - Cancellation: if the user edits/disables the automation before fire time, mark the job cancelled; worker checks status before executing.
4 Β· Worker pool simulator
Schedule automations, watch them flow through the pool. Toggle a 30% failure rate to see retries β DLQ.
5 Β· Scale numbers (throw these out)
- 10M active automations, avg 1h delay β steady ~2,700 fires/sec.
- Monday 9am peak: 10Γ β ~27k fires/sec.
- Worker doing an external HTTP call: ~50 RPS each β ~600 workers at peak.
- Redis ZSET: ~50k ops/sec per shard β shard 5β10Γ for headroom.
Other questions β plausible, not confirmed my inference
Be honest with yourself about these. After deep research, the automations question above is the only system design prompt that appears in candidate reports. The three below are not in any report β they're the systems monday would plausibly pick from their own product, each chosen because it exercises a different muscle. Prep them as insurance, not as predictions. (Two things you might expect here are deliberately absent: "notify users" is a coding question you've already done, and "board ordering" is the E2E question you've seen.)
Activity feed / Notifications tab plausible
monday's Notifications tab is a single feed across all boards: items assigned to you, @mentions, updates on items you follow, status changes. Classic fan-out design β the most likely "real" system design pick if they go beyond automations.
The core decision: fan-out on write vs read
| Strategy | How | Cost |
|---|---|---|
| Fan-out on write (push) | When an event happens, write a feed entry into each relevant user's feed up front. | Fast reads, expensive writes, storage heavy. Good default. |
| Fan-out on read (pull) | Store events once; assemble each user's feed at read time from their subscriptions. | Cheap writes, expensive reads. Good for high-fan-out sources. |
| Hybrid | Push for normal sources; pull for "celebrity" sources (a board 10k people follow). | Best of both β the senior answer. |
Data model
feed_entries (user_id, event_id, type, source_ref, created_at, read_at) // per-user, push follows (user_id, target_type, target_id) // item/board subscriptions events (id, account_id, type, actor_id, payload, created_at) // the source log, pull
- Subscriptions: assignment + @mention auto-follow the item; explicit "follow" too.
- Read/unread:
read_atper entry; badge count = unread count (cached, decremented on read). - Real-time: new entry pushes via the
user:{id}websocket channel β badge updates instantly. - Ranking: chronological is fine for monday (no algorithmic ranking like Twitter β say this, it shows you scoped correctly).
- Retention: cap feed at N recent or trim older than 90 days.
Generation path
Real-time collaborative editing plausible
Multiple users editing the same board at once β cells, item names, long-text. The deepest algorithm discussion, and a direct extension of the cell-change concurrency from your E2E prep.
Strategy by field type
| Field | Strategy | Why |
|---|---|---|
| Status / number / date (scalar cell) | Versioned last-write-wins | Atomic value; full OT/CRDT is overkill. Optimistic lock detects conflict. |
| People column (collection) | OR-Set CRDT (add/remove delta ops) | Concurrent adds must commute β the people-column trap from E2E. |
| Item name / long-text (free text) | OT or a text CRDT (e.g. Yjs) | Char-level concurrent editing needs real merge β this is where OT/CRDT earns its keep. |
OT vs CRDT β know the tradeoff
- Operational Transformation (OT): transform concurrent ops against each other (Google Docs). Needs a central server + careful transform functions. Compact on the wire.
- CRDT: data structures that merge deterministically with no central authority. Easier to reason about, works offline/P2P, but more memory/metadata.
- For monday's mostly-structured data: you rarely need full OT β versioned LWW + OR-Sets cover most columns. Reserve a text CRDT for genuine free-text fields.
Supporting pieces
- Presence: who's viewing/editing + live cursors β ephemeral, Redis +
board:{id}websocket, never persisted. - Sync transport: ops broadcast on the board channel; clients apply + ack; reconnect replays from
last_op_id. - Server authority: server validates + orders ops; clients are optimistic and reconcile.
Search across boards plausible
Typeahead search over every item the user can see, sub-200ms. Ties back to the EAV problem β JSON column values aren't SQL-queryable, so search and filtering both go through the same index.
- Index: each item is a doc β name + flattened column values +
account_id+board_id+permissions. - Permission filter at query time: include the user's accessible board ids (or group ids) as an ES
termsfilter. Never fetch-then-filter. - Async indexing: the same change event updates ES β eventual consistency, a second of lag is fine for search.
- The EAV tie-in: JSON cell values can't be queried in SQL, so this index is also how board filtering ("status = Done") works β one index, two features.
- Client UX: debounce ~150ms, cancel in-flight on new keystroke (AbortController), cache recent queries.
- Multi-tenant: shard the index by
account_id(or filter on it); big accounts get dedicated index shards.
Failure tolerance β the patterns they grade
monday explicitly grades this. Memorize the five patterns and when each applies.
| Pattern | Problem it solves | How |
|---|---|---|
| At-least-once + idempotency | Exactly-once across systems is a myth | Unique event id as idempotency key; consumer checks "seen this?" in a store before acting, records it in the same txn. |
| Retry + exponential backoff | Transient downstream failure (500, timeout) | 1m, 5m, 30m, 2h, 12h. Jitter to avoid thundering herd. |
| Dead Letter Queue | Poison messages that always fail | After N retries, move out of the main queue β DLQ. Alert + user dashboard with replay. |
| Saga / compensation | Multi-step automation where step 3 fails | Each step has a compensating action that undoes prior steps. No distributed txn needed. |
| Lease / claim | Worker dies mid-task | Claim with SET NX EX; lease expires β task reclaimable. No task lost, none double-run (with idempotency). |
The questions they WILL ask β have the answer ready
- "What if a worker dies mid-task?" β Lease expires, task is re-claimed by another worker. Idempotency key means re-running is safe.
- "What if the external API returns 500?" β Retry with backoff; after N fails β DLQ + alert. Per-integration circuit breaker so one bad endpoint doesn't burn workers.
- "What if the queue is down?" β Producers buffer locally / the event bus (Kafka) is the durable log; consumers resume from offset on recovery. No events lost.
- "What if the same event is delivered twice?" β Idempotency key dedups; the action runs once.
- "What if a slow integration backs up?" β Per-action-type queues isolate it; autoscale that pool; the fast integrations are unaffected.
Real-time board updates
The cross-over with E2E. If they steer toward "how do all viewers see a change live," this is the answer β and it shares the same event bus as automations.
- Channel per board (
board:{id}). Subscribe on board open; verify permission on subscribe. - Broadcast deltas, not full boards.
- Catch-up after disconnect: client reconnects with
last_event_id; server replays missed events from a Redis stream or the DB. - Conflict resolution: last-write-wins per cell for scalars; set-union for collections (the people-column story from E2E).
- Connection scale: 100k+ concurrent β stateless WS servers + shared Redis; a broker library (Centrifugo / Socket.IO adapter) handles fan-out.
Webhook delivery sub-topic of automations
"External apps triggered by table actions" often drills into delivering webhooks to third-party URLs. Not a separate confirmed prompt, but a natural extension of the automations question.
- Ordering: webhooks for the same item should arrive in order β partition Kafka by
item_id. - Slow consumers: one slow customer URL can't block others β per-subscription queues or worker pools partitioned by URL.
- Security: HMAC-SHA256 sign the payload; customer verifies. Reject SSRF (no
127.0.0.1, no cloud metadata endpoints). - Retry/DLQ: exp backoff; after 5 fails β DLQ; user dashboard shows failures with a Replay button.
- Idempotency: send
X-Event-Idheader; instruct subscribers to dedup.
Notification delivery plausible sub-topic
Don't confuse this with the coding round. The notify(IDs, message) dedup algorithm was your coding question β already done. This is the infrastructure side: how a notification, once its recipients are resolved, gets delivered across channels reliably. It's a common automation action and a fair sub-topic, not a standalone confirmed prompt.
- Preference filter: per-user channel settings, quiet hours, mute-this-board.
- Throttle: max 1 email/user/hour β Redis token bucket.
- Digest mode: batch per user, cron rolls up, send one daily.
- Idempotency: hash of
(user, event_id)checked before send β at-least-once stays safe. - In-app channel writes to the activity-feed store (see above) + pushes the badge update.
Multi-tenancy background
Not a standalone prompt, but the isolation assumption behind everything. Know it cold so you can drop it into any answer.
- Shared DB, row-level isolation by
account_idβ monday's model. Every table carries it; it's first in composite indexes. - Sharded by account. All of an account's data lives together; a lookup service maps
account_id β shard. Big accounts β dedicated shard. - Noisy neighbor: per-account rate limits, connection-pool quotas, query timeouts. Partition automation queues by account so one tenant can't starve others.
- Tenant-aware everything: caches keyed by
(account_id, β¦); events carry account; never JOIN across tenants in OLTP.
Phrases that signal senior
Common mistakes
- Direct service-to-service calls. They grade event-driven. Publish events; let consumers react. Don't have the table write call the Slack API directly.
- Claiming exactly-once. Say it's a myth; design at-least-once + idempotent.
- Ignoring failure modes until asked. Lead with them. It's literally what they grade.
- One shared queue for all actions. A slow webhook starves notifications. Per-action-type queues.
- FAANG-scale over-engineering. Kafka + Redis + workers + Postgres is the right palette. No need for exotic stores.
- No idempotency key. Retries + at-least-once delivery without it = double-fired automations. Always key on event id.
- Forgetting cancellation. Scheduled automations can be edited/disabled before they fire β check status before executing.
- Not sizing it. Throw out throughput + worker-count numbers; shows you can reason about scale.