Lesson 28 of 36
Worked Scenario: Design a Collaborative Wiki (Confluence)
A full worked answer to the Atlassian-style wiki prompt — a left-nav tree over tens of thousands of pages, autosave with a real conflict story, concurrent editors, and presence done honestly.
A Confluence-shaped wiki — nested spaces, tens of thousands of pages, real-time multi-editor editing, autosave, presence, mentions, inline comments — is a canonical Atlassian interview prompt because every piece of it touches a different stage of this course. Loading the tree without blocking the editor; editing concurrently without data loss; saving without round-tripping on every keystroke; keeping presence honest when a tab closes.
Clarifying requirements first
Before proposing anything, the questions worth asking out loud: Rough workspace size — hundreds of pages or tens of thousands? Full concurrent editing (two cursors in the same paragraph), or append-only comments? Autosave cadence expected? Presence (who else is here) required, or nice-to-have? Mobile first-class, or desktop editor with mobile reader? For this answer, assume: up to ~30 000 pages across a large workspace, full concurrent editing in the body, autosave every few seconds, presence shown at the top of the page, and desktop-first editing with a usable mobile reader.
Rendering strategy: SSR for the reader view, CSR for the editor
A wiki has two distinct views with different needs. The read-only page view is read-heavy, often linked from elsewhere, and benefits from server rendering: SSR (or ISR if a short staleness window is acceptable) gives a fast first paint, works for a bookmarked link in a browser without JS yet, and makes internal search indexing feasible. The editor view, by contrast, is heavy JS, personalised, and never meant to be crawled — it is naturally a CSR concern after the auth check lands. Treating one view as "the whole feature" leaves perf on the floor on whichever side you compromised.
The left-nav tree: load progressively
pages.byId
Each page stored once, with parent_id and child_count. No embedded children — just the pointer up.
tree.cache
A map of parent_id → ordered child id array. Populated lazily, on expand, on the currently-open page's ancestors.
openPath
The ids from root to the currently-rendered page — pre-fetched so the breadcrumb and open-branch render immediately.
A naive implementation fetches the entire tree on page load and hangs the editor behind a multi-second request. The real pattern: the initial payload is small — root's direct children plus the ancestors of the page currently being viewed — and the rest of the tree streams in as the user expands nodes. Prefetch on hover is a cheap polish once that is in place.
For display, the tree is a candidate for virtualization when a single expanded parent has thousands of children; most of the tree is deep and narrow, not wide, so plain rendering per expanded subtree is usually fine.
The editor: CRDT (or OT) for honest concurrent editing
Two editors in the same paragraph is the hard case. Last-write-wins silently drops one of their sentences; a per-paragraph server lock destroys the live-editing feel; a "pick one on save" dialog puts a merge conflict in front of a user who shouldn't have to see one. The honest answer is a conflict-free merge primitive — a CRDT like Yjs/Automerge, or an OT engine — applied on every keystroke, so both edits transform into a single coherent document without needing a server round-trip to confirm each character. This is what makes the typing feel local even when two cursors are in the same paragraph.
Choosing between CRDT and OT is itself an interview-worthy answer: CRDTs are easier to reason about for arbitrary offline peers (each client's operations commute by construction), while OT needs a central authority to transform operations against each other but produces slightly tighter conflict behaviour in some edge cases. For a web wiki with a central server, either works and the choice is usually driven by library ecosystem rather than pure theory.
Autosave: debounced, with a real conflict story
Autosave fires every few seconds — debounced so typing bursts don't become a
request per keystroke. The write includes the client's base revision; the
server rejects with 409 Conflict if someone else has written since. The
frontend's job on 409: pull the server's new base, re-apply the local
operations via the same CRDT/OT already in play, re-save. The user ideally
sees nothing except a brief "saving…" spinner. A rejection dialog here is
the wrong failure mode — it exposes a merge conflict to the user who is
least able to resolve it.
A plain "save failed, retry" without the merge step is the common bug: the retry sends the same stale base and gets the same 409, forever. The merge has to happen between the fetch and the retry.
Presence done honestly
Presence (the "3 people are viewing this page" row) is cheaper than it
looks when done right and awful when done wrong. The right version: each
client sends a heartbeat to a server pub/sub channel every few seconds for
the page they're on, the server broadcasts the current viewer set to the
same channel, and clients render it. On tab close (beforeunload), the
client fires a navigator.sendBeacon leave event; if that misses (power
cut, tab killed), the server times out stale viewers after a short window
anyway, so the "3 people viewing" never gets stuck lying when nobody is.
The common anti-pattern is counting WebSocket connections for presence — that works until a user has the same page open in two tabs, their mobile is also on the same page, or they've reloaded three times in the last minute. A heartbeat with timeout is honest; a connection count is a lie dressed as a number.
What's explicitly out of scope, and why
Not solved in this answer: full-text search across the workspace (a backend indexing problem, not a frontend one); mentions-with-permissions against large org directories (treat as a separate autocomplete design, mostly about async lookup and virtualization of the dropdown); the long-tail of import/export formats (a backend conversion pipeline); page-level permissions model (the frontend reads what the server exposes and renders accordingly; the enforcement lives on the server). Scoping these out is a strength, not an oversight.
What to remember
- Split the SSR shell + CSR editor rather than treating one view as the whole feature; the reader and the editor have genuinely different performance profiles.
- Load the nav tree progressively — root + current-page ancestry up front, every other subtree on expand — rather than fetching tens of thousands of nodes on first paint.
- Use a CRDT or OT for concurrent editing. Last-write-wins silently loses data; per-paragraph locks kill the live-editing feel; "pick one on save" is a conflict shown to the worst-equipped person to resolve it.
- Autosave with a base-revision check; on 409, merge via the same primitive used for live edits and re-save without a user-visible dialog.
- Presence is a heartbeat with a server-side timeout, not a connection count — the connection count lies the moment a user has two tabs open.
Check yourself
3 questions · pass 3/3 to unlock Worked Scenario: Design a Code Review Diff Viewer (GitLab / GitHub)
1.The left-nav tree can hold ~30 000 pages across a mature workspace. Loading the full tree on first paint takes seconds and blocks the editor. What's the right structural fix?
2.Two editors are typing in the same paragraph at the same time. The product requires both characters to end up in the document without one overwriting the other. Which approach does the frontend commit to, and what does it buy versus the alternative?
3.Autosave fires every few seconds while editing. The server rejects one save because of a stale revision. What should the editor do?
3 left to answer
Discussion
Sign in to postNo comments yet. Be the first to say something.