AniUI Academy

Worked Scenario: Design a Kanban Board (Jira / Trello)

A full worked answer to the Atlassian-style prompt — column rendering under thousands of cards, drag-and-drop under concurrent writes, filter state in the URL, and real-time card updates. Trade-offs named.

15 min read

The Kanban board — Jira, Trello, Linear, GitHub Projects — is one of the most-asked frontend prompts, and specifically what Atlassian's own interviews tend to centre on. It's not just "render columns of cards": it's rendering them at scale, dragging under concurrent writes, filtering by URL state, and staying usable when fifty people are on the same board at once.

Clarifying requirements first

Before touching the data model, the questions worth asking out loud: How many cards per column at the extreme — tens, hundreds, thousands? How many simultaneous viewers per board at peak? Is drag-and-drop between columns required, or just reordering within one? Do updates need to appear in near real-time (another user moving a card), or is "refresh to see" acceptable? Mobile first-class, or secondary? For this answer, assume: up to ~2 000 cards in the biggest column, up to 50 concurrent viewers on a popular sprint board, cross-column drag required, real-time updates expected for cards currently on screen, and desktop-first with a usable mobile view.

The data model: normalized, with per-column rank

cards.byId

Every card stored once by id — title, assignee id, label ids, epic id, description.

columns.byId

Each column holds an ordered array of card ids plus a monotonically increasing rank per card for sort stability.

users.byId / labels.byId

Shared references so an assignee rename or label-colour change updates every card that references them.

Normalization matters here for the same reason as a feed: an assignee's avatar or a label's colour appears on potentially hundreds of cards, and denormalising forces hunting through arrays on every edit. The per-column rank (a float, or a fractional-indexing scheme like LexoRank) is what makes reordering a card-between-two-others an O(1) write rather than renumbering every subsequent card.

Rendering strategy: SSR shell, CSR interactions

A Kanban board is behind a login, so SEO is irrelevant and SSG is out. Pure CSR would ship a blank page while the auth check and initial board load resolve. SSR for the authenticated shell — columns laid out, first screen of cards per column rendered on the server with cookies attached — gives a visually complete first paint; CSR takes over for virtualization, drag, and the WebSocket stream of updates. Streaming SSR is a bonus here: the shell ships, each column streams in as its first page of cards resolves, so a slow query on one column does not block the whole board.

Virtualizing each column independently

A column with 2 000 cards cannot have 2 000 nodes in the DOM — the board goes sluggish on scroll long before that, and dragging a card into or out of a long column triggers a layout pass over all its siblings. The structural fix is per-column virtualization: each column is its own scroll container rendering only the window of cards currently visible (plus a small overscan on either side).

Fixed-row-height windowing is usually enough here — cards are mostly uniform. If some cards have expanded details or large attachments, the variable-height case applies, measured-on-render with an estimated height while off-screen, same as the social-feed variable-virtualization case.

Drag-and-drop under concurrency

Two things make Kanban drag harder than a single-user drag demo. One: a card dragged into a virtualized column cannot be inserted into the DOM between two existing children if those children aren't in the DOM. The implementation needs to work on the ordered id array for the column, not the rendered rows — drag over a column's drop zone, compute the insertion index from the pointer y plus the row heights, update the id array, let virtualization re-render. The ghost card is a single floating node following the pointer, not a real sibling being reparented.

Two: concurrent writes. Two users drag the same card at the same time, or one drags card A while the other filters the board out from under them. The contract: both users' drags apply optimistically on their own screens, the server receives both writes with a (card_id, revision) check-and-set, the first write wins and the server broadcasts the authoritative state over the WebSocket. The losing user's card visibly snaps to where the first write put it — annoying but honest, and visibly correct rather than silently diverging.

Filter state in the URL, not component state

A core Kanban UX is "send this filtered view to a teammate" — assignee: me, label: release-blocker, this sprint. If filter state lives in component state or React context, that URL is not shareable; if it lives in localStorage, every user has a different view at the same URL (worse). Filters belong in the URL (search params): the router is the source of truth, components read them, the back button navigates between filter states, and copy-paste reproduces the exact same view. The side benefit: the component tree does not need a filter context, which is one less global dependency to think about.

Real-time updates: WebSocket, scoped to the board

Real-time card edits (title change, status change, new comment count) arrive over a WebSocket channel scoped to the board id. The frontend's job here is to apply updates idempotently against the normalized store — a new revision of cards.byId[cardId] replaces the old one, and because the view layer derives from the store, every rendered copy updates together. Updates that reorder cards or move them between columns are applied to the columns.byId[*].order arrays and virtualization re-renders.

The honest cost: a reconnection replay. If the WebSocket drops and reconnects, the client needs to re-fetch the current board state (not just the stream of missed updates) because trusting a server to replay every missed event back to a specific sequence number is fragile. Treat reconnect as "re-hydrate the current state" and move on.

What's explicitly out of scope, and why saying so is a strength

Not solved in this answer: full offline support (requires conflict resolution over long partitions, not just concurrent writes); permissions UI (enforcement is a server concern; the frontend just hides what the server says is hidden); the drag-across-boards flow (a separate interaction with its own prompt). Calling these out shows the interviewer the answer is scoped on purpose, not out of oversight.

What to remember

  • Virtualize each column independently; the DOM holds only the visible range per column, which also makes drag-and-drop a state operation rather than a reparenting operation.
  • Store the per-column order as an ordered id array with a sortable rank per card, so reordering is an O(1) write rather than renumbering.
  • For concurrent drags, both clients apply optimistically and reconcile to the server's authoritative state — a card visibly snapping is correct behaviour, not a bug.
  • Put filter state in the URL so a filtered view is shareable and the back button works; context and localStorage both break the primary use case.
  • Scope the real-time channel to the board, apply updates idempotently against the normalized store, and treat a reconnect as a re-hydrate rather than a replay.

Check yourself

3 questions · pass 3/3 to unlock Worked Scenario: Design a Collaborative Wiki (Confluence)

up to 50
  1. 1.A Kanban board shows four columns, each potentially holding thousands of cards. A naive implementation renders every card up front and the board becomes janky after ~400. What's the right structural fix, and why does it also simplify drag-and-drop?

  2. 2.Two teammates drag the same card to different columns almost simultaneously. The UI needs to feel instant for both. What's the correct frontend contract?

  3. 3.The board has filters: assignee, label, epic, due-this-week. A product manager wants to share a filtered view with their team by sending a URL. Where should the filter state live, and why?

3 left to answer

Discussion

Sign in to post
Sign in to join the conversation.Sign in

No comments yet. Be the first to say something.