Skip to main content

Projects Overview

Real Spring Boot projects dissected layer by layer — this overview gives you the architecture, key design decisions, and interview-ready talking points for the Loan Application Evaluator.

Projects Index

ProjectStackKey Concepts
Loan Application EvaluatorSpring Boot 4, JPA, H2, MockitoLayered architecture, @Embeddable, BigDecimal, @RestControllerAdvice, @Nested tests

Loan Application Evaluator — At a Glance

What It Does

A POST /applications REST endpoint that evaluates a loan request and returns either:

  • An approved offer with interest rate, EMI, and total payable
  • A rejected response listing every rejection reason

All applications (approved and rejected) are persisted for audit.

Architecture at a Glance

HTTP Request (POST /applications)

LoanApplicationController (@RestController, constructor injection)

LoanApplicationService (business logic: risk → rate → EMI → eligibility)

ILoanApplicationRepository (JpaRepository<LoanApplication, String>)

H2 in-memory DB (loan_applications table + rejection_reasons join table)

Key Class Responsibilities

ClassResponsibility
LoanApplicationControllerAccept POST /applications, delegate to service, return 201 Created
LoanApplicationServiceOrchestrate risk classification → rate → EMI → eligibility check → persist
LoanApplicationJPA @Entity (aggregate root); stores all application data in one table
Applicant, Loan, Offer@Embeddable value objects — columns merged into loan_applications
GlobalExceptionHandler@RestControllerAdvice — catches validation failures and unknown errors
ErrorResponseJava record for structured 400/500 JSON error bodies

Business Rules Quick Reference

Risk Band (credit score → interest rate premium):

Credit ScoreRisk BandRate Premium
≥ 750LOW+0%
650–749MEDIUM+1.5%
600–649HIGH+3.0%
< 600Rejected

Interest Rate Formula:

Final Rate = 12% (base)
+ risk premium (0 / 1.5 / 3.0)
+ employment premium (0 for SALARIED, +1% for SELF_EMPLOYED)
+ size premium (+0.5% if loan > ₹10,00,000)

EMI Formula:

EMI = P × r × (1 + r)^n  /  ((1 + r)^n - 1)
where r = annual rate ÷ 12 ÷ 100

Eligibility Rules (applied in order):

  1. creditScore < 600CREDIT_SCORE_TOO_LOW
  2. age + ceil(tenureMonths/12) > 65AGE_TENURE_LIMIT_EXCEEDED
  3. EMI > 60% of monthlyIncomeEMI_EXCEEDS_60_PERCENT
  4. (if rules 1–3 pass) EMI > 50% of monthlyIncomeEMI_EXCEEDS_50_PERCENT

DTO / Entity Field Validation Quick Reference

FieldConstraint
nameLetters + spaces only
age21–60
creditScore300–900
monthlyIncome> 0
amount₹10,000 – ₹50,00,000
tenureMonths6–360

Key Design Decisions

DecisionWhat Was ChosenWhy
DTOsJava RecordsImmutable, no boilerplate, no Lombok dependency
Domain objects@Embeddable (single table)Simple lifecycle, always accessed via LoanApplication
Rejection reasons@ElementCollection of enumNo separate entity needed; never queried independently
Financial mathBigDecimal with HALF_UPAvoids floating-point precision errors
Primary keyUUID StringGlobally unique, can be assigned before DB insert
Testing@ExtendWith(MockitoExtension) — no Spring contextFast, isolated, pure logic verification

H2 Schema at a Glance

loan_applications (single table — includes applicant + loan + offer columns)
application_id (PK, UUID)
name, age, monthly_income, employment_type, credit_score ← Applicant
loan_amount, loan_tenure_months, loan_purpose ← Loan
status, risk_band
offer_interest_rate, offer_tenure_months, offer_emi, offer_total_payable ← Offer (nullable)

rejection_reasons (join table)
application_id (FK)
reason (enum string)

Test Coverage at a Glance

@Nested ClassWhat It Tests# Tests
RiskClassificationTestsLOW / MEDIUM / HIGH risk bands3
EmiCalculationTestsStandard rate, large loan, self-employed, total payable5
EligibilityLogicTestsAll 4 rejection reasons + boundary cases + multi-reason7

Top 5 Interview Questions

Q1: Why does the API return 201 Created even for rejected applications?
A: Because a LoanApplication record is created in the database regardless of outcome. 201 describes the HTTP operation (resource created), not the business decision. The status field inside the response body holds APPROVED or REJECTED.

Q2: What is the difference between @Embeddable and a separate @Entity?
A: An @Embeddable has no @Id and is stored in the parent entity's table. It models a value object whose lifecycle is entirely controlled by the parent. A separate @Entity has its own identity, its own table, and can be queried independently.

Q3: Why use BigDecimal instead of double for financial calculations?
A: double cannot represent many decimal fractions exactly in IEEE 754 binary floating-point (e.g., 0.1 becomes 0.10000000000000001). In financial calculations, these small errors compound across many multiplications. BigDecimal with explicit scale and HALF_UP rounding gives exact, auditable results.

Q4: How does @Valid work in Spring MVC, and what happens if you forget it?
A: @Valid on @RequestBody triggers the Jakarta Bean Validation engine before the controller method executes. If you omit it, all constraint annotations (@Min, @NotBlank, etc.) on the DTO fields are silently ignored — every request passes through unchanged.

Q5: Why does the test use assertEquals(0, a.compareTo(b)) instead of assertEquals(a, b) for BigDecimal?
A: BigDecimal.equals() considers scale, so new BigDecimal("1.50") and new BigDecimal("1.5") are not equal by .equals(). compareTo() ignores scale and compares numeric value only. Always use compareTo for BigDecimal equality checks in tests.


Learning Path

  1. Project Overview — run it locally, understand what it does.
  2. Domain Model — entities, DTOs, enums.
  3. API Contract — endpoint, validation, request/response shapes.
  4. Service & Business Logic — the core evaluation logic.
  5. Persistence Layer — JPA mapping details.
  6. Exception Handling@RestControllerAdvice patterns.
  7. Testing Strategy — Mockito @Nested unit tests.