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

required
required


Implementation

You have your requirements documented, your system designed, and your API contracts agreed upon. Now it is time to build. But implementation is not just “start coding.” It is a disciplined process that separates professional engineers from hobbyists. In this post, we walk through how to turn a design into working software, using our running example: building a task management app (like a simplified Trello).

1. Project Setup

Before writing a single line of business logic, establish your foundation. A clean project setup saves you hundreds of hours over the life of a project.

Set up four things immediately:

  • Project structure — organize code by layer (domain, service, controller, repository) or by feature. Pick one and stay consistent.
  • Version control — initialize a Git repository. Create a .gitignore for your language. Make your first commit the empty project skeleton.
  • CI/CD pipeline — even a basic pipeline that runs tests on every push catches problems before they reach production.
  • Development environment — use Docker or a script so every team member runs the same setup. “It works on my machine” is not acceptable.

A clean project structure for our task management app looks like this:

task-manager/
├── src/
│   ├── main/java/com/taskmanager/
│   │   ├── domain/          # Entities and value objects
│   │   ├── service/         # Business logic
│   │   ├── controller/      # REST endpoints
│   │   ├── repository/      # Data access
│   │   └── config/          # App configuration
│   └── test/java/com/taskmanager/
├── docker-compose.yml
├── Dockerfile
├── .gitignore
└── README.md

This structure tells any developer exactly where to find things. Domain logic is isolated. Controllers are thin. Configuration is centralized. This is not accidental — it is intentional design.

2. Start with the Domain Layer

The most important rule of implementation: build your core business logic first, independent of any framework. Your domain layer should work without Spring Boot, without Flask, without a database. It is pure logic.

Why? Because business rules change slowly. Frameworks change fast. If your business logic is tangled with your framework, every upgrade becomes a rewrite.

Here is the TaskService for our task management app in Java:

@Service
public class TaskService {

    private final TaskRepository taskRepository;

    public TaskService(TaskRepository taskRepository) {
        this.taskRepository = taskRepository;
    }

    public Task createTask(String title, String description, Long assigneeId) {
        if (title == null || title.isBlank()) {
            throw new IllegalArgumentException("Task title cannot be empty");
        }
        Task task = new Task();
        task.setTitle(title.trim());
        task.setDescription(description);
        task.setAssigneeId(assigneeId);
        task.setStatus(TaskStatus.TODO);
        task.setCreatedAt(LocalDateTime.now());
        return taskRepository.save(task);
    }

    public Task getTask(Long id) {
        return taskRepository.findById(id)
            .orElseThrow(() -> new TaskNotFoundException("Task not found: " + id));
    }

    public Task updateStatus(Long id, TaskStatus newStatus) {
        Task task = getTask(id);
        validateStatusTransition(task.getStatus(), newStatus);
        task.setStatus(newStatus);
        task.setUpdatedAt(LocalDateTime.now());
        return taskRepository.save(task);
    }

    private void validateStatusTransition(TaskStatus current, TaskStatus next) {
        if (current == TaskStatus.DONE && next == TaskStatus.TODO) {
            throw new InvalidStatusTransitionException(
                "Cannot move a completed task back to TODO"
            );
        }
    }
}

And the same logic in Python using Flask:

from datetime import datetime
from enum import Enum

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

class TaskService:

    def __init__(self, task_repository):
        self.task_repository = task_repository

    def create_task(self, title, description=None, assignee_id=None):
        if not title or not title.strip():
            raise ValueError("Task title cannot be empty")
        task = {
            "title": title.strip(),
            "description": description,
            "assignee_id": assignee_id,
            "status": TaskStatus.TODO.value,
            "created_at": datetime.utcnow().isoformat()
        }
        return self.task_repository.save(task)

    def get_task(self, task_id):
        task = self.task_repository.find_by_id(task_id)
        if not task:
            raise LookupError(f"Task not found: {task_id}")
        return task

    def update_status(self, task_id, new_status):
        task = self.get_task(task_id)
        self._validate_status_transition(task["status"], new_status)
        task["status"] = new_status
        task["updated_at"] = datetime.utcnow().isoformat()
        return self.task_repository.save(task)

    def _validate_status_transition(self, current, new_status):
        if current == TaskStatus.DONE.value and new_status == TaskStatus.TODO.value:
            raise ValueError("Cannot move a completed task back to TODO")

Notice what both implementations share: input validation, clear error handling, and a status transition rule that enforces business logic. The framework is irrelevant. The logic is identical. That is the sign of a well-designed domain layer.

3. Build the API Layer

With your domain layer solid, the API layer becomes a thin wrapper. Controllers should do three things: accept the request, call the service, and return the response. Nothing more.

Here is a Spring Boot controller for the task endpoints we designed in the previous post:

@RestController
@RequestMapping("/api/v1/tasks")
public class TaskController {

    private final TaskService taskService;

    public TaskController(TaskService taskService) {
        this.taskService = taskService;
    }

    @PostMapping
    public ResponseEntity<Task> createTask(@RequestBody CreateTaskRequest request) {
        Task task = taskService.createTask(
            request.getTitle(),
            request.getDescription(),
            request.getAssigneeId()
        );
        return ResponseEntity.status(HttpStatus.CREATED).body(task);
    }

    @GetMapping("/{id}")
    public ResponseEntity<Task> getTask(@PathVariable Long id) {
        return ResponseEntity.ok(taskService.getTask(id));
    }

    @PatchMapping("/{id}/status")
    public ResponseEntity<Task> updateStatus(
            @PathVariable Long id,
            @RequestBody UpdateStatusRequest request) {
        Task task = taskService.updateStatus(id, request.getStatus());
        return ResponseEntity.ok(task);
    }
}

The controller is deliberately thin. There is no business logic here. No validation beyond what Spring handles automatically. Every decision lives in TaskService where it can be tested independently.

4. Version Control Workflow

Code without version control discipline is a liability. Every professional team follows a branching strategy. Here is a practical workflow that scales from two developers to two hundred:

# Create a feature branch from main
git checkout main
git pull origin main
git checkout -b feature/task-status-update

# Make small, focused commits as you work
git add src/main/java/com/taskmanager/service/TaskService.java
git commit -m "Add status transition validation to TaskService"

git add src/test/java/com/taskmanager/service/TaskServiceTest.java
git commit -m "Add tests for status transition rules"

# Push and open a pull request
git push origin feature/task-status-update

# After code review and CI passes, merge via pull request
# Delete the feature branch after merge
git branch -d feature/task-status-update

The key rules:

  • Never commit directly to main. Every change goes through a feature branch and a pull request.
  • Keep branches short-lived. A branch that lives for more than a few days is a branch that will have merge conflicts.
  • Write descriptive commit messages. “Fixed bug” tells you nothing. “Add status transition validation to prevent completed tasks from reverting to TODO” tells you everything.
  • Require code reviews. A second pair of eyes catches bugs, enforces standards, and spreads knowledge across the team.

5. Code Quality Practices

Writing code that works is the minimum bar. Writing code that others can read, maintain, and extend is the real job. Follow these practices consistently:

Use meaningful names. validateStatusTransition is clear. check is not. A good name eliminates the need for a comment.

Keep functions small. Each function should do one thing. If you need to scroll to read a function, it is too long. The TaskService above has four methods, each under fifteen lines. That is intentional.

Follow SOLID principles. Single Responsibility means your TaskService handles task logic and nothing else. It does not send emails, generate reports, or manage users. Each of those gets its own service.

Handle errors explicitly. Notice that TaskService throws specific exceptions: TaskNotFoundException, InvalidStatusTransitionException. Never swallow exceptions silently. Never return null when you mean “not found.”

