π 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; Securecookies in production -
Session fixation protection via
migrateSession() - Single-session enforcement β one session per user, period
http.sessionManagement(session -> session
.sessionFixation().migrateSession()
.maximumSessions(1));
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)));
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]));
}
π± 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 || '/' }
});
});
π¨ 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
- Add Redis β Caffeine works great for single-instance rate limiting, but for horizontal scaling I'd swap to Redis-backed Bucket4j
- Implement recurring transactions β auto-detect salary, rent, subscriptions
- Add bank connectivity β integrate with Plaid or a similar aggregator
- 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 (0)