Skip to content

Repository files navigation

Smart Expense Tracker API

A REST API for managing personal expenses, built with Java 17 + Spring Boot 3.5. Expenses are persisted to a local JSON file — no database required.

Bonus feature implemented: OpenAPI 3 / Swagger UI documentation.


What it does

Capability Endpoint
Add an expense POST /api/expenses
View all expenses GET /api/expenses
Filter expenses by category GET /api/expenses?category=food
Total expenses (overall and by category) GET /api/expenses/summary
Total for one category GET /api/expenses/summary?category=food
Fetch a single expense GET /api/expenses/{id}
Delete an expense DELETE /api/expenses/{id}
Interactive API docs (bonus) GET /swagger-ui.html

Prerequisites

  • JDK 17 or newer — that is the only requirement.
  • Maven is not required. The repo ships with the Maven Wrapper (mvnw), which downloads the correct Maven version automatically on first run.

Verify your JDK:

java -version

Install dependencies

This downloads every dependency and compiles the project.

macOS / Linux:

./mvnw -B clean package -DskipTests

Windows (PowerShell or cmd):

.\mvnw.cmd -B clean package -DskipTests

The first run downloads Maven and the Spring Boot dependencies, so it takes a couple of minutes. Subsequent runs are fast.


Start the server

macOS / Linux:

./mvnw spring-boot:run

Windows:

.\mvnw.cmd spring-boot:run

The server starts on http://localhost:8080.

Alternatively, run the packaged jar produced by the install step above:

java -jar target/smart-expense-tracker-api-1.0.0.jar

To use a different port or data file:

java -jar target/smart-expense-tracker-api-1.0.0.jar --server.port=9090 --expense.data-file=/tmp/expenses.json

Run the tests

macOS / Linux:

./mvnw -B test

Windows:

.\mvnw.cmd -B test

71 tests across three suites. They use a temporary data file, so running them never touches your real data/expenses.json.

Suite Tests Covers
ExpenseApiIntegrationTest 32 Full HTTP layer: routing, JSON binding, validation, error mapping, persistence
ExpenseServiceTest 27 Business rules: normalisation, filtering, money arithmetic, totals
JsonFileExpenseRepositoryTest 12 File persistence: restart durability, corrupt files, concurrent writes

API reference

Add an expense

curl -X POST http://localhost:8080/api/expenses \
  -H "Content-Type: application/json" \
  -d '{"title":"Groceries at BigBasket","amount":1250.50,"category":"Food","date":"2026-07-15"}'

201 Created, with a Location header pointing at the new resource:

{
  "id": "fb2803ba-bb1d-46cd-be65-02e03128dd4c",
  "title": "Groceries at BigBasket",
  "amount": 1250.50,
  "category": "food",
  "date": "2026-07-15",
  "createdAt": "2026-07-31T10:16:14.823686900Z"
}

The id is always server-generated; an id supplied by the client is ignored.

Field rules

Field Rules
title required, non-blank, max 200 chars, trimmed
amount required, > 0, at most 2 decimal places
category required, non-blank, max 100 chars, stored lowercase
date required, ISO yyyy-MM-dd, must be a real calendar date

View all expenses

curl http://localhost:8080/api/expenses

Returns a JSON array, newest expense date first.

Filter by category

curl "http://localhost:8080/api/expenses?category=food"

Case-insensitive — food, Food and FOOD all match. An unknown category returns [].

Totals

curl http://localhost:8080/api/expenses/summary
{
  "total": 6650.80,
  "count": 4,
  "byCategory": {
    "travel": 5400.00,
    "food": 1250.80
  }
}

byCategory is ordered highest-spend first. Add ?category=travel to narrow both the total and the breakdown to a single category.

Delete an expense

curl -X DELETE http://localhost:8080/api/expenses/{id}

204 No Content on success, 404 if the id is unknown.

Errors

Every failure returns the same shape:

{
  "timestamp": "2026-07-31T10:16:42.270193500Z",
  "status": 400,
  "error": "Bad Request",
  "message": "Validation failed",
  "path": "/api/expenses",
  "fieldErrors": {
    "amount": "amount must be greater than 0",
    "date": "date is required"
  }
}

fieldErrors is present only for validation failures, and reports every invalid field at once rather than failing on the first one.

Interactive docs (bonus)

With the server running:


Project structure

.
├── README.md
├── AI_NOTES.md
├── pom.xml
├── mvnw / mvnw.cmd          # Maven Wrapper — no local Maven install needed
├── src/
│   └── main/
│       ├── java/com/expensetracker/
│       │   ├── ExpenseTrackerApplication.java
│       │   ├── controller/ExpenseController.java
│       │   ├── service/ExpenseService.java        # business rules
│       │   ├── repository/                        # storage abstraction + JSON file impl
│       │   ├── model/Expense.java
│       │   ├── dto/                               # request/response shapes
│       │   ├── exception/                         # error model + global handler
│       │   └── config/OpenApiConfig.java
│       └── resources/application.yml
├── tests/
│   └── java/com/expensetracker/
│       ├── ExpenseApiIntegrationTest.java
│       ├── service/ExpenseServiceTest.java
│       └── repository/
│           ├── JsonFileExpenseRepositoryTest.java
│           └── InMemoryExpenseRepository.java     # test double
└── data/expenses.json       # created on first write; git-ignored

Note on the layout: the assignment asks for top-level src/ and tests/ directories. Maven's default is src/test/java, so pom.xml explicitly points testSourceDirectory at tests/java to match the requested structure.


Design notes

Money is BigDecimal, never double. Repeated double addition drifts — 0.10 + 0.20 is 0.30000000000000004. Amounts are normalised to a scale of 2 with RoundingMode.HALF_UP on the way in, and every total is computed and returned at scale 2. There is a test asserting exactly this case.

Categories are normalised to lowercase. Otherwise Food and food become two separate buckets in the totals, which is almost never what a user means. Filtering is therefore case-insensitive for free.

Storage is a JSON file, loaded into memory at startup. Reads are served from a LinkedHashMap; every mutation rewrites the file. For a personal expense tracker the dataset is small enough that this is simpler and faster than an append log.

  • A ReentrantReadWriteLock guards the map so two concurrent requests cannot interleave a read-modify-write.
  • Writes go to a temp file in the same directory and are then atomically moved into place, so a crash mid-write cannot leave a half-written, unparseable store behind.
  • If the data file exists but is corrupt, the app fails to start rather than silently starting empty and overwriting the user's history on the next write.

ExpenseRepository is an interface. The service layer is unit-tested against an in-memory fake, and swapping the JSON file for a real database would not touch a single line of business logic.

Dates are parsed strictly. 2026-02-31 is rejected rather than being clamped to the 28th — see AI_NOTES.md, this was a real bug the tests caught.


Troubleshooting

JAVA_HOME is set to an invalid directoryJAVA_HOME must point at the JDK root, not its bin folder:

# macOS / Linux
export JAVA_HOME=/path/to/jdk-17

# Windows PowerShell
$env:JAVA_HOME='C:\Program Files\Java\jdk-17'

Port 8080 already in use — start on another port:

./mvnw spring-boot:run -Dspring-boot.run.arguments=--server.port=9090

Reset the stored data — stop the server and delete data/expenses.json.

About

REST API to manage personal expenses - Java 17 + Spring Boot 3.5, JSON-file storage, 71 tests, OpenAPI/Swagger docs

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages