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-todo

  • java21-log-analyzer

  • java21-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

  1. Persistence in ~/.todo/tasks.json. Directory auto-created.

  2. Atomic writes: write to tasks.json.tmp then Files.move(..., ATOMIC_MOVE). No half-written files if you kill the process mid-write.

  3. Argument parsing by hand — no picocli, no jcommander. This is a fundamentals project. Later projects can use libraries.

  4. JSON by hand or Jackson (your call). Doing it by hand for records under 3 fields is educational; Jackson if fields grow.

  5. Exit codes: 0 on success, non-zero on error. todo done <unknown-id> exits 1 with a message on stderr.

  6. 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 list on an empty file prints "No tasks.", not a stack trace

  • README.md shows a 10-line quickstart with actual copy-pasteable commands

  • mvn test (or gradle test) passes with 0 failures

  • Runnable via java -jar target/todo.jar list after mvn package

Stretch (only after minimum works)

  • Colored output on ANSI terminals

  • --json output flag for piping to other tools

  • Recurring 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

  1. Top N IP addresses by request count

  2. Top N URLs by request count

  3. Response code distribution (2xx / 3xx / 4xx / 5xx counts and percentages)

  4. Requests per hour (histogram, ASCII bar chart)

  5. Average bytes per response, min, max, p95 (bonus: p99)

  6. Slowest requests (only if latency present in the log)

  7. Suspected bots: any user-agent containing bot, spider, crawler

CLI

loganalyzer --input access.log --top 10 --report ips,urls,codes,hourly

Requirements

  1. Streaming: Files.lines(...) inside try-with-resources. Never readAllLines on a real log — they get huge.

  2. Regex-based parser compiled once as a static Pattern. Malformed lines are counted separately, not silently dropped.

  3. Records for the parsed line: record LogEntry(String ip, LocalDateTime ts, String method, String path, int status, long bytes, String userAgent) {}.

  4. Streams + Collectors for the aggregations (groupingBy, counting, summarizingLong).

  5. Percentiles computed manually from a sorted long[], not with a library. It’s five lines.

  6. 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.md includes sample output for every report

  • Test suite covers regex edge cases (quoted fields with escaped quotes)

Stretch

  • --follow flag that tails the file (like tail -f) and updates stats live

  • Output 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

  1. Money arithmetic never uses double. Only BigDecimal. Scale enforced by Currency.getDefaultFractionDigits().

  2. Different currencies cannot be combined. Money(USD 10) + Money(EUR 5) throws IllegalArgumentException.

  3. Withdrawal on an account with insufficient funds throws a custom InsufficientFundsException including the shortfall. Balance is unchanged (no partial state).

  4. Transfer.execute(bank) atomically debits from and credits to. If credit somehow fails, the debit is rolled back — or the whole thing throws before any change. Test this.

  5. Ledger is immutable from outside: account.ledger() returns List.copyOf(...) or Collections.unmodifiableList(...).

  6. 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;
    };
}
  1. Test coverage ≥ 85% (measured with JaCoCo). Every exception path tested.

Acceptance criteria

  • Cannot construct a Money with wrong scale or null currency

  • Mixing currencies throws consistently

  • withdraw insufficient-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(...) throws UnsupportedOperationException)

  • Coverage report at target/site/jacoco/index.html shows ≥ 85%

Stretch

  • Currency conversion via a pluggable ExchangeRateProvider interface

  • Interest calculation with a SavingsAccount subtype (careful: records are final; use interface Account if 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

BigDecimal correctness

LocalDateTime / Instant

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