A Spring Boot REST API for issuing and prolonging fixed-term loans. It uses SQLite and sample data for local development.

Loan REST API
A Spring Boot REST API for issuing and prolonging fixed-term loans. It uses SQLite and sample data for local development.
Requirements
- Java 21
- Gradle Wrapper, included with the repository
Run the tests:
./gradlew test
Generate XML and HTML test coverage reports:
./gradlew jacocoTestReport
Open the HTML report at build/reports/jacoco/test/html/index.html.
Run the full verification used for continuous integration:
./gradlew check
The build requires at least 90% line coverage and 80% branch coverage.
Dependency versions are locked in gradle.lockfile. Refresh locks deliberately after dependency changes:
./gradlew dependencies --write-locks
GitHub Actions validates the Gradle Wrapper, then runs the same check and bootJar tasks for every push and pull request. It publishes the JaCoCo report even when verification fails and the executable JAR after successful builds. New pushes cancel outdated builds for the same branch or pull request. The job has a 15-minute timeout, and artifacts are retained for 14 days.
Create the packaged executable JAR:
./gradlew bootJar
Start the application:
./gradlew bootRun --args='--spring.profiles.active=dev'
The API is available at http://localhost:8080/api/loans.
Interactive API documentation is available at http://localhost:8080/swagger-ui/index.html. The generated OpenAPI document is available at http://localhost:8080/v3/api-docs.
Operational health endpoints return UP or DOWN without exposing component details:
http://localhost:8080/actuator/healthhttp://localhost:8080/actuator/health/livenesshttp://localhost:8080/actuator/health/readiness, including database availability
When port 8080 is already in use, choose another port:
./gradlew bootRun --args='--spring.profiles.active=dev --server.port=8081'
Run the packaged application:
java -jar build/libs/loan-rest-spring-boot-0.0.2-SNAPSHOT.jar --spring.profiles.active=dev --server.port=8081
Profiles and Database Lifecycle
The application requires an explicit dev or prod Spring profile. Startup without either profile fails before datasource initialization with a clear configuration error. Flyway owns schema creation and applies migrations from src/main/resources/db/migration; Hibernate validates the resulting schema and never creates or drops it.
devuses a disposable shared in-memory SQLite database and loads fifteen sample loans fromsrc/main/resources/db/dev/seed-loans.sql. Seeding runs after Flyway completes and only when theloanstable is empty, so restarts never duplicate sample data.produses a persistent SQLite database at./data/loan-rest.db, applies pending Flyway migrations, and does not load sample data.
Loan timestamps are stored through Hibernate in UTC. The maximum-amount overnight rule uses loan.risk.time-zone, which defaults to UTC and can be overridden with the LOAN_RISK_TIME_ZONE environment variable using an IANA time-zone ID such as Europe/Warsaw. An invalid time-zone ID prevents startup and reports the loan.risk.time-zone configuration error.
Start the production-like profile:
./gradlew bootRun --args='--spring.profiles.active=prod'
On first startup, Flyway creates the schema. Later startups validate and reuse the existing database; local files under data/ are intentionally ignored by Git.
SQLite is an embedded file database and does not use database usernames or passwords. The prod profile accepts a database-location override through LOAN_DB_URL. For example, use a different persistent SQLite location:
LOAN_DB_URL='jdbc:sqlite:/srv/loan-rest/loans.db' \
./gradlew bootRun --args='--spring.profiles.active=prod'
To create the default local persistent catalog:
make run-prod
To reset that local catalog, first stop the application, then run make reset-local-db. This permanently removes the SQLite database and its journal files. Start make run-prod again to recreate the catalog and apply Flyway migrations.
Logging
The dev profile logs application messages from pl.ultimasolution.loan at DEBUG for local diagnostics. The prod profile logs at INFO and emits structured Logstash JSON to standard output for collection by the deployment platform. Logs must not be treated as a source of loan or audit data; use the audit-trail endpoint for lifecycle history.
Business Rules
- Loan amounts must be between
0and1000inclusive. - A maximum amount request (
1000) is rejected from00:00through05:59in the server time zone. - The fourth request for the same client IP address and loan name is rejected.
- Concurrent applications are isolated so the IP/name attempt limit cannot be bypassed by simultaneous requests.
- New loans have a fixed 14-day term.
- A loan can be prolonged once, extending its existing term by 14 days.
- Deleted loans are retained for audit purposes but are excluded from normal loan reads and application-limit calculations.
The server derives the client IP address from the HTTP connection (getRemoteAddr) and accepts IPv4 or IPv6 values up to 45 characters. The ID, client IP address, timestamps, calculated term, and prolongation state are server-managed. Create requests cannot set or override these values.
Request Validation
| Field | Rules |
|---|---|
loanName |
Required, nonblank, 3 to 20 characters |
amount |
Required number from 0 through 1000 inclusive |
API
All non-empty API responses use application/json. Requests with an incompatible Accept header receive 406 Not Acceptable.
Create a loan
POST /api/loans
The endpoint accepts application/json only.
Returns 201 Created with the persisted loan:
The response includes a Location header for the created resource, for example http://localhost:8080/api/loans/4.
curl --request POST http://localhost:8080/api/loans \
--header 'Content-Type: application/json' \
--data ''
List loans
GET /api/loans returns 200 OK with a page of active loans. Use zero-based page, size, and sort query parameters. The default is page 0, size 20, sorted by createdAt; requested sizes are capped at 100; deleted loans are excluded.
curl 'http://localhost:8080/api/loans?page=0&size=20&sort=createdAt,asc'
Get a loan
GET /api/loans/ returns 200 OK or 404 Not Found.
curl http://localhost:8080/api/loans/1
Prolong a loan
PUT /api/loans/prolong/ returns 200 OK with the updated loan, 404 Not Found for an unknown loan, or 422 Unprocessable Entity when the loan is expired or was already prolonged.
curl --request PUT http://localhost:8080/api/loans/prolong/1
Delete a loan
This is an intentional CRUD extension beyond the original loan-issuing and prolongation exercise.
DELETE /api/loans/ returns 204 No Content or 404 Not Found. Deletion is a soft delete: the loan cannot be read or prolonged afterwards, but its immutable audit trail remains available.
curl --request DELETE --include http://localhost:8080/api/loans/1
Get loan audit trail
GET /api/loans//audit-trail returns 200 OK and every lifecycle event for a loan, including after deletion. Events are ordered by creation time and event ID. An unknown loan returns 404 Not Found.
curl http://localhost:8080/api/loans/1/audit-trail
[
]
All audit event timestamps are UTC. Each lifecycle event and its associated loan change are committed in the same transaction, so failed operations leave no audit record.
Errors
Malformed JSON returns 400 Bad Request with MALFORMED_REQUEST; invalid request values return 400 Bad Request with VALIDATION_FAILED; invalid path parameters return 400 Bad Request with INVALID_PATH_PARAMETER; incompatible response media types return 406 Not Acceptable with NOT_ACCEPTABLE; unsupported HTTP methods return 405 Method Not Allowed with METHOD_NOT_ALLOWED; unsupported request media types return 415 Unsupported Media Type with UNSUPPORTED_MEDIA_TYPE; unknown endpoints return 404 Not Found with ENDPOINT_NOT_FOUND; missing loans return 404 Not Found with LOAN_NOT_FOUND; business-rule violations return 422 Unprocessable Entity; concurrent updates return 409 Conflict with CONCURRENT_MODIFICATION; and unexpected failures return 500 Internal Server Error with INTERNAL_ERROR.
| Code | HTTP status | Meaning |
|---|---|---|
MALFORMED_REQUEST |
400 |
The request body is not valid JSON. |
VALIDATION_FAILED |
400 |
loanName or amount violates request validation. |
INVALID_PATH_PARAMETER |
400 |
A path parameter such as a loan ID is not a valid number. |
NOT_ACCEPTABLE |
406 |
The request must accept an application/json response. |
METHOD_NOT_ALLOWED |
405 |
The resource does not support the requested HTTP method; Allow lists supported methods. |
UNSUPPORTED_MEDIA_TYPE |
415 |
The create request body is not application/json. |
ENDPOINT_NOT_FOUND |
404 |
The requested endpoint does not exist. |
LOAN_NOT_FOUND |
404 |
No active loan has the requested ID, or no loan exists for an audit-trail request. Deleted loans are not active. |
LOAN_RULE_VIOLATION |
422 |
A loan amount, timing, attempt-limit, expiry, or one-time prolongation rule was violated. |
CONCURRENT_MODIFICATION |
409 |
Another request changed the loan or transaction state; retry the request. |
INTERNAL_ERROR |
500 |
An unexpected server error occurred; implementation details are not exposed. |
Test Coverage
The suite covers Flyway-backed application startup with the dev and prod profiles; list, get, create, prolong, delete, and audit endpoints; request parsing and validation; amount and time boundaries; IP/name attempt limits; metadata ownership; retained deletion behavior; missing resources; expiry; concurrency error handling; and one-time prolongation.