Write tests alongside your code. Not after. Not “when you have time.” Every method you write should have a corresponding test before you move to the next method. Tests are not optional — they are part of the implementation.

6. Senior Tip

There is a well-known principle in software engineering: “Make it work, make it right, make it fast” — in that order.

Make it work means get a functioning solution that passes your tests. Do not worry about elegance or performance. Just make it correct.

Make it right means refactor for clarity. Clean up names, extract methods, remove duplication. This is where SOLID principles and clean code practices come in.

Make it fast means optimize, but only where you have measured a bottleneck. Premature optimization is the root of countless engineering disasters. Profile first, then optimize the hotspot.

Most engineers skip step two and jump straight to step three. They end up with fast code that no one can read or maintain. Resist the urge. Readable code that performs adequately will always beat clever code that no one can debug at two in the morning.

Key Takeaway

Implementation is where engineering discipline matters most. Set up your project cleanly. Build the domain layer first. Keep your controllers thin. Use version control like a professional. Write code that your future self will thank you for. Follow the mantra: make it work, make it right, make it fast. In that order, always.

March 19, 2020

Designing and Prototyping

In the previous post, we turned our task management app idea into wireframes. Now it is time to turn those wireframes into something real. This is where you define how the system works, not just how it looks.

1. From Wireframes to Design

Wireframes show structure — where elements go on the page. Design answers everything else: visual identity, interaction patterns, error states, and the system architecture underneath.

For our task app, the wireframes showed boards, columns, and draggable cards. Now we answer the hard questions: How does the frontend talk to the backend? What does the data look like? What happens when two users drag the same card?

Design runs on two parallel tracks:

  • Technical design — architecture, APIs, database schema
  • Visual design — UI components, interactions, responsive behavior

Both must stay in sync. A beautiful drag-and-drop UI backed by an API that cannot handle reordering is a design failure.

2. System Architecture Design

Before writing code, define the high-level architecture. Our task app uses a classic client-server pattern with a REST API layer:

Frontend (React SPA)  --HTTPS/JSON-->  API Gateway (Nginx)  --HTTP-->  Backend (REST API)  --SQL-->  Database (PostgreSQL)
Component Responsibility Technology
Frontend UI rendering, user interactions, local state React, TypeScript
API Gateway Routing, rate limiting, SSL, authentication Nginx or AWS ALB
Backend Business logic, validation, authorization Spring Boot or Flask
Database Persistent storage, data integrity PostgreSQL

3. API Design

The API contract is the agreement between frontend and backend. Design it before implementation so both teams can work in parallel.

Method Endpoint Description
GET /api/v1/boards List all boards for the authenticated user
POST /api/v1/boards Create a new board
GET /api/v1/boards/{id}/tasks List all tasks on a board
POST /api/v1/tasks Create a new task
PUT /api/v1/tasks/{id} Update a task
DELETE /api/v1/tasks/{id} Delete a task
GET /api/v1/users/me Get current user profile

Here is the Spring Boot controller contract defining these endpoints:

// TaskController.java — API contract for task management

@RestController
@RequestMapping("/api/v1")
public class TaskController {

    public record TaskRequest(
        String title, String description,
        String status, Long columnId, Long assigneeId
    ) {}

    public record TaskResponse(
        Long id, String title, String description,
        String status, String columnName, String assigneeName,
        LocalDateTime createdAt, LocalDateTime updatedAt
    ) {}

    @GetMapping("/boards/{boardId}/tasks")
    public ResponseEntity<List<TaskResponse>> getTasksByBoard(
            @PathVariable Long boardId) {
        return ResponseEntity.ok(taskService.findByBoard(boardId));
    }

    @PostMapping("/tasks")
    public ResponseEntity<TaskResponse> createTask(
            @RequestBody @Valid TaskRequest request) {
        return ResponseEntity.status(HttpStatus.CREATED)
            .body(taskService.create(request));
    }

    @PutMapping("/tasks/{id}")
    public ResponseEntity<TaskResponse> updateTask(
            @PathVariable Long id,
            @RequestBody @Valid TaskRequest request) {
        return ResponseEntity.ok(taskService.update(id, request));
    }

    @DeleteMapping("/tasks/{id}")
    public ResponseEntity<Void> deleteTask(@PathVariable Long id) {
        taskService.delete(id);
        return ResponseEntity.noContent().build();
    }
}

The same contract in Flask with Python dataclasses:

# task_controller.py — API contract for task management

from dataclasses import dataclass, asdict
from datetime import datetime
from flask import Flask, request, jsonify

app = Flask(__name__)

@dataclass
class TaskRequest:
    title: str
    description: str
    status: str
    column_id: int
    assignee_id: int

@dataclass
class TaskResponse:
    id: int
    title: str
    description: str
    status: str
    column_name: str
    assignee_name: str
    created_at: datetime
    updated_at: datetime

@app.route("/api/v1/boards/<int:board_id>/tasks", methods=["GET"])
def get_tasks_by_board(board_id):
    tasks = task_service.find_by_board(board_id)
    return jsonify([asdict(t) for t in tasks]), 200

@app.route("/api/v1/tasks", methods=["POST"])
def create_task():
    data = TaskRequest(**request.get_json())
    return jsonify(asdict(task_service.create(data))), 201

@app.route("/api/v1/tasks/<int:task_id>", methods=["PUT"])
def update_task(task_id):
    data = TaskRequest(**request.get_json())
    return jsonify(asdict(task_service.update(task_id, data))), 200

@app.route("/api/v1/tasks/<int:task_id>", methods=["DELETE"])
def delete_task(task_id):
    task_service.delete(task_id)
    return "", 204

Both implementations define the same contract. The DTOs enforce data shape. The HTTP methods and status codes follow REST conventions. This is the specification both teams work against.

4. Database Design

The database schema is the foundation everything rests on. Here is the schema for our task app:

Table Column Type Constraint
users id, email, display_name, created_at BIGINT, VARCHAR, VARCHAR, TIMESTAMP PK, UNIQUE NOT NULL, NOT NULL, DEFAULT NOW()
boards id, name, owner_id, created_at BIGINT, VARCHAR, BIGINT, TIMESTAMP PK, NOT NULL, FK -> users(id), DEFAULT NOW()
columns id, name, board_id, position BIGINT, VARCHAR, BIGINT, INTEGER PK, NOT NULL, FK -> boards(id), NOT NULL
tasks id, title, description, column_id, assignee_id, position, created_at, updated_at BIGINT, VARCHAR, TEXT, BIGINT, BIGINT, INTEGER, TIMESTAMP, TIMESTAMP PK, NOT NULL, NULLABLE, FK -> columns(id), FK -> users(id), NOT NULL, DEFAULT NOW(), DEFAULT NOW()

Key relationships: A user owns many boards. A board has many columns. A column has many tasks. A task belongs to one column and one assignee. The position field enables drag-and-drop reordering — move a card and you update the position integers.

5. UI/UX Design

