Post #5 in the How It Works series. We are building a task management app (simplified Trello) as our running example. Now it is time to plan the work.
“Weeks of coding can save you hours of planning.” Every senior developer has lived through a project where the team jumped straight into code, only to realize weeks later they built the wrong thing, in the wrong order, with components that do not integrate.
Poor planning leads to:
Planning forces the team to think through work before coding, surface risks early, and agree on priorities.
The most valuable planning activity is decomposition — breaking a large project into pieces small enough to understand, estimate, and assign.
| Level | Definition | Example |
|---|---|---|
| Epic | Major capability | User Management |
| Story | User-facing feature | User can register with email |
| Task | Developer work unit | Create User JPA entity |
Our task app epics:
| Epic | Key Stories |
|---|---|
| User Management | Registration, login, password reset |
| Task CRUD | Create, edit, delete, view, assign tasks |
| Board & Column Management | Create board, add columns, drag-and-drop |
| Notifications | Email on assignment, in-app alerts, daily digest |
Each epic is independent enough for a different developer. Decomposition creates parallelism.
The goal is not precision — it is shared understanding of relative size.
Task CRUD estimates:
| Story | Points | Time Range | Notes |
|---|---|---|---|
| Create task | 3 | 1-2 days | Form + API endpoint |
| Edit task | 3 | 1-2 days | Reuse form component |
| Delete task | 2 | 0.5-1 day | Soft delete + confirmation |
| View task details | 2 | 1-2 days | Detail page + activity log |
| Assign task | 5 | 2-4 days | User search, permissions, notification |
If a story estimates above 8 points, break it down further. Large estimates hide uncertainty.
Not everything ships in v1. The MoSCoW method forces explicit decisions:
| Priority | Meaning | Our Features |
|---|---|---|
| Must Have | Product does not work without it | Registration, login, create/view tasks, basic board |
| Should Have | Important but usable without it | Edit, delete, assign tasks, drag-and-drop |
| Could Have | Nice if time allows | Email notifications, task comments |
| Won’t Have | Out of scope for v1 | Daily digest, Slack integration, mobile app |
The “Won’t Have” category is the most important. Saying no explicitly prevents scope creep.
Plan in sprints — 1-2 week windows where the team commits to specific work. Only pull in what the team can finish. Teams operate at 60-70% capacity after meetings, reviews, and bugs.
Sample Sprint 1 (2-week sprint, 2 developers, ~30 points capacity):
| Story | Points | Assignee | Definition of Done |
|---|---|---|---|
| User registration | 5 | Dev A | Register, confirmation email, stored in DB |
| User login/logout | 5 | Dev A | JWT auth, session management |
| Create task | 3 | Dev B | API + UI form, task persisted |
| View task list | 3 | Dev B | Paginated list with status and assignee |
| Board with default columns | 5 | Dev B | To Do, In Progress, Done columns |
| DB schema + scaffolding | 5 | Dev A | Entities, migrations, CI pipeline |
Total: 26 points. Four-point buffer for the unexpected work that will appear.
Product planning is what to build. Technical planning is how. Agree on database schema, API contracts, and architecture decisions before writing application code.
Here is the planned Task entity in Java using JPA:
import jakarta.persistence.*;
import java.time.LocalDateTime;
@Entity
@Table(name = "tasks")
public class Task {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 255)
private String title;
@Column(columnDefinition = "TEXT")
private String description;
@Enumerated(EnumType.STRING)
@Column(nullable = false, length = 20)
private TaskStatus status;
@Enumerated(EnumType.STRING)
@Column(nullable = false, length = 10)
private TaskPriority priority;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "assignee_id")
private User assignee;
@Column(name = "created_at", nullable = false, updatable = false)
private LocalDateTime createdAt;
@Column(name = "updated_at", nullable = false)
private LocalDateTime updatedAt;
@PrePersist
protected void onCreate() {
this.createdAt = LocalDateTime.now();
this.updatedAt = LocalDateTime.now();
}
@PreUpdate
protected void onUpdate() {
this.updatedAt = LocalDateTime.now();
}
}
The same schema in Python using SQLAlchemy:
from sqlalchemy import Column, Integer, String, Text, Enum, ForeignKey, DateTime
from sqlalchemy.orm import relationship, DeclarativeBase
from datetime import datetime
import enum
class Base(DeclarativeBase):
pass
class TaskStatus(enum.Enum):
TODO = "TODO"
IN_PROGRESS = "IN_PROGRESS"
DONE = "DONE"
class TaskPriority(enum.Enum):
LOW = "LOW"
MEDIUM = "MEDIUM"
HIGH = "HIGH"
CRITICAL = "CRITICAL"
class Task(Base):
__tablename__ = "tasks"
id = Column(Integer, primary_key=True, autoincrement=True)
title = Column(String(255), nullable=False)
description = Column(Text, nullable=True)
status = Column(Enum(TaskStatus), nullable=False, default=TaskStatus.TODO)
priority = Column(Enum(TaskPriority), nullable=False, default=TaskPriority.MEDIUM)
assignee_id = Column(Integer, ForeignKey("users.id"), nullable=True)
created_at = Column(DateTime, nullable=False, default=datetime.utcnow)
updated_at = Column(DateTime, nullable=False, default=datetime.utcnow,
onupdate=datetime.utcnow)
assignee = relationship("User", back_populates="tasks")
Both define the same contract: ID, title, description, status, priority, assignee foreign key, and timestamps. Writing models during planning forces alignment between frontend and backend before any application code exists.
Eisenhower said it: “Plans are useless, but planning is everything.”
Your sprint plan will not survive contact with reality. A critical bug derails day three. A dependency team misses their deadline. A 3-point story turns into an 8. That is normal.
The value is the shared understanding built during the process. Decomposing together surfaces hidden dependencies. Estimating together lets the person who built something similar warn about edge cases. Prioritizing together aligns product and engineering on what “done” means.
The plan is a starting point. The process of planning is the actual product. Revisit every sprint. Update. Adjust scope. A plan that never changes is a plan nobody is looking at.