DEV Community

Gokul Gokul
Gokul Gokul

Posted on

I Built a Full-Stack Personal Finance Tracker — Here's What Makes It Different 💰

🔗 Live App: spendsmart.gokulmithran.com


Most personal finance apps on the internet fall into one of two camps: either they're pretty UIs with a Firebase backend and zero security thinking, or they're enterprise monsters that take 6 months to set up. SpendSmart is neither.

I built SpendSmart as a production-grade, full-stack personal finance tracker with the same security and performance patterns you'd find in a real fintech product — deployed, live, and free to use.

Here's the deep dive into what I built, how I built it, and why the technical decisions matter.


🧩 What is SpendSmart?

SpendSmart is a web-based personal finance application that lets users:

  • Track transactions (income & expenses) with category tagging and filtering
  • Set budgets per spending category and get visual progress tracking
  • Create savings goals with contributions, withdrawals, and milestone tracking
  • View detailed reports — monthly breakdowns, annual overviews, daily spending trends, category donut charts, CSV export, and auto-generated insights
  • Manage multiple accounts (bank accounts, wallets, credit cards)
  • Receive push notifications for budget alerts and goal milestones
  • Install as a PWA — works offline and behaves like a native mobile app

🏗️ Tech Stack at a Glance

Layer Technology
Frontend React 18 + TypeScript + Vite 7
Styling Tailwind CSS 4 + Radix UI primitives
State TanStack React Query v5
Charts Recharts (Bar, Line, Pie, Donut)
Animations Framer Motion
Forms React Hook Form + Zod validation
PWA Workbox (vite-plugin-pwa) + Service Worker
Backend Spring Boot 3.5 + Java 21
Database PostgreSQL 15
ORM Spring Data JPA + MapStruct DTOs
Auth Spring Security + OAuth2 (Google)
Rate Limiting Bucket4j + Caffeine Cache
Notifications Web Push (RFC 8291) + BouncyCastle
Observability Micrometer + Prometheus + Spring Actuator
API Docs Springdoc OpenAPI (Swagger UI)
Secrets AWS Parameter Store
Infra Docker + Docker Compose
CI/CD GitHub Actions (AWS + GCP deploy pipelines)
CDN/Hosting Cloudflare

🔐 Security — Not an Afterthought

This is where SpendSmart diverges from most side-project expense trackers. I treated security like a first-class citizen from Day 1.

OAuth2 with Session-Based Auth (No JWTs)

Most tutorials push JWT-based auth for SPAs. I went with session-based authentication with Spring Security OAuth2 instead. Why?

  • Server-side session invalidation — if a user logs out, the session is dead immediately. No waiting for token expiry.
  • CSRF protection built-in via CookieCsrfTokenRepository with SameSite=None; Secure cookies in production
  • Session fixation protection via migrateSession()
  • Single-session enforcement — one session per user, period
http.sessionManagement(session -> session
    .sessionFixation().migrateSession()
    .maximumSessions(1));
Enter fullscreen mode Exit fullscreen mode

Production Security Headers

The prod security config sets a tight CSP, disables iframes, enforces HSTS with preload, and strips referrers:

http.headers(headers -> headers
    .contentSecurityPolicy(csp -> csp.policyDirectives(
        "default-src 'self'; script-src 'self'; ..."))
    .frameOptions(FrameOptionsConfig::deny)
    .httpStrictTransportSecurity(hsts -> hsts
        .includeSubDomains(true)
        .maxAgeInSeconds(31536000)
        .preload(true))
    .referrerPolicy(referrer -> referrer
        .policy(ReferrerPolicy.NO_REFERRER)));
Enter fullscreen mode Exit fullscreen mode

Per-Endpoint Rate Limiting with Bucket4j

I didn't slap a global rate limiter and call it a day. SpendSmart uses a policy-based rate limiting system where different API endpoint groups have different limits:

Endpoint Group Limit Key Strategy
Auth (/oauth2/, /login/) 5 req/min IP-based
Dashboard 120 req/min User-based
Transactions 60 req/min User-based
Budgets 30 req/min User-based
Goals 20 req/min User-based
Default API 100 req/min User-based

Auth endpoints are rate-limited by IP (to prevent brute-force attacks on login flows), while all other endpoints are rate-limited by authenticated user. The policies are backed by Caffeine in-memory cache for near-zero-latency bucket lookups.


📊 Reports & Insights — More Than Just Charts

The Reports module isn't a simple "show me a pie chart" screen. It provides:

  • Annual overview — stacked bar chart comparing income vs. expenses across all 12 months
  • Monthly summary cards — income, expenses, net savings, savings rate percentage with month-over-month change percentages
  • Daily spending trend — line chart showing spending patterns within a month
  • Category breakdown — interactive donut chart with percentage splits
  • Top spending categories — ranked list with progress bars
  • Budget performance — shows how each budget performed for the month
  • Auto-generated insights — the backend computes contextual financial insights
  • CSV export — one-click download of monthly transaction data

All of this is computed server-side in a single optimized query pass with batch fetching to avoid N+1 problems:

private Map<Long, BigDecimal> batchFetchSpentAmounts(List<Budget> budgets, User user) {
    List<Long> categoryIds = budgets.stream()
        .map(b -> b.getCategory().getId())
        .distinct().collect(Collectors.toList());

    List<Object[]> results = transactionRepository
        .findTotalSpentPerCategory(user, categoryIds, start, end);

    return results.stream().collect(Collectors.toMap(
        r -> (Long) r[0], r -> (BigDecimal) r[1]));
}
Enter fullscreen mode Exit fullscreen mode

📱 PWA with Real Push Notifications

SpendSmart isn't just "responsive" — it's a full Progressive Web App:

  • Workbox-powered service worker with precaching and offline navigation
  • Web Push notifications using RFC 8291 (VAPID keys + BouncyCastle encryption)
  • Backend has a dedicated notification subsystem with:
    • NotificationDispatcher — routes notifications
    • NotificationEventPublisher — event-driven architecture
    • NotificationRateLimiter — prevents notification spam
    • PushNotificationService — sends encrypted push payloads

The service worker handles push events and notification clicks with deep-link routing:

self.addEventListener('push', (event) => {
    const data = event.data.json();
    self.registration.showNotification(data.title, {
        body: data.body,
        icon: '/favicon.png',
        vibrate: [100, 50, 100],
        data: { url: data.url || '/' }
    });
});
Enter fullscreen mode Exit fullscreen mode

🎨 Frontend Architecture

React Query for Server State

No Redux. No Zustand. TanStack React Query v5 handles all server state — with automatic caching, background refetching, optimistic updates, and stale-while-revalidate patterns. The result is a UI that feels instant.

Component Architecture

  • Radix UI primitives (Dialog, Select, Popover, AlertDialog) for accessibility-first components
  • Framer Motion for page transitions and micro-animations
  • Recharts for responsive data visualizations
  • React Hook Form + Zod for type-safe form validation with zero re-renders
  • Custom undo-action hook — delete operations show a toast with an "Undo" option before actually firing the API call

Context-driven Theming

Four React Contexts power the UX:

  • AuthContext — authentication state
  • CurrencyContext — locale-aware currency formatting
  • ThemeContext — dark/light mode with system preference detection
  • SidebarContext — responsive sidebar state

☁️ Deployment & Infrastructure

  • Dockerized backend + frontend + PostgreSQL via Docker Compose
  • GitHub Actions CI/CD with separate workflows for AWS and GCP deployments
  • AWS Parameter Store for secret management (no .env files in production)
  • Cloudflare for CDN, DDoS protection, and analytics
  • Spring Boot Actuator + Prometheus metrics endpoint for production observability

💡 What I'd Do Differently

  1. Add Redis — Caffeine works great for single-instance rate limiting, but for horizontal scaling I'd swap to Redis-backed Bucket4j
  2. Implement recurring transactions — auto-detect salary, rent, subscriptions
  3. Add bank connectivity — integrate with Plaid or a similar aggregator
  4. Mobile app — the PWA is solid, but a React Native companion would unlock background sync and native widgets

🔗 Try It Out

Live: spendsmart.gokulmithran.com

Login with Google, add a few transactions, and see how it feels. It's free, your data is yours, and no credit card is needed.


If this was helpful, drop a ❤️ and follow for more full-stack deep dives. Questions? Hit me in the comments!


Tags: #webdev #react #java #springboot

Top comments (1)

Collapse
 
markusbnet profile image
Mark Barnett

Solid architecture decisions throughout - the session-based auth over JWTs is a deliberate choice most side projects skip, and laying out the rate limiting policy per endpoint is genuinely useful to see.

One thing caught my eye in the "What I'd Do Differently" section - bank connectivity via Plaid. I went the opposite direction on my finance app (money-me.com) - manual entry only, no bank linking. Curious whether you've had any users say they'd prefer not connecting bank credentials? My experience is there's a real segment who want full control over what data exists, and Plaid's data sharing model puts some people off regardless of how well it's secured on your end.

Nice work shipping this - the deployment setup with separate AWS/GCP CI pipelines is more robust than most side projects get.