The UI/UX design phase turns wireframes into pixel-perfect mockups. Build a design system — a shared vocabulary of reusable components:

  • Colors — Primary blue (#2563EB) for actions, grays for backgrounds, red for destructive actions
  • Typography — Inter for UI text, monospace for IDs
  • Components — Task cards, column headers, board nav, modals, dropdowns
  • Spacing — 4px base unit for consistent padding and margins

Tools like Figma and Adobe XD let designers and developers collaborate on the same file, inspect spacing, and export assets.

Critical principle: design for states, not just screens. Every component needs empty, loading, error, and populated state designs.

6. Prototyping

A prototype is a clickable, interactive mockup that stakeholders can experience without production code. Click “Create Task” and a modal opens. Drag a card between columns. Navigate between boards.

Prototyping serves two purposes:

  1. Validation — Put it in front of real users. Where do they hesitate? Fix those problems now, not after 10,000 lines of code.
  2. Alignment — Walk stakeholders through the prototype. Misunderstandings surface early, not during code review.

Figma’s prototyping mode lets you define click targets, transitions, and flows directly on design frames. Remember: a prototype is not production code. It is a communication tool.

7. Senior Tip

“Design is not how it looks, it’s how it works.” — Steve Jobs

Every hour spent on design saves five hours of rework during implementation. Focus on user experience over aesthetics. A plain-looking app with clear navigation and predictable behavior beats a stunning app that confuses users. Get the API contracts right. Get the data model right. The colors are the easy part.

In the next post, we start building. That is where implementation begins.

March 19, 2020

Wireframing

This is post #6 in the How It Works series. We are building a task management app (simplified Trello) as our running example. In the previous post on Planning, we defined what we are building and why. Now it is time to figure out what it looks like before writing a single line of code.

1. What is Wireframing?

A wireframe is a low-fidelity visual blueprint of your application. It defines the layout and user flow of each screen — no colors, no fonts, no pixel-perfect polish. Think of it as the architectural blueprint you review before the construction crew shows up.

A wireframe answers three questions:

  • What screens exist? — Login, Dashboard, Task Detail, Settings.
  • What goes on each screen? — Navbar at top, sidebar left, content center.
  • How do users move between screens? — Click a task card to open details. Click the gear icon for settings.

Wireframes are intentionally ugly. If you are picking shades of blue, you have wandered into design. That comes later.

2. Why Developers Should Care

Many developers skip wireframing because they think it is a designer’s job. That is a mistake. Wireframes prevent the most expensive pattern in software: building, then redesigning. Without a shared visual plan, three things go wrong:

  1. Scope creep. New screens materialize mid-sprint that nobody agreed on because nobody drew them out.
  2. Frontend/backend mismatch. The backend returns a flat list; the frontend expected a nested tree. A wireframe would have made the data shape obvious.
  3. Wasted refactoring. You build a single-column layout, then stakeholders want a two-panel view. A 30-second sketch would have caught it.

Wireframing is cheap. Refactoring is not.

3. Types of Wireframes

Type Fidelity Tools When to Use
Low-fidelity Boxes, lines, labels Pen & paper, whiteboard Early brainstorming, first 30 minutes of a project
Mid-fidelity Structured layouts with placeholder text Balsamiq, Figma wireframe mode Sprint planning, stakeholder reviews
High-fidelity Near-final look with realistic content Figma, Sketch, Adobe XD Final design approval before implementation

Start low, go higher. Begin with pen and paper. If the idea survives that conversation, move to a mid-fidelity tool. High-fidelity wireframes are worth it only when you need stakeholder sign-off before committing to code.

4. Wireframing Our Task Management App

Let us wireframe the four core screens of our simplified Trello clone.

Login Page

A centered card on a blank background. The card contains a logo placeholder at the top, an email input field, a password input field, a “Log In” button spanning the full width of the card, and a “Sign Up” link below it. No sidebar, no navigation bar. The user has one job: authenticate.

Dashboard / Board View

The main screen after login. A top navbar spans full width — app name on the left, user avatar dropdown on the right. Below it, three vertical columns represent statuses: To Do, In Progress, and Done. Each column contains stacked task cards (title + assignee avatar) with an “+ Add Task” button at the bottom. Classic Kanban layout.

Task Detail Modal

Clicking a task card opens a modal overlay. Editable title at top, description text area below, and a right sidebar showing metadata: assignee, due date, priority, status dropdown. Comments section at the bottom. The dashboard stays dimmed behind the modal so the user retains spatial context.

Settings Page

Standard settings layout. Left sidebar lists setting categories: Profile, Notifications, Team Members, Integrations. The right content area changes based on the selected category. The Profile section shows avatar upload, name, and email fields. The Notifications section shows toggle switches. Keep it simple — settings pages rarely need creative layouts.

5. From Wireframe to Code Structure

Every screen in your wireframe maps to a route, a controller method, and a template. When the wireframe is done, your routing structure writes itself.

Here is the route structure for our task app in Spring Boot (Java):

@Controller
public class TaskAppController {

    @GetMapping("/login")
    public String loginPage() {
        // Wireframe: Login Page - centered card, email/password fields
        return "login";
    }

    @GetMapping("/dashboard")
    public String dashboard(Model model) {
        // Wireframe: Board View - three columns (To Do, In Progress, Done)
        List<Task> tasks = taskService.getAllTasksForUser();
        model.addAttribute("tasks", tasks);
        return "dashboard";
    }

    @GetMapping("/tasks/{id}")
    public String taskDetail(@PathVariable Long id, Model model) {
        // Wireframe: Task Detail Modal - title, description, metadata sidebar
        Task task = taskService.getTaskById(id);
        model.addAttribute("task", task);
        return "task-detail";
    }

    @GetMapping("/settings")
    public String settings() {
        // Wireframe: Settings Page - sidebar categories, right content area
        return "settings";
    }

    @GetMapping("/settings/{section}")
    public String settingsSection(@PathVariable String section, Model model) {
        // Wireframe: Settings subsections - profile, notifications, team, integrations
        model.addAttribute("section", section);
        return "settings";
    }
}

And the same structure in Flask (Python):

from flask import Flask, render_template, redirect, url_for
from services import task_service

app = Flask(__name__)

@app.route("/login")
def login_page():
    """Wireframe: Login Page - centered card, email/password fields"""
    return render_template("login.html")

@app.route("/dashboard")
def dashboard():
    """Wireframe: Board View - three columns (To Do, In Progress, Done)"""
    tasks = task_service.get_all_tasks_for_user()
    return render_template("dashboard.html", tasks=tasks)

@app.route("/tasks/<int:task_id>")
def task_detail(task_id):
    """Wireframe: Task Detail Modal - title, description, metadata sidebar"""
    task = task_service.get_task_by_id(task_id)
    return render_template("task_detail.html", task=task)

@app.route("/settings")
@app.route("/settings/<section>")
def settings(section="profile"):
    """Wireframe: Settings Page - sidebar categories, right content area"""
    return render_template("settings.html", section=section)

if __name__ == "__main__":
    app.run(debug=True)

Every route maps to a wireframe screen. Every template name matches a wireframe label. No guessing about what pages the app needs — the wireframe already defined them. This is why wireframing matters for developers: it turns vague requirements into a concrete routing table before you write any business logic.

6. Tools

  • Pen & paper — Fastest option. Zero learning curve. Use for initial brainstorming.
  • Balsamiq — Sketch-style keeps conversations focused on layout, not aesthetics.
  • Figma — Industry standard. Quick wireframes to high-fidelity prototypes. Generous free tier.
  • Sketch — macOS-only. Solid for high-fidelity work.
  • Excalidraw — Open-source, browser-based whiteboard. Great for remote teams.

The tool matters far less than the habit. A napkin sketch beats no wireframe every time.

7. Senior Tip

Show wireframes to real users early. Not after polishing. Not after development starts. Show the rough sketch and watch what happens. Where they squint, you have a clarity problem. Where they ask “how do I get back,” you have a navigation problem. Where they ignore a section, you might not need it.

Their confusion is your UX debt — cheaper to fix now by erasing a box than later by refactoring components and rewriting endpoints.

Also: wireframe the unhappy paths. What does the screen look like with zero tasks? What happens when the API fails? If you only wireframe the golden path, you will discover edge cases in production.

What is Next

With our wireframes defined and our route structure mapped, the next step in the series is Designing and Prototyping — where we take these skeletal layouts and add the visual layer: color, typography, spacing, and interaction design. The wireframe tells us what goes where. The design tells us how it looks and feels.

March 19, 2020

Planning

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.

1. Why Planning Matters

“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:

  • Scope creep — Every meeting adds “one more feature” until the deadline is fantasy.
  • Missed deadlines — You cannot predict delivery if you do not know how much work exists.
  • Burnt-out teams — Constant context-switching and moving targets kill motivation.
  • Integration nightmares — Teams build components that do not fit together.

Planning forces the team to think through work before coding, surface risks early, and agree on priorities.

2. Break It Down

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.

3. Estimate Effort

The goal is not precision — it is shared understanding of relative size.

  • Story points — Fibonacci (1, 2, 3, 5, 8, 13). Track velocity over time.
  • T-shirt sizing — S, M, L, XL. Fast for roadmap-level planning.
  • Time-based ranges — “2-5 days” not “3 days.” Ranges communicate uncertainty honestly.

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.

4. Prioritize

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.

5. Sprint Planning

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.

6. Technical Planning

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.

7. Senior Tip

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.

March 19, 2020

Validating the Solution

This is Part 4 of the How Software Engineering Works series. We are building a task management app (think simplified Trello) from scratch.

You have identified the problem. You have brainstormed solutions. Now comes the step most developers skip entirely: validation. Before you write a single line of production code, you need proof that your solution is worth building.

Why Validate Before Building?

Building the wrong thing is the most expensive mistake in software engineering. It is not a bug you can patch in a sprint. It is weeks or months of wasted effort, burned budget, and team morale damage. Studies consistently show that 70-80% of features in enterprise software are rarely or never used.

Validation is cheap. Building is expensive. A week of validation can save you six months of building something nobody wants. As a senior developer, I treat validation as a non-negotiable phase, not an optional nice-to-have.

Validate the Problem

Before you validate the solution, validate the problem itself. Do real users actually experience the pain you think they do?

For our task management app, the question is: Do small teams really struggle with task tracking?

Here is how you find out:

  • User interviews – Talk to 5-10 people in your target audience. Ask open-ended questions. “How do you currently track tasks?” not “Would you use a task management app?”
  • Surveys – Send a short survey to a broader group. Keep it under 5 questions. Focus on their current pain, not your proposed solution.
  • Competitor analysis – If Trello, Asana, and Jira exist, the problem is validated. But are they solving it well for your target audience? Small teams often find Jira bloated and overwhelming.

If nobody confirms the problem exists, stop. You just saved yourself months.

Validate the Solution

The problem is confirmed. Now test whether your specific approach solves it. You do not need code for this.

  • Paper prototypes – Sketch your UI on paper. Put it in front of users and ask them to “use” it. You will learn more in 30 minutes than in 30 days of coding.
  • Mockups – Use Figma or Balsamiq to create clickable wireframes. Share them and watch how people interact with the design.
  • Landing page test – Create a simple landing page describing your app. Add a “Sign up for early access” button. If nobody signs up, your solution does not resonate.
  • Fake door test – Add a button for a feature that does not exist yet. Measure how many people click it. High clicks mean high demand.

For our task management app, we might create a simple mockup showing boards, columns, and drag-and-drop cards. If test users say “this looks just like Trello,” we know we need stronger differentiation.

Technical Feasibility

Can your team actually build this? Before committing, build a proof of concept (POC) for the hardest technical piece. Do not build the whole app. Build the one thing you are least sure about.

For our app, the riskiest part might be real-time collaboration. Can we sync task updates across multiple users instantly? Let us write a quick POC to find out.

Here is a minimal Java prototype, an in-memory task store to validate the core concept quickly:

import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

// Throwaway POC - NOT production code
public class TaskStorePOC {

    private final Map> tasks = new ConcurrentHashMap<>();

    public String createTask(String title, String assignee) {
        String id = UUID.randomUUID().toString().substring(0, 8);
        Map task = new HashMap<>();
        task.put("id", id);
        task.put("title", title);
        task.put("assignee", assignee);
        task.put("status", "TODO");
        task.put("created", System.currentTimeMillis());
        tasks.put(id, task);
        return id;
    }

    public Map getTask(String id) {
        return tasks.get(id);
    }

    public void moveTask(String id, String newStatus) {
        Map task = tasks.get(id);
        if (task != null) {
            task.put("status", newStatus);
        }
    }

    public List> getTasksByStatus(String status) {
        List> result = new ArrayList<>();
        for (Map task : tasks.values()) {
            if (status.equals(task.get("status"))) {
                result.add(task);
            }
        }
        return result;
    }

    public static void main(String[] args) {
        TaskStorePOC store = new TaskStorePOC();
        String id1 = store.createTask("Design database schema", "Alice");
        String id2 = store.createTask("Set up CI/CD pipeline", "Bob");

        store.moveTask(id1, "IN_PROGRESS");

        System.out.println("TODO: " + store.getTasksByStatus("TODO"));
        System.out.println("IN_PROGRESS: " + store.getTasksByStatus("IN_PROGRESS"));
    }
}

And the same concept in Python, a quick throwaway prototype using a dictionary:

import uuid
from datetime import datetime

# Throwaway POC - NOT production code
tasks = {}

def create_task(title, assignee):
    task_id = str(uuid.uuid4())[:8]
    tasks[task_id] = {
        "id": task_id,
        "title": title,
        "assignee": assignee,
        "status": "TODO",
        "created": datetime.now().isoformat()
    }
    return task_id

def move_task(task_id, new_status):
    if task_id in tasks:
        tasks[task_id]["status"] = new_status

def get_tasks_by_status(status):
    return [t for t in tasks.values() if t["status"] == status]

# Quick validation
id1 = create_task("Design database schema", "Alice")
id2 = create_task("Set up CI/CD pipeline", "Bob")
move_task(id1, "IN_PROGRESS")

print("TODO:", get_tasks_by_status("TODO"))
print("IN_PROGRESS:", get_tasks_by_status("IN_PROGRESS"))

Notice these prototypes use in-memory storage. No database, no API layer, no authentication. That is intentional. This code exists to answer one question: does the core data model work? If it does, we move forward. If it does not, we pivot before investing in infrastructure.

Market Validation

Technical feasibility is only half the equation. Is there a market for this?

  • Competitor landscape – Trello (simple, visual), Asana (mid-range), Jira (enterprise). Where is the gap? For our app, the gap might be teams of 2-5 people who want something simpler than Trello with built-in time tracking.
  • Differentiation – If you cannot finish the sentence “Unlike [competitor], our app…” then you do not have a differentiator. That is a red flag.
  • Willingness to pay – Ask potential users directly: “Would you pay $5/month for this?” Watch their reaction. Polite interest means no. Genuine excitement means maybe.

Build a Prototype

Once problem, solution, technical feasibility, and market are validated, build a throwaway prototype. This is not production code. This is a tool to test your remaining assumptions with real users.

Key rules for prototypes:

  • Time-box it to 1-2 weeks maximum
  • Skip authentication, error handling, and edge cases
  • Hard-code what you can
  • Use the fastest tools available, not the “right” tools
  • Plan to throw it away completely

The goal is learning speed, not code quality. If your prototype validates the remaining assumptions, you now have confidence to invest in building the real thing.

Senior Tip

Fall in love with the problem, not the solution.

The most dangerous trap in engineering is emotional attachment to your solution. I have seen senior engineers spend months defending an architecture that users do not want, simply because they already built it. Validation exists to protect you from this. If your research shows users need something different from what you proposed, that is not failure. That is the process working exactly as it should. Be willing to pivot. The best engineers I have worked with treat every assumption as a hypothesis to be tested, not a truth to be defended.

Next up: Part 5 – Planning, where we turn our validated solution into an actionable roadmap.

March 19, 2020