Full-stack personal finance management — Spring Boot · Flutter · AWS EC2
Frontend Repository: expense-tracker-frontend
ExpenseTracker is a production-deployed personal finance application. The backend is a Spring Boot REST API secured with JWT authentication, backed by MySQL, containerised with Docker, and running on AWS EC2. The Flutter mobile client connects to the live server over HTTPS, supporting income/expense tracking, real-time analytics, and user profile management — all with dark and light mode.
Every push to main triggers a GitHub Actions workflow: the JAR is built, copied to EC2 via SSH, the old container is stopped, a new Docker image is built, and the container is restarted — with zero manual steps.
| Layer | Technology |
|---|---|
| Backend framework | Spring Boot 3.x, Java 21 |
| Security | Spring Security, JWT, BCrypt |
| Persistence | Spring Data JPA, Hibernate, MySQL 8 |
| Brevo SMTP | |
| Frontend | Flutter 3.x (Android) |
| Containerisation | Docker |
| Cloud | AWS EC2 (Ubuntu) |
| CI/CD | GitHub Actions |
| Testing | JUnit 5, Mockito, MockMvc |
| Build | Maven |
- Registration with OTP email verification — unverified users are held in memory only, never persisted until OTP is confirmed
- JWT-based stateless authentication on all protected routes
- Password reset via OTP email
- BCrypt password hashing
- Full CRUD for expenses and incomes, scoped strictly to the authenticated user
- Category tagging, date tracking, descriptions
- Monthly category breakdown
- Yearly reports
- Top spending categories
- Chart data for pie, bar, and line charts
- Home dashboard: balance, top 3 spending categories, last 3 transactions
- Add, view, edit, and delete income and expenses
- Profile with yearly statistics charts
- Dark and light mode
http://localhost:8080
All protected endpoints require:
Authorization: Bearer <jwt-token>
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/auth/register |
Register and trigger OTP email |
| POST | /api/auth/verify-otp |
Confirm OTP, persist user |
| POST | /api/auth/login |
Authenticate, receive JWT |
Register
POST /api/auth/register
{
"username": "seshathri",
"email": "seshathri686@gmail.com",
"password": "SecurePass123!"
}Login response
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"userId": 1,
"email": "seshathri686@gmail.com",
"username": "seshathri"
}| Method | Endpoint | Description |
|---|---|---|
| POST | /api/expenses |
Create expense |
| GET | /api/expenses?page=0&size=10 |
Paginated list |
| GET | /api/expenses/{id} |
Get by ID |
| PUT | /api/expenses/{id} |
Update |
| DELETE | /api/expenses/{id} |
Delete |
Create expense
{
"title": "Grocery Shopping",
"amount": 1500.00,
"date": "2024-10-23",
"category": "Food",
"description": "Weekly groceries"
}| Method | Endpoint | Description |
|---|---|---|
| POST | /api/income |
Create income |
| GET | /api/income?page=0&size=10 |
Paginated list |
| PUT | /api/income/{id} |
Update |
| DELETE | /api/income/{id} |
Delete |
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/stats/dashboard |
Balance, min/max, latest transactions |
| GET | /api/stats/chart |
Data for pie, bar, line charts |
Dashboard response
{
"totalIncome": 50000.00,
"totalExpense": 25000.00,
"balance": 25000.00,
"latestIncomes": [],
"latestExpenses": [],
"minExpense": 100.00,
"maxExpense": 5000.00
}| Method | Endpoint | Description |
|---|---|---|
| GET | /api/profile |
Get profile |
| PUT | /api/profile |
Update username / email |
| POST | /api/profile/reset-password |
Change password via OTP |
CREATE TABLE user_entity (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(255) NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
password VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
CREATE TABLE expense (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
title VARCHAR(255) NOT NULL,
amount DECIMAL(19,2) NOT NULL,
date DATE NOT NULL,
category VARCHAR(100),
description TEXT,
user_id BIGINT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES user_entity(id)
);
CREATE TABLE income (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
title VARCHAR(255) NOT NULL,
amount DECIMAL(19,2) NOT NULL,
date DATE NOT NULL,
category VARCHAR(100),
description TEXT,
user_id BIGINT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES user_entity(id)
);Tables are created manually. Hibernate auto-DDL is disabled in production due to a dialect compatibility issue with Spring Boot 3 and Hibernate 6.
- Java 21+
- Maven 3.6+
- MySQL 8.0+
# 1. Clone
git clone https://github.com/seshathri044/expense-tracker-backend.git
cd expense-tracker-backend
# 2. Create database
mysql -u root -p -e "CREATE DATABASE expense_tracker;"
# 3. Configure environment
cp src/main/resources/application.properties.example \
src/main/resources/application.properties
# Edit datasource URL, credentials, and JWT secret
# 4. Build and run
mvn clean install
mvn spring-boot:runApplication starts at http://localhost:8080.
openssl rand -base64 64spring.datasource.url=jdbc:mysql://localhost:3306/expense_tracker?useSSL=false&serverTimezone=Asia/Kolkata
spring.datasource.username=root
spring.datasource.password=YOUR_PASSWORD
jwt.secret.key=YOUR_GENERATED_KEY
spring.mail.host=smtp-relay.brevo.com
spring.mail.username=YOUR_BREVO_EMAIL
spring.mail.password=YOUR_BREVO_SMTP_KEYThe application runs in a Docker container on an AWS EC2 Ubuntu instance. The MySQL database runs on the EC2 host. Disk was expanded to 20 GB to accommodate Docker images and logs.
⚠️ Note: This server is hosted on AWS EC2 Free Tier. The free tier period ends in ~6 months, after which the server may be taken offline or migrated.
FROM openjdk:21-jdk-slim
WORKDIR /app
COPY target/expense-tracker-*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]docker build -t expense-tracker .
docker run -d \
-p 8080:8080 \
-e SPRING_PROFILES_ACTIVE=prod \
-e DB_PASSWORD=$DB_PASSWORD \
-e JWT_SECRET=$JWT_SECRET \
-e MAIL_PASSWORD=$MAIL_PASSWORD \
--name expense-tracker \
expense-trackerThe pipeline triggers on every push to main:
- Build JAR with Maven
- Copy JAR to EC2 via SCP
- SSH into EC2 — stop old container, build new image, start container
All secrets (DB password, JWT key, SSH key, email credentials) are stored in GitHub repository secrets and injected at runtime — nothing is hardcoded.
- Uses Mockito to mock the repository layer
- No real database required
- Covers: create, read, update, delete, ownership validation
- Uses MockMvc to simulate full HTTP request/response cycles
- JWT tokens are generated and included in test requests
- Covers: success paths, validation errors, unauthorized access (401), resource not found (404)
# Run all tests
mvn test
# With coverage report
mvn clean test jacoco:report- All sensitive configuration is injected via environment variables — no secrets in source control
- Every API request passes through the
JwtRequestFilterbefore reaching any controller - User data isolation: every repository query is scoped by
userIdextracted from the JWT - CORS is configured globally in
SecurityConfig— only the Flutter app's origin is whitelisted in production - Passwords are hashed with BCrypt; the raw password is never stored or logged
{
"timestamp": "2024-10-23T10:15:30",
"status": 400,
"error": "Bad Request",
"message": "Validation failed for field 'amount'",
"path": "/api/expenses"
}| Code | Meaning |
|---|---|
| 200 | OK |
| 201 | Created |
| 400 | Validation error |
| 401 | Missing or invalid JWT |
| 403 | Forbidden (wrong user) |
| 404 | Resource not found |
| 500 | Internal server error |
ExpenseTracker/
├── src/main/java/com/example/ExpenseTracker/
│ ├── Controller/ # REST controllers
│ ├── DTO/ # Data transfer objects
│ ├── Entity/ # JPA entities
│ ├── Filter/ # JWT request filter
│ ├── IO/ # Auth and profile request/response models
│ ├── Repository/ # Spring Data repositories
│ ├── Service/ # Business logic (interface + impl)
│ ├── SpringConfig/ # Security and CORS configuration
│ └── Util/ # JWT utility
├── src/main/resources/
│ ├── application.properties
│ ├── application-dev.properties
│ └── application-prod.properties
├── src/test/ # JUnit + Mockito + MockMvc tests
├── .github/workflows/ # GitHub Actions CI/CD
├── Dockerfile
└── pom.xml
The Flutter frontend connects to the live EC2 server. An installable APK is available on the Releases page.
Seshathri M LinkedIn · GitHub · seshathri686@gmail.com