Task Management
Design a Trello-shaped tool: boards contain tasks, tasks move through statuses and get assigned to users. It reads like a straightforward CRUD app, so the prompt is really testing whether you notice the one rule that isn't CRUD: a task can't jump straight from "not started" to "done".
Requirements
Functional
- A
BoardcontainsTasks. - A task has a status -
TODO,IN_PROGRESS, orDONE- and can be assigned to aUser. - Status changes are constrained:
TODO -> IN_PROGRESS -> DONE, andDONE -> TODO(reopening) is allowed, butTODO -> DONEdirectly is not. - A user can list every task assigned to them, filtered by status, across every board they belong to.
Non-functional
- Assignment logic (who can a task be assigned to - anyone, or only board members) needs
to change per board without touching
Taskitself. - Listing "my tasks by status" must not force a linear scan of every task in every board; it should be answerable straight from an index kept in sync as tasks change.
Design
Status transitions are the one place this domain has real rules, so they get pulled out of
Task and into a TaskStatus enum that owns its own canTransitionTo() check - the same
"is this jump legal" question the fee strategy in
Parking Lot answers for pricing, just applied to a state
enum here instead of a pluggable strategy, because the rule set (three statuses, two
directions) is fixed rather than swappable.
This is a cycle, not a one-way pipeline - DONE can move back to TODO (reopening a task),
but nothing skips a step: TODO cannot jump straight to DONE, and IN_PROGRESS cannot
jump back to TODO. Every one of those illegal jumps is exactly what canTransitionTo()'s
switch rejects, and the diagram makes the shape of that rule visible at a glance instead
of requiring a reader to trace the switch statement by hand.
- 1Assignment always goes through the board - it owns the policy for who is eligible.
- 2The board checks its own assignment policy before touching the task at all.
- 3Only once the policy approves does the task record its assignee.
- 4A status change is requested directly on the task, since legality only depends on the current status.
- 5The task delegates the legality check to the enum value itself rather than hard-coding the transition table.
- 6Whichever field changed, the board refreshes that user’s per-status index so lookups stay O(1).
Board maintains the assignable-member policy and a TaskIndex per user that gets updated
on every assignment or status change, so "my tasks in progress" is a lookup, not a scan.
Class diagram
Code
Design decisions
TaskStatus.canTransitionTo()lives on the enum, not inTask.setStatus(). Keeping the legality check next to the values it governs means adding a fourth status (BLOCKED, say) means updating one enum's transition table, not hunting through every method that ever callssetStatus.Task.assign()asks the board, not the user, whether an assignment is allowed. The rule "who can this go to" is a property of the board (a personal to-do board allows anyone; a company board might restrict to members), not of the task or the user, so it's the board'sAssignmentPolicythat gets consulted - swapping policies never touchesTask.- Each user's task list is a maintained index (
Map<Status, Set<Task>>per user), not a filter over every task on every read. The index is updated exactly where a task's assignee or status changes - two call sites - which is cheap enough to keep synchronous and avoids ever re-scanning a board to answer "what's assigned to me." - What's missing for a real system: cross-board task search needs a
denormalized index outside any single
Board(this design's per-user index is board-scoped and would need merging across boards), and concurrent status updates from two clients need optimistic locking onTask.version- both cut here to keep the transition-and-assignment logic the whole focus.
Common follow-ups
- How do you add a BLOCKED status a task can enter from IN_PROGRESS and must leave before
DONE? Add
BLOCKEDto theTaskStatusenum and extend its transition table (IN_PROGRESS -> BLOCKED,BLOCKED -> IN_PROGRESS) -Task.moveTodoesn't change since it only ever callscanTransitionTo. - How would you support cross-board "my tasks" search? Add an index that isn't
board-scoped - a top-level
Map<User, Map<TaskStatus, Set<Task>>>maintained the same way a board's per-board index is, updated from the same two call sites, since board-scoped indexes can't be merged cheaply per query. - What happens if two clients change the same task's status at the same time? Without
extra work, the second write silently wins - a real fix adds a
versionfield toTaskand hasmoveTocheck-and-increment it, rejecting a write based on stale data (optimistic locking). - How would you let a board restrict assignment to only members with a specific role?
Write a new
AssignmentPolicyimplementation (RoleBasedAssignmentPolicy) and construct the board with it -Board.assignTaskandTask.assigndon't change at all, since they only ever callpolicy.canAssign(user).
Check yourself
Why does canTransitionTo() live on the TaskStatus enum instead of inside Task.moveTo()?