Salta al contenuto principale

Buone pratiche

Preferire l'injection da costruttore a quella su campo

Inietta le dipendenze tramite il costruttore così che siano esplicite, obbligatorie e final, rendendo la classe immutabile e banalmente testabile senza container. L'injection su campo nasconde le dipendenze, consente agli oggetti di esistere in uno stato parzialmente costruito e impone la reflection nei test.

  • Fai: public OrderService(OrderRepository repo, Clock clock) { this.repo = repo; this.clock = clock; }
  • Non fare: annotare campi privati mutabili con @Autowired senza fornire un costruttore.

Rendere immutabili per default gli oggetti di dominio

Modella valori e DTO come tipi immutabili (record Java, campi final, strutture congelate) così che lo stato condiviso non possa corrompersi e gli oggetti siano thread-safe. Modifica producendo una nuova istanza invece di cambiare l'originale.

  • Fai: record Money(BigDecimal amount, Currency currency) {} e restituisci un nuovo Money da add.
  • Non fare: esporre setter su un value object, lasciando che qualunque chiamante rompa i suoi invarianti dopo la costruzione.

Tenere il livello di dominio libero da dipendenze dai framework

Applica la regola delle dipendenze della Clean Architecture: le regole di business non devono importare tipi web, di persistenza o di framework. Dipendi da interfacce definite dal dominio e implementale nei livelli esterni, così la logica centrale è testabile senza avviare Spring o un database.

  • Fai: definire un'interfaccia OrderRepository nel dominio e un'implementazione JPA in infrastructure.
  • Non fare: annotare un'entità di dominio con @Entity e @Table, accoppiando le regole di business a Hibernate.

Validare l'input alle frontiere del sistema

Tratta tutti i dati esterni (richieste HTTP, payload di messaggi, contenuti di file, risposte di terze parti) come non fidati e validali nel momento in cui entrano nel sistema, fallendo rapidamente con errori chiari. I livelli interni possono poi assumere dati validi.

  • Fai: validare un DTO di richiesta con Bean Validation (@NotBlank, @Email) nel controller prima di mapparlo al dominio.
  • Non fare: passare stringhe grezze della richiesta in profondità nella logica di business e scoprire tre livelli più sotto che sono malformate.

Non ingoiare mai le eccezioni in silenzio

Un blocco catch vuoto nasconde i fallimenti e produce sistemi che falliscono in modo invisibile. Gestisci l'errore in modo significativo, registralo con contesto sufficiente a diagnosticarlo, oppure rilancialo incapsulato in un'eccezione di dominio significativa.

  • Fai: catch (IOException e) { throw new ReportGenerationException("impossibile scrivere il report " + id, e); }
  • Non fare: catch (Exception e) {} - il fallimento svanisce e il debug diventa indovinello.

Usare query parametrizzate per ogni SQL dinamico

Invia l'input dell'utente come parametro bound, mai concatenato nella stringa di query, per rendere la SQL injection strutturalmente impossibile. Vale per JDBC grezzo, query builder e query native dell'ORM allo stesso modo.

  • Fai: SELECT * FROM users WHERE email = ? con il valore associato come parametro.
  • Non fare: "SELECT * FROM users WHERE email = '" + email + "'" - un input creato ad arte compromette il database.

Scrivere prima il test per la logica non banale

Guida il design dalla prospettiva del consumatore con il TDD: scrivi un test fallito, fallo passare con il codice più semplice possibile, poi rifattorizza. Il test chiarisce il requisito e la suite risultante è una rete di regressione che rende sicuro il cambiamento futuro.

  • Fai: scrivere un test fallito per il caso limite (carrello vuoto, token scaduto) prima di implementare l'handler.
  • Non fare: scrivere prima tutto il codice di produzione e attaccare test che confermano solo ciò che già fa.

Rendere sicuri i retry con l'idempotenza

