Vai al contenuto

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 a validate).
  • @Version su Project e Task per optimistic locking.
  • Cascade ALL + orphanRemoval sulla relazione Project -> 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.