System design interview
Design a collaborative whiteboard
Per-property merging, live cursors, tiles, snapshots and offline edits.
Press Play to follow a request through the design, or step through it.
- Client and edge
- Service
- Data store
- Cache and queue
- Alice drags a sticky note. Her app moves it at once and sends one operation: shape 41, new x and y, stamped clock 87 from client A.
- The gateway forwards it to the board server that owns this board.
- The server keeps each property only if its stamp is newer than the stored one, then appends the operation to the log.
- It sends the accepted operation back through the gateway to everyone on the board, with its log position.
- Alice's app marks the operation as acknowledged. Bob's app applies it, and the note slides to its new place.
- Board app
- Gateway: WebSocket
- Board directory: board → server
- Board server: merges, orders ops
- Cursor relay: cursors, not stored
- Snapshot store: tiles by area
- Op log: append-only
On this page
In 2 minutes
I would give each board one owner, a board server chosen through a directory with a lease, so that all operations on a board meet in one place. Apps connect over WebSockets through a gateway. They apply every edit locally at once and send it as an operation that sets properties on a shape. Each property carries a stamp, a logical clock plus the client ID, and the newest stamp wins for each property separately.
So edits to different properties both survive, and replays and offline merges are safe. Stacking order uses order keys that sort between neighbours, and deletes are tombstones. The server appends operations to a log, acknowledges after the append is durable, and writes snapshots split into tiles. Opening a large board loads the latest snapshot tiles under the window, about 40 KB instead of 4 MB, and replays the short log after it.
Cursors and drag previews use a separate lossy channel, capped at 20 updates a second, never acknowledged and never stored, because in our estimate they are 10 times heavier than durable operations. The estimate gives about 100 board servers for 100,000 active boards. If a server dies, its lease expires, another server loads the snapshot and log, and apps resend unacknowledged operations.
Step 1 of 6: Clarify the scope
Interviewer asks
Design a shared whiteboard with an infinite canvas. Many people place and move shapes, sticky notes and arrows at the same time, see each other's cursors, and come back to the board later. Everyone should end up with the same board, and no edit should be lost without a reason.
Questions to ask first
Ask these before you draw anything. Under each one is why its answer changes the design.
- How many people edit one board at once?
Ten people is simple. A hundred makes cursor traffic the main cost, because every cursor goes to every other person.
- How many shapes can a board hold?
A board of 20,000 shapes cannot be sent whole to everyone who opens it. It has to load by region.
- Is freehand pen drawing included?
A stroke is one shape with hundreds of points, drawn at a high rate, so it needs its own handling.
- Do people edit offline and merge later?
Offline edits must be merged by rules, not by the order in which they reach a server.
- Is there undo, and does it cover only my own edits?
Undoing one person's own changes means keeping each person's operations apart, not rolling the whole board back.
Requirements
Functional
- Create, move, resize, recolor, edit and delete shapes, sticky notes, text and arrows.
- Show every change to everyone on the board within a fraction of a second.
- Show other people's cursors, selections and what they are dragging.
- Keep the stacking order of shapes, and let people bring a shape to the front or send it back.
- Save the board and reopen it later, quickly, even when it is large.
Non-functional
- A change appears for the other people within 150 ms at p95 in the same region.
- No acknowledged edit is lost, even if a board server crashes.
- Opening a board with 20,000 shapes shows the first screen in under 2 seconds.
- Cursor updates are capped at 20 a second for each person.
- Handles 200,000 durable operations and 2 million cursor updates a second coming in, at peak.
Step 2 of 6: Size it
The durable stream is moderate, but cursors are 10 times heavier and carry nothing worth saving. Send them on their own lossy channel. Big boards must load by region, because 4 MB for every viewer on every open would be slow and wasteful.
Assumed for the exercise, not facts about a real system:
- 100,000 boards are open at the busiest time, with 4 people on each, so 400,000 people.
- Each person produces 0.5 durable operations a second on average, because a drag is sent as a few updates and one final one. An operation takes about 100 bytes.
- A moving cursor sends 5 updates a second on average, and each update goes to the 3 other people on the board.
- A shape takes about 200 bytes. One board server holds 1,000 active boards.
- Durable operations per second
- 400,000 people × 0.5
- 200,000/s
- Cursor messages per second
- 400,000 × 5 = 2M in; 2M × 3 others = 6M out
- 2M/s in and 6M/s out, 10 times the operation traffic (200,000 in, 600,000 out)
- Operation log growth
- 200,000 × 100 B = 20 MB/s; × 86,400 s
- ≈ 1.7 TB a day if kept raw, so compact it
- Board servers
- 100,000 boards ÷ 1,000 per server
- 100 servers
- A large board
- 20,000 shapes × 200 B; a screen shows about 200 shapes × 200 B
- 4 MB for the whole board, 40 KB for the viewport
Step 3 of 6: Sketch the path of one request
Step through the diagram at the top of the page to watch a request move through these parts.
Each person's Board app keeps a WebSocket open to a Gateway. The gateway asks the Board directory which Board server owns the board, and forwards the person's changes there. A board has one owner at a time, so all its operations meet in one place and get one order.
The app applies every change to its own copy at once, so dragging feels instant, and sends an operation: set these properties of this shape. The board server merges it with a simple rule, the newest value of each property wins, appends it to the Op log and broadcasts it. Now and then it writes the board to the Snapshot store, split into tiles by area of the canvas.
Cursors and 'currently dragging' previews take a second path, through the Cursor relay. They are small, frequent and useless a moment later, so they are capped in rate, sent without acknowledgement and never stored.
Other paths through the system
Open a big board
- Bob opens a board of 20,000 shapes. His app sends the board ID and the area of the canvas in his window.
- The gateway asks the directory which server owns the board.
- The directory answers: server 12. If no server holds the board, it picks one and records it with a lease.
- The gateway joins Bob to the board on server 12, naming the tiles his window covers.
- If the board is not in memory, the server loads the latest snapshot, split into tiles, with the log position it covers.
- It replays the operations logged after that position, at most a few thousand.
- The server sends Bob only the shapes in his tiles, about 200 shapes or 40 KB, and subscribes him to changes there.
- Bob sees the first screen in under 2 seconds. Other tiles load as he pans.
Show a cursor
- Alice moves her mouse. The app sends her pointer position and her selection, at most 20 times a second.
- The gateway passes it to the cursor relay, which keeps only the latest position of each person.
- The relay returns the latest positions of everyone else who can see that part of the board.
- Bob sees Alice's cursor and her dragging preview. A position with no update for 5 seconds disappears.
Step 4 of 6: Go deep on the hard part
Merging edits shape by shape
Interviewer asks
Two people change the same shape at the same moment. How does everyone end up with the same board?
A shape is a map of properties: x, y, width, height, color, text, rotation, order and a deleted flag.
Read moreShow less: Merging edits shape by shape
An edit says 'set these properties on this shape'. Each stored value carries a stamp: a logical clock number and the ID of the client that wrote it.
The logical clock is a counter on each client. When a client makes an edit it uses its counter plus one. When it receives any operation, it sets its counter to the larger of its own and the one it saw. So an edit made after seeing another always has a higher clock. Two edits made without seeing each other may tie, and the client ID breaks the tie. Stamps compare as a pair: clock first, then client ID.
The rule is last writer wins, for each property separately. An incoming value replaces the stored one only if its stamp is higher. The same set of operations gives the same board in any order, and a repeated operation changes nothing, so replays, retries and offline merges are safe. If Alice resizes a note while Bob recolors it, both edits survive, because they touch different properties. If both drag it, one position wins on every screen, and the loser sees the note jump. That loss is acceptable on a whiteboard.
Stacking order. If the order were an array index, two people sending shapes to the front at once would clash. Give each shape an order key instead, a value that sorts between its neighbours. Between 0.40 and 0.50 a new shape can take 0.45. In practice keys are short strings that sort in order, so precision never runs out. Two people who pick the same key are ordered by shape ID. Moving a shape in the stack is one property change on one shape, so no other shape is rewritten. After many inserts at one spot keys grow long, and the server rebalances them in a quiet moment.
Deletes. Deleting sets a deleted flag with a stamp. This is a tombstone, and it is just another property, so it merges like the rest. A later edit to a deleted shape is kept but hidden, and undoing the delete is a newer write of the flag. Compaction drops a tombstone once every connected client has passed it and a retention period, such as 30 days, is over.
Text inside a note is one property here, so two people typing in the same note at once overwrite each other. For long text, use a text-merging technique like the one in the collaborative editor problem, for that property only.
Two channels: edits and cursors
Interviewer asks
What do you send reliably and store, and what do you send fast and forget?
Our estimate shows that the two kinds of traffic differ by a factor of 10.
Read moreShow less: Two channels: edits and cursors
Durable operations are 200,000 a second in and 600,000 out, while cursors are 2 million in and 6 million out. They need different treatment.
The durable channel. Operations go to the board server, which merges them, appends them to the log, acknowledges, and broadcasts. The app keeps each operation until it is acknowledged, and resends after a reconnect. Each carries a client ID and a sequence number, so the server drops duplicates. The acknowledgement is sent only after the append is durable.
The ephemeral channel. Cursor positions, selections and drag previews go to the cursor relay. The rules:
- Cap each person at 20 updates a second, one every 50 ms, and send only the latest position when several happen.
- Do not acknowledge, store or retry. A lost update is replaced by the next one.
- Expire each person's entry after 5 seconds with no update, so a closed tab disappears.
- Send updates only to people whose window covers that part of the board.
A drag preview shows a shape moving while it is still held. Send its in-progress position on the ephemeral channel. Send one durable operation when the person lets go, and one every second or so during a long drag, so that a crash does not lose the move.
Crowds. With n people, cursor messages grow as n × (n − 1). For 4 people that is 12 per round. For 100 it is 9,900. So lower the rate as the board fills: 20 a second at 10 people or fewer, 5 a second above 50, and only cursors in view. Past a few hundred people, show most as viewers with no live cursor.
Load shedding. If a connection's send buffer backs up, discard the older cursor updates for it. Never drop durable operations. Slow the sender down instead.
Big boards, snapshots and offline edits
Interviewer asks
How do you open a board with 20,000 shapes quickly, and how do offline edits come back?
One owner. Like a document, a board has one board server at a time, chosen by the directory with a lease.
Read moreShow less: Big boards, snapshots and offline edits
All its operations meet there, so ordering needs no consensus protocol. Our estimate gives 100 servers for 100,000 active boards.
Log plus snapshots. Every operation is appended to the log with a position number. Every few thousand operations, or after a quiet period, the server writes a snapshot of the whole board and the log position it covers. After a crash, a new owner loads the latest snapshot and replays the log from that position.
Tiles. Divide the canvas into square tiles, say 1,000 by 1,000 units, and store the snapshot per tile. A shape belongs to the tile that holds its center. Opening a board loads the tiles under the window: about 200 shapes and 40 KB, against 4 MB for the whole board. The app then subscribes to those tiles, and the server sends it only the operations for shapes in them. As the person pans, the app asks for the next tiles. An arrow between shapes in different tiles is sent together with the shapes at its ends.
A shape that is moved across a tile boundary is just a property change, and the server updates its tile index. A very large shape is listed in each tile that it touches.
Offline. While offline, the app keeps applying edits locally and queues the operations, each with its stamp. On reconnect it sends the queue and the last log position it saw. The server merges each property by stamp, so an offline edit that a newer edit has already overtaken loses that property, and the rest still land. The server replies with the operations after that position. If the app has been away longer than the retention period, tombstones may be gone, so it reloads the board and keeps its queue as a draft to apply again.
Step 5 of 6: Name the trade-offs
Merge rule
Last writer wins for each property over Last writer wins for the whole shape
Edits to different properties both survive. The cost is a stamp stored on every property, and a lost move when two people drag at once.
Stacking order
Order keys that sort between neighbours over An array position for each shape
Two people reordering do not clash, and a move touches one shape. The cost is keys that grow and need an occasional rebalance.
Cursors
A separate lossy channel with a rate cap over The same acknowledged operations as shapes
Cursor traffic, 10 times larger, never touches the log or the acknowledgement path. The cost is a second channel, and cursors that can be briefly stale or missing.
Loading
Tiles for the visible area over The whole board on open
A board of 20,000 shapes opens with 40 KB, not 4 MB. The cost is tile bookkeeping and loading as people pan.
Step 6 of 6: Say what breaks next
Questions an interviewer asks once the design is on the board. Answer each one out loud first.
- How does undo work when others are editing?
Undo is per person. Each app keeps the inverse of its own recent operations, such as the old position of a shape. Undo sends the inverse as a new operation with a new stamp, and the app can skip any property that someone else has changed since.
- How do arrows that connect shapes stay attached?
An arrow stores the IDs of the shapes at its ends and where on each one it attaches, not fixed coordinates. Each app redraws the arrow from the current positions of those shapes. Moving a shape is the only operation, and every arrow follows without extra operations.
- How would you support 500 people watching a presentation?
Make most of them viewers who get operations in batches from a fan-out layer, with no cursors of their own. Show them only the presenter's cursor and window. Keep the editors capped at a small number, such as 50, on the board server.
- What if the board server dies in the middle of a session?
Its lease expires and the directory gives the board to another server, which loads the latest snapshot and replays the log. Apps reconnect and resend operations that were not acknowledged, and the server drops any it already has by client ID and sequence number. People see a pause of a few seconds, and no acknowledged edit is lost.