Architettura¶
Visione d'insieme¶
L'applicazione è un servizio Spring Boot 3 monolitico (sano!) organizzato per feature invece che per layer tecnico.
flowchart LR
Client[Client HTTP] -->|JSON| Controller
subgraph App[Spring Boot Application]
Controller --> Service
Service --> Repository
Service --> Domain[Domain Model]
Repository --> JPA[(JPA / Hibernate)]
end
JPA --> DB[(PostgreSQL 16)]
Actuator[(Actuator)] -->|/prometheus| Prom[Prometheus] Layering¶
| Layer | Responsabilità | Esempio |
|---|---|---|
| Controller | Adapt HTTP ↔ service. Bean Validation, status code, header Location. | ProjectController, TaskController |
| Service | Orchestrazione, transazioni, validazione di regole tra entità. | ProjectService, TaskService |
| Domain | Regole intrinseche dell'entità (state machine, invarianti). | Task#changeStatus, Project#archive |
| Repository | Accesso ai dati JPA. Nessuna logica di business. | ProjectRepository, TaskRepository |
| DTO + Mapper | Decoupling tra modello JPA mutabile e contratto API immutabile (records). | TaskResponse, TaskMapper |
| Error handler | Traduzione eccezioni → ProblemDetail (RFC 7807). | GlobalExceptionHandler |
Package layout¶
dev.federicocalo.sbfs
├── SpringBootFromScratchApplication
├── config/
│ └── OpenApiConfig
├── common/
│ ├── error/GlobalExceptionHandler
│ └── exception/
│ ├── ResourceNotFoundException
│ ├── InvalidStateTransitionException
│ └── ProjectArchivedException
├── project/
│ ├── Project (entity)
│ ├── ProjectStatus
│ ├── ProjectRepository
│ ├── ProjectService
│ ├── ProjectMapper
│ ├── ProjectController
│ └── dto/
│ ├── ProjectRequest
│ └── ProjectResponse
└── task/
├── Task (entity)
├── TaskStatus (con state machine)
├── TaskPriority
├── TaskRepository
├── TaskService
├── TaskMapper
├── TaskController
└── dto/
├── TaskRequest
├── TaskStatusUpdateRequest
└── TaskResponse
State machine del Task¶
stateDiagram-v2
[*] --> TODO : create
TODO --> IN_PROGRESS
TODO --> DONE
IN_PROGRESS --> DONE
IN_PROGRESS --> TODO : reopen
DONE --> TODO : reopen Le transizioni sono enforced nel dominio (Task#changeStatus), non in service o controller. Una transizione invalida lancia InvalidStateTransitionException → HTTP 409.
Perché nel dominio?
Mettere la regola nell'entità garantisce che valga sempre, anche da chiamate non REST (es. job, console, test). È il principio "always-valid object".
Flusso di un POST /api/v1/projects/{id}/tasks¶
sequenceDiagram
actor C as Client
participant TC as TaskController
participant TS as TaskService
participant PS as ProjectService
participant TR as TaskRepository
participant DB as PostgreSQL
C->>TC: POST /api/v1/projects/{id}/tasks
TC->>TS: create(projectId, request)
TS->>PS: loadOrThrow(projectId)
PS->>DB: SELECT project
DB-->>PS: row
PS-->>TS: Project
TS->>TS: check !archived
TS->>TR: save(new Task(...))
TR->>DB: INSERT tasks
DB-->>TR: row
TR-->>TS: Task
TS-->>TC: TaskResponse
TC-->>C: 201 Created + Location header Gestione errori (RFC 7807)¶
Esempio di risposta di validazione fallita:
HTTP/1.1 400 Bad Request
Content-Type: application/problem+json
{
"type": "https://federicocalo.dev/errors/validation",
"title": "Validation error",
"status": 400,
"detail": "Validation failed for one or more fields",
"errors": [
{"field": "name", "message": "must not be blank"}
],
"timestamp": "2026-05-20T18:25:43.511Z"
}
Mappature usate:
| Eccezione | HTTP | type |
|---|---|---|
ResourceNotFoundException | 404 | /errors/not-found |
MethodArgumentNotValidException | 400 | /errors/validation |
HttpMessageNotReadableException, MethodArgumentTypeMismatchException | 400 | /errors/bad-request |
InvalidStateTransitionException, ProjectArchivedException, DataIntegrityViolationException | 409 | /errors/conflict |
Tutte le altre Exception | 500 | /errors/internal |
Persistenza¶
- JPA + Hibernate 6 con
open-in-view: false(no Session aperta nella view layer). - PostgreSQL 16 come unico database supportato (no H2 nemmeno in test — Testcontainers).
- Flyway gestisce lo schema. Mai
ddl-auto: create/update(settato avalidate). @VersionsuProjecteTaskper optimistic locking.- Cascade
ALL+orphanRemovalsulla relazioneProject -> Task: cancellare un progetto cancella i task.
Observability¶
| Endpoint | Scopo |
|---|---|
/actuator/health | Health complessivo (con probe liveness/readiness) |
/actuator/info | Metadati app + git |
/actuator/metrics | Metriche Micrometer |
/actuator/prometheus | Scrape Prometheus |
In profilo prod la health espone solo UP/DOWN, niente dettagli interni.