In ogni sistema che può ritentare una richiesta (client, code di messaggi, gateway), assicurati che l'operazione sia idempotente così che un duplicato non abbia effetti aggiuntivi. Per operazioni non idempotenti come la creazione di risorse, usa una idempotency key su cui il server deduplica.

  • Fai: accettare un header Idempotency-Key sulle POST e restituire il risultato originale per una chiave ripetuta.
  • Non fare: processare ciecamente ogni POST in arrivo, così che un timeout-and-retry del client addebiti due volte il cliente.

Proteggere le chiamate fra servizi con timeout e circuit breaker

Una chiamata remota senza timeout può bloccare un thread per sempre e degenerare in fallimento totale sotto carico. Limita ogni chiamata con un timeout, aggiungi retry limitati con backoff e avvolgi le dipendenze inaffidabili in un circuit breaker che fallisce rapidamente quando sono chiaramente malsane.

  • Fai: configurare un circuit breaker Resilience4j più un timeout per chiamata sul WebClient verso il servizio di pagamento.
  • Non fare: chiamare una API a valle con timeout infiniti di default e nessun fallback, lasciando che una dipendenza lenta esaurisca il thread pool.

Mantenere le funzioni piccole e con un solo scopo

Una funzione dovrebbe fare una cosa a un solo livello di astrazione, idealmente ben sotto le cinquanta righe e senza nesting profondo. Funzioni piccole e ben nominate sono autodocumentanti, più facili da testare e da riusare rispetto a lunghe procedure che mescolano responsabilità.

  • Fai: estrarre validazione, mapping e persistenza in metodi helper nominati richiamati da un breve metodo orchestratore.
  • Non fare: stipare parsing, regole di business, logging e I/O in un metodo di cento righe con cinque livelli di if annidati.

Non ripetere la conoscenza, ma tollerare la duplicazione incidentale

Applica DRY alla conoscenza duplicata: una regola di business o una costante che deve cambiare all'unisono va in un solo posto. Resisti alla tentazione di unire codice che si somiglia soltanto ma cambia per ragioni diverse, perché un'astrazione prematura accoppia concetti non correlati.

  • Fai: estrarre un'unica costante per l'aliquota IVA referenziata ovunque l'aliquota venga applicata.
  • Non fare: forzare due funzioni casualmente simili dietro un unico helper parametrizzato che cresce di flag per ogni differenza.

Praticare la continuous integration su un mainline rilasciabile

Integra piccole modifiche nel ramo principale frequentemente, con ogni integrazione che avvia una build e una suite di test automatizzate, e mantieni il mainline sempre rilasciabile. Branch di breve durata e feedback rapido prevengono le dolorose integrazioni big-bang causate dai branch di lunga durata.

  • Fai: integrare almeno giornalmente dietro un feature flag, con la CI che blocca il merge sui test verdi.
  • Non fare: sviluppare una funzionalità su un branch per tre settimane e tentare un merge enorme con conflitti irrisolti.

Strumentare il codice per l'osservabilità fin dall'inizio

Inserisci log strutturati, metriche e tracce distribuite così da poter diagnosticare il comportamento in produzione, inclusi i fallimenti non previsti. Correla le richieste con un trace o correlation ID e registra contesto significativo, non righe opache "si è verificato un errore".

  • Fai: emettere un log strutturato con un correlation ID, registrare metriche di latenza ed error rate e tracciare le chiamate cross-service.
  • Non fare: affidarsi a semplici righe System.out.println senza contesto, poi indovinare cosa è successo in produzione.

Gestire la configurazione tramite l'ambiente, non il codice

Seguendo l'approccio Twelve-Factor, tieni nell'ambiente la configurazione che varia tra i deploy (credenziali, URL, feature toggle), mai cablata né committata. Questo permette allo stesso artefatto di girare in ogni ambiente e tiene i segreti fuori dal controllo di versione.

  • Fai: leggere l'URL del database e le chiavi API da variabili d'ambiente, validandone la presenza all'avvio.
  • Non fare: committare un file di config con le password di produzione, o ramificare su un if (env == "prod") cablato nel codice di business.