Career OS

Capstone — SplitEase CLI

Ten modules in, you’ve collected the pieces: OOP, collections, exceptions, streams, file IO. This module is where you stop collecting and start building — a complete terminal app, designed by you, written by you, with no full solution to copy. This is the difference between “I learned Java” and “I build with Java.”

The Goal

By the end of this module you can:

  • Design a small system on paper — classes, responsibilities, relationships — before writing a line of code
  • Build a complete working CLI app from a requirements list, milestone by milestone
  • Combine six earlier modules’ skills in one codebase and feel where each one earns its place
  • Implement a real algorithm (settle-up debt simplification) from a description and a diagram, not from pasted code
  • Ship something finished — a Definition of Done met, committed, and demoable in an interview

The Project

SplitEase is a terminal expense-splitter for friends. The scenario: five of you went to Goa. Asha paid for dinner, you paid for the cab, Rohit paid the homestay. By day three nobody knows who owes whom, and the WhatsApp group is doing painful mental math. SplitEase answers it in one command.

One more thing, said plainly: this project is the seed of the Spring Boot track, where it becomes a real backend. The Ledger you design today becomes the service layer; the commands become REST endpoints; the file save becomes a database (SQL track — your data even gets its own schema there). Build it well — you’ll be living in this domain for weeks.

Requirements

Your app must do exactly this — no more, no less:

#RequirementExample
R1Add friends by nameadd friend Asha
R2Add an expense — amount, description, who paid, who shares itadd expense 3000 dinner paidby Asha among Asha Rohit Darshan
R3Show balances — each person’s net positionbalancesAsha is owed Rs 2000
R4Settle up — print the minimal list of who-pays-whomsettleDarshan pays Asha Rs 1000
R5Save to a file and load it backsave trip.txt, load trip.txt
R6Reject invalid operations with clear errorsExpense paid by an unknown friend → error, app keeps running

Design first — this is the skill

Before any code, draw the design. Here’s why this paragraph matters more than any code in this module: AI can generate code from a design in seconds, but producing the design — deciding what the classes are, what each one owns, and what talks to what — is the judgment work that survives the AI era. Beginners open the editor and start typing; engineers decide the shape first and then the typing is almost mechanical. Practising design-before-code on a small project builds the muscle you’ll need when the project isn’t small.

Here is the shape of SplitEase — four pieces, each with one job:

flowchart TD
    CLI[SplitEase CLI loop reads commands] --> L[Ledger owns all friends and expenses]
    L --> F[Friend has a name]
    L --> E[Expense has amount description payer and sharers]
    E -->|paid by one| F
    E -->|split among many| F
ClassIts one jobWhat it holds
FriendIdentitya name; proper equals and hashCode so collections behave
ExpenseOne spending event, immutable once createdamount, description, the payer, the list of sharers
LedgerThe brain — all rules live herethe friends, the expenses, and every operation: add, balances, settle
SplitEaseThe face — main plus the command loopa Scanner, a Ledger, and zero business logic

The golden rule baked into that table: the CLI never does math, the Ledger never does printing. When this becomes a Spring Boot app, the CLI is the layer you throw away and the Ledger is the layer you keep. Separate them now and future-you says thanks.

One design decision to make consciously: store money in paise as long, not rupees as double. Floating point math drops paise (0.1 + 0.2 is not 0.3 — same trap as JS). Real payment systems store integer minor units. Do the same; convert to rupees only when printing.

How a command flows

sequenceDiagram
    participant U as You at the terminal
    participant C as CLI loop
    participant L as Ledger
    U->>C: add expense 3000 dinner paidby Asha among Asha Rohit
    C->>C: parse the line into parts
    C->>L: addExpense with parsed values
    L->>L: validate then store
    L-->>C: success or a thrown exception
    C-->>U: prints confirmation or the error then loops again

Note who does what: the CLI parses and prints; the Ledger validates and stores. A bad command throws inside the Ledger, the CLI catches it, prints one clear line, and keeps looping — the app never dies from bad input.

The Build Plan

Six milestones. Each is a sitting of 1-2 hours, ends with something runnable, and names the module it exercises. Build them in order — each stands on the last.

M1 — Model classes (Module 03, OOP)

Create Friend and Expense. Friend wraps a name — implement equals, hashCode, and toString (you’ll be putting friends in sets and maps; broken equals breaks everything downstream). Expense takes amount-in-paise, description, payer, and list of sharers through its constructor, exposes getters, has no setters — an expense never changes after creation.

Done when: a throwaway main creates two Friends and an Expense and prints them, and two Friends with the same name are equals.

M2 — The Ledger stores things (Module 04, Collections)

