Understanding the Problem

This is post #2 in our How It Works series. Our running example is building a task management app — a simplified Trello for small dev teams. In this post, we cover the step that separates experienced engineers from everyone else: understanding the problem before writing a single line of code.

Start With “Why”

The number one mistake developers make is jumping straight to code. Someone mentions an idea and within an hour there is a half-built prototype with zero clarity on what the software actually needs to do.

Before you open your editor, answer two questions:

  1. What problem are we solving?
  2. Who has this problem?

If you cannot answer both clearly, you are not ready to build. Every hour spent understanding the problem saves ten hours of rework later.

Talk to Stakeholders

Requirements come from the people who will use your software. Proven techniques:

  • User interviews: Ask 3–5 potential users open-ended questions. “Walk me through how you currently assign tasks” reveals more than “Would you use a task board?”
  • Surveys: Validate patterns from interviews. Keep them to 5–10 questions.
  • Observe workflows: Watch people work. You will discover pain points they have stopped noticing.

Ask “What are you trying to accomplish?” not “What feature do you want?” For our task app, interviews revealed a consistent theme: nobody knew who was working on what.

Define the Problem Statement

Distill your findings into a single, clear problem statement — specific, measurable, and free of solution language.

Bad: “We need a Kanban board.”
Good: “Small development teams lose track of who is working on what, leading to duplicated effort and missed deadlines.”

The bad version assumes a solution. The good version describes the pain. Write it down and put it somewhere visible — every decision should trace back to it.

Identify Constraints

Every project operates within boundaries. Map them early:

Technical: Budget, timeline, team size, tech stack knowledge, infrastructure limits.

Business: Compliance requirements (GDPR, SOC 2), required integrations (Slack, GitHub), expected user base size.

For our app: 2 developers, 6-week timeline, GitHub integration required, targeting teams of 5–20 people.

User Stories

User stories translate the problem into buildable units: “As a [role], I want [feature], so that [benefit].”

  1. As a team lead, I want to create and assign tasks, so that everyone knows who is responsible for what.
  2. As a developer, I want to update my task status, so that the team sees my progress without asking.
  3. As a team lead, I want to see tasks grouped by status, so that I can identify bottlenecks quickly.
  4. As a developer, I want to link tasks to GitHub PRs, so that code reviews and tracking stay connected.

Modeling stories as structured data ensures every story is complete and traceable:

public class UserStory {

    private String role;
    private String action;
    private String benefit;
    private List<String> acceptanceCriteria;

    public UserStory(String role, String action, String benefit) {
        this.role = role;
        this.action = action;
        this.benefit = benefit;
        this.acceptanceCriteria = new ArrayList<>();
    }

    public void addCriteria(String criteria) {
        this.acceptanceCriteria.add(criteria);
    }

    public String format() {
        return String.format(
            "As a %s, I want to %s, so that %s.",
            role, action, benefit
        );
    }

    public static void main(String[] args) {
        UserStory story = new UserStory(
            "team lead",
            "create tasks and assign them to team members",
            "everyone knows who is responsible for what"
        );
        story.addCriteria("Lead can create a task with title and description");
        story.addCriteria("Lead can assign a task to any team member");
        story.addCriteria("Assigned member receives a notification");

        System.out.println(story.format());
    }
}

Acceptance Criteria

User stories tell you what to build. Acceptance criteria tell you when it is done — testable conditions that leave no room for ambiguity.

For “As a developer, I want to update my task status,” the criteria might be:

  • Valid statuses: TODO, IN_PROGRESS, IN_REVIEW, DONE
  • Only the assignee or a team lead can change status
  • Each change records a timestamp and the user who made it

The best way to ensure criteria are testable is to write them as executable tests:

import pytest
from enum import Enum
from datetime import datetime


class TaskStatus(Enum):
    TODO = "TODO"
    IN_PROGRESS = "IN_PROGRESS"
    IN_REVIEW = "IN_REVIEW"
    DONE = "DONE"


class Task:
    def __init__(self, title, assignee):
        self.title = title
        self.assignee = assignee
        self.status = TaskStatus.TODO
        self.history = []

    def update_status(self, new_status, changed_by):
        if changed_by != self.assignee and changed_by.role != "lead":
            raise PermissionError("Only assignee or lead can update")
        self.history.append({
            "from": self.status, "to": new_status,
            "by": changed_by.name, "at": datetime.now()
        })
        self.status = new_status


class User:
    def __init__(self, name, role="developer"):
        self.name = name
        self.role = role


def test_task_starts_as_todo():
    task = Task("Build login page", User("Alice"))
    assert task.status == TaskStatus.TODO


def test_assignee_can_update_status():
    alice = User("Alice")
    task = Task("Build login page", alice)
    task.update_status(TaskStatus.IN_PROGRESS, alice)
    assert task.status == TaskStatus.IN_PROGRESS


def test_lead_can_update_any_task():
    alice = User("Alice")
    lead = User("Bob", role="lead")
    task = Task("Build login page", alice)
    task.update_status(TaskStatus.IN_REVIEW, lead)
    assert task.status == TaskStatus.IN_REVIEW


def test_non_assignee_cannot_update():
    task = Task("Build login page", User("Alice"))
    with pytest.raises(PermissionError):
        task.update_status(TaskStatus.DONE, User("Charlie"))


def test_status_change_records_history():
    alice = User("Alice")
    task = Task("Build login page", alice)
    task.update_status(TaskStatus.IN_PROGRESS, alice)
    assert len(task.history) == 1
    assert task.history[0]["from"] == TaskStatus.TODO
    assert task.history[0]["to"] == TaskStatus.IN_PROGRESS

Each test maps directly to an acceptance criterion. When all tests pass, the story is done. No debate, no ambiguity.

A Senior Developer’s Tip

The projects that succeed spend more time in this phase than you expect. The projects that fail skip it entirely. Resist the urge to start coding immediately. When you feel the pull to “just start building,” write a problem statement instead. If you cannot write one that is clear and specific, you do not understand the problem yet — and your code will not solve it.

In the next post, we move into Finding the Solution — where we explore possible approaches, evaluate trade-offs, and choose the right path forward.




Subscribe To Our Newsletter
You will receive our latest post and tutorial.
Thank you for subscribing!

required
required


Leave a Reply

Your email address will not be published. Required fields are marked *