Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

27 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Expense Tracker

A full-stack personal expense tracker. Record, view, filter, and summarize expenses with exact decimal precision and resilience to network failures.


Getting Started

Install dependencies

# Backend
cd backend && npm install

# Frontend
cd frontend && npm install

Run the backend

cd backend
npm run dev

The API server starts on http://localhost:3000.

Run the frontend

cd frontend
npm run dev

The dev server starts on http://localhost:5173 (or the next available port).

Run tests

# Backend tests
cd backend
npm test

# Frontend tests
cd frontend
npm test

Key Design Decisions

Money as integer cents

Monetary amounts are stored as integer cents rather than floating-point decimals. For example, ₹12.50 is stored as 1250. This sidesteps IEEE 754 precision errors — integer arithmetic in floating point is exact, so 1250 + 750 === 2000 is always true, whereas 12.50 + 7.50 can drift. Conversion between decimal strings and integer cents happens at the API boundary: the client sends "12.50", the server multiplies by 100 and rounds, stores 1250, and the client divides by 100 for display.

Idempotency keys

The client generates a UUID when the expense form mounts and sends it as an Idempotency-Key request header on every POST. The server stores each processed key alongside its response in the idempotency_keys table. If the same key arrives again (retry, double-click, page reload mid-submit), the server returns the cached response immediately without creating a duplicate record. Keys expire after 24 hours. The insert uses INSERT OR IGNORE so concurrent duplicate requests are handled atomically.

SQLite with better-sqlite3

SQLite was chosen for simplicity and durability. A single file (backend/expenses.db) holds all data and survives server restarts with no external process required. The better-sqlite3 driver exposes a synchronous API, which keeps the route handlers straightforward — no async/await chains around database calls. WAL (Write-Ahead Logging) mode is enabled so reads do not block writes.

React + Vite frontend

The frontend is plain React with Vite. No state management library is needed given the small scope — component-local useState and a single top-level fetch on mount are sufficient. Vite provides fast HMR during development and a small production bundle.

Case-insensitive category filtering

Category filtering is implemented at the SQL level using LOWER(category) = LOWER(?). This avoids pulling all rows into JavaScript and filtering in memory, and it handles mixed-case input (e.g., food, Food, FOOD all match the same expenses).

Date-descending sort with tie-breaking

The default sort is ORDER BY date DESC, created_at DESC. The secondary created_at key ensures deterministic ordering when two expenses share the same date — the more recently created record appears first. This is applied server-side so the client always receives a stable, predictable list.


Trade-offs

Decision What was chosen What was not chosen Rationale
Database SQLite PostgreSQL, MySQL Sufficient for a single-user personal tool. No separate process, no connection pooling needed. Would need PostgreSQL for multi-user or high-concurrency scenarios.
Idempotency store Same SQLite DB Redis, Memcached Keeps the stack simple — one dependency instead of two. The downside is that idempotency keys are lost if the database file is deleted. A Redis store would be more robust in production.
Decimal precision Integer cents DECIMAL SQL type, decimal.js library Integer arithmetic is exact and requires no extra library. The conversion is a single Math.round(parseFloat(value) * 100) at the boundary.
Frontend state Component-local state Redux, Zustand, React Query The app has one list and one form. A global store would add complexity with no benefit at this scale.

Intentionally Not Implemented

  • User authentication / authorization — out of scope for this exercise. All expenses are visible to anyone with access to the running server.
  • Pagination — the list endpoint returns all expenses. Acceptable for personal use; would need cursor- or offset-based pagination at scale.
  • Expense editing or deletion — expenses are append-only. Editing and deletion were not part of the requirements.
  • Currency selection — the currency symbol is hardcoded to ₹. Supporting multiple currencies would require storing a currency code per expense and handling conversion rates.

About

To Record and Review my personal expenses

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages