Reactive REST API built with Spring Boot for managing franchises, branches, and products.
This project was developed as a technical assessment. It provides endpoints to:
- create a franchise
- add branches to a franchise
- add products to a branch
- delete products from a branch
- update product stock
- retrieve the product with the highest stock per branch for a given franchise
It also includes bonus features such as:
- update franchise name
- update branch name
- update product name
- JWT authentication
- Docker support
- Swagger / OpenAPI documentation
- integration testing with Testcontainers
- Java 21
- Spring Boot 4.0.5
- Spring WebFlux
- Spring Data R2DBC
- MySQL
- Spring Security
- JWT
- Swagger / OpenAPI
- Docker + Docker Compose
- Testcontainers
- JUnit 5
The application follows a layered architecture with feature-based packaging:
com.dan.franchiseapi
├── auth
├── branch
├── common
├── config
├── franchise
├── product
└── report
Each feature contains its own:
- controller
- dto
- mapper
- model
- repository
- service
A franchise contains:
idname
A branch contains:
idnamefranchiseId
A product contains:
idnamestockbranchId
Used for authentication:
idusernamepasswordrole
The application uses MySQL with the following tables:
franchisesbranchesproductsapp_users
- franchise name must be unique globally
- branch name must be unique within a franchise
- product name must be unique within a branch
- product stock must be greater than or equal to 0
POST /api/auth/login
Request:
{
"username": "admin",
"password": "admin123"
}Response:
{
"token": "your-jwt-token"
}POST /api/franchises
Request:
{
"name": "McDonalds"
}PATCH /api/franchises/{franchiseId}/name
Request:
{
"name": "Burger King"
}POST /api/franchises/{franchiseId}/branches
Request:
{
"name": "Downtown Branch"
}PATCH /api/branches/{branchId}/name
Request:
{
"name": "Central Branch"
}POST /api/branches/{branchId}/products
Request:
{
"name": "Big Mac",
"stock": 100
}DELETE /api/branches/{branchId}/products/{productId}
PATCH /api/products/{productId}/stock
Request:
{
"stock": 120
}PATCH /api/products/{productId}/name
Request:
{
"name": "Big Mac Combo"
}GET /api/franchises/{franchiseId}/top-stock-products
Response:
[
{
"branchId": 1,
"branchName": "Downtown Branch",
"productId": 10,
"productName": "Big Mac",
"stock": 120
}
]If multiple products in the same branch share the same highest stock, all matching products may be returned.
All business endpoints are protected with JWT authentication.
- username:
admin - password:
admin123
Include the token in the Authorization header:
Authorization: Bearer your-jwt-token- Docker
- Docker Compose
docker compose up --buildThe API will be available at:
- API:
http://localhost:8080 - Swagger UI:
http://localhost:8080/swagger-ui/index.html
docker compose downdocker compose down -v
docker compose up --buildIntegration tests use Testcontainers, so Docker must be running.
Run tests with:
./mvnw testSwagger UI is available at:
http://localhost:8080/swagger-ui/index.html
You can authenticate in Swagger by:
- calling
/api/auth/login - copying the returned JWT
- clicking the Authorize button
- pasting:
Bearer your-jwt-token
TOKEN=$(curl -s -X POST http://localhost:8080/api/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"admin123"}' | sed -E 's/.*"token":"([^"]+)".*/\1/')curl -X POST http://localhost:8080/api/franchises \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '{"name":"McDonalds"}'curl -X POST http://localhost:8080/api/franchises/1/branches \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '{"name":"Downtown Branch"}'curl -X POST http://localhost:8080/api/branches/1/products \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '{"name":"Big Mac","stock":100}'curl -X PATCH http://localhost:8080/api/products/1/stock \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '{"stock":120}'curl -X GET http://localhost:8080/api/franchises/1/top-stock-products \
-H "Authorization: Bearer $TOKEN"The API includes centralized exception handling.
Common responses:
400 Bad Requestfor validation errors401 Unauthorizedfor missing or invalid JWT404 Not Foundwhen an entity does not exist409 Conflictfor duplicate names500 Internal Server Errorfor unexpected errors
Example error response:
{
"timestamp": "2026-04-03T06:55:41.882564749Z",
"status": 400,
"error": "Bad Request",
"message": "Franchise name is required",
"path": "/api/franchises"
}The project includes integration tests using:
- Spring Boot Test
- WebTestClient
- Testcontainers
- MySQL container
Current test coverage includes:
- successful login
- unauthorized access to protected endpoint
- successful authenticated franchise creation
This project seeds a default admin user for demonstration purposes.
For a production environment, the following should be improved:
- externalize secrets securely
- rotate JWT secret and use environment variables
- disable default test credentials
- restrict Swagger in production
- add role-based authorization if needed
This Terraform scaffold provisions a minimal AWS deployment path for the Franchise API:
- Amazon RDS MySQL instance
- AWS Secrets Manager secrets for the database password and JWT secret
- AWS App Runner service that runs the Docker image from Amazon ECR
- AWS App Runner VPC connector so the service can reach the private RDS instance
- IAM roles for ECR image access and secret access
- Security groups and subnet group
- This scaffold assumes you have already built and pushed the application image to Amazon ECR.
- It uses the default VPC and default subnets in the selected AWS region to keep the example short.
- The application is not "already deployed" just because it has a Dockerfile. Terraform plus a real apply step is what turns it into cloud infrastructure.
- The App Runner health check is pointed at
/swagger-ui/index.htmlbecause that endpoint is available in the current app. You can change it later to a dedicated health endpoint.
terraform init
terraform plan -var-file="terraform.tfvars"
terraform apply -var-file="terraform.tfvars"- Replace default VPC usage with dedicated VPC, private subnets, and NAT
- Add a proper
/actuator/healthendpoint and point the App Runner health check to it - Store additional app configuration in AWS Systems Manager Parameter Store
- Add Route 53, ACM, and a custom domain
- Add Terraform remote state
Dan V.