You have a clear problem statement. The stakeholders agree on what needs to be built. Now comes the part where most teams either get it right or waste three months heading in the wrong direction: finding the solution.
This is post #3 in the How It Works series. We are building a task management application, something like a simplified Trello, and using it as a running example. In the previous post, we defined the problem. Now we figure out how to solve it.
Before writing a single line of code, study what already exists. Trello, Jira, Asana, Monday.com, and dozens of other tools have already solved variations of this problem. Your job is not to ignore them. Your job is to learn from them.
Open each one. Use them. Take notes on:
Senior engineers spend more time studying existing systems than junior engineers expect. This research saves you from building features that already exist in better form elsewhere.
Once you understand the landscape, you need to make architectural decisions. For our task management app, here are the real trade-offs a small team faces:
Monolith vs. Microservices: You are a small team building an MVP. Pick a monolith. Microservices add deployment complexity, network latency debugging, and distributed system headaches that a three-person team does not need. You can extract services later when you actually hit scaling problems.
SQL vs. NoSQL: Task data is relational. Tasks belong to boards, boards belong to users, users belong to teams. PostgreSQL gives you ACID transactions, mature tooling, and JOINs that make this straightforward. NoSQL shines for unstructured data at massive scale, which is not where you are starting.
REST vs. GraphQL: REST is simpler to implement, easier to cache, and understood by every developer on the planet. GraphQL is powerful when clients need flexible queries across deeply nested data. For a task app with predictable data shapes, REST wins on simplicity.
The pattern here is clear: choose the simpler option unless you have a specific, measurable reason not to.
Your tech stack should be driven by three factors: what your team already knows, what the project requires, and what has a strong ecosystem.
For our task management app, here is a defensible stack:
Notice that none of these choices are exotic. That is intentional. Every technology listed has years of Stack Overflow answers, production war stories, and hiring pools behind it.
Before committing to a full build, write a spike. A spike is a small, throwaway prototype that tests your riskiest assumption. For our app, the riskiest assumption is real-time task updates: when one user moves a card, every other user on that board should see it instantly.
Start with a simple REST endpoint that serves tasks, then layer WebSocket support on top.
Java Spring Boot spike — a basic task controller:
@RestController
@RequestMapping("/api/tasks")
public class TaskController {
private final List tasks = new ArrayList<>(List.of(
new Task(1L, "Set up CI/CD pipeline", "IN_PROGRESS"),
new Task(2L, "Design database schema", "TODO"),
new Task(3L, "Write API documentation", "DONE")
));
@GetMapping
public ResponseEntity> getTasks() {
return ResponseEntity.ok(tasks);
}
@PostMapping
public ResponseEntity createTask(@RequestBody Task task) {
task.setId((long) tasks.size() + 1);
tasks.add(task);
return ResponseEntity.status(HttpStatus.CREATED).body(task);
}
}
Python Flask spike — the same endpoint:
from flask import Flask, jsonify, request
app = Flask(__name__)
tasks = [
{"id": 1, "title": "Set up CI/CD pipeline", "status": "IN_PROGRESS"},
{"id": 2, "title": "Design database schema", "status": "TODO"},
{"id": 3, "title": "Write API documentation", "status": "DONE"},
]
@app.route("/api/tasks", methods=["GET"])
def get_tasks():
return jsonify(tasks), 200
@app.route("/api/tasks", methods=["POST"])
def create_task():
task = request.get_json()
task["id"] = len(tasks) + 1
tasks.append(task)
return jsonify(task), 201
if __name__ == "__main__":
app.run(debug=True)
Both spikes prove you can serve and create tasks through an API. The next step would be adding a WebSocket endpoint to test real-time updates with your frontend. If the spike works, you have validated your riskiest assumption. If it fails, you found out in two days instead of two months.
Every significant architectural decision should be recorded in an Architecture Decision Record (ADR). ADRs capture what you decided, why, and what the consequences are. Six months from now, when someone asks “why PostgreSQL over MongoDB?”, the ADR answers that question.
Here is the standard format:
# ADR-003: Use PostgreSQL for primary data store ## Status Accepted ## Context Our task management app has relational data (users, teams, boards, tasks). The team has production experience with PostgreSQL. We need ACID transactions for task state changes. ## Decision We will use PostgreSQL 15 as the primary database. ## Consequences - Positive: Strong relational model, mature tooling, team familiarity. - Negative: Vertical scaling limits compared to distributed NoSQL. - Mitigation: Add read replicas if query volume grows beyond single-node capacity.
Keep ADRs in your repository, usually in a docs/adr/ directory. They are version-controlled, searchable, and become part of your project’s institutional memory.
Pick boring technology. The latest JavaScript framework with 200 GitHub stars is not what you want in production. Proven tools like Spring Boot, Django, PostgreSQL, and React have survived years of real-world abuse. They have known failure modes, established best practices, and communities that have already solved the problem you will hit at 2 AM on a Saturday. Boring technology lets you focus on your business problem instead of fighting your tools.
In the next post, we will take our chosen stack and start designing the system architecture: data models, API contracts, and component boundaries.