Qual è la differenza tra un test unitario e un test di integrazione?
Un test unitario isola un piccolo pezzo di codice (una classe o una funzione) e ne verifica il comportamento senza toccare sistemi esterni, usando fake o mock per i collaboratori. Un test di integrazione esercita più componenti insieme, spesso con infrastruttura reale come un database o un message broker, per confermare che collaborino correttamente. I test unitari sono veloci e numerosi e formano la base della piramide dei test; quelli di integrazione sono meno numerosi, più lenti e catturano difetti di cablaggio, SQL e serializzazione che i test unitari con mock non possono cogliere.
Quando dovrei scegliere un monolite invece dei microservizi?
Parti da un monolite a meno che tu non abbia una ragione concreta e presente per non farlo. Un monolite modulare è più semplice da costruire, testare, distribuire e debuggare, ed evita la tassa dei sistemi distribuiti fatta di guasti di rete, consistenza eventuale e overhead operativo. I microservizi ripagano quando servono deploy e scaling indipendenti delle parti, quando i team sono abbastanza grandi da possedere i servizi in autonomia e quando hai la maturità di piattaforma (osservabilità, CI/CD, on-call) per gestirne molti. Suddividere prematuramente tende a trasformare semplici chiamate in-process in difficili problemi distribuiti.
Come evito il problema delle query N+1 con un ORM?
Il problema N+1 si verifica quando caricare N righe padre innesca una query aggiuntiva per padre per recuperare un campo correlato, per un totale di N+1 query. Risolvilo recuperando l'associazione nello stesso round trip: in JPA usa una JOIN FETCH nel JPQL, un entity graph o il batch fetching, e verifica ispezionando l'SQL generato con il logging abilitato. Evita di affidarti al caricamento lazy predefinito dentro una vista o un serializzatore, un trigger comune; carica esattamente i dati che ti servono alla frontiera.
Cosa mi dà concretamente SOLID nella pratica?
SOLID è un insieme di euristiche che, applicate con giudizio, mantengono il codice facile da cambiare. I più rilevanti nella pratica sono il Principio di Singola Responsabilità, che tiene separate ragioni di cambiamento non correlate, e il Principio di Inversione delle Dipendenze, che permette di dipendere da astrazioni così le implementazioni possono essere sostituite e testate. Tratta SOLID come una guida, non un dogma: applicarlo in eccesso produce indirezione inutile e interfacce con una sola implementazione, il che viola principi di semplicità come KISS e YAGNI.
In Java dovrei usare eccezioni checked o unchecked?
La pratica Java moderna privilegia nettamente le eccezioni unchecked (runtime) per la maggior parte delle condizioni di errore, perché le checked costringono i chiamanti a gestirle o dichiararle e tendono a far trapelare dettagli implementativi su per lo stack, inquinando le firme. Riserva le eccezioni a condizioni davvero eccezionali e non recuperabili, e restituisci un result type o un Optional per l'assenza prevista. Qualunque scelta tu faccia, non ingoiare mai un'eccezione in silenzio: registrala con contesto o rilanciala incapsulata in un'eccezione di dominio significativa.
Qual è la differenza tra autenticazione e autorizzazione?
L'autenticazione risponde a "chi sei?" verificando un'identità, ad esempio tramite password, token o certificato. L'autorizzazione risponde a "cosa ti è permesso fare?" verificando che il principal autenticato abbia il permesso per l'azione o la risorsa richiesta. Sono preoccupazioni distinte: una richiesta può essere autenticata ma non autorizzata. Un errore comune è applicare solo l'autenticazione e assumere che ogni utente loggato possa eseguire qualsiasi azione.
Come dovrei versionare una API REST?
Scegli una strategia e applicala in modo coerente: il versioning su URI (ad esempio /api/v1/...) è il più visibile e cache-friendly, mentre quello su header o media-type mantiene gli URI puliti al costo della scopribilità. Qualunque scelta, tratta la versione come un contratto governato dalla logica del Semantic Versioning: aggiungi campi ed endpoint in modo retrocompatibile all'interno di una versione major, e rompi la compatibilità solo in una nuova versione major. Documenta le deprecazioni e concedi ai client una finestra di migrazione prima di rimuovere qualsiasi cosa.
Cosa rende un'operazione sicura da ritentare?
Un'operazione è sicura da ritentare quando è idempotente: eseguirla più di una volta produce lo stesso effetto di eseguirla una sola volta. In HTTP, GET, PUT e DELETE sono definiti idempotenti mentre POST in genere non lo è, quindi creare risorse in sicurezza richiede di solito una idempotency key che il server usa per deduplicare. Combina i retry con backoff esponenziale e jitter per evitare tempeste di retry, e proteggi i downstream con un circuit breaker così da fallire rapidamente quando una dipendenza è chiaramente malsana.
Quando conviene aggiungere una cache?
Aggiungi una cache solo dopo aver misurato un problema di latenza o di carico e quando il dato viene letto molto più spesso di quanto cambi. Una cache introduce una seconda fonte di verità e il difficile problema dell'invalidazione, quindi pesa questo costo contro il beneficio. Scegli una strategia di invalidazione adatta alla tua tolleranza alla staleness (TTL per una freschezza "sufficiente", write-through o eviction esplicita per garanzie più forti), e progetta sempre per il cache miss così che la correttezza non dipenda mai dalla cache calda.
Quanta copertura di test è sufficiente?
La percentuale di copertura è un proxy debole: il 100% di copertura delle righe può comunque mancare comportamenti, e un numero alto può nascondere test che non asseriscono nulla di significativo. Punta a coprire i comportamenti che contano - regole di business, casi limite e percorsi di errore - invece di una singola soglia globale. Un obiettivo pragmatico comune è circa l'80% come soglia minima che segnala le aree non testate, abbinato a una code review che giudica se i test verificano davvero il comportamento invece di eseguire soltanto le righe.
Cos'è il debito tecnico e come dovrei gestirlo?
Il debito tecnico è il costo futuro derivante dallo scegliere ora una soluzione sbrigativa invece di una migliore ma più lenta. Come il debito finanziario, addebita interessi: il design subottimale rallenta ogni cambiamento futuro in quell'area. Gestiscilo rendendolo visibile (tracciandolo, etichettando le scorciatoie deliberatamente), ripagandolo opportunisticamente quando tocchi di nuovo quel codice, e distinguendo il debito deliberato e tracciato da quello sconsiderato e accidentale. Il pericolo è il debito silenzioso e non gestito che si accumula finché il sistema resiste al cambiamento.
Perché preferire l'immutabilità?
Gli oggetti immutabili non possono cambiare dopo la costruzione, il che elimina un'intera classe di bug causati dallo stato mutabile condiviso e dall'aliasing, e li rende intrinsecamente thread-safe. Sono più facili su cui ragionare perché il valore di un riferimento non cambia sotto i tuoi piedi, e possono essere liberamente messi in cache e condivisi. Il costo è l'allocazione di nuove istanze a ogni modifica, di solito trascurabile e ampiamente ripagato dalla riduzione dei difetti; i record Java e gli aggiornamenti copy-on-write rendono il pattern ergonomico.
Qual è la regola delle dipendenze nella Clean Architecture?
La regola delle dipendenze afferma che le dipendenze nel codice sorgente possono puntare solo verso l'interno, verso le politiche di più alto livello, mai verso l'esterno verso i dettagli. Il livello di dominio (regole di business) non sa nulla di framework, database o web; i livelli esterni dipendono da quelli interni tramite interfacce definite dal livello interno. Questo mantiene la logica di business testabile in isolamento e permette di sostituire un database o un framework web senza toccare il nucleo. Le violazioni tipicamente emergono come classi di dominio che importano tipi JPA o Spring.
Come prevengo la SQL injection?
Usa sempre query parametrizzate o prepared statement così che l'input dell'utente venga inviato come dato, mai concatenato nella stringa SQL. Gli ORM e i query builder lo fanno per te quando passi i valori come parametri invece di interpolarli. Valida e vincola l'input alla frontiera come difesa in profondità, applica account di database a privilegio minimo e non costruire mai SQL dinamico per concatenazione di stringhe con input non fidato: è questa la causa radice di quasi ogni vulnerabilità di injection.
Qual è la differenza tra concorrenza e parallelismo?
La concorrenza è una proprietà strutturale: un programma è concorrente se è composto da task eseguiti in modo indipendente le cui durate si sovrappongono, il che gli permette di gestire molte cose insieme anche su un solo core. Il parallelismo è una proprietà di esecuzione: i task girano letteralmente nello stesso istante su più core. Puoi avere concorrenza senza parallelismo (task interlacciati su una sola CPU) e progetti per la concorrenza per gestire molte cose, mentre sfrutti il parallelismo per farle più velocemente.
La logica di business dovrebbe stare nel database o nell'applicazione?
Mantieni le regole di business centrali nel livello di dominio dell'applicazione, dove sono testabili, versionate e espresse nel linguaggio ubiquo, invece di sparpagliarle tra stored procedure e trigger. Usa il database per ciò che fa meglio: far rispettare i vincoli di integrità dei dati (chiavi esterne, vincoli unique e check, not-null) come ultima linea di difesa. Logica pesante nel database è difficile da testare, distribuire e su cui ragionare, e accoppia le tue regole a uno specifico motore; i vincoli di integrità nel database, al contrario, ti proteggono indipendentemente dal percorso di codice che scrive i dati.