Designing a Sprint Board
Build a flexible drag-and-drop task management system like Trello or Jira
Designing a Sprint Board
Build a flexible digital Sprint Board that helps teams visualize and manage tasks efficiently with drag-and-drop functionality, real-time collaboration, and modern frontend architecture.
Problem Statement
Design a comprehensive task management system similar to Trello, Jira, or Asana with the following capabilities:
- Drag-and-drop task movement between columns and within columns
- Real-time collaboration with multiple users
- Task creation and editing with rich metadata
- Dynamic workflow columns that can be customized
- Task filtering and search capabilities
- Progress tracking and analytics
- Mobile responsiveness for all devices
- Offline support with synchronization
Clarifying Questions
Functional Requirements
- Team Size: How many users will be using the board simultaneously?
- Task Volume: How many tasks per column/board are expected?
- Workflow Complexity: How many columns and workflow stages are needed?
- Collaboration: Do multiple users need to edit the same task simultaneously?
- Integrations: Should it integrate with other tools (GitHub, Slack, etc.)?
- Reporting: What kind of analytics and reporting are required?
Technical Constraints
- Performance: How many tasks should it handle efficiently?
- Real-time Updates: What's the acceptable latency for collaborative updates?
- Data Persistence: How should data be stored and backed up?
- Mobile Support: Should it work on touch devices with gestures?
- Offline Capability: Should it work without internet connection?
High-Level Architecture
1. Component Architecture
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Board View │ │ State Manager │ │ Real-time │
│ (Main App) │◄──►│ (Redux/Zustand)│◄──►│ Sync (WebSocket)│
└─────────────────┘ └─────────────────┘ └─────────────────┘
│ │ │
▼ ▼ ▼
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Column List │ │ Cache Layer │ │ API Client │
│ (Drag Context)│ │ (IndexedDB) │ │ (REST/GraphQL)│
└─────────────────┘ └─────────────────┘ └─────────────────┘2. Core Components
Board Component
- Main container for the entire sprint board
- Drag context provider for cross-column operations
- Real-time updates handling
- Responsive layout management
Column Component
- Task container with drop zones
- Column header with title and task count
- Add task button and column settings
- Scrollable task list with virtualization
Task Component
- Draggable task card with rich content
- Task details (title, description, assignee, due date)
- Priority indicators and status badges
- Quick actions (edit, delete, duplicate)
Drag-and-Drop System
- Cross-column movement with visual feedback
- Within-column reordering with smooth animations
- Drop zone indicators and validation
- Touch gesture support for mobile devices
Drag-and-Drop Implementation
React DnD Setup
import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import { TouchBackend } from 'react-dnd-touch-backend';Task Dragging Logic
interface Task {
id: string;
title: string;
description?: string;
assignee?: string;
priority: 'low' | 'medium' | 'high';
dueDate?: Date;
columnId: string;
order: number;
createdAt: Date;
updatedAt: Date;
}
interface Column {
id: string;
title: string;
order: number;
taskIds: string[];
color?: string;
isArchived: boolean;
}
const useTaskDrag = (task: Task, onMove: (taskId: string, sourceColumnId: string, targetColumnId: string, newOrder: number) => void) => {
const [{ isDragging }, drag] = useDrag({
type: 'TASK',
item: { id: task.id, columnId: task.columnId, type: 'TASK' },
collect: (monitor) => ({
isDragging: monitor.isDragging(),
}),
end: (item, monitor) => {
const dropResult = monitor.getDropResult();
if (item && dropResult) {
const { columnId: targetColumnId, order: newOrder } = dropResult;
onMove(item.id, item.columnId, targetColumnId, newOrder);
}
},
});
return { drag, isDragging };
};
const TaskCard: React.FC<{ task: Task; onMove: Function }> = ({ task, onMove }) => {
const { drag, isDragging } = useTaskDrag(task, onMove);
return (
<div
ref={drag}
className={`task-card ${isDragging ? 'dragging' : ''}`}
style={{
opacity: isDragging ? 0.5 : 1,
cursor: 'grab',
}}
>
<div className="task-header">
<span className={`priority-badge priority-${task.priority}`}>
{task.priority}
</span>
{task.dueDate && (
<span className={`due-date ${isOverdue(task.dueDate) ? 'overdue' : ''}`}>
{formatDate(task.dueDate)}
</span>
)}
</div>
<h4 className="task-title">{task.title}</h4>
{task.description && (
<p className="task-description">{task.description}</p>
)}
<div className="task-footer">
{task.assignee && (
<div className="assignee">
<Avatar user={task.assignee} size="small" />
</div>
)}
<div className="task-actions">
<button onClick={() => onEdit(task.id)}>Edit</button>
<button onClick={() => onDelete(task.id)}>Delete</button>
</div>
</div>
</div>
);
};Column Drop Zone
const useColumnDrop = (columnId: string, onDrop: (taskId: string, columnId: string, order: number) => void) => {
const [{ isOver }, drop] = useDrop({
accept: 'TASK',
drop: (item: { id: string; columnId: string }, monitor) => {
if (item.columnId !== columnId) {
const order = getNewOrder(columnId, monitor.getClientOffset());
onDrop(item.id, columnId, order);
}
},
collect: (monitor) => ({
isOver: monitor.isOver(),
}),
});
return { drop, isOver };
};
const Column: React.FC<{ column: Column; tasks: Task[]; onDrop: Function }> = ({ column, tasks, onDrop }) => {
const { drop, isOver } = useColumnDrop(column.id, onDrop);
return (
<div
ref={drop}
className={`column ${isOver ? 'drop-over' : ''}`}
style={{
backgroundColor: isOver ? '#f0f8ff' : '#f5f5f5',
border: isOver ? '2px dashed #007acc' : '1px solid #ddd',
}}
>
<div className="column-header">
<h3>{column.title}</h3>
<span className="task-count">{tasks.length}</span>
<button onClick={() => onAddTask(column.id)}>+</button>
</div>
<div className="task-list">
{tasks.map((task, index) => (
<TaskCard
key={task.id}
task={task}
onMove={onMoveTask}
/>
))}
</div>
</div>
);
};State Management
Redux Store Structure
interface BoardState {
columns: {
[columnId: string]: Column;
};
tasks: {
[taskId: string]: Task;
};
board: {
id: string;
title: string;
description?: string;
columnOrder: string[];
settings: BoardSettings;
};
ui: {
selectedTask: string | null;
editingTask: string | null;
draggedTask: string | null;
loading: boolean;
error: string | null;
};
collaboration: {
onlineUsers: User[];
typingUsers: { [columnId: string]: string[] };
lastActivity: { [taskId: string]: Date };
};
}
interface BoardSettings {
allowTaskCreation: boolean;
allowTaskEditing: boolean;
allowColumnCreation: boolean;
maxTasksPerColumn: number;
autoArchive: boolean;
notifications: NotificationSettings;
}
interface NotificationSettings {
taskAssigned: boolean;
taskMoved: boolean;
taskCompleted: boolean;
mentions: boolean;
}Actions and Reducers
const boardActions = {
// Task actions
createTask: createAction<{ columnId: string; task: Partial<Task> }>('board/createTask'),
updateTask: createAction<{ taskId: string; updates: Partial<Task> }>('board/updateTask'),
deleteTask: createAction<string>('board/deleteTask'),
moveTask: createAction<{ taskId: string; sourceColumnId: string; targetColumnId: string; newOrder: number }>('board/moveTask'),
// Column actions
createColumn: createAction<Partial<Column>>('board/createColumn'),
updateColumn: createAction<{ columnId: string; updates: Partial<Column> }>('board/updateColumn'),
deleteColumn: createAction<string>('board/deleteColumn'),
reorderColumns: createAction<string[]>('board/reorderColumns'),
// UI actions
selectTask: createAction<string | null>('board/selectTask'),
setEditingTask: createAction<string | null>('board/setEditingTask'),
setDraggedTask: createAction<string | null>('board/setDraggedTask'),
// Collaboration actions
userJoined: createAction<User>('board/userJoined'),
userLeft: createAction<string>('board/userLeft'),
setTypingUser: createAction<{ columnId: string; userId: string; isTyping: boolean }>('board/setTypingUser'),
};
const boardReducer = createReducer(initialState, (builder) => {
builder
.addCase(boardActions.createTask, (state, action) => {
const { columnId, task } = action.payload;
const newTask: Task = {
id: generateId(),
...task,
columnId,
order: state.columns[columnId].taskIds.length,
createdAt: new Date(),
updatedAt: new Date(),
} as Task;
state.tasks[newTask.id] = newTask;
state.columns[columnId].taskIds.push(newTask.id);
})
.addCase(boardActions.moveTask, (state, action) => {
const { taskId, sourceColumnId, targetColumnId, newOrder } = action.payload;
// Remove from source column
const sourceColumn = state.columns[sourceColumnId];
sourceColumn.taskIds = sourceColumn.taskIds.filter(id => id !== taskId);
// Add to target column
const targetColumn = state.columns[targetColumnId];
targetColumn.taskIds.splice(newOrder, 0, taskId);
// Update task
state.tasks[taskId].columnId = targetColumnId;
state.tasks[taskId].updatedAt = new Date();
// Reorder tasks in both columns
this.reorderTasksInColumn(state, sourceColumnId);
this.reorderTasksInColumn(state, targetColumnId);
})
.addCase(boardActions.updateTask, (state, action) => {
const { taskId, updates } = action.payload;
state.tasks[taskId] = {
...state.tasks[taskId],
...updates,
updatedAt: new Date(),
};
});
});Real-Time Collaboration
WebSocket Integration
class BoardWebSocket {
private ws: WebSocket | null = null;
private reconnectAttempts = 0;
private maxReconnectAttempts = 5;
constructor(private boardId: string, private userId: string) {}
connect(): Promise<void> {
return new Promise((resolve, reject) => {
this.ws = new WebSocket(`ws://localhost:3001/board/${this.boardId}?userId=${this.userId}`);
this.ws.onopen = () => {
console.log('Connected to board');
this.reconnectAttempts = 0;
resolve();
};
this.ws.onmessage = (event) => {
const message = JSON.parse(event.data);
this.handleMessage(message);
};
this.ws.onclose = () => {
console.log('Disconnected from board');
this.handleReconnect();
};
this.ws.onerror = reject;
});
}
private handleMessage(message: any) {
switch (message.type) {
case 'TASK_CREATED':
store.dispatch(boardActions.createTask(message.payload));
break;
case 'TASK_UPDATED':
store.dispatch(boardActions.updateTask(message.payload));
break;
case 'TASK_MOVED':
store.dispatch(boardActions.moveTask(message.payload));
break;
case 'USER_JOINED':
store.dispatch(boardActions.userJoined(message.payload));
break;
case 'USER_LEFT':
store.dispatch(boardActions.userLeft(message.payload.userId));
break;
case 'TYPING_UPDATE':
store.dispatch(boardActions.setTypingUser(message.payload));
break;
}
}
sendMessage(type: string, payload: any) {
if (this.ws?.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify({ type, payload }));
}
}
private handleReconnect() {
if (this.reconnectAttempts < this.maxReconnectAttempts) {
this.reconnectAttempts++;
setTimeout(() => {
this.connect().catch(console.error);
}, 1000 * this.reconnectAttempts);
}
}
}UI Components
Main Board Component
const BoardView: React.FC = () => {
const dispatch = useDispatch();
const { columns, tasks, board, ui } = useSelector((state: RootState) => state.board);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
loadBoardData().finally(() => setIsLoading(false));
}, []);
const handleTaskMove = useCallback((taskId: string, sourceColumnId: string, targetColumnId: string, newOrder: number) => {
dispatch(boardActions.moveTask({ taskId, sourceColumnId, targetColumnId, newOrder }));
// Send to server
websocket.sendMessage('TASK_MOVED', { taskId, sourceColumnId, targetColumnId, newOrder });
}, [dispatch]);
const handleTaskCreate = useCallback((columnId: string, taskData: Partial<Task>) => {
dispatch(boardActions.createTask({ columnId, task: taskData }));
// Send to server
websocket.sendMessage('TASK_CREATED', { columnId, task: taskData });
}, [dispatch]);
if (isLoading) {
return <div className="loading">Loading board...</div>;
}
return (
<div className="board-container">
<div className="board-header">
<h1>{board.title}</h1>
<div className="board-actions">
<button onClick={() => setShowAddColumn(true)}>Add Column</button>
<button onClick={() => setShowBoardSettings(true)}>Settings</button>
</div>
</div>
<div className="board-content">
<div className="columns-container">
{board.columnOrder.map(columnId => {
const column = columns[columnId];
const columnTasks = column.taskIds.map(taskId => tasks[taskId]);
return (
<Column
key={columnId}
column={column}
tasks={columnTasks}
onDrop={handleTaskMove}
onCreateTask={handleTaskCreate}
/>
);
})}
</div>
</div>
{ui.selectedTask && (
<TaskModal
task={tasks[ui.selectedTask]}
onClose={() => dispatch(boardActions.selectTask(null))}
onUpdate={(updates) => {
dispatch(boardActions.updateTask({ taskId: ui.selectedTask!, updates }));
websocket.sendMessage('TASK_UPDATED', { taskId: ui.selectedTask!, updates });
}}
/>
)}
</div>
);
};Task Creation Modal
const TaskModal: React.FC<{ task?: Task; onClose: () => void; onUpdate: (updates: Partial<Task>) => void }> = ({ task, onClose, onUpdate }) => {
const [formData, setFormData] = useState({
title: task?.title || '',
description: task?.description || '',
assignee: task?.assignee || '',
priority: task?.priority || 'medium',
dueDate: task?.dueDate ? formatDate(task.dueDate) : '',
});
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
const updates: Partial<Task> = {
title: formData.title,
description: formData.description,
assignee: formData.assignee,
priority: formData.priority as Task['priority'],
dueDate: formData.dueDate ? new Date(formData.dueDate) : undefined,
};
onUpdate(updates);
onClose();
};
return (
<div className="modal-overlay" onClick={onClose}>
<div className="modal-content" onClick={e => e.stopPropagation()}>
<h2>{task ? 'Edit Task' : 'Create Task'}</h2>
<form onSubmit={handleSubmit}>
<div className="form-group">
<label>Title</label>
<input
type="text"
value={formData.title}
onChange={e => setFormData(prev => ({ ...prev, title: e.target.value }))}
required
/>
</div>
<div className="form-group">
<label>Description</label>
<textarea
value={formData.description}
onChange={e => setFormData(prev => ({ ...prev, description: e.target.value }))}
rows={3}
/>
</div>
<div className="form-row">
<div className="form-group">
<label>Assignee</label>
<select
value={formData.assignee}
onChange={e => setFormData(prev => ({ ...prev, assignee: e.target.value }))}
>
<option value="">Unassigned</option>
<option value="user1">John Doe</option>
<option value="user2">Jane Smith</option>
</select>
</div>
<div className="form-group">
<label>Priority</label>
<select
value={formData.priority}
onChange={e => setFormData(prev => ({ ...prev, priority: e.target.value }))}
>
<option value="low">Low</option>
<option value="medium">Medium</option>
<option value="high">High</option>
</select>
</div>
</div>
<div className="form-group">
<label>Due Date</label>
<input
type="date"
value={formData.dueDate}
onChange={e => setFormData(prev => ({ ...prev, dueDate: e.target.value }))}
/>
</div>
<div className="modal-actions">
<button type="button" onClick={onClose}>Cancel</button>
<button type="submit">{task ? 'Update' : 'Create'}</button>
</div>
</form>
</div>
</div>
);
};Performance Optimization
Virtual Scrolling for Large Task Lists
import { FixedSizeList as List } from 'react-window';
const VirtualizedTaskList: React.FC<{ tasks: Task[]; columnId: string }> = ({ tasks, columnId }) => {
const TaskRow = ({ index, style }: { index: number; style: CSSProperties }) => {
const task = tasks[index];
return (
<div style={style}>
<TaskCard task={task} />
</div>
);
};
return (
<List
height={600}
itemCount={tasks.length}
itemSize={120}
width="100%"
>
{TaskRow}
</List>
);
};Optimistic Updates
const useOptimisticUpdate = () => {
const dispatch = useDispatch();
const optimisticMoveTask = useCallback((taskId: string, sourceColumnId: string, targetColumnId: string, newOrder: number) => {
// Optimistic update
dispatch(boardActions.moveTask({ taskId, sourceColumnId, targetColumnId, newOrder }));
// Send to server
return websocket.sendMessage('TASK_MOVED', { taskId, sourceColumnId, targetColumnId, newOrder })
.catch(error => {
// Revert on error
dispatch(boardActions.moveTask({ taskId, targetColumnId, sourceColumnId, newOrder }));
throw error;
});
}, [dispatch]);
return { optimisticMoveTask };
};Security and Validation
Input Validation
const validateTask = (task: Partial<Task>): ValidationResult => {
const errors: string[] = [];
if (!task.title?.trim()) {
errors.push('Task title is required');
}
if (task.title && task.title.length > 200) {
errors.push('Task title must be less than 200 characters');
}
if (task.description && task.description.length > 1000) {
errors.push('Task description must be less than 1000 characters');
}
if (task.dueDate && new Date(task.dueDate) < new Date()) {
errors.push('Due date cannot be in the past');
}
return {
isValid: errors.length === 0,
errors,
};
};Best Practices
Performance
- Virtual Scrolling for large task lists
- Optimistic Updates for immediate feedback
- Debounced API calls to reduce server load
- Image optimization for avatars and attachments
User Experience
- Smooth animations for drag-and-drop operations
- Visual feedback for all user actions
- Keyboard shortcuts for power users
- Mobile-responsive design with touch gestures
Collaboration
- Real-time updates with WebSocket
- Conflict resolution for simultaneous edits
- User presence indicators
- Activity logging and audit trails
Accessibility
- Keyboard navigation support
- Screen reader compatibility
- ARIA labels and roles
- Focus management for modals and forms
Interview Tips
What Interviewers Look For:
- Drag-and-drop implementation understanding
- Real-time collaboration handling
- State management for complex UI interactions
- Performance optimization for large datasets
Sample Questions:
- "How would you handle 10,000 tasks in a column?"
- "What if two users edit the same task simultaneously?"
- "How would you implement undo/redo functionality?"
- "How would you optimize for mobile devices?"
Key Points to Mention:
- Use React DnD or similar library for drag-and-drop
- Implement optimistic updates for better UX
- Plan for real-time collaboration with WebSocket
- Consider performance with virtualization
- Think about accessibility from the beginning
Next: Pinterest Design - Learn how to build a responsive image grid with infinite scrolling.
Designing Pinterest's Frontend Architecture
A comprehensive guide to building a scalable and responsive frontend for a visual discovery platform like Pinterest.
Design Rich Text Editor (WYSIWYG)
Step-by-step guide to building a rich text editor using the browser's native `contentEditable` feature with real-time collaboration and modern React patterns.