Table of Contents
- 01 — CLI Todo
- Overview
- Demo
- Architecture
- Tech Stack
- Getting Started
- Project Layout
- API
- Testing Strategy
- Lessons Learned
- Future Improvements
- 🎒 Portfolio
01 — CLI Todo¶
A tiny, single-binary command-line task manager: add, list, complete, and remove todos, persisted to a human-readable JSON file. Zero third-party dependencies.
Overview¶
todo is a beginner-friendly but cleanly layered Go CLI. It manages a list of
tasks stored as pretty-printed JSON in ~/.todo/tasks.json (override with
TODO_FILE). The point of the project is to practice the everyday Go toolkit —
flag parsing, structs/slices, encoding/json, safe file I/O, and idiomatic
error handling — inside a small, layered, fully tested codebase. See the full
SPEC.
Demo¶
$ todo add "Buy milk"
Added task #1: Buy milk
$ todo add "Read Effective Go"
Added task #2: Read Effective Go
$ todo list
ID STATUS TITLE
1 [ ] Buy milk
2 [ ] Read Effective Go
$ todo done 1
Completed task #1: Buy milk
$ todo list --all
ID STATUS TITLE
1 [x] Buy milk
2 [ ] Read Effective Go
$ todo list --json
[
{
"id": 2,
"title": "Read Effective Go",
"done": false,
"created_at": "2026-06-26T09:05:00Z"
}
]
$ todo rm 2
Removed task #2: Read Effective Go
$ todo done 99
Error: task not found
$ echo $?
1
Architecture¶
The program is a thin CLI shell over a small domain core. main builds the
dependency graph and installs a signal-cancelled context; the cli layer parses
args and renders output; the service layer orchestrates each use case; the
task package holds pure domain logic; and store is the only component that
touches the filesystem. Each layer depends only on the one beneath it, and the
two interfaces (Repository, TaskService) are defined at the consumer.
flowchart TD
User([User / Terminal]) -->|args & flags| Main[cmd/todo/main.go<br/>wiring + signal ctx]
Main --> CLI[internal/cli<br/>flag parsing + rendering]
CLI -->|TaskService iface| Service[internal/service<br/>use cases: add/list/done/rm]
Service -->|domain ops| Task[internal/task<br/>Task, List, rules]
Service -->|Repository iface| Store[internal/store<br/>JSON persistence]
Store -->|read & atomic write| File[("~/.todo/tasks.json")]
CLI -->|stdout / stderr| Out([Terminal])
Key decisions:
- Layered with consumer-defined interfaces.
service.Repositoryandcli.TaskServiceare declared where they are used, so dependencies point inward and the concrete store can be swapped (e.g. for SQLite) without touching higher layers. - Pure domain.
internal/taskdoes no I/O, so its rules (ID assignment, empty-title rejection, idempotent completion) are trivially unit-tested. - Atomic writes.
store.Savewrites to a temp file thenos.Renames it into place, so a crash mid-write never corrupts existing data. - Injected clock. The service takes a
Clockfunc, making timestamps deterministic in tests. - No
os.Exitoutsidemain. Every layer returns errors; onlymaintranslates them into exit codes.
Tech Stack¶
Go 1.22 · flag · encoding/json · text/tabwriter · os/path/filepath ·
testing — standard library only, no third-party imports.
Getting Started¶
Prerequisites¶
- Go 1.22+
Run¶
This project is part of the repo root module github.com/nabin747/go-from-zero,
so there is no local go.mod. Run from this directory:
cd projects/01-cli-todo
make build # builds ./bin/todo
./bin/todo add "Buy milk"
# or without building:
make run ARGS="add Buy milk"
go run github.com/nabin747/go-from-zero/projects/01-cli-todo/cmd/todo list
Point the data file somewhere local instead of ~/.todo:
Test¶
Project Layout¶
01-cli-todo/
├── cmd/
│ └── todo/
│ └── main.go # entrypoint: wiring, signal ctx, exit codes
├── internal/
│ ├── task/ # domain: Task, List, rules (no I/O)
│ │ ├── task.go
│ │ └── task_test.go
│ ├── store/ # persistence: JSON load/save, atomic write
│ │ ├── store.go
│ │ └── store_test.go
│ ├── service/ # use cases over Repository
│ │ ├── service.go
│ │ └── service_test.go
│ └── cli/ # transport: flag parsing + rendering
│ ├── cli.go
│ └── cli_test.go
├── Makefile
├── README.md
├── SPEC.md
└── .gitignore
API¶
The "API" is the CLI command surface: todo <command> [arguments] [flags].
| Command | Arguments | Flags | Description |
|---|---|---|---|
todo add |
<title> |
— | Add a new task |
todo list / ls |
— | --all, --json |
List tasks (open by default) |
todo done |
<id> |
— | Mark a task complete |
todo rm |
<id> |
— | Delete a task |
todo help / -h |
— | — | Show usage |
Exit codes: 0 success, 1 runtime error (e.g. unknown ID), 2 usage error.
Data file: ~/.todo/tasks.json (override with TODO_FILE). Stored as a
versioned JSON object:
{
"version": 1,
"tasks": [
{ "id": 1, "title": "Buy milk", "done": false, "created_at": "2026-06-26T09:00:00Z" }
]
}
Testing Strategy¶
Table-driven tests throughout, organised by layer:
task— pure domain assertions: monotonic IDs, trimmed/empty titles, idempotent completion, removal,errors.Is(err, ErrNotFound).store— real filesystem viat.TempDir(): save/load round-trip (includingdone_atpointer omission), missing file → empty list, malformed JSON → wrapped error, no temp-file leftovers, overwrite semantics.service— an in-memory fakeRepositoryfor deterministic, fast tests; verifies orchestration, error wrapping, and context cancellation.cli— drives the whole command surface throughApp.Run, capturing stdout/stderr buffers and asserting on output and exit codes.
Lessons Learned¶
- Splitting
task(domain) fromstore(persistence) andservice(use cases) made each layer testable in isolation — the domain needs no files, the service needs no disk (in-memory fake repo), and onlystoretests toucht.TempDir(). - Wrapping infrastructure errors with
%wwhile returning domain sentinels (ErrNotFound,ErrEmptyTitle) directly keeps CLI messages clean and lets callers branch witherrors.Is. - The temp-file-plus-rename write is a few extra lines that buy crash safety: the real file is only ever replaced atomically.
Future Improvements¶
- Due dates (
--due) and overdue highlighting - Priorities and tags with filtered listing
-
todo edit <id>and undo of the last mutation - Swap the JSON store for SQLite behind the same
Repositoryinterface - Shell completion scripts
🎒 Portfolio¶
Résumé bullets:
- "Built a cross-platform CLI todo manager in Go, distributed as a single static binary with zero runtime dependencies and sub-50 ms startup."
- "Designed a four-layer architecture (CLI / service / domain / storage) with
consumer-defined interfaces and crash-safe atomic JSON persistence, covered by
table-driven tests with
t.TempDir()isolation and an injected clock."
Interview talking points:
- Why interfaces (
Repository,TaskService) are defined at the consumer and how that keeps dependencies pointing inward and enables swapping JSON for SQLite. - Idiomatic error handling: sentinel errors vs.
%wwrapping, surfaced as clean CLI messages and distinct exit codes (0/½). - Guaranteeing data integrity with atomic temp-file-plus-rename writes instead of writing in place.
⬅ Projects · Repo README