Design Figma
Run this for someone else. You hold the answers; they do not. Read the prompt, keep the clock, and use the probes below when an answer is thin. Do not show them this page.
Open with this
500 k design files open at once, some with 200 people: objects converge live, cursors stream, and 300 MB files open in seconds. Take a couple of minutes on requirements, then we will do some numbers, then the design. I will interrupt to keep us moving.
The clock
- 4 min: functional requirements and scope
- 4 min: non-functional requirements, with numbers
- 5 min: back-of-envelope estimates
- 16 min: high-level design and one or two flows
- 16 min: deep dives and the close
Move them on out loud when a section overruns. The commonest failure is spending twenty minutes on requirements and never reaching a deep dive, and preventing that is your job as much as theirs.
Requirements · 8 min
Listen for: a scoped set of capabilities, an explicit out-of-scope list, and numeric targets rather than adjectives. Prompt with “what are you not building?” if they never scope, and “what number would make that requirement real?” if they say “fast” or “highly available”.
Functional (10)
- Many people edit one file at once: Shapes, frames, text, components and their instances. A change shows up for everyone in the file within a couple of hundred milliseconds, and every client ends up showing the same document.
- Local edits never wait: Creating, moving, resizing and restyling apply on the next frame. Even creating an object needs no round trip, which is why object ids are generated on the client.
- Structure edits: Group, ungroup, move a layer into another frame, reorder layers. Concurrent moves must never lose an object or make a frame its own ancestor.
- Live cursors and selections: Who is in the file, where their pointer is, what they have selected, and follow mode for a presenter. High frequency, lossy, and never saved.
- Brief offline, then reconnect: A dropped connection or twenty minutes on a train. Edits made meanwhile are kept and applied when the connection returns, without a conflict dialog.
- Version history: Autosaved and named versions; view any of them and restore one. A restore is a new edit that everyone in the file sees, not a rewrite of the past.
- Comments: Threads pinned to an object or a point on the canvas, with mentions. A pin follows its object when someone moves it.
- Open very large files quickly: Design systems and big product files reach hundreds of MB of document data plus images. The first page must be usable in seconds, not after the whole file downloads.
- Sharing and permissions: View, comment and edit roles per file, team and link. A viewer’s socket cannot send changes, and a revoke applies to a session that is already open.
- Out of scope: The rendering engine itself, prototyping and Dev Mode, plugins, publishing component libraries across files (instances inside one file are in scope), and billing.
Non-functional (8)
- Local responsiveness (< 16 ms, next frame): The server is never on the path of showing your own change. Everything about the conflict model follows from applying edits locally first.
- Propagation to others (p95 < 200 ms same region): Edits and cursors from collaborators. Cursors are sent at 10 Hz and interpolated on the receiver, so they look smooth without 60 Hz traffic.
- Convergence (every client = server state): Once changes stop flowing, every client shows exactly the server’s document. This drives the hardest trade-off: one server ordering each file makes property-level last-writer-wins enough and avoids a general CRDT, but that server is then a single owner whose failure must be survivable.
- Durability (no acknowledged change lost): An acknowledgement means the change is in the journal. Periodic checkpoints alone would lose everything since the last one when a server dies.
- Recovery from a server crash (file live again < 10 s): Clients keep editing locally meanwhile and resend what was not acknowledged. Bounded by the lease expiry plus loading the checkpoint and the journal after it.
- Open time (first page usable < 3 s): For any file size; typical files open in under 1.5 s. A 300 MB file cannot be downloaded whole in that time, which forces page-at-a-time loading.
- Scale (500 k open files · 1 M sockets): 10 M weekly users. Most files have 1 to 5 people in them; a design review can have 200, nearly all of them watching.
- Access control (server-side · revoke < 5 s): Design files are unreleased products. Roles are checked when the socket opens and enforced on every change, and a revoke ejects a live session.
Estimates · 5 min
Ask for two or three numbers, not all of them. What matters is whether they state assumptions, round sensibly, and say what the number implies. Push once with “where did that come from?”
The numbers (8)
- Open WebSocket connections: ~1 M. 500 k files with someone in them × an average of 2 clients each = 1 M sockets. At ~50 k per gateway host that is about 20 gateways. Connections are not the hard part.
- Change batches per second: ~500 k/s in · ~1 M/s out. About 20 % of open files have someone mid-edit at any instant: 100 k files. Each sends ~5 coalesced batches a second (a drag flushes at up to 20 Hz, then goes quiet) = 500 k batches/s, ~300 B each. With ~3 people in an active file, each batch goes to 2 others: ~1 M pushes/s.
- Cursor updates per second: ~1.8 M/s. About 600 k clients share a file with someone else (a solo file sends no presence at all). If 30 % are moving their pointer at once at 10 updates a second: 180 k × 10 = 1.8 M/s, 3.6 times the edit rate. This is why presence gets its own lossy channel.
- The 200-person design review: ~400 k msgs/s → ~4 k/s. 200 people each sending 10 cursor updates a second, each delivered to the 199 others as its own message: 200 × 10 × 199 ≈ 400 k messages/s from one file. Batching per recipient every 50 ms caps it at 200 × 20 = 4 k/s, each listing the cursors that moved (up to ~2 KB, ~40 KB/s per viewer). Batching, not more servers, is the fix.
- Multiplayer hosts (memory): ~100–150 hosts · 3–5 k files each. 500 k open files × 20 MB average in memory (median ~4 MB; the largest decode to 1–2 GB) = 10 TB. At ~100 GB of document memory per host: 100 hosts, about 5 k files each. Run ~150 so a host loss or a burst of big files has room, ~3 k files each. Memory, not CPU, sizes this tier.
- Journal writes: ~150 MB/s · ~13 TB/day. 500 k batches/s × 300 B = 150 MB/s, about 13 TB a day. Entries expire an hour after a checkpoint covers them, so the live journal is roughly the last hour of writes: ~550 GB across all files. A write-heavy, short-retention log, nothing like a document database.
- Checkpoint writes: ~170 MB/s. 100 k actively edited files, one checkpoint every 10 minutes each, writing only the pages that changed (~1 MB on average): 100 k × 1 MB ÷ 600 s ≈ 170 MB/s, ~15 TB a day. Rewriting whole files (5 MB mean) would be 5× that. Unpinned checkpoints expire after 7 days (~100 TB rolling); pinned versions stay.
- Downloading the largest file: ~48 s whole · ~1.6 s first page. A 300 MB checkpoint over a 50 Mbps connection: 300 × 8 ÷ 50 = 48 s. The first page’s chunk, capped near 10 MB, takes ~1.6 s. Downloading the whole file before showing anything is not an option at the bandwidth users have.
High-level design · 16 min
Let them draw. Interrupt only to ask what backs a component or what a box actually does. Then pick one flow below and ask them to walk it end to end.
Components (14)
- Comments service (pinned to object ids): Comment threads stored as rows next to the file metadata, each pinned to an object id plus an offset, or to a canvas point. The client places the pin from the object’s current position, so a pin follows its frame when someone moves it. Deliberately outside the multiplayer document: comments need permissions, mentions, notifications and queries across files, none of which the document model is good at.
- Metadata DB (sharded Postgres): Files, projects, teams, roles, share links and comments, sharded by file or team id. Relational and strongly consistent because a revoke must be exact and comment threads are ordinary rows. Nothing in it is on the per-change path: it is read when a file is opened and written when a comment is posted or a share changes.
- File API (open · auth · session token): The HTTP entry point for opening a file: checks the caller’s role in the metadata DB and returns a signed session token (file, user, role, 10-minute expiry) and the multiplayer address. The gateway validates that token without a database call. Also serves the file browser, sharing changes and other ordinary CRUD.
- Change journal (append-only · (file, seq)): Every accepted change batch, keyed by file id and sequence number, written before the change is acknowledged or broadcast. Appends are conditional writes on (file, seq), so a new owner writes a fence entry and any late append from an old owner collides and fails. Entries expire an hour after a checkpoint covers them: a high-write, short-retention log, a DynamoDB-style table or a BookKeeper-style log.
- File router (host leases · file → owner): Maps each open file to the host and worker that own it, with an epoch that increases on every move. Leases are held per host and renewed every second, not per file, so 500 k open files cost about 150 heartbeats a second. Places a new file by memory headroom using the size of its last checkpoint. etcd or ZooKeeper class: small, strongly consistent, and off the edit path.
- Editor client (local copy · pending queue): Holds the document (or the pages loaded so far) in memory and applies every edit locally on the next frame. Creates object ids as its session number plus a counter, so making a layer needs no round trip. Keeps unacknowledged changes in a pending queue and ignores server values for any property it has a pending change on, which is what stops remote edits from flickering over your own.
- Multiplayer gateway (WebSocket · fan-out): Terminates about 50 k WebSockets per host, validates the signed session token once at connect, and routes each file’s traffic to the worker that owns it. It also fans out: an owner sends each outbound batch once per gateway and the gateway copies it to its own sockets. Because browsers stay connected to the gateway, a multiplayer host failover is a re-route behind it, not a reconnect.
- Multiplayer server (one owner per file · in memory): The authority for each open file: holds the whole document in memory as a map from object id to properties, applies changes in arrival order with last-writer-wins per (object, property), and rejects changes that would break the tree. Journals each batch, then acknowledges and broadcasts it. Each file lives on exactly one single-threaded worker, about a hundred files per worker, so ordering is local and needs no locks; it also writes the file’s checkpoints.
- Checkpoint store (object storage · page chunks): The document at a sequence number: a small manifest plus one compressed binary chunk per page, and a shared chunk for components and styles used across pages, each named by its content hash. Only changed pages are rewritten, so a checkpoint of a 300 MB file usually writes a few MB, and versions share unchanged chunks for free. Immutable objects, so the CDN can cache them forever.
- CDN (immutable chunks · images): Serves checkpoint chunks and images from the edge with year-long cache headers, because both are named by content hash and never change. Access is through short-lived signed URLs, so a cached chunk is still unreadable without access to the file. Takes the bulk of every file open off the multiplayer servers.
- Presence relay (in memory · lossy · 50 ms): Cursors, selections, viewports and who is in the file, held in memory per file and never written anywhere. Batches each file’s updates every 50 ms into one message per gateway, drops stale updates instead of queueing them, and in files over 50 people sends full-rate cursors only for people near the viewer’s viewport. Placed by consistent hashing with no lease: if a relay dies, the next updates rebuild its state within a second.
- Event bus (Kafka · keyed by file id): Low-rate file events from the multiplayer servers: checkpoint written, editing went idle, file renamed. Never individual changes, which would be 500 k messages a second that no consumer needs. Keyed by file id so each file’s events stay in order for version history, thumbnails, search and notifications.
- Version history (pinned checkpoints · restore): Turns checkpoints into versions: pins the one taken when editing goes quiet for 30 minutes, plus every named version, and lets unpinned ones expire after 7 days. Viewing a version opens its manifest read-only from the CDN. Restoring asks the file’s owner to apply the difference as ordinary changes, so the restore is itself undoable and everyone in the file sees it arrive.
- Asset store (images by content hash): Image and video fills, uploaded by the client and named by the hash of their bytes, so the same screenshot pasted into a thousand files is stored once and a repeat upload is skipped. The document holds only the hash in a fill property. Several resolutions are stored per image, so a zoomed-out canvas fetches thumbnails, not originals.
Flows to ask them to walk (5)
- Open a large file: The main read path. Most of the bytes come from the CDN rather than the multiplayer server, and the first page is usable before the rest of the file has arrived.
- Client asks the File API to open the file; it checks the caller’s role and returns a session token.
- Client opens a WebSocket; the gateway looks up which worker owns the file.
- If the file is not in memory, the owner loads its latest checkpoint and the journal entries after it.
- The owner tells the client which checkpoint it is based on and gives this tab a session number.
- Client downloads the manifest, the shared chunk and the current page’s chunk from the CDN, and renders.
- Images are fetched lazily as they come into view, at the resolution the zoom level needs.
- The owner streams the changes made since the checkpoint, and the client joins presence.
- Two people edit the same object: The core loop. Both edits apply locally first; the server orders them, the last one it receives wins for each property, and both clients end up showing the same thing without flicker.
- A designer drags a frame and changes its fill; the client applies both on the next frame.
- The client coalesces changes per frame and sends at most 20 batches a second.
- The owner checks the role and the tree rules, then applies each property in arrival order.
- The batch is appended to the change journal before anyone hears about it.
- The owner acknowledges the author and broadcasts the batch to everyone else through their gateways.
- A client with a pending change to a property ignores the server’s value for it until its own is acknowledged.
- A pasted image is uploaded through the CDN to the asset store, and only its hash enters the document.
- A 200-person design review: Where per-file scale, not file count, breaks the naive design: one presenter, 199 watchers, and 400 k cursor messages a second if nothing is batched.
- Two hundred people open the file; their sockets are spread over about 20 gateways.
- The owner sends each outbound batch once per gateway, and each gateway copies it to its own sockets.
- Outbound changes are batched per 33 ms tick for files with many subscribers.
- Every client throttles its cursor to 10 updates a second and sends it on the presence channel.
- The presence relay merges the file’s cursors every 50 ms into one message per gateway.
- The presenter’s viewport is part of their presence, so followers’ screens track it.
- A viewer on a slow connection loses presence first, then gets a resync if changes back up.
- A multiplayer server dies mid-session: What the single owner costs: when its host dies, every file on it must come back elsewhere from the last checkpoint plus the journal, with no acknowledged change lost and no batch applied twice.
- A host dies; gateways lose their internal connections, but browsers stay connected and keep editing locally.
- The host’s lease expires and the router reassigns its files to other hosts with a higher epoch.
- The new owner fences the journal, then loads the latest checkpoint plus every entry after it.
- Gateways re-route the sockets; each client reports its last seen sequence and resends its unacknowledged batches.
- The owner skips any resent batch the journal already holds, by session and batch number.
- The same path serves a laptop that was offline for twenty minutes.
- Versions, restore and comments: Everything outside the live loop. Versions are pinned checkpoints, a restore is an ordinary edit, and comments are rows in a database; none of it can slow down a drag.
- Every 10 minutes of activity, the owner checkpoints the pages that changed and announces it on the bus.
- Version history pins a checkpoint when editing goes quiet for 30 minutes, or when someone names a version.
- Opening an old version loads its manifest read-only from the CDN, with no multiplayer session.
- Restoring a version asks the owner to apply the difference as ordinary changes.
- A comment is posted to the comments service, pinned to an object id and an offset.
- The client draws each pin from its object’s current position, so pins follow moved frames.
Deep dives · 16 min
Pick two. Ask the headline question, let them answer, then use the follow-ups. The follow-ups are where the level gets decided, so leave time for at least three of them.
Property-level last-writer-wins
Ask: Google Docs needs OT or a CRDT to merge edits. Why can a design tool get away with last-writer-wins?
Good answers name: Server-ordered last-writer-wins per (object, property), A general CRDT for the whole tree (Automerge or Yjs style), Operational transformation, Lock an object while someone edits it.
Our pick: The server holds each file as a map from object id to a map of property to value, and applies changes in the order its single worker receives them: for each (object id, property), the latest value wins, and client clocks play no part. Object ids are generated on the client as session number plus a counter, so creating an object is just setting properties on an id nobody else can have, with no round trip. The client applies its own changes at once and ignores incoming values for any property where it has an unacknowledged change, so remote edits never flicker over yours; once yours is acknowledged, the server’s later values are applied, so everyone converges. Structure is properties too (a parent id and a position key), covered in the tree deep dive. Figma has written publicly (Evan Wallace’s 2019 post on its multiplayer technology) that it considered OT and chose a server-authoritative design inspired by CRDTs instead, because it has a central server anyway. The details here beyond that are my design.
- Two people type in the same text layer at the same time. What happens?
The text is one property, so the server keeps whichever batch arrived last and the other person’s characters in that layer are lost. That is acceptable in a design tool: text layers are short, two people rarely type in the same one, and presence shows that someone else has it selected, which steers people away. If research said otherwise, the fix is local: make only the characters property a sequence CRDT, as in Google Docs, and keep last-writer-wins for everything else. - Someone edits offline for twenty minutes. Whose changes win?
Theirs, for any property they changed, because their batches reach the server last. That is the honest cost of last-writer-wins with no merge: a stale offline value can overwrite a colleague’s newer one on the same property. Changes to objects that someone deleted meanwhile are dropped, and the client tells the user. Version history makes it recoverable, and twenty minutes of offline edits rarely touch the same properties as a colleague’s. - Why can the client not just apply incoming server values immediately?
Because its own change is still in flight. You set the fill to blue, a colleague’s red reaches the server first and is broadcast to you, and your screen would flash red until your blue is acknowledged and comes back. Ignoring server values for properties with a pending local change removes the flicker, and it is safe: once the server acknowledges your change, any later value it sends is genuinely newer. - How do you test that every client converges?
Simulate it: a deterministic harness with one server and several clients, random edits on all of them, random delays, drops and reconnects, then assert that every client’s document equals the server’s once traffic stops. Run millions of seeds in CI and keep each failing seed as a regression test. In production, clients send a periodic hash of a few objects so the server can detect divergence and force a reload. - What does undo do in a multiplayer file?
It undoes only your own changes, by setting the properties you changed back to their earlier values, as a new edit. If a colleague has since changed the same property, I would skip it, so undo never destroys someone else’s later work. Undoing a delete re-creates the object with its original id and properties, which the client kept on its undo stack.
Reparenting, cycles and child order
Ask: Two people drag two frames into each other at the same moment. What happens, and how do you keep layers in order?
Good answers name: Parent and fractional position on the child; the server rejects cycles, Children as an ordered list on the parent, Move as delete plus re-create, A tree CRDT with move operations.
Our pick: Each object stores parent_id and a position key, and children are sorted by key. Keys are fractional indexes: strings that compare lexicographically, so inserting between "a0" and "a1" picks "a0V" without touching either neighbour, and a reorder is one property change. The client sends parent and position together in one batch so the server applies them atomically. Before applying a parent change, the server walks up from the new parent; if it meets the moving object, it rejects the change and sends the client the current values as a correction. A client that briefly creates a cycle locally (its own move plus a remote one) keeps the affected objects where they were until the answer arrives, tens of milliseconds later. Concurrent inserts can choose the same key; ties sort by object id so every client agrees, and the server rewrites keys that grow past about 50 characters as an ordinary change. Figma has written about using fractional indexing for child order and about its server rejecting parent changes that would create a cycle; the tie-break and rebalancing rules are my design.
- Walk me through the A-into-B, B-into-A race exactly.
The server receives Alice’s batch first: A’s parent becomes B, which is valid, so it is applied and broadcast. Bob’s batch arrives next: B’s parent would become A, but walking up from A reaches B, so it is rejected and Bob gets B’s current parent and position as a correction. Bob’s client had both moves applied for a moment, saw the cycle and held both frames in place until the correction came. Everyone ends with A inside B. - Someone moves a layer into a frame that another person just deleted.
The server checks that the new parent exists. If the delete arrived first, the move is rejected and the mover’s client gets a correction putting the layer back under its old parent, where it still exists. If the move arrived first, the layer is now a child of the frame and is deleted with it when the delete lands. Either way the result is a valid tree in the server’s order, and undo can bring things back. - How long can fractional keys get, and what do you do about it?
Inserting repeatedly at the same spot grows a key by about one character per insert. Real files almost never pass a few characters, but a script inserting thousands of layers in one place could. The server watches key length and, past a threshold like 50 characters, reassigns evenly spaced keys to that parent’s children as a normal batch, which every client applies like any other change. - Why does the multiplayer server need to understand the tree at all? Could it just relay?
A relay cannot reject a cycle, check that a parent exists, or enforce that an instance still matches its component. Those checks need the whole document in memory, which is the main reason the owner holds the full file rather than passing changes through. It costs memory, about 10 TB across the fleet, and buys a document that is always a valid tree.
One owner per file, and when it dies
Ask: How do all 200 editors of a file end up talking to the same process, and what happens when that process dies?
Good answers name: Router assigns files to hosts; host leases; journal fencing by conditional write, Consistent hashing of file id to host, no registry, Stateless servers, every change written to a shared database, Hot standby replica per file.
Our pick: A file router (etcd or ZooKeeper class) holds file → (host, worker, epoch) and a lease per host of 5 seconds, renewed every second. On connect the gateway asks the router once and caches the answer until the epoch changes; if nobody owns the file, the router places it by memory headroom, using the size of its last checkpoint. Inside a host, each file belongs to one single-threaded worker, about a hundred files per worker, so its changes are applied in order with no locks. When a host’s lease expires, the router reassigns its files with epoch + 1; each new owner writes a fence entry into the journal at the next sequence number, loads the latest checkpoint and the journal after it, and starts serving. Clients, still connected to their gateways, report their last seen sequence and resend unacknowledged batches, which the owner deduplicates by session and batch number. Users see a few seconds of reconnecting and lose nothing they saw acknowledged.
- The old owner was not dead, just stuck in a 30-second pause. It wakes up and carries on.
Its next journal append is a conditional write at a sequence number the new owner’s fence already occupies, so it fails, and the old process learns it no longer owns the file and drops it. It may still try to broadcast, but gateways route by the router’s epoch and discard messages from an owner with an older one. Nothing it does after the fence reaches storage or clients. - A host with 3 000 files dies at 2 pm on a Tuesday. What load does recovery put on the system?
About 20 files to reload on each of 149 hosts, mostly small: at a 5 MB average that is 15 GB of checkpoint reads from object storage, a few seconds of parallel reads. Journal reads are small, at most an hour of changes per file. Clients do not reconnect, because their sockets are on the gateways, so there is no reconnect storm, only a burst of resume messages. - How do you deploy a new multiplayer server version without dropping everyone?
Drain hosts one at a time: for each file, checkpoint it, bump the epoch and let an upgraded host load it, a pause of tens of milliseconds for small files. Gateways re-route without the browser noticing. Moving a few hundred files a second drains a host in a minute or so and the fleet in a few hours, which is also as fast as you would want to roll out code that owns every open document. - Why are comments and permissions not part of the multiplayer document?
Because they need what the document is bad at and nothing it is good at. Comments are queried across files (my mentions, unresolved threads), trigger notifications, and are written by commenters who cannot edit the design; permissions must be checked before the document is even loaded. As rows in Postgres behind ordinary services, they never compete with drags for the owner’s CPU, and a comments outage leaves editing untouched. - A permission is revoked while the person is editing. How quickly does it take effect?
The sharing change publishes an event; the file’s owner, found through the router, downgrades or closes that session within a couple of seconds and rejects its batches from then on. As a backstop, session tokens expire every 10 minutes and are refreshed against the metadata DB, so even a lost event is bounded. Batches accepted before the revoke stay, since they were legitimate when made.
Checkpoints plus a journal
Ask: Why not just save the whole file to storage every 30 seconds?
Good answers name: Page-chunked checkpoints plus an append-only journal, Whole-file save every 30 seconds, Every object as rows in a database, Keep the full journal forever (event sourcing).
Our pick: Every accepted batch goes into a journal keyed by (file id, sequence number) before it is acknowledged, with per-file group commit every 10 ms; a DynamoDB-style table with cheap conditional writes fits, at about 150 MB/s fleet-wide. Every 10 minutes while a file is being edited, and when its last editor leaves, the owner writes a checkpoint: a manifest at that sequence number listing one content-addressed chunk per page plus a shared chunk for components and styles, in a compact binary encoding. Only changed pages produce new chunks, so checkpoints average about 1 MB and the fleet writes about 170 MB/s. Journal entries expire an hour after a checkpoint covers them, which is the window in which a reconnecting client can catch up from the journal. Version history is pinned checkpoints, so the feature costs only the changed chunks. Figma has written publicly about adding a journal alongside its periodic checkpoints so that a server crash no longer loses recent edits; the intervals, the chunking and the store choices here are my own.
- How do you checkpoint a 1 GB in-memory document without freezing the editors?
Copy the changed pages’ objects into an immutable snapshot on the worker, which is quick because only changed pages are copied, then encode, compress and upload from a background thread while the worker keeps applying changes. The manifest records the sequence number of the snapshot, so changes applied during the upload are simply after the checkpoint and stay in the journal. If the upload fails nothing is lost; the next attempt includes more. - The journal store is slow for a minute. What do editors see?
Acknowledgements and broadcasts wait, because both follow the journal write, so collaborators see each other’s edits late while their own still apply locally. Pending queues grow on the clients, which is bounded and fine for a minute. If it lasts longer, the owner refuses new opens on that host; it does not acknowledge from memory, since that would break the one promise the journal exists to keep. - Why broadcast only after the journal write? That costs latency.
About 10 to 20 ms, inside a 200 ms budget. In exchange, nothing any client has seen can be taken back by a crash, which keeps recovery simple: the new owner’s state includes everything ever shown. Broadcasting first would save those milliseconds, but a crash could then show a change to colleagues and make it vanish from the file until the author’s client resends it. - What is in a checkpoint chunk, and why binary?
For each object on the page: its id, its type and its properties, with property names as small integer tags and values in a compact schema-based encoding, then compressed. JSON would be several times larger and much slower to parse, and parsing is a large part of opening a big file. The schema is versioned, so old checkpoints stay readable after new properties are added. - A bad deploy corrupts some files. How do you recover?
From the history the design already keeps: take the last checkpoint written before the deploy, apply the journal entries from before the bad version started writing, and apply the result as a restore, which is an ordinary edit. That works for the last hour from the journal and for anything older from pinned checkpoints. It also argues for making the checkpoint writer refuse to write a document that fails validation.
Opening a 300 MB file in seconds
Ask: Your biggest customers have 300 MB files, and downloading one takes 48 seconds. How does it open in under 3?
Good answers name: Page-chunked checkpoints from the CDN, progressive render, lazy images, Download the whole file before rendering, Stream the document from the multiplayer server, Server-rendered tiles for the first view.
Our pick: The checkpoint is stored as a manifest plus one chunk per page and a shared chunk holding components, styles and anything referenced across pages; pages larger than about 10 MB are split by top-level frame. On open, the owner tells the client which manifest it is based on; the client fetches the manifest, the shared chunk and the current page’s chunk from the CDN, renders, then streams in other pages in the background. Image fills are fetched only when they are in or near the viewport, at a size chosen by the zoom, with the image’s dimensions and average colour as a placeholder. Changes for pages not yet loaded wait in a small buffer and are applied when the page arrives. Most opens are served from the edge, because chunk names never change. Figma has described loading the page a user opens first and the rest on demand; how it does that internally goes beyond what it has published, so the chunking here is my design.
- An instance on page 1 uses a component defined on page 9. Does the client need page 9?
Not all of it. When the checkpoint is written, every component, style and variable referenced from a page other than its own goes into the shared chunk, which loads before any page. The cost is that the shared chunk can be large in a design-system file; it is cached aggressively, because it changes only when those components do. - If the client loads one page, what does the multiplayer server hold?
The whole file, still. It validates structure (cycles, parents that exist, instance rules) across every page, and any client may navigate anywhere. That is why server memory, about 10 TB across the fleet, is the sizing number and why placement looks at file size. If memory became the constraint, the server could keep cold pages in their compact encoded form and decode a page only when a change touches it. - 200 people open the same file at 10 am for a review. What happens to the origin?
The first request for each chunk in each CDN region goes to the checkpoint store, and everything after is an edge hit, because chunks are immutable. Request coalescing at the edge stops 200 simultaneous misses in one region from becoming 200 origin reads. The multiplayer server sends each person only the catch-up since the checkpoint, kilobytes to a few MB. - How do you know opens are fast enough, and where the time goes?
Measure on the client, per open: time to manifest, to first page chunk, decode time, first render and time to interactive, by file-size bucket and region. Large files are few but opened often, so report percentiles weighted by opens, not by files. Most regressions show up as decode time from a new property, or a page that stopped being split.
Presence and the 200-person file
Ask: 200 people join one file for a design review. What breaks first, and what do you do about it?
Good answers name: Separate lossy presence relay with per-recipient batching and viewport filtering, Presence through the multiplayer server, on the ordered stream, Peer-to-peer cursors over WebRTC, Fan-out at the gateway.
Our pick: Presence (cursor, selection, current page and, for presenters, the viewport) shares the socket but is routed by the gateway to a presence relay, placed by consistent hashing on the file id, with no lease and nothing persisted. Clients send at most 10 updates a second and only when something changed; receivers interpolate. The relay merges each file’s updates every 50 ms into one message per gateway, and above 50 people sends full-rate cursors only for people on the viewer’s page and near its viewport, with everyone else as a once-a-second avatar list. Edits are batched per 33 ms tick for files with many subscribers and sent once per gateway, which copies them to its sockets. Each socket has two queues: presence keeps only the newest message, and changes are capped at a few MB, past which the gateway closes the socket and the client reloads from a checkpoint. With this, the review costs about 4 k presence messages a second, and the owner’s work grows with the number of gateways rather than the number of people.
- Now it is 2 000 people watching a keynote file. What changes?
Treat most of them as an audience. Beyond a few hundred, viewers stop sending cursors, see an avatar count and the presenter’s viewport, and receive edits on a slower tick, say 100 ms. Edits still flow through one owner, which is fine because only a handful of people are editing; if 2 000 sockets span every gateway, a relay tier between owner and gateways keeps the owner’s sends constant. - A presence relay dies. What does a user see?
Cursors freeze for up to a second. Gateways notice the broken connection, rehash the file to another relay, and the next update from each client, within 100 ms, repopulates it. Clients re-send their selection when they reconnect to presence. No document state is involved, so there is nothing to recover and nothing to fence. - Why not send cursors at 60 Hz for smoothness?
Six times the traffic for something the eye cannot tell apart from interpolated 10 Hz. Receivers draw remote cursors a frame or two behind and interpolate between the last two positions, which looks smooth and costs nothing on the network. In large files the rate drops further, which is fine because nobody watches 199 cursors at once. - How would you measure whether multiplayer feels good?
End to end, on clients: the time from a change leaving one client to it rendering on another, at p50, p95 and p99, by region and file-size bucket, and the same for cursors. Add the rate of resyncs (sockets closed for backlog), rejected batches and corrections. Server CPU per worker matters for capacity, but alerts should fire on the client-measured latency.
Close · 5 min
Ask what breaks first at ten times the load, and what they would build next. Then give them your read: one thing that was strong, one thing that was missing, one thing to practise. Be specific; “good job” helps nobody.