Phase 01 Projects¶
Three small projects that force everything from files 1–4 to come together. The 50-programs challenge builds muscle memory in isolated chunks; these projects prove you can assemble the chunks into working software. Each is designed to take 6–12 hours — no more. If a project balloons past 15 hours, cut scope; you’re already learning something else.
All three go into public GitHub repos with a proper README, a pom.xml (or build.gradle.kts), and passing tests. Recruiters will look at these before Phase 07. Make them presentable but do not over-polish — they are foundation exercises, not portfolio centerpieces.
Repo naming convention¶
java21-cli-todojava21-log-analyzerjava21-mini-bank
Use java21- prefix on everything in this phase. In Phase 07 and 08 you will build larger apps with different naming; the prefix here signals “foundation, not production”.
Project 1 — CLI TODO with JSON Persistence¶
Goal: a small command-line TODO app whose entire state persists as a single JSON file on disk. Every mutation reads the file, mutates, and rewrites atomically.
Commands¶
todo add "Buy milk" # returns id
todo add "Book flights" --due 2026-08-15 --priority HIGH
todo list # all tasks
todo list --status OPEN
todo done <id>
todo delete <id>
todo show <id>
Model (records + sealed)¶
public record Task(int id, String title, LocalDate due, Priority priority, Status status, Instant createdAt) {}
public enum Priority { LOW, MEDIUM, HIGH }
public enum Status { OPEN, DONE }
Requirements¶
Persistence in
~/.todo/tasks.json. Directory auto-created.Atomic writes: write to
tasks.json.tmpthenFiles.move(..., ATOMIC_MOVE). No half-written files if you kill the process mid-write.Argument parsing by hand — no picocli, no jcommander. This is a fundamentals project. Later projects can use libraries.
JSON by hand or Jackson (your call). Doing it by hand for records under 3 fields is educational; Jackson if fields grow.
Exit codes: 0 on success, non-zero on error.
todo done <unknown-id>exits 1 with a message on stderr.Unit tests: at least 8 covering happy paths and error paths (JUnit 5).
Acceptance criteria¶
All 6 commands above work end-to-end
Killing the process mid-write leaves either the old file or the new file, never corruption (test this manually)
todo liston an empty file prints"No tasks.", not a stack traceREADME.mdshows a 10-line quickstart with actual copy-pasteable commandsmvn test(orgradle test) passes with 0 failuresRunnable via
java -jar target/todo.jar listaftermvn package
Stretch (only after minimum works)¶
Colored output on ANSI terminals
--jsonoutput flag for piping to other toolsRecurring tasks (daily / weekly)
Est: 8–10 hours.
Project 2 — Access Log Analyzer¶
Goal: parse an Apache/nginx access log and output structured statistics. This forces streams, regex, LocalDateTime, and grouping into a real workflow.
Input¶
Combined log format:
192.168.1.1 - - [05/Jul/2026:10:15:32 +0000] "GET /api/users HTTP/1.1" 200 1234 "-" "curl/7.81.0"
Provide a sample sample.log in src/test/resources/ with ~500 lines (mix of 2xx/3xx/4xx/5xx). Generate with a tiny script or hand-craft.
Reports the tool must produce¶
Top N IP addresses by request count
Top N URLs by request count
Response code distribution (2xx / 3xx / 4xx / 5xx counts and percentages)
Requests per hour (histogram, ASCII bar chart)
Average bytes per response, min, max, p95 (bonus: p99)
Slowest requests (only if latency present in the log)
Suspected bots: any user-agent containing
bot,spider,crawler
CLI¶
loganalyzer --input access.log --top 10 --report ips,urls,codes,hourly
Requirements¶
Streaming:
Files.lines(...)inside try-with-resources. NeverreadAllLineson a real log — they get huge.Regex-based parser compiled once as a static
Pattern. Malformed lines are counted separately, not silently dropped.Records for the parsed line:
record LogEntry(String ip, LocalDateTime ts, String method, String path, int status, long bytes, String userAgent) {}.Streams +
Collectorsfor the aggregations (groupingBy,counting,summarizingLong).Percentiles computed manually from a sorted
long[], not with a library. It’s five lines.Unit tests for the regex (5+ line formats), for one aggregation, and for percentile math.
Acceptance criteria¶
Runs on a 1 GB log file without OOM (test with a generated file)
Reports parse errors on stderr with a count, doesn’t crash
All reports match hand-computed values on the 500-line sample
README.mdincludes sample output for every reportTest suite covers regex edge cases (quoted fields with escaped quotes)
Stretch¶
--followflag that tails the file (liketail -f) and updates stats liveOutput as JSON
Filter by date range:
--from 2026-07-01 --to 2026-07-05
Est: 10–12 hours.
Project 3 — Mini-Bank Domain Model¶
Goal: a small in-memory banking domain that exercises records, sealed types, exception design, and equals/hashCode correctness. No persistence, no CLI — this one is pure model + tests. It’s a JUnit-driven project.
Domain¶
public record AccountId(UUID value) {}
public record Money(BigDecimal amount, Currency currency) {
public Money { /* validate non-null, currency required, amount scale matches currency */ }
public Money plus(Money other) { /* same-currency check */ }
public Money minus(Money other) { /* same-currency check */ }
}
public sealed interface Transaction permits Deposit, Withdrawal, Transfer {
Instant timestamp();
Money amount();
}
public record Deposit(Instant timestamp, AccountId to, Money amount) implements Transaction {}
public record Withdrawal(Instant timestamp, AccountId from, Money amount) implements Transaction {}
public record Transfer(Instant timestamp, AccountId from, AccountId to, Money amount) implements Transaction {}
public final class Account {
private final AccountId id;
private Money balance;
private final List<Transaction> ledger = new ArrayList<>();
// deposit, withdraw, transferTo methods that append to ledger and update balance
}
public class Bank {
private final Map<AccountId, Account> accounts = new HashMap<>();
// openAccount, closeAccount, get, transfer
}
Requirements¶
Moneyarithmetic never usesdouble. OnlyBigDecimal. Scale enforced byCurrency.getDefaultFractionDigits().Different currencies cannot be combined.
Money(USD 10) + Money(EUR 5)throwsIllegalArgumentException.Withdrawalon an account with insufficient funds throws a customInsufficientFundsExceptionincluding the shortfall. Balance is unchanged (no partial state).Transfer.execute(bank)atomically debitsfromand creditsto. If credit somehow fails, the debit is rolled back — or the whole thing throws before any change. Test this.Ledger is immutable from outside:
account.ledger()returnsList.copyOf(...)orCollections.unmodifiableList(...).Pattern-switch method on
Transaction:
public String describe(Transaction t) {
return switch (t) {
case Deposit(_, var to, var amt) -> "Deposit " + amt + " to " + to;
case Withdrawal(_, var from, var amt) -> "Withdraw " + amt + " from " + from;
case Transfer(_, var from, var to, var amt) -> "Transfer " + amt + " from " + from + " to " + to;
};
}
Test coverage ≥ 85% (measured with JaCoCo). Every exception path tested.
Acceptance criteria¶
Cannot construct a
Moneywith wrong scale or null currencyMixing currencies throws consistently
withdrawinsufficient-funds path leaves balance untouched (assert in test)Transfer atomicity: use a mock where credit throws and assert debit did not commit
Ledger returned to caller cannot be modified (
.add(...)throwsUnsupportedOperationException)Coverage report at
target/site/jacoco/index.htmlshows ≥ 85%
Stretch¶
Currency conversion via a pluggable
ExchangeRateProviderinterfaceInterest calculation with a
SavingsAccountsubtype (careful: records are final; use interfaceAccountif you go here)Concurrency-safe version with
ReentrantReadWriteLock(this is really a Phase 05 exercise, note it and move on)
Est: 8–10 hours.
What all three teach you together¶
Skill |
TODO |
Log Analyzer |
Mini-Bank |
|---|---|---|---|
Records |
✔ |
✔ |
✔ |
Sealed types |
✔ |
||
Pattern matching for switch |
✔ |
||
Text blocks |
✔ (JSON) |
||
NIO.2 file ops |
✔ |
✔ |
|
Streams |
✔ |
(mild) |
|
Custom exceptions |
✔ |
(parse errors) |
✔ |
JUnit 5 |
✔ |
✔ |
✔ |
Maven/Gradle |
✔ |
✔ |
✔ |
Regex |
✔ |
||
|
✔ |
||
|
✔ |
✔ |
✔ |
If any row of that table is empty across all three, that skill isn’t wired in yet.
⚠️ What most people get wrong¶
They start with Project 3 because it looks meatiest. Wrong order. TODO first — it’s the smallest, forces the file-I/O + persistence loop that you’ll rebuild in every future project. Log Analyzer second — it forces streaming, which is the mental model shift from Python. Mini-Bank last — the sealed types and pattern switch pay off more once you’re not still fumbling with Path API.
Also: don’t chase coverage numbers on the first two. 60% is plenty for those. Reserve the 85% bar for Mini-Bank where every exception path is a real branch worth testing.
Return to README.md · Phase 02: ../02_core_java_dsa/README.md