Simple Expense Tracker RESTful API built with Spring Boot to allow users to create, read, update, and delete expenses.
- General Info
- Technologies Used
- Features
- Setup
- Usage
- Authentication
- Caching
- Rate and Usage Limits
- HTTP Response Codes
- Project Status
- Acknowledgements
- License
Spring Boot Expense Tracker API is a simple RESTful API that allows users to manage their expenses. It supports pagination, sorting, and filtering by date range. This API uses JWT for authentication and Redis for caching. This project is designed to explore and practice working with the Java programming language, data modeling, and user authentication in Spring Boot.
- Java 21.0.6 LTS
- Maven 3.9.11
- H2 Database 2.3.232
- Microsoft SQL Server 2022
- Spring Boot 3.4.12
- Lombok 1.18.42
- JJWT 0.12.6
- Redis 8.0.3
- Flyway 10.20.1
- Docker 29.2.0
- User Registration: Register a new user using the
POSTmethod. - User Login: Authenticate the user using the
POSTmethod. - Create a new expense: Create a new expense using the
POSTmethod. - Update an existing expense: Update an existing expense using the
PUTmethod. - Delete an existing expense: Delete an existing expense using the
DELETEmethod. - List and filter all past expenses: Get the list of expenses with pagination and filtering by date range using the
GETmethod. - List all expenses categories: Get the list of expense categories using the
GETmethod. - Refresh Token: Get a new access token using the
POSTmethod. - Revoke Tokens: Invalidates all refresh tokens for the authenticated user using the
POSTmethod. - Expenses Caching: Speeds up repeated requests for filtered/paginated expenses using Redis with 60-minute TTL.
- Brute-force Protection: Automatically jail IP addresses after repeated failed login attempts.
Database migration is managed using Flyway.
- The migration scripts are located in the
src/main/resources/db/migrationdirectory. - The initial migration script is defined in
V1__Create_initial_schema.sql. - Migrations are automatically executed on application startup
Before running the application, make sure the database exists.
To run this API, you’ll need:
- Java: Version 21 or higher
- Microsoft SQL Server 2022 or higher
- Redis: Version 8 or higher
- Docker: Version 29 or higher (optional)
How to install:
-
Clone the repository
git clone https://github.com/krisnaajiep/springboot-expense-tracker-api.git
-
Change the current working directory
cd springboot-expense-tracker-api -
Copy and rename
.env.examplecp .env.example .env
-
Set environment variables in
.envfor database and JWT secret configurationDATABASE_NAME=ExpenseTrackerAPI SPRING_DATASOURCE_URL=jdbc:sqlserver://localhost:1433;databaseName=${DATABASE_NAME};encrypt=true;trustServerCertificate=true SPRING_DATASOURCE_USERNAME=<database_username> SPRING_DATASOURCE_PASSWORD=<database_password> JWT_SECRET=<your_strong_secret> SPRING_DATA_REDIS_HOST=localhost SPRING_DATA_REDIS_PORT=6379 SPRING_DATA_REDIS_USERNAME=<redis_username> SPRING_DATA_REDIS_PASSWORD=<redis_password> SPRING_DATA_REDIS_DATABASE=<redis_database_index>
-
Start the application.
- Using Docker Compose:
- Copy and rename
.envto.env.prodcp .env .env.prod
- Change the
SPRING_DATASOURCE_URLandSPRING_DATA_REDIS_HOSTin.env.prodto use service names defined incompose.yamlSPRING_DATASOURCE_URL=jdbc:sqlserver://db:1433;databaseName=${DATABASE_NAME};encrypt=true;trustServerCertificate=true SPRING_DATA_REDIS_HOST=redis
- Run the following command to start the services:
docker compose up
- Copy and rename
- Using local installations:
- Start Microsoft SQL Server and Redis server.
- Create the database specified in
DATABASE_NAMEenvironment variable. - Build the project
./mvnw clean package -DskipTests
- Run the JAR file
java -jar target/expense-tracker-api-3.0.0.jar
- Using Docker Compose:
-
Register
POST http://localhost:8080/register Content-Type: application/json { "name": "John Doe", "email": "john@doe.com", "password": "MyPass_1234" }
-
Create a new expense
POST http://localhost:8080/expenses Content-Type: application/json Authorization: Bearer <your_access_token> { "description": "Purchase of new computer", "amount": "800", "date": "2025-06-30", "categoryId": 1 }
Expense categories are stored in a separate table (ExpenseCategory).
Each expense references a category using a foreign key (CategoryID).
Categories must exist before they can be assigned to an expense.
The following expense categories are automatically created during database migration:
- Others
- Health
- Clothing
- Utilities
- Electronics
- Leisure
- Groceries
How to get all expense categories:
GET http://localhost:8080/categories
Authorization: Bearer <your_access_token>This API uses Bearer Token for authentication. You can generate an access token by registering a new user or login.
You must include an access token in each request to the API with the Authorization request header.
If an API key is missing, malformed, or invalid, you will receive a 401 Unauthorized HTTP status.
{
"message": "Unauthorized"
}This API implements brute-force protection by tracking failed login attempts per IP address using Redis.
- After
5consecutive failed login attempts, the IP address will be temporarily jailed. - While jailed, the IP cannot make further login attempts for
10 minutes. - The jailed IP will receive a
429 Too Many RequestsHTTP status. - A
Retry-Afterresponse header (in milliseconds) is included to indicate how long the client must wait before retrying.
Example error response when jailed:
{
"message": "Too many failed login attempts. Please try again later."
}This API uses a Cache-Aside strategy to cache expense listing responses.
- Responses to
GET /expensesare cached in Redis using a combination of:- User ID
- Date filter (e.g.
PAST_MONTH) - Page number, page size, sort order
- Cache is stored for 60 minutes (TTL).
- Whenever an expense is created, updated, or deleted, all related cache entries are automatically evicted.
- Cache keys follow the pattern:
expenses::userId=<long>:filter=<ExpenseFilter>&from=<LocalDate>&to=<LocalDate>&page=<int>&size=<int>&sort=<Sort>
See: Cache-Aside pattern
The GET /expenses endpoint also supports HTTP caching using Cache-Control and ETag headers.
Cache-Control:no-cache, private, must-revalidateETag: Generated from the MD5 hash of the response body.
API access rate limits apply on a per-IP address basis in unit time.
The limit is 10 requests per 10 seconds.
If you exceed either limit, your request will return an HTTP 429 Too Many Requests status code.
Each API response returns the following set of headers to help you identify your use status:
| Header | Description |
|---|---|
X-RateLimit-Limit |
The maximum number of requests that the consumer is permitted to make per minute. |
X-RateLimit-Remaining |
The number of requests remaining in the current rate limit window. |
X-RateLimit-Reset |
The time at which the current rate limit window resets in UTC epoch seconds. |
The API returns the following status codes depending on the success or failure of the request.
| Status Code | Description |
|---|---|
| 200 OK | The request was processed successfully. |
| 201 Created | The new resource was created successfully. |
| 400 Bad Request | The server could not understand the request due to invalid syntax. |
| 401 Unauthorized | Authentication is required or the access token is invalid. |
| 403 Forbidden | Access to the requested resource is forbidden. |
| 404 Not Found | The requested resource was not found. |
| 405 Method Not Allowed | The HTTP method is not supported for the requested resource. |
| 409 Conflict | Indicates a conflict between the request and the current state of a resource on a web server. |
| 415 Unsupported Media Type | The media format of the requested data is not supported by the server. |
| 429 Too Many Request | The client has sent too many requests in a given amount of time (rate limiting). |
| 500 Internal Server Error | An unexpected server error occurred. |
| 503 Service Unavailable | The server is temporarily unable to handle the request, usually due to maintenance or overload. |
Project is: complete.
This project was inspired by roadmap.sh.
This project is licensed under the MIT License—see the LICENSE file for details.