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:
| # | Requirement | Example |
|---|---|---|
| R1 | Add friends by name | add friend Asha |
| R2 | Add an expense — amount, description, who paid, who shares it | add expense 3000 dinner paidby Asha among Asha Rohit Darshan |
| R3 | Show balances — each person’s net position | balances → Asha is owed Rs 2000 |
| R4 | Settle up — print the minimal list of who-pays-whom | settle → Darshan pays Asha Rs 1000 |
| R5 | Save to a file and load it back | save trip.txt, load trip.txt |
| R6 | Reject invalid operations with clear errors | Expense 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
| Class | Its one job | What it holds |
|---|---|---|
Friend | Identity | a name; proper equals and hashCode so collections behave |
Expense | One spending event, immutable once created | amount, description, the payer, the list of sharers |
Ledger | The brain — all rules live here | the friends, the expenses, and every operation: add, balances, settle |
SplitEase | The face — main plus the command loop | a 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)
- Unequal splits —
among Asha=2000 Rohit=1000for exact shares. Forces you to rethink the Expense model. - Undo last expense — one command reverting the most recent add. A taste of command-history design.
- Monthly report — give expenses a date, then a
reportcommand usinggroupingBymonth with per-month totals. Pure Module 07 flexing.
Build This
This whole module is the build — here’s the kickoff procedure:
- Create the project folder and a git repo:
mkdir splitease
cd splitease
git init
- 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.
- 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"
- 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.
- When stuck more than 30 minutes, ask Claude about the concept (see Still Unclear) — never “write me the method.”
- After DoD: run the full Goa scenario start to finish, save, reload, settle. That run is your interview demo.
Where to Practice
| Resource | What to do there | How long |
|---|---|---|
| exercism.org Java track | Do “Bank Account” before M4 — money state plus operations, a mini Ledger | 45 min |
| LeetCode | Search the problem “Optimal Account Balancing” — read the problem statement only to see your settle-up as an interview question | 10 min |
| Baeldung | Search “java collectors groupingby” when M4 streams get tangled | 20 min |
| dev.java | Reread the file IO tutorial before M6 | 20 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
- Why does the Ledger contain all the rules while the CLI contains none? What’s the payoff when this becomes a Spring Boot app?
- Why store money as paise in a
longinstead of rupees in adouble? - Why must
FriendimplementequalsandhashCodefor this design to work? - Explain the settle-up algorithm in three sentences without looking at the diagram.
- What invariant should the net balances always satisfy, and how is it useful while debugging?
- Why do
UnknownFriendExceptionand friends get caught in the CLI loop rather than crashing the app? - What does try-with-resources guarantee in M6 that a plain
close()call doesn’t? - Which milestone exercises which earlier module? Go through all six from memory.
Answers
- 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.
doubleis 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.- Friends go into Sets and are used as Map keys for balances. Default
equalscompares object identity, so twoFriendobjects named Asha would count as different people, breaking dedupe and balance lookups. - 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.
- 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.
- 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.
- 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. - 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