Courses › System Design › Question Breakdowns
Design Figma (Multiplayer)
Property-level last-writer-wins, one server per file, optimistic local edits, journals and checkpoints, presence.
Design a collaborative design tool like Figma: many people edit the same design file at once — moving shapes, editing text, adding comments — and see each other's changes and cursors live, with full version history. It's close to Google Docs (see Real-time Updates), but the data is a tree of objects with properties, not a text string, which changes the conflict model (see Merging Concurrent Edits). It's a question about multiplayer sync, conflict resolution, one authority per document, and persisting large documents.
Requirements
Functional
- Multiple users can open and edit the same file simultaneously
- Edits (create, move, resize, recolor, reparent, delete objects) appear for others in real time, along with live cursors and selections
- Users can comment and view version history, and restore an old version
- Edits made while briefly offline are kept and synced on reconnect
Out of scope: the rendering engine, plugins, design-to-code, prototyping.
Non-functional
- Low latency: a collaborator sees an edit within ~100–200ms
- Local-first feel: your own edits apply instantly, never waiting for the server
- Convergence: everyone ends up with the same document
- Scale: ~10M+ files edited per day; most sessions have 1–5 editors; some (design reviews) have 100+ viewers
- Durable: acknowledged edits are never lost; files can be hundreds of MB of objects
[!TIP] Properties, not characters Text editors need OT or sequence CRDTs because insertions shift positions. A design file is mostly objects with properties (x, fill, parent). Two users changing *different* properties never conflict; two users changing the *same* property can simply resolve last-writer-wins. Figma built its multiplayer on exactly this observation — a central server and per-property LWW, borrowing CRDT ideas without full CRDT complexity.
Core Entities
- File — ID, owner, team, permissions, latest checkpoint, version
- Node — object ID, type (frame, rectangle, text…), properties, parent ID + fractional position among siblings
- Change — a set of property updates
(nodeId, property, value)with a sequence number assigned by the server - Checkpoint — a full snapshot of the document at a sequence number
- Comment — file, anchor (node or position), thread
API
GET /v1/files/{fileId} → { "checkpointUrl": "...", "sequence": 18233, "multiplayerServer": "wss://mp-17..." }
WS wss://mp-17.example.com/files/{fileId}
→ { "type": "changes", "clientSeq": 12, "ops": [{ "node": "1:42", "set": { "x": 120, "fill": "#FF0" } }] }
← { "type": "ack", "clientSeq": 12, "seq": 18234 }
← { "type": "changes", "seq": 18235, "ops": [...], "from": "user_b" }
↔ { "type": "presence", "cursor": [512, 300], "selection": ["1:42"] }High-Level Design
a = client "Editor A" @0,0
b = client "Editor B" @0,2
api = service "File API" @1,1
mp = service "Multiplayer Server\n(owns the file)" @2,1
journal = store "Change Journal" @3,0
ckpt = blob "Checkpoints (S3)" @3,1
meta = store "Files & Permissions DB" @1,2
hist = service "History / Snapshot\nWorker" @3,2
a -> api : open file
api -> meta
a <-> mp : changes + presence
b <-> mp
mp -> journal : append
mp -> ckpt : periodic
hist -> journal
hist -> ckpt- Open: the file API checks permissions, returns the latest checkpoint and the multiplayer server assigned to this file.
- Edit: the client applies its change locally at once and sends it; the multiplayer server — the single authority for the file — orders it, assigns a sequence number, broadcasts it, and acknowledges.
- Persist: the server appends changes to a journal (durable before ack) and periodically writes a full checkpoint to object storage.
- History: a worker turns checkpoints + journal into named versions for version history.
Deep Dives
1. Resolving conflicts
- Different properties or objects: both changes apply — no conflict.
- Same property, same object: the server's order decides — last writer wins per property.
- Optimistic local state: a client applies its own edits immediately. When a remote change for a property the client has an unacknowledged edit on arrives, the client ignores it (its own pending value will win once acked), so the UI never flickers back.
- Deletes vs. edits: if A deletes a frame while B edits a child, the delete wins; the server can keep deleted subtrees briefly so undo works.
2. Ordering children and reparenting
Sibling order uses fractional indexing: each child has a position like "a5" between "a" and "b", so inserting between two items never renumbers siblings. Reparenting can create cycles when two users move A into B and B into A at once; the server validates each change and rejects one that would create a cycle (the client reverts it).
3. Text inside designs
Text layers are the exception: concurrent edits inside one text box need character-level merging. Use a sequence CRDT or OT for text content only; everything else stays property-LWW.
4. One authority per file
All editors of a file connect to the same multiplayer server process, chosen by consistent hashing on file ID (or a registry). That makes ordering trivial and avoids distributed locking. If the process dies, the file is reloaded elsewhere from checkpoint + journal and clients reconnect, resending unacknowledged changes.
[!WARNING] Ack only after the journal write If the server acknowledges a change and crashes before it's durable, the editor believes it's saved and it's gone. Append to the journal (a replicated log or DB) before acking — a millisecond or two, worth it.
5. Presence
Cursors and selections change constantly but don't need durability. Send them over the same WebSocket, throttled (~20–30 updates/s), never persisted, and dropped when stale. Large viewer audiences get presence for editors only.
6. Loading large files fast
A big file can have hundreds of thousands of nodes. Load the checkpoint (compact binary, compressed) from object storage/CDN, then replay the few journal entries after it. Checkpoint every N changes or minutes so the replay tail stays short. For huge files, load visible pages first.
7. Offline edits
The client queues changes while disconnected. On reconnect, it fetches changes since its last seen sequence and re-sends its queue; property-LWW and the server's validation resolve the rest.
What's expected at each level
- Mid-level: WebSockets to a server per document, broadcasting edits, saving document state.
- Senior: property-level last-writer-wins with server ordering, optimistic local apply with ack tracking, journal + checkpoints, presence as ephemeral data.
- Staff: why OT/CRDT is overkill for objects (and where it isn't — text), fractional indexing and reparent cycles, file-to-server ownership and failover, large-file loading.