Design Trello (LLD)
Boards, lists, cards, labels, and drag-drop ordering with activity feeds — Kanban in objects.
Problem Statement
Design the object model for Trello, a Kanban-style collaboration tool. Users create boards, add ordered lists such as To Do or Done, create cards inside those lists, attach labels, assign members, add checklist items, and move cards between lists through drag-drop operations.
The LLD focus is the Board → List → Card ownership chain, how a move is represented as a reversible command, and how board activity is broadcast to an activity feed without coupling the domain model to UI code.
Business context
Trello-style design problems are common at Atlassian and other product companies because they test practical object modelling. The domain looks simple, but it forces decisions around aggregate ownership, ordered collections, card metadata, undoable operations, and collaboration events. A strong answer keeps cards inside lists, lists inside boards, uses a Command for move/undo, exposes an Observer seam for feeds and notifications, and leaves ordering policy pluggable through Strategy.
Functional Requirements
Create a board with multiple ordered lists or columns.
Create cards inside a list with title, description, labels, assigned members, and checklist items.
Move a card from one list to another at a requested target position.
Undo the last move operation by restoring the card to its original list and position.
Publish activity events when cards are created, moved, or move operations are undone.
Allow observers such as an activity feed, notification service, or audit sink to subscribe to a board.
Keep card insertion policy configurable, for example append by default or prioritize urgent labels.
Expose read-only board, list, and card views so callers cannot corrupt ordering directly.
Non-Functional Requirements
Consistency of ordered lists
A card must exist in exactly one list on a board at a time, and move/undo must preserve list order without duplicate card ids.
Extensibility
New card metadata, activity sinks, ordering rules, and future workflow rules should be additive classes or strategies, not edits to every domain object.
Low-latency interactions
Move card should be fast enough for drag-drop UI feedback. The in-memory baseline uses list scans; indexed maps can be added if board size grows.
Encapsulation
Only BoardService and MoveCardCommand mutate list membership. Callers receive unmodifiable views of lists, cards, labels, and checklists.
Auditable actions
Every user-visible mutation should produce an activity event that can later be persisted, indexed, or pushed to subscribers.
Requirement Clarification
QDo we need real-time multi-user collaboration and conflict resolution?
Not in the base LLD. We model a single process domain with clean events. Real-time synchronization can subscribe to the activity stream later.
QShould cards have comments, attachments, due dates, and custom fields?
Represent labels, members, and checklists in the core design. Treat comments, attachments, due dates, and custom fields as extensions on Card.
QHow precise must card ordering be?
Use integer positions for the interview baseline. Production can switch to fractional ranks or sparse ordering without changing the command boundary.
QDoes undo apply to all operations or only moving cards?
Implement undo for move because it is the highest-value drag-drop operation. The same Command interface can be generalized for create, archive, and update.
QAre permissions and board visibility in scope?
Keep permissions out of the core model. A production BoardService would check access before executing commands, but ownership and movement remain the same.
UML Class Diagram
Sequence Diagram
Entity Identification
Board
Aggregate root for a Kanban workspace. Owns ordered lists and board-level observers, and provides list lookup for commands.
BoardList
A column such as To Do, Doing, or Done. Owns ordered cards and guards card insertion, removal, and duplicate id invariants.
Card
Work item inside exactly one list. Carries title, description, labels, assigned members, and checklist items.
Label
Small immutable value object used to tag cards by category, priority, team, or risk.
MoveCardCommand
Captures a requested move plus the original list and index. It can execute once and undo by replaying the inverse mutation.
ActivityObserver
Observer seam for activity feed, notifications, audit logs, and websocket broadcasters. The board only calls onActivity.
BoardService
Application facade around the domain. Creates boards, lists, and cards; builds move commands; and injects the card ordering strategy.
CardOrderingStrategy
Policy interface that decides where newly created cards should be inserted. The default appends; alternatives can prioritize labels.
Design Patterns Used
MoveCardCommand turns drag-drop into an object with execute and undo. It stores the original list and index so reversal is deterministic.
ActivityObserver decouples board mutations from activity feed rendering, notification delivery, and audit persistence.
The domain is a whole-part hierarchy: Board owns BoardList children, and each BoardList owns Card children with metadata. Operations traverse this composition rather than using global card state.
CardOrderingStrategy lets the service vary card insertion policy. Append, urgent-first, or rank-based ordering can change without rewriting card creation.
Step-by-Step Design
1Start with the ownership chain
Model the aggregate as Board → BoardList → Card. The parent owns child lifetime and exposes read-only views so external callers cannot reorder lists or cards directly.
public class Board { private final List<BoardList> lists = new ArrayList<>(); public void addList(BoardList list) { lists.add(Objects.requireNonNull(list)); } public List<BoardList> getLists() { return Collections.unmodifiableList(lists); } }2Let a list guard card ordering
BoardList owns the ordered card collection. It validates insertion positions, blocks duplicate card ids, and returns a removed card to the command.
public void insertCardAt(Card card, int position) { if (position < 0 || position > cards.size()) { throw new IllegalArgumentException("Invalid position"); } if (findCard(card.getId()).isPresent()) { throw new IllegalStateException("Duplicate card id"); } cards.add(position, card); }3Represent move as a reversible command
A drag-drop action needs both current intent and prior state. Store source list, target list, card id, target position, original index, and moved card inside MoveCardCommand.
public void undo() { if (!executed) { throw new IllegalStateException("Command was not executed"); } targetList.removeCard(cardId); sourceList.insertCardAt(movedCard, originalPosition); executed = false; }4Broadcast successful mutations through observers
Board maintains subscribers and emits an activity event after create, move, or undo succeeds. Feed and notification concerns stay outside the domain mutation.
5Inject ordering policy
Card creation asks CardOrderingStrategy for the insertion index. The default is append, but an urgent-first strategy can place high-priority cards at the top.
6Keep service orchestration thin
BoardService checks ids, creates commands, and delegates mutation to domain objects. It does not manually edit raw card arrays outside list methods.
Complete Java Implementation
Explanation of Every Class
Board
The aggregate root. It owns board lists and observers, exposes read-only list views, validates duplicate list ids, and publishes activity events to subscribers.
BoardList
Represents one Kanban column. It owns the ordered list of cards and centralizes insert, remove, lookup, and index operations so card order stays consistent.
Card
Represents a work item with title, description, labels, members, and checklist items. Mutators return the card for clean construction, while getters expose unmodifiable views.
Label
Immutable value object for visual card classification. Equality is id-based, so the same label can be reused across cards safely.
MoveCardCommand
Command object for drag-drop. It removes a card from the source list, inserts it into the target list, records original position, rolls back on failed insert, and supports undo.
ActivityObserver
Observer contract plus nested event and console observer demo. Production observers can write activity feed rows, send notifications, or publish websocket messages.
BoardService
Thin application service that stores boards, creates lists and cards, delegates move behavior to commands, and injects nested ordering strategies.
Main
Small executable demo that creates a board, subscribes a console feed, creates a card with label/member/checklist metadata, moves it, and undoes the move.
Dry Run
Sample input
Board Launch Plan with lists To Do, Doing, Done. Card Build landing page has label High, members alice and bob, and two checklist items. Actions: create in To Do, move to Doing position 0, undo.
| Step | Action | To Do | Doing | Done | Activity |
|---|---|---|---|---|---|
| 1 | createCard(card-1, To Do) | [card-1] | [] | [] | CARD_CREATED |
| 2 | moveCard(To Do → Doing, position 0) | [] | [card-1] | [] | CARD_MOVED |
| 3 | undo() | [card-1] | [] | [] | CARD_MOVE_UNDONE |
| 4 | read board | [card-1 with labels/members/checklist] | [] | [] | No mutation event |
The command captures the original To Do index before removing the card. Undo therefore restores both membership and order, not merely the list name.
Complexity Analysis
| Operation | Time | Space | Note |
|---|---|---|---|
| createBoard | O(1) | O(1) | One map insertion plus a new Board object. |
| createCard | O(C) | O(1) | C is cards in the target list; duplicate id check scans the list before insertion. |
| moveCard | O(S + T) | O(1) | S and T are source and target list sizes; lookup/removal/insertion are list operations. |
| undo move | O(S + T) | O(1) | Inverse remove and insert across the same lists. |
| publish activity | O(O) | O(1) | O observers are invoked synchronously in the baseline. |
For interview clarity the implementation stores ordered cards in arrays. Large boards can add a card id index per board and a sparse rank field per card to reduce scans and avoid shifting many cards.
Extensibility
Comments, attachments, and due dates
Add fields or child value objects to Card. The Board → List → Card ownership and move command stay unchanged.
Persistent activity feed
Implement ActivityObserver with a repository-backed feed writer. Board mutation code still only publishes events.
Different ordering models
Replace integer insertion with a rank object or another CardOrderingStrategy. Existing service methods can keep the same shape.
Permissions and workspace roles
Add an authorization decorator around BoardService to verify board membership before creating cards or executing commands.
Additional undoable operations
Extract a small command interface and implement create-card, archive-card, update-description, and checklist commands with the same execute/undo contract.
Alternative Designs
Global card repository with list ids
Store all cards in a board-level map and put only card ids in lists. Moving a card updates ids and possibly the card's current list field.
Tradeoffs
Faster lookup and persistence mapping, but weaker object ownership. The clean Board → List → Card composition becomes less explicit.
Event-sourced board
Represent create, move, label, assign, and checklist changes as immutable events. Board state is rebuilt by replaying the event stream.
Tradeoffs
Great auditability and collaboration history, but more complex snapshots, replay, and consistency rules for an LLD interview.
Rank-based ordering
Give each card a sparse rank such as 1000, 2000, 3000. Moving between cards assigns a rank between neighbors instead of shifting arrays.
Tradeoffs
Excellent for large lists and concurrent edits, but rank compaction and collision handling add production concerns.
Common Mistakes
- ×
Treating cards as global objects and letting multiple lists point to the same mutable card without a clear owner.
- ×
Moving a card by only changing a status string, which loses list order and makes undo ambiguous.
- ×
Implementing undo by guessing the previous list instead of storing original list and original index in the command.
- ×
Publishing feed messages directly from UI code rather than using an Observer seam on board mutations.
- ×
Exposing mutable card lists to callers, allowing them to bypass duplicate checks and activity events.
- ×
Hard-coding append-only insertion so future priority or rank-based ordering requires rewriting service logic.
- ×
Forgetting to handle rollback if insertion into the target list fails after removal from the source list.
Follow-up Interview Questions
QHow would you support concurrent users moving the same card?
Add optimistic versioning on cards or lists. A command includes the expected version; if another move already changed it, reject or rebase and publish a conflict event.
QHow do you make card lookup O(1)?
Maintain a board-level index from card id to list id and position. BoardList remains the owner of order, while the index accelerates lookup and is updated in move/undo.
QWhere should comments and attachments live?
As child collections on Card or as separate entities keyed by card id if they are large. The move command should not care because it moves the whole card.
QHow would an activity feed persist reliably?
Use an observer that writes feed rows in the same transaction as the command, or use an outbox table so feed delivery can retry without duplicating domain logic.
QHow would you support WIP limits on a list?
Add a policy check to BoardList.insertCardAt or a validation strategy used by MoveCardCommand before insertion. The command boundary remains reusable.
Production Considerations
Persistence model
Persist boards, lists, cards, labels, checklist items, and card ranks with foreign keys that mirror the aggregate. Move card should be transactional.
Concurrency control
Use list or card version columns to prevent lost updates when two users drag the same card or reorder the same list simultaneously.
Activity feed delivery
Feed writes, notifications, and websocket pushes should be idempotent. An outbox pattern keeps observers reliable across process crashes.
Ordering at scale
Integer positions are easy but expensive to shift. Production Trello-like systems often use sparse ranks and periodic compaction.
Permissions and audit
Every command should carry actor identity. The service validates board access and writes actor, timestamp, and before/after metadata for audit.
What Interviewers Look For
Did the candidate preserve the Board → List → Card composition instead of flattening everything into service maps?
Is moving a card a Command with enough state to undo precisely?
Are feed and notification concerns decoupled through Observer?
Is ordering policy represented by Strategy rather than hard-coded conditionals?
Do list methods guard invariants such as no duplicate card ids and valid insertion positions?
Can the candidate explain how this baseline evolves toward persistence, concurrency, and rank-based ordering?
Quiz
0/5 answered
1.Why is **MoveCardCommand** a good fit for drag-drop card movement?
2.Which object should own the ordered collection of cards?
3.What does the Observer pattern decouple in this design?
4.Why inject **CardOrderingStrategy** into **BoardService**?
5.What invariant must hold after every successful move or undo?
Practice Variants
Add comments and mentions
IntermediateAdd a comment collection to Card, include actor and timestamp, and emit activity events when comments mention assigned members.
Implement WIP limits
IntermediateGive BoardList an optional maximum active-card count and reject move commands that would exceed it.
Rank-based drag ordering
AdvancedReplace integer positions with sparse rank strings so cards can move between neighbors without shifting an entire list.
Flashcards
Cheat Sheet
Entities: Board, BoardList, Card, Label, MoveCardCommand, ActivityObserver, BoardService, CardOrderingStrategy.
Composition: Board → BoardList → Card → labels/members/checklist. Keep ownership local and expose read-only views.
Patterns: Command for move/undo, Observer for activity feed, Composite for whole-part hierarchy, Strategy for card ordering.
Move flow: find source and target lists → record original index → remove card → insert at target position → publish activity.
Undo flow: remove from target → insert back into source at original index → publish undo activity.
Invariants: one card belongs to one list; positions are valid; duplicate card ids are rejected; feed is emitted after successful mutation.
Scale path: add board-level card index, sparse ranks, optimistic versions, persistence, outbox-backed observers, and permission checks.
References
- BookDesign Patterns: Elements of Reusable Object-Oriented Software — Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides
- BookEffective Java — Joshua Bloch
- DocsRefactoring Guru — Command Pattern
- DocsRefactoring Guru — Observer Pattern