Skip to main content

Task Management

Design a Trello-shaped tool: projects contain boards, 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 Project contains one or more Boards; a Board contains Tasks.
  • A task has a status - TODO, IN_PROGRESS, or DONE - and can be assigned to a User.
  • Status changes are constrained: TODO -> IN_PROGRESS -> DONE, and DONE -> TODO (reopening) is allowed, but TODO -> DONE directly is not.
  • A user can list every task assigned to them, filtered by status, across every project 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 Task itself.
  • Listing "my tasks by status" must not force a linear scan of every task in every project; 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.

ClientBoardTaskTaskStatusassignTask(task, user)1policy.canAssign(user)2assign(user)3moveTo(IN_PROGRESS)4canTransitionTo(IN_PROGRESS)5index.update(task)6
  1. 1Assignment always goes through the board - it owns the policy for who is eligible.
  2. 2The board checks its own assignment policy before touching the task at all.
  3. 3Only once the policy approves does the task record its assignee.
  4. 4A status change is requested directly on the task, since legality only depends on the current status.
  5. 5The task delegates the legality check to the enum value itself rather than hard-coding the transition table.
  6. 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

«interface»AssignmentPolicy+ canAssign(u): boolProject- boards: List<Board>Board- tasks: List<Task>- policy: AssignmentPolicy- taskIndex: Map<User, Map<TaskStatus, Set<Task>>>+ assignTask(t, u)+ tasksFor(u, status): Set<Task>Task- title: string- status: TaskStatus- assignee: User+ moveTo(status)+ assign(u)User- id: string- name: string
uses
Task delegates transition legality to TaskStatus; Board maintains an assignment policy and a per-user index.

Code

import java.util.*;
 
enum TaskStatus {
TODO, IN_PROGRESS, DONE;
 
boolean canTransitionTo(TaskStatus next) {
return switch (this) {
case TODO -> next == IN_PROGRESS;
case IN_PROGRESS -> next == DONE;
case DONE -> next == TODO;
};
}
}
 
class User {
final String id;
final String name;
 
User(String id, String name) {
this.id = id;
this.name = name;
}
}
 
class Task {
final String title;
private TaskStatus status = TaskStatus.TODO;
private User assignee;
 
Task(String title) {
this.title = title;
}
 
void moveTo(TaskStatus next) {
if (!status.canTransitionTo(next)) {
throw new IllegalStateException("Cannot move from " + status + " to " + next);
}
status = next;
}
 
void assign(User user) {
this.assignee = user;
}
 
TaskStatus getStatus() { return status; }
User getAssignee() { return assignee; }
}
 
interface AssignmentPolicy {
boolean canAssign(User user);
}
 
class OpenAssignmentPolicy implements AssignmentPolicy {
public boolean canAssign(User user) { return true; }
}
 
class Board {
private final List<Task> tasks = new ArrayList<>();
private final AssignmentPolicy policy;
private final Map<User, Map<TaskStatus, Set<Task>>> taskIndex = new HashMap<>();
 
Board(AssignmentPolicy policy) {
this.policy = policy;
}
 
void addTask(Task task) {
tasks.add(task);
}
 
void assignTask(Task task, User user) {
if (!policy.canAssign(user)) {
throw new IllegalStateException(user.name + " is not allowed on this board");
}
task.assign(user);
reindex(task, user);
}
 
private void reindex(Task task, User user) {
taskIndex.computeIfAbsent(user, u -> new HashMap<>())
.computeIfAbsent(task.getStatus(), s -> new HashSet<>())
.add(task);
}
 
Set<Task> tasksFor(User user, TaskStatus status) {
return taskIndex.getOrDefault(user, Map.of()).getOrDefault(status, Set.of());
}
}

Design decisions

  • TaskStatus.canTransitionTo() lives on the enum, not in Task.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 calls setStatus.
  • 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's AssignmentPolicy that gets consulted - swapping policies never touches Task.
  • 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 project to answer "what's assigned to me."
  • What's missing for a real system: cross-project 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 on Task.version - both cut here to keep the transition-and-assignment logic the whole focus.
0%0 of 122 pages studied