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 "", 204Both 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:
- Validation — Put it in front of real users. Where do they hesitate? Fix those problems now, not after 10,000 lines of code.
- 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.