Spring Boot REST API for a Medallion Architecture data platform.
This service has two responsibilities:
- Read Gold analytics from Azure PostgreSQL (
top_porsche_models) - Accept new raw sales events and upload them as JSON files to Azure Blob Storage Bronze container (
1-bronze)
- Bronze (raw):
POST /api/porsche/new-salevalidates and uploads sale events as JSON blobs - Gold (serving):
GET /api/porsche/top-modelsreturns aggregated model metrics already computed by your PySpark pipeline
This keeps ingestion and serving concerns separate while preserving a simple API layer for UI/integrations.
- Java 21
- Spring Boot 4.0.3 (Web MVC, Validation, Data JPA, Actuator)
- Azure Spring Cloud Storage starter + Azure Blob SDK
- PostgreSQL JDBC
- Lombok
- springdoc OpenAPI (
springdoc-openapi-starter-webmvc-ui:3.0.2)
src/main/java/com/porsche/sales_api
|- config/
| |- CorrelationIdFilter.java
|- controller/
| |- PorscheController.java
| |- ApiExceptionHandler.java
|- service/
| |- PorscheDataService.java
|- dto/
| |- BronzeUploadResult.java
| |- SaleRequest.java
| |- UploadSaleResponse.java
|- entity/
| |- TopPorscheModel.java
|- repository/
| |- TopPorscheModelRepository.java
- GET
/api/porsche/top-models - Returns all rows from
top_porsche_models
Example response:
[
{
"modelName": "718 Cayman",
"totalRevenue": 10452231.0,
"carsSold": 92
}
]- POST
/api/porsche/new-sale - Validates request using Bean Validation (fail fast)
- Uploads JSON payload to Azure Blob with generated name:
sale_<sale_id>_<timestamp>.json
- Returns 201 Created with
Locationheader set to the uploaded blob URL - Uses
X-Correlation-Idfor end-to-end traceability (request, logs, blob metadata) - Reads DB/blob credentials from a local gitignored secrets file
Example request:
{
"sale_id": "015ddcf1-1b32-4a17-b6bf-ae1ebcb91cc9",
"vin_number": "64321",
"model_name": "718 Cayman",
"price": 124986,
"currency": "EUR",
"country": "China",
"sale_timestamp": "2026-03-02T00:44:15.163198",
"is_electric": false
}- GET
/api/porsche/sales/{vinNumber} - Reads Bronze JSON files and returns the latest sale matching the VIN
Example:
GET /api/porsche/sales/64321
X-Correlation-Id: test-corr-002Example success response:
{
"message": "Sale uploaded successfully to bronze container",
"fileName": "sale_015ddcf1-1b32-4a17-b6bf-ae1ebcb91cc9_1763156792000.json"
}Example response header:
Location: https://<storage-account>.blob.core.windows.net/1-bronze/sale_015ddcf1-1b32-4a17-b6bf-ae1ebcb91cc9_1763156792000.json
X-Correlation-Id: corr-123Example validation error (400):
{
"timestamp": "2026-03-15T19:12:42.123Z",
"status": 400,
"error": "Validation failed",
"field": "vinNumber",
"message": "vin_number must be a numeric value between 10000 and 99999"
}spring.validation.fail-fast=true is enabled.
SaleRequest constraints:
sale_id: required, not blankvin_number: required, numeric string in range 10000..99999model_name: required and must be one of:911 CarreraTaycanCayennePanamera718 CaymanMacan911 GT3
price: required,> 0currency: required, 3 uppercase letters (ex:EUR)country: required, length2..64sale_timestamp: required, ISO local datetimeis_electric: required, boolean
After startup, open:
- Swagger UI:
http://localhost:8080/swagger-ui/index.html - OpenAPI JSON:
http://localhost:8080/v3/api-docs
Note: this project uses
springdoc-openapi-starter-webmvc-ui:3.0.2to stay compatible with Spring Boot 4 / Spring Framework 7.
Current properties used by the app:
spring.datasource.urlspring.datasource.usernamespring.datasource.passwordspring.datasource.driver-class-namespring.jpa.hibernate.ddl-auto=noneazure.storage.connection-stringazure.storage.container-namelogging.pattern.level(includes MDCcorrelationId)
This project reads secrets from src/main/resources/application-local-secrets.properties.
That file is ignored by git, so your credentials stay local.
- Copy template file:
src/main/resources/application-local-secrets.properties.example- to
src/main/resources/application-local-secrets.properties
- Fill in your local credentials.
Expected keys in local file:
app.db.urlapp.db.usernameapp.db.passwordapp.azure.storage.connection-stringapp.azure.storage.container-name
Set-Location "C:\Horatiu\Proiect personale\sales-api\sales-api"
Copy-Item "src\main\resources\application-local-secrets.properties.example" "src\main\resources\application-local-secrets.properties" -ErrorAction SilentlyContinue
.\mvnw.cmd clean spring-boot:runSet-Location "C:\Horatiu\Proiect personale\sales-api\sales-api"
.\mvnw.cmd testTests include controller validation scenarios so invalid payloads do not reach upload service logic.
PorscheDataServicelogs ingestion lifecycle (received/uploaded/serialization errors)ApiExceptionHandlercentralizes validation errors into clean400payloadsTopPorscheModelis read-only from API perspective and maps to existing pipeline-generated Gold tablePOST /api/porsche/new-salesetsLocationheader to the Azure blob URL for downstream traceabilityCorrelationIdFilterenforces/propagatesX-Correlation-Idand stores it in MDC ascorrelationId- Bronze blobs include metadata keys
correlation-idandsale-idfor downstream Spark tracing
A request correlation ID is a unique identifier attached to one API request and propagated through all logs/events produced by that request.
In this pipeline, the same correlation ID can be:
- Logged by this API when receiving
/new-sale - Added to blob metadata or filename-adjacent metadata
- Emitted by PySpark jobs when reading/processing that blob
Result: you can trace one business event end-to-end (API -> Bronze blob -> Spark processing -> Gold outputs) quickly during debugging and incident response.
- Inbound header:
X-Correlation-Id(optional) - If missing, the API generates a UUID
- Outbound response always includes
X-Correlation-Id - The value is injected into MDC (
correlationId) and appears in logs - The same value is attached to uploaded blob metadata (
correlation-id)
Example call with custom correlation ID:
Invoke-RestMethod -Method Post `
-Uri "http://localhost:8080/api/porsche/new-sale" `
-Headers @{ "X-Correlation-Id" = "spark-trace-2026-03-15-001" } `
-ContentType "application/json" `
-Body '{"sale_id":"015ddcf1-1b32-4a17-b6bf-ae1ebcb91cc9","vin_number":"64321","model_name":"718 Cayman","price":124986,"currency":"EUR","country":"China","sale_timestamp":"2026-03-02T00:44:15.163198","is_electric":false}'- Swagger returns 500 with
NoSuchMethodError ControllerAdviceBean- Ensure
springdoc-openapi-starter-webmvc-uiis3.0.2(or newer Boot 4 compatible)
- Ensure
- Validation returns 400 unexpectedly
- Confirm payload keys are snake_case (
sale_id,vin_number,model_name,sale_timestamp,is_electric)
- Confirm payload keys are snake_case (
- PostgreSQL connection errors
- Verify Azure PostgreSQL server state, firewall rules, SSL mode, and local secrets values
- Blob upload failures
- Validate
app.azure.storage.connection-stringand that container1-bronzeexists
- Validate
- Netty version mismatch warnings from Azure SDK
- Usually non-blocking; align versions only if runtime issues appear