A Spring Boot REST API that assesses whether a client can afford a home or car loan, based on their financial data (income, expenses), using explicit, configurable business rules (DTI threshold, interest rate, inflation factor)
A client submits their financial data (salary, additional income, monthly expenses and liabilities) and requests an assessment of whether they can afford a home or car loan, and under what conditions. The system calculates the result based on clear rules instead of a black box — every decision (APPROVED / REJECTED / APPROVED_WITH_NOTE) follows concrete, explainable steps.
- Java 21
- Spring Boot 4.1 (Web, Data JPA, Validation)
- Hibernate ORM
- PostgreSQL 16 (Docker)
- Lombok
- Bean Validation (Jakarta Validation)
- JUnit 5 + Mockito
- Maven
com.example.TalosFin
├── entity/
│ ├── Klijent.java (Client)
│ ├── FinansijskaStavka.java (FinancialItem)
│ ├── TipStavke.java (enum: PRIHOD/INCOME, TROSAK/EXPENSE)
│ ├── ZahtevZaKredit.java (LoanRequest)
│ ├── TipKredita.java (enum: STAN/HOME, AUTO/CAR)
│ ├── Preporuka.java (Recommendation)
│ ├── StatusPreporuke.java (enum: APPROVED, REJECTED, APPROVED_WITH_NOTE)
│ └── ParametarSistema.java (SystemParameter)
├── repository/
│ ├── KlijentRepository.java
│ ├── FinansijskaStavkaRepository.java
│ ├── ZahtevZaKreditRepository.java
│ ├── PreporukaRepository.java
│ └── ParametarSistemaRepository.java
├── service/
│ ├── KlijentService.java
│ ├── ZahtevZaKreditService.java
│ ├── ParametarSistemaService.java
│ └── KalkulacijaService.java (the decision engine — core business logic)
├── controller/
│ ├── KlijentController.java
│ ├── ZahtevZaKreditController.java
│ └── ParametarSistemaController.java
├── dto/
│ ├── KlijentDTO.java / KreirajKlijentDTO.java
│ ├── FinansijskaStavkaDTO.java
│ ├── ZahtevZaKreditDTO.java / KreirajZahtevDTO.java
│ └── PreporukaDTO.java
├── exception/
│ ├── KlijentNotFoundException.java
│ ├── ZahtevNotFoundException.java
│ ├── ParametarNotFoundException.java
│ ├── NevalidanZahtevException.java
│ └── GlobalExceptionHandler.java
└── TalosFinApplication.java
Klijent = Client,
ZahtevZaKredit = LoanRequest,
Preporuka = Recommendation
Client (1) ──── (N) FinancialItem [salary, additional income, monthly expenses]
Client (1) ──── (N) LoanRequest [request: type (HOME/CAR), amount, term]
LoanRequest (1) ──── (1) Recommendation [result: status, max installment, explanation]
SystemParameter (standalone table) [interest rate, max % of income, inflation]
Design rationale:
FinancialItemis a separate entity (not a field onClient) because a client can realistically have multiple income sources and multiple ongoing liabilities (other loan installments, rent...).LoanRequest → Recommendationis a 1:1 relationship — every request gets exactly one recommendation, which also stores a historical snapshot of the parameters (e.g. the interest rate) used at calculation time, even if system parameters change later.SystemParameteris a separate table, not hardcoded values in the code — business rules (interest rate, DTI threshold) can be changed without redeploying the application.
The core of the system — how a loan decision is made:
- Calculate the client's total monthly income (sum of
FinancialItemof typeINCOME) - Calculate total monthly liabilities (sum of type
EXPENSE) - Read
maxIncomePercentageForInstallmentfromSystemParameter(e.g. 40% — standard DTI) - Adjust that percentage based on current inflation (higher inflation → more conservative threshold)
- Calculate the maximum installment the client can afford = income × adjusted% − liabilities
- Calculate the actual installment for the requested loan using the standard annuity formula (amount, interest rate, repayment term)
- Compare and return a
Recommendation:APPROVED/REJECTED/APPROVED_WITH_NOTE(borderline case), with a text explanation
All monetary values use BigDecimal (never double/float), since binary rounding
introduces errors in financial calculations.
| Method | Path | Description |
|---|---|---|
| POST | /api/klijenti |
Create a client |
| GET | /api/klijenti/{id} |
Get client details |
| GET | /api/klijenti |
List all clients |
| POST | /api/klijenti/{id}/stavke |
Add an income/expense item |
| GET | /api/klijenti/{id}/stavke |
List all financial items for a client |
| POST | /api/klijenti/{id}/zahtevi |
Submit a loan request |
| GET | /api/zahtevi/{id} |
Get request details + recommendation |
| GET | /api/klijenti/{id}/zahtevi |
Get a client's request history |
| GET | /api/parametri |
Get current system parameters |
| PUT | /api/parametri/{naziv} |
Update a parameter (e.g. interest rate) |
- Java 21 (JDK)
- Maven (or use the included
./mvnwwrapper) - Docker + Docker Compose
# 1. Start the PostgreSQL database in Docker
docker compose up -d
# 2. Run the application
./mvnw spring-boot:runThe application is available at http://localhost:8080.
On first startup, Hibernate automatically generates the database schema
(spring.jpa.hibernate.ddl-auto=update), and data.sql inserts the initial system
parameters (interest rate, DTI threshold, inflation) if they don't already exist.
docker exec -it TalosFin-db psql -U user -d TalosFin -c '\dt'# Create a client
curl -X POST http://localhost:8080/api/klijenti \
-H "Content-Type: application/json" \
-d '{"ime":"Marko","prezime":"Marković","email":"marko@example.com"}'
# Add income (replace {id} with the id from the previous response)
curl -X POST http://localhost:8080/api/klijenti/1/stavke \
-H "Content-Type: application/json" \
-d '{"tip":"PRIHOD","naziv":"Salary","iznos":2000}'
# Submit a loan request
curl -X POST http://localhost:8080/api/klijenti/1/zahtevi \
-H "Content-Type: application/json" \
-d '{"tipKredita":"STAN","traženiIznos":10000,"rokOtplateMeseci":120}'The last call returns the full ZahtevZaKreditDTO with a nested preporuka section —
status, calculated installment, maximum installment, and a text explanation.
./mvnw clean testThe focus is on KalkulacijaServiceTest — unit tests for the core business logic
(annuity formula, DTI check, inflation impact), isolated from the database using
Mockito mocks.
- Bean Validation (
@NotBlank,@Email,@Positive,@Min) on input DTOs GlobalExceptionHandler(@RestControllerAdvice) centrally catches all errors (missing resources → 404, invalid requests → 400) and returns a readable JSON response instead of a raw stack trace