C++ Capstone: Build a Task Tracker
Plan, implement, test, and document a multi-file command-line task tracker using modern C++ ownership, containers, algorithms, files, and CMake.
Turn user outcomes into testable requirements
Build a complete feature through model storage and console layers
Verify edge cases and document build and run steps
The short answer
Build the task tracker as vertical slices: one valid task, a vector-backed list, status changes, filtered summaries, persistence, and tests. Finish the promised behavior before adding frameworks or speculative abstractions.
Define observable outcomes first
Write requirements as behavior a user or test can observe. Adding “Pay electricity bill” creates one incomplete task with a stable identifier. Completing that identifier changes its status once. Restarting the program restores successfully saved tasks. “Use polymorphism” is not a user outcome.
Define error cases too: blank title, malformed command, unknown identifier, missing first-run file, and malformed saved record. Decide whether each is rejected, skipped with a warning, or fatal before implementation spreads inconsistent rules.
- Add a task with a non-empty title and generated identifier.
- List all tasks with completion status.
- Complete an existing incomplete task.
- Filter pending and completed tasks.
- Save after a successful change and reload on startup.
- Quit explicitly without losing confirmed work.
Model valid task state
Start with a Task value containing identifier, title, and completion state. A constructor or factory validates the title and identifier. A complete operation owns the transition instead of exposing unrestricted status assignment.
A TaskList can own std::vector<Task>, generate identifiers, find tasks, and calculate filtered views. Do not put console prompts or file paths in these domain types; tests should call them with typed values.
class Task
{
public:
Task(int id, std::string title)
: id_{id}, title_{std::move(title)}
{
if (id_ <= 0 || title_.empty())
throw std::invalid_argument{"Invalid task"};
}
void complete() { completed_ = true; }
int id() const { return id_; }
const std::string& title() const { return title_; }
bool completed() const { return completed_; }
private:
int id_;
std::string title_;
bool completed_{};
};Build one vertical slice at a time
First make Add travel from parsed command to validated Task to vector to visible output. Test it. Then add List. Next add Complete, including the unknown identifier path. Only after in-memory behavior works should persistence enter the flow.
Commit each working slice. Large speculative layers make failures harder to localize and encourage abstractions that have no caller. Extract a boundary when it clarifies ownership, isolates I/O, or supports a demonstrated second implementation.
| Slice | Proof | Edge case |
|---|---|---|
| Add | Task appears once | Blank title |
| List | Every task and status visible | Empty list |
| Complete | Target status changes | Unknown and already-complete ID |
| Filter | Correct subset returned | No matches |
| Save/load | Round trip preserves state | Missing or malformed file |
Add persistence without mixing concerns
Define a repository boundary that saves and loads task values. The file implementation owns paths, serialization, parsing, and I/O errors. The domain collection owns task rules. The console layer decides what message a user sees after a recoverable failure.
Write a complete representation, verify the stream, and replace durable data carefully. Do not truncate the only good file before knowing the replacement is valid. Include a format version if later field changes are plausible.
Test package and review the result
Automated tests should cover title validation, identifier generation, completion transitions, unknown identifiers, filters, and a save-load round trip in a temporary location. A manual checklist should cover prompts, malformed commands, file permissions, and first run.
Provide a README with prerequisites, configure, build, test, and run commands. Remove machine-specific paths and build artifacts. Run from a clean checkout or ask another person to follow the instructions without unstated setup.
Review ownership: containers and values should do most work, raw owning pointers should be absent, and interfaces should represent actual variation. Review warnings and sanitizer results. The finished project should be small, understandable, and reliable before it becomes ambitious.
Quick knowledge check
Answer before you reveal.
01Why build one vertical slice before all classes and interfaces?
It proves a real user outcome through the layers and reveals necessary boundaries with working evidence.
02What makes the capstone complete?
Promised flows work, invalid cases are deliberate, data survives restart, tests protect core rules, and another person can build and run it.
Exercise
Practice challenge
Complete a task tracker that can add, list, complete, filter, save, and load tasks. Include automated domain tests and instructions another person can follow.
Requirements
- Invalid titles and unknown identifiers are handled without corrupting data
- Saved tasks reload with identifiers and completion state preserved
- A clean configure build and test cycle succeeds from the README
- Domain calculations and transitions have automated boundary tests
Optional extension: Add due dates as a deliberate optional value and sort overdue work first.
Open in C++ compilerLesson checkpoint
One small step locks it in
Mark this lesson complete, then keep the momentum going.
Clear up the details
Frequently asked questions
Must the project use inheritance?
No. Add a language feature only when it improves the model or boundary; composition and ordinary values are sufficient for this project.
Which storage format should I choose?
Use a documented format you can parse correctly. A JSON library is practical for structured data; a carefully specified line format can suit the exercise.
What should I build after this course?
Extend the project with a GUI, service, database, concurrency, or performance goal that forces one new concept at a time.