Task management system
Low-level design for a Jira-style task tracker -- projects, boards, task lifecycle (To Do -> In Progress -> Done), assignment, priority, sprint management, and filtering/search.
The Problem
Your engineering team manages work across six repositories using spreadsheets, Slack threads, and sticky notes. Tasks slip through the cracks because nobody knows which items are blocked, which sprint they belong to, or who is actually working on what. A junior developer just spent two days duplicating work that was already in review because the status lived in someone's head.
A task management system like Jira or Linear solves this. It organizes work into projects with boards, tracks each task through a defined lifecycle (To Do, In Progress, In Review, Done), assigns owners, enforces priorities, groups tasks into sprints, and notifies stakeholders when things change. The board becomes the single source of truth for what is happening and who is responsible.
Design the core classes for a task management system that supports projects with boards and columns, a task lifecycle with state validation, assignment and priority, sprint management, task hierarchy (epics containing stories containing subtasks), comment threads, label tagging, filtering/search, and event notifications.
Requirements
Clarifying Questions
Before jumping into class design, ask questions to turn the vague prompt into a concrete specification. Cover four areas: core actions, error handling, boundaries, and future extensions.
You: "What does the task lifecycle look like? A simple To Do / Done split, or a multi-step workflow?"
Interviewer: "Multi-step: TODO, IN_PROGRESS, IN_REVIEW, DONE. Also support a BLOCKED state that a task can enter from any active state and return to its previous state."
Five states with directional transitions. That is a state machine, which points toward the State pattern.
You: "Do tasks have a hierarchy? Can an epic contain stories, and stories contain subtasks?"
Interviewer: "Yes. Epics contain stories, stories contain subtasks. Maximum depth of two levels: epic > story > subtask."
Fixed depth, not arbitrary nesting. We can model this with a parent reference and a TaskType enum rather than a full Composite tree.
You: "Should we support sprints? If so, what does a sprint lifecycle look like?"
Interviewer: "Yes. A sprint has a name, start date, end date, and a backlog of tasks. Sprints move through PLANNING, ACTIVE, and COMPLETED states. Only one sprint per project can be active at a time."
Sprint has its own state machine. Tasks get assigned to a sprint during planning.
You: "How should filtering work? By one attribute at a time, or composable filters like 'all HIGH priority bugs assigned to Alice in the current sprint'?"
Interviewer: "Composable. Users combine multiple filter criteria with AND logic."
Composable filters. That screams Strategy or Specification pattern for filter predicates.
You: "When something changes on a task, who gets notified? Just the assignee, or also watchers?"
Interviewer: "The assignee, the reporter, and anyone who explicitly watches the task. Notify on status changes, new assignments, comments, and approaching due dates."
Multiple event types, multiple listeners. Observer pattern fits perfectly here.
You: "Do we need to support custom workflows where teams define their own columns and transitions?"
Interviewer: "Not in the core design. Mention it as an extension."
Good. We will hardcode the five-state lifecycle for now.
You: "Should tasks within a column be orderable? For example, drag-and-drop reordering or auto-sort by priority?"
Interviewer: "Yes. Support multiple ordering strategies: priority-based, due-date-based, and manual ordering."
Multiple ordering algorithms on the same data. Strategy pattern for column ordering.
You: "Are we handling persistence, or is this in-memory only?"
Interviewer: "In-memory only. Persistence is out of scope."
Perfect. You have now clarified scope and ruled out unnecessary complexity.
Final Requirements
Functional Requirements:
- Create projects, each with a board containing columns (TODO, IN_PROGRESS, IN_REVIEW, DONE, BLOCKED)
- Create tasks with title, description, type (BUG, FEATURE, STORY, EPIC), priority (CRITICAL, HIGH, MEDIUM, LOW), assignee, reporter, labels, and optional due date
- Move tasks between columns with state validation (only valid transitions allowed)
- Support task hierarchy: epics contain stories, stories contain subtasks (max depth 2)
- Create and manage sprints with PLANNING, ACTIVE, COMPLETED lifecycle; assign tasks to sprints
- Add comments to tasks; support @-mention notifications
- Filter tasks by any combination of assignee, priority, label, sprint, status, and type
Non-Functional Requirements:
- Thread safety for concurrent task moves and sprint operations
- Extensibility for new task states, ordering strategies, and filter criteria
- Notification delivery within the same process (no external messaging)
Out of Scope:
- Persistent storage / database
- UI rendering or REST API layer
- Custom workflows (configurable columns/transitions)
- Time tracking
- Task dependencies or DAG execution
- File attachments
Example Inputs and Outputs
Scenario 1: Basic task lifecycle
- Input: Create a FEATURE task "Add dark mode" with HIGH priority, assign to Alice, move through TODO > IN_PROGRESS > IN_REVIEW > DONE
- Expected: Each
moveTask()call validates the transition. Alice and the reporter receive notifications at each state change. The task lands in the DONE column. - Why: Validates the core state machine and observer notification flow.
Scenario 2: Sprint management
- Input: Create a sprint "Sprint 23" (Apr 1-14). Add three tasks to the sprint backlog during PLANNING. Start the sprint. Complete two tasks during the sprint. End the sprint.
- Expected:
startSprint()moves the sprint to ACTIVE. Only one sprint can be active per project.completeSprint()returns a summary with completed count (2) and incomplete count (1). Incomplete tasks are unassigned from the sprint. - Why: Validates sprint lifecycle and the one-active-sprint constraint.
Scenario 3: Composable filtering
- Input: Filter all tasks where priority = HIGH AND status = IN_PROGRESS AND assignee = "alice" AND sprint = "Sprint 23"
- Expected: Returns only tasks matching ALL four criteria. Adding a fifth filter (label = "backend") further narrows results without changing any existing filter code.
- Why: Validates that filters compose with AND logic and new filters plug in via OCP.
Try it yourself
Before reading the solution, spend 15-20 minutes sketching your own class diagram. Focus on the task state machine first: which transitions are valid, and how do you prevent invalid ones? Then think about how filtering should compose. Compare your approach with the walkthrough below.
Step 1: Identify Core Entities
Start by asking: what are the main "things" in this problem? Look for nouns in your requirements. A project contains a board. A board contains columns. Columns contain tasks. Tasks have assignees, priorities, labels, comments, and belong to sprints. Each of these nouns is a candidate entity.
A common mistake is stuffing everything into a giant Task class that knows about boards, sprints, and notifications. Good design means each class has a single, clear job.
| Entity | Responsibility | Key attributes |
|---|---|---|
| Project | Top-level container. Owns a board and sprints. | id, name, members, board, sprints |
| Board | The visual representation. Holds columns. | id, columns |
| Column | Groups tasks by status. Owns ordering. | status, tasks, orderingStrategy |
| Task | The unit of work. Tracks lifecycle state. | id, title, type, priority, status, assignee, reporter, labels, dueDate, parentTask, subtasks |
| User | A team member. Assigns and reports tasks. | id, name, email |
| Sprint | A time-boxed iteration. Contains a backlog of tasks. | id, name, startDate, endDate, state, tasks |
| Comment | A threaded note on a task. | id, author, content, createdAt |
| Label | A tag for categorization. | id, name, color |
Notice that Board is separate from Project because a project could later support multiple boards (Kanban + Scrum) without changing the Project class. Column is separate from Board because each column owns its own ordering strategy and task list. This separation follows SRP.
The TaskStatus and TaskType enums round out the model:
TaskStatus: TODO, IN_PROGRESS, IN_REVIEW, DONE, BLOCKEDTaskType: EPIC, STORY, BUG, FEATUREPriority: CRITICAL, HIGH, MEDIUM, LOWSprintState: PLANNING, ACTIVE, COMPLETED
Step 2: Define Relationships and Class Design
Class Diagram
Deriving the Task Class
Task is the central entity. Every operation flows through it.
Deriving state from requirements:
| Requirement | What Task must track |
|---|---|
| "Move tasks between columns with validation" | Current status, valid transitions |
| "Assign to a user" | Assignee (nullable), reporter |
| "Support hierarchy: epics > stories > subtasks" | parentTask reference, subtasks list, TaskType |
| "Tag with labels" | Set of Label objects |
| "Optional due date" | dueDate (nullable) |
| "Watchers get notified" | Set of watchers |
Deriving methods from needs:
| Need from requirements | Method |
|---|---|
| "Move tasks between columns" | transitionTo(newStatus) |
| "Add comments" | addComment(comment) |
| "Assign to user" | assignTo(user) |
| "Add subtask under a story" | addSubtask(task) |
| "Watch a task" | addWatcher(user) |
The key insight: Board's moveTask() does not decide if the transition is valid. That is the Task's responsibility (it owns the state machine). Board only handles the physical column transfer after the Task approves the transition.
Step 3: Choose Design Patterns
Pattern: State -- for task lifecycle transitions
Continue Reading with Premium
Unlock this article and every other in-depth system design guide on the platform with NotesFromSDE Premium.