Create Ledger holding the friends and expenses. Pick your collections deliberately and be able to defend each pick: does friend order matter? Are duplicates allowed? (Hint: a Set of friends makes duplicate names impossible by construction.) Add addFriend, addExpense, and simple list-all methods. No validation yet — that’s M5.

Done when: a throwaway main adds three friends and two expenses through the Ledger and lists them back.

M3 — The command loop (Module 02, language core and Scanner)

Create SplitEase with main: a Scanner on System.in, a while loop, read a line, split it, switch on the first word, call the Ledger, print the result. Commands: add friend, add expense, list, exit. Parsing the expense command is the fiddly part — split on spaces and use the paidby and among keywords as anchors to slice the parts.

Done when: you can run the app and build up a ledger interactively, and exit ends it cleanly.

M4 — Balance math and reports (Module 07, streams)

The heart. First balances: for each friend, net position = everything they paid minus their share of everything they’re part of. (Share = amount divided by number of sharers — decide consciously what happens to the leftover paise when 1000 doesn’t divide by 3. Simplest fair answer: give the remainder to the payer’s share.) Build it with streams — this is a groupingBy-and-sum shaped problem.

Then settle — the satisfying one. Printing every expense’s debts gives a messy web; you want the minimal payment list. The algorithm:

flowchart TD
    A[All expenses] --> B[Compute net balance per friend]
    B --> C[Creditors with positive balance]
    B --> D[Debtors with negative balance]
    C --> E[Pick biggest creditor]
    D --> F[Pick biggest debtor]
    E --> G[Debtor pays creditor the smaller of the two amounts]
    F --> G
    G --> H[Reduce both balances by that payment]
    H -->|someone still nonzero| E
    H -->|all balances zero| I[Done print the payments]

In words: collapse everything to one net number per person — positive means the group owes them, negative means they owe the group (and all the numbers sum to zero, which is your built-in correctness check). Then greedily match: biggest debtor pays biggest creditor as much as possible, repeat until everyone is at zero. Work one example by hand on paper first — three friends, two expenses — before coding it. Seriously. The hand-worked example becomes your test case.

Done when: balances matches your hand math, the balances sum to zero, and settle for the Goa scenario prints 2-3 payments instead of a web of six.

M5 — Custom exceptions (Module 06, exceptions)

Create UnknownFriendException and InvalidExpenseException (extend Exception or RuntimeException — decide and be able to say why). The Ledger now validates: expense payer must exist, sharers must exist, amount must be positive, sharer list non-empty. The CLI catches them, prints one clear human line, loops on. Bad input must never produce a stack trace or kill the app.

Done when: add expense 500 chai paidby Nobody among Asha prints a friendly error and the app keeps running.

M6 — Save and load (Module 06, try-with-resources and file IO)

Design a simple line-based text format — for example one FRIEND line per friend, one EXPENSE line per expense with fields separated by a delimiter (pick one that can’t appear in a description — | is safer than a comma in pav bhaji, extra butter). Write save and load on the Ledger using try-with-resources so files always close. Loading replaces the current state and must validate as it reads — a corrupt file should produce a clear error, not a half-loaded ledger.

Done when: add data, save trip.txt, restart the app, load trip.txt, and balances prints exactly what it printed before.

Definition of Done

  • All six requirements R1-R6 work end to end
  • Money stored as paise in long; rupees only at print time
  • Balances always sum to zero; settle-up matches a hand-worked example
  • No invalid input can crash the app or show a raw stack trace
  • Ledger has zero printing; CLI has zero business logic
  • Save then load round-trips perfectly, including descriptions with spaces
  • Committed to git milestone by milestone with [project]: messages

Stretch goals (after DoD, not before)

  1. Unequal splitsamong Asha=2000 Rohit=1000 for exact shares. Forces you to rethink the Expense model.
  2. Undo last expense — one command reverting the most recent add. A taste of command-history design.
  3. Monthly report — give expenses a date, then a report command using groupingBy month with per-month totals. Pure Module 07 flexing.

Build This

This whole module is the build — here’s the kickoff procedure:

  1. Create the project folder and a git repo:
mkdir splitease
cd splitease
git init
  1. On paper, redraw the class diagram from memory and write each class’s one-job sentence. Don’t skip this — it’s the skill being trained.
  2. Work the milestones in order. After each one: compile, run, exercise it manually, then commit:
javac *.java
java SplitEase
git add .
git commit -m "[project]: splitease - M1 model classes"
  1. Before M4, work the settle-up example by hand: Asha pays 3000 dinner among all three, you pay 600 cab among all three. Write down the three net balances and the payments. Code must reproduce this exactly.
  2. When stuck more than 30 minutes, ask Claude about the concept (see Still Unclear) — never “write me the method.”
  3. After DoD: run the full Goa scenario start to finish, save, reload, settle. That run is your interview demo.

Where to Practice

ResourceWhat to do thereHow long
exercism.org Java trackDo “Bank Account” before M4 — money state plus operations, a mini Ledger45 min
LeetCodeSearch the problem “Optimal Account Balancing” — read the problem statement only to see your settle-up as an interview question10 min
BaeldungSearch “java collectors groupingby” when M4 streams get tangled20 min
dev.javaReread the file IO tutorial before M620 min

How to Practice

  • One milestone per sitting. Finished-and-committed beats half of two.
  • Paper before keyboard for M4 — the settle-up bugs you’ll hit are math bugs, and they’re cheaper on paper.
  • Test like a user after every milestone: run the app and type garbage at it on purpose.
  • Keep a decisions note — one line per choice (“Set for friends because duplicates are nonsense”). This becomes interview gold.

Check Yourself

  1. Why does the Ledger contain all the rules while the CLI contains none? What’s the payoff when this becomes a Spring Boot app?
  2. Why store money as paise in a long instead of rupees in a double?
  3. Why must Friend implement equals and hashCode for this design to work?
  4. Explain the settle-up algorithm in three sentences without looking at the diagram.
  5. What invariant should the net balances always satisfy, and how is it useful while debugging?
  6. Why do UnknownFriendException and friends get caught in the CLI loop rather than crashing the app?
  7. What does try-with-resources guarantee in M6 that a plain close() call doesn’t?
  8. Which milestone exercises which earlier module? Go through all six from memory.
Answers
  1. Separation of concerns — the CLI is a replaceable face, the Ledger is the durable brain. In the Spring Boot version the CLI is discarded and replaced by REST controllers, while the Ledger logic survives almost untouched.
  2. double is binary floating point and cannot represent most decimal fractions exactly, so paise silently vanish in arithmetic. Integer minor units make every operation exact — the standard in real payment systems.
  3. Friends go into Sets and are used as Map keys for balances. Default equals compares object identity, so two Friend objects named Asha would count as different people, breaking dedupe and balance lookups.
  4. Reduce all expenses to one net number per person — positive is owed, negative owes. Repeatedly match the biggest debtor with the biggest creditor and transfer the smaller of the two amounts. Repeat until all balances are zero.
  5. They must sum to zero — money is only moved between friends, never created or destroyed. If the sum is ever nonzero, your share math (probably the leftover-paise handling) is wrong, and you know instantly.
  6. They represent invalid user input, not program bugs. The right response is a clear message and a fresh prompt — the catch in the loop turns a thrown exception into a printed line and keeps the app alive.
  7. The file is closed no matter what — normal completion, early return, or an exception mid-write. Plain close() is skipped if an exception fires before it.
  8. M1 OOP (03), M2 collections (04), M3 Scanner and language core (02), M4 streams (07), M5 exceptions (06), M6 try-with-resources file IO (06).

Explain it out loud: Pitch SplitEase as if an interviewer just said “walk me through a project you built.” Two minutes: the problem, the four classes and why they’re shaped that way, the settle-up algorithm, and one decision you’d defend (paise as long is a strong pick). If the architecture part is mumbly, redraw the diagram from memory first.

Still Unclear?

Copy-paste any of these into Claude — concept help, not solution generation:

I'm designing a CLI expense splitter with Friend, Expense, Ledger, and a CLI
loop. Before I code, interview me about my design: ask me what each class
owns, where validation lives, and what breaks if I put logic in the CLI.
Challenge weak answers. Do not write any code.
Explain the greedy settle-up algorithm (net balance per person, then match
biggest debtor to biggest creditor) using 4 friends and 3 expenses as a
worked example with real numbers. Then give me a fresh scenario and check
my hand-worked answer. No Java code.
My balances don't sum to zero when an expense amount doesn't divide evenly
among sharers. Explain integer division and remainder-handling strategies
for splitting money, with the trade-offs of each. Don't write my method -
help me reason about which strategy fits my design.

Why AI Can’t Do This For You

AI can generate a complete SplitEase in thirty seconds — and you’d learn nothing, demo nothing, and fold the moment an interviewer asks “why a Set for friends?” The code was never the point; the hundred small decisions are, and decisions only become yours by making them.

When the Spring Boot version of this hits a real bug — balances off by one paisa across a month of data — the person who hand-worked the settle-up math finds it in minutes. The person who pasted generated code reads their own project like a stranger.

Module done? Add it to today’s tracker

Saves your progress on this device.