Functional Personal Expense Tracker
Build an expense tracker in Scala written in a purely functional style: expenses are immutable case classes, categories are an algebraic data type (a sealed trait or enum), every operation that can fail returns an Either with a clear error instead of throwing, and the ledger is saved to and loaded from a JSON file with Circe. A small command-line interface adds, removes, lists and categorizes expenses, and a monthly summary shows spending per category, compared with a budget. You'll learn modeling a domain with algebraic data types, functional error handling with Either (and collecting several errors), immutable collections, JSON codecs with Circe, and testing pure functions, including property-based tests.
- Estimate
- ~9.5h
- Steps
- 6
- Completed by
- 0
- Proposed by
- codeseed.app
Circe · sbt
Project roadmap
- 01
Model the domain
~1.5hCreate an sbt project with Circe and munit. Define Category as a Scala 3 enum (such as Food, Transport, Housing, Fun and Other), Money as an amount in cents (a Long wrapped in an opaque type), Expense (id, date, amount, category and description), a Ledger holding expenses and monthly budgets per category, and a sealed trait of errors (such as InvalidAmount, UnknownCategory, DuplicateId and NotFound). Done when: the model compiles, and invalid states are hard to create.
- 02
Add and remove expenses with Either
~2hWrite pure functions that return Either[LedgerError, Ledger]: add (validating a positive amount, a date not in the future and a unique id), remove by id, and update. Write a parse function that turns user input into an Expense, collecting all errors at once with Cats' ValidatedNel or by combining Eithers. Done when: unit tests cover every success and error case.
- 03
Save the ledger as JSON
~1.5hWrite Circe encoders and decoders (derived or hand-written for Money, dates and the Category enum), and load and save the ledger from a JSON file, returning Either with a clear error for a missing or malformed file. Save to a temporary file and rename it, so a crash never leaves a broken ledger. Done when: saving then loading gives an identical ledger.
- 04
Build the monthly summary
~1.5hWrite pure functions that group a month's expenses by category with groupMapReduce, compare each total with its budget (showing the amount over or under), and compute the month's total and the change from the previous month. Print the summary as an aligned table. Done when: the summary totals match the expenses, and budget differences are correct.
- 05
Add the command line
~1.5hWrite a small CLI (with scopt or decline, or hand-written parsing): add, remove, list (with --month and --category), budget CATEGORY AMOUNT and summary --month. Keep all logic in pure functions and do reading, writing and printing only at the edges. Print errors clearly and exit with a non-zero code. Done when: every command works on a JSON ledger file.
- 06
Write tests
~1.5hWrite munit tests for every operation and error, the summary calculations, and JSON round trips, plus property-based tests with ScalaCheck: encoding then decoding any ledger gives the same ledger, and adding then removing an expense gives back the original ledger. Done when: sbt test passes.
Ready to build this?
Get a GitHub repo and start building. Your AI reviewer checks each step as you go.
Tech stack