System Design

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.
Your edge: you've already reasoned about worker pools and scheduled execution. This round is that, framed as event-driven automations. Lead with the event bus, nail the failure patterns, and you're done.

Design framework

Verbalize these beats so the interviewer can follow. Score 2Γ— for leading with failure modes before they ask.

BeatWhat to cover
1 Β· ClarifyWhat triggers? (cell change, status, date) What actions? (notify, webhook, move, create) Scale β€” automations/sec at peak? Latency SLA?
2 Β· Trigger sourceEvery table action already emits a change event (from the E2E round). Automations subscribe to it.
3 Β· Event busKafka. Partition by account_id (isolation) or item_id (ordering). Decouples producers from consumers.
4 Β· Condition evalMatch the event against subscribed automation rules. "When status = Done."
5 Β· Action workersStateless pool, autoscaled on queue depth. One per action type so a slow integration can't block others.
6 Β· Failure toleranceIdempotency, retry+backoff, DLQ, Saga for multi-step. The section they care about most.
7 Β· Scale numbersThroughput 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.

Full walkthrough
Confirmed~45 min
β€Ί

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

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” item/cell change β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Table write │──────event──────────▢│ Event Bus β”‚ Kafka β”‚ (E2E round) β”‚ β”‚ partition β”‚ by account_id β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ by item_id β”‚ (ordering) β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜ β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Trigger/Rule β”‚ match event vs β”‚ Matcher β”‚ subscribed automations β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ "when status = Done" β”‚ enqueue action jobs β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Action Queues β”‚ one per action type β”‚ (notify / webhook / β”‚ β†’ slow integration can't β”‚ move / email) β”‚ block the fast ones β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Action Worker Pool β”‚ stateless, autoscaled β”‚ - executes the action β”‚ on queue depth β”‚ - HMAC-signs webhooks β”‚ β”‚ - acks / retries β”‚ β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ on repeated failure β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ DLQ + dashboard alert β”‚ user can replay β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

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 now for 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.

Automation worker pool
⏰ Delayed queue (waiting for fire-at)
βš™ Workers (executing)
Scheduled
0
Completed
0
Retries
0
DLQ
0

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.
The close: "The table-change event from the feature layer is the single source of truth. Everything downstream β€” automations, webhooks, real-time sync, audit β€” subscribes to that event bus. That's why event-driven is the right backbone: one event, many independent consumers, each failing and retrying in isolation."

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.

Full walkthrough
Plausiblefan-out
β€Ί

The core decision: fan-out on write vs read

StrategyHowCost
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.
HybridPush for normal sources; pull for "celebrity" sources (a board 10k people follow).Best of both β€” the senior answer.
The monday-specific insight: "Most feed sources are low fan-out β€” an item is followed by a handful of people, so I'd push. But a public board followed by thousands is the celebrity problem β€” for those I'd pull at read time and merge. Hybrid by fan-out threshold."

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_at per 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

Table change event ─▢ Feed Fan-out Service β”‚ look up followers of (item/board) β”œβ”€ low fan-out β†’ write feed_entries (push) └─ high fan-out β†’ record event only (pull at read) β”‚ β–Ό per-user WS push β†’ badge count++
Reuses the event bus. The same table-change event that feeds automations also feeds the activity feed β€” one event, many consumers.

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.

Full walkthrough
PlausibleOT vs CRDT
β€Ί
The senior answer in one line: "Match the conflict strategy to the data type β€” don't use one hammer for everything." That single sentence beats most candidates, who reach for OT/CRDT on every field.

Strategy by field type

FieldStrategyWhy
Status / number / date (scalar cell)Versioned last-write-winsAtomic 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.

Failure tolerance β€” the patterns they grade

monday explicitly grades this. Memorize the five patterns and when each applies.

PatternProblem it solvesHow
At-least-once + idempotencyExactly-once across systems is a mythUnique event id as idempotency key; consumer checks "seen this?" in a store before acting, records it in the same txn.
Retry + exponential backoffTransient downstream failure (500, timeout)1m, 5m, 30m, 2h, 12h. Jitter to avoid thundering herd.
Dead Letter QueuePoison messages that always failAfter N retries, move out of the main queue β†’ DLQ. Alert + user dashboard with replay.
Saga / compensationMulti-step automation where step 3 failsEach step has a compensating action that undoes prior steps. No distributed txn needed.
Lease / claimWorker dies mid-taskClaim 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.
Say "exactly-once is a myth" out loud. "I'd do at-least-once delivery with idempotent handlers β€” true exactly-once across a network boundary isn't achievable, so I make re-delivery safe instead." Instant senior signal.

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.

Client ─WS─▢ Edge LB ─▢ WS Server ─writes─▢ DB ─change event─▢ Event Bus β”‚ β”‚ β–Ό β–Ό Redis Pub/Sub ◀────────────────────────── publish board:{id} β”‚ Other WS servers subscribed to board:{id} push the delta to their connected clients
  • 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.

Webhook delivery details
Sub-topic
β€Ί
  • 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-Id header; 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.

Delivery infrastructure
Sub-topic
β€Ί
resolved recipients ─▢ Preference Filter ─▢ Channel Routers β”œβ”€β–Ά Email β†’ SES/Sendgrid β”œβ”€β–Ά Push β†’ FCM/APNs └─▢ In-app β†’ feed store + WS push
  • 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.
The dedup is upstream. Resolving IDs β†’ users and deduping (the coding problem) happens before this stage. Here you assume a clean recipient set and focus on multi-channel delivery + reliability.

Multi-tenancy background

Not a standalone prompt, but the isolation assumption behind everything. Know it cold so you can drop it into any answer.

Multi-tenancy essentials
Background
β€Ί
  • 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

"The table-change event is the source of truth β€” automations, webhooks, real-time, and audit all subscribe to the same bus."
"Exactly-once across a network boundary is a myth β€” I do at-least-once with idempotent handlers keyed on the event id."
"One queue per action type, so a slow integration backs up its own pool without blocking the fast ones."
"Partition by account_id for isolation, or by item_id when I need per-item ordering β€” I'd confirm which matters more here."
"Crashed worker? The lease expires and the task is re-claimed. Idempotency makes the re-run safe."
"After N retries it goes to a DLQ with an alert and a user-facing replay button β€” failures are visible, not silent."

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.