IT EN

Django e Spring: la stessa app, due volte

Il circolo di padel vuole le prenotazioni online e tu le scrivi due volte, in Django e in Spring Boot. Una redazione del Kansas contro l'inverno di J2EE, query scritte nei nomi, l'admin in quattro righe, la trappola del 403 e i numeri misurati.

Campo da padel visto dall'alto, con Django in una metà e Spring Boot nell'altra, una pallina in volo e un tabellone con righe di codice e tempi di avvio

Martedì 16 settembre 2025. Il circolo di padel sotto casa gestisce le prenotazioni con un quaderno a quadretti appeso al bancone e un gruppo WhatsApp da 214 persone, dove ogni sera alle nove qualcuno scrive «c'è un campo libero domani alle 18?» e riceve in risposta quaranta pollici in su, due vocali e nessuna informazione utile. Ti sei offerto di sistemare la faccenda. Una piccola app: i campi, le prenotazioni, e un controllo che impedisca a due gruppi di presentarsi sullo stesso campo alla stessa ora con le racchette in mano e lo sguardo omicida.

Uno dei due framework lo conosci bene, l'altro lo hai sempre guardato da lontano, con il misto di curiosità e diffidenza che si riserva al vicino di casa che fa le cose in modo diverso. Così decidi di scriverla due volte: una in Django, il framework web di Python, e una in Spring Boot, il modo moderno di usare Spring in Java. Stessa app, stessi test, e alla fine si contano le differenze. Usi le versioni di oggi: Django 5.2.6, uscito il 3 settembre, e Spring Boot 3.5.5, uscito il 21 agosto, su Java 21. Anche se, per la cronaca, proprio oggi esce Java 25.

Due nascite e un inverno

Prima di scrivere codice, un po' di storia, perché spiega quasi tutte le differenze che vedrai.

Django nasce in una redazione. Nell'autunno del 2003 due sviluppatori del Lawrence Journal-World, quotidiano di una cittadina del Kansas, Adrian Holovaty e Simon Willison, abbandonano PHP e cominciano a usare Python. Il loro problema è quello di ogni redazione: le scadenze. Come racconta la FAQ ufficiale di Django, spesso avevano «solo poche ore» per portare un'applicazione web complicata dall'idea alla pubblicazione. Per non impazzire estraggono dai loro siti un framework generico, e nell'estate del 2005 l'editore decide di renderlo open source. Il nome è un omaggio a Django Reinhardt, chitarrista jazz manouche. Si pronuncia «GIANG-o»: la D è muta, e la FAQ ci tiene a ricordarlo.

Spring nasce da un libro. Nel novembre del 2002 Rod Johnson, consulente australiano, pubblica Expert One-on-One J2EE Design and Development, una critica feroce al modo in cui allora si scrivevano le applicazioni aziendali in Java, con gli Enterprise JavaBeans: pesanti, pieni di configurazione XML, quasi impossibili da testare. Il libro è accompagnato da 30.000 righe di codice di un framework alternativo. Due lettori, Juergen Hoeller e Yann Caroff, propongono di farne un progetto open source, che diventa pubblico nel giugno 2003. Il nome lo propone Caroff, e Johnson ha spiegato il perché: Spring, cioè primavera, rappresentava «un nuovo inizio dopo l'"inverno" del J2EE tradizionale».

Dieci anni dopo, però, anche Spring aveva accumulato il suo strato di configurazione. Il 17 ottobre 2012 uno sviluppatore, Mike Youngstrom, apre una segnalazione nel bug tracker di Spring per chiedere un modo più semplice di avviare applicazioni web, senza dover installare un server applicativo separato. Nell'agosto 2013 Phil Webb risponde che invece di correggerlo nel framework hanno deciso di creare un progetto nuovo, Spring Boot, e ringrazia Youngstrom: «Questo bug è stato la genesi di Spring Boot». La versione 1.0 esce il 1° aprile 2014. Non era uno scherzo.

Due framework nati quasi insieme, entrambi come reazione a qualcosa di troppo lento: le scadenze di una redazione da una parte, la burocrazia di J2EE dall'altra. Tienilo a mente, perché si sente in ogni riga.

Il primo comando

Con Django apri il terminale e scrivi tre comandi:

$ pip install django==5.2.6
$ django-admin startproject circolo .
$ python manage.py startapp padel

Il primo installa il framework, il secondo crea il progetto (impostazioni, URL principali) e il terzo crea un'app al suo interno, cioè un modulo con modelli, viste e test. Poi aggiungi "padel" all'elenco INSTALLED_APPS nelle impostazioni, e il database c'è già: un file SQLite, configurato di serie.

Con Spring Boot vai su start.spring.io, un sito che genera lo scheletro del progetto, e scegli tre «starter», cioè pacchetti di dipendenze già assortiti:

<!-- pom.xml -->
<parent>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-parent</artifactId>
  <version>3.5.5</version>
</parent>

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
  <groupId>com.h2database</groupId>
  <artifactId>h2</artifactId>
  <scope>runtime</scope>
</dependency>

Qui succede la prima cosa interessante. Non configuri nessun database: Spring Boot vede che nel progetto c'è la libreria di H2, un database in memoria, e lo configura da solo. È la autoconfigurazione: Spring Boot guarda cosa hai messo nel progetto e ne deduce cosa ti serve. Django fa una cosa simile scrivendo nel file delle impostazioni dei valori ragionevoli che puoi cambiare. Sono due modi diversi di applicare lo stesso principio, «convenzione invece di configurazione»: fai la cosa normale senza dire niente, e parla solo quando vuoi qualcosa di diverso.

Il modello: quattro righe contro venti

Il cuore dell'app sono due tabelle: i campi e le prenotazioni. In Django le descrivi con delle classi Python:

from django.core.exceptions import ValidationError
from django.db import models


class Campo(models.Model):
    nome = models.CharField(max_length=50, unique=True)
    coperto = models.BooleanField(default=False)

    def __str__(self):
        return self.nome


class Prenotazione(models.Model):
    campo = models.ForeignKey(Campo, on_delete=models.CASCADE, related_name="prenotazioni")
    giocatore = models.CharField(max_length=80)
    inizio = models.DateTimeField()
    fine = models.DateTimeField()

    def clean(self):
        if self.fine <= self.inizio:
            raise ValidationError("fine prima dell'inizio")
        sovrapposte = Prenotazione.objects.filter(
            campo=self.campo, inizio__lt=self.fine, fine__gt=self.inizio
        ).exclude(pk=self.pk)
        if sovrapposte.exists():
            raise ValidationError("campo già occupato")

    def as_dict(self):
        return {"id": self.pk, "giocatore": self.giocatore,
                "inizio": self.inizio.isoformat(), "fine": self.fine.isoformat()}

In Spring descrivi le stesse tabelle con delle classi Java annotate secondo lo standard JPA (Jakarta Persistence). Ecco il campo:

package it.circolo.padel;

import jakarta.persistence.*;

@Entity
public class Campo {
    @Id @GeneratedValue
    private Long id;

    @Column(unique = true, nullable = false, length = 50)
    private String nome;

    private boolean coperto;

    protected Campo() {}  // per JPA

    public Campo(String nome, boolean coperto) {
        this.nome = nome;
        this.coperto = coperto;
    }

    public Long getId() { return id; }
    public String getNome() { return nome; }
    public boolean isCoperto() { return coperto; }
}

La classe Prenotazione è uguale, solo più lunga. Le cose da notare sono due. La prima è la quantità: in Java il costruttore vuoto «per JPA» (Hibernate, la libreria che fa il lavoro sotto, ne ha bisogno per creare gli oggetti quando li legge dal database), i getter, i modificatori. C'è chi la riduce con Lombok o scrivendo in Kotlin, ma la differenza resta. La seconda è più sottile: il modello Django è la tabella e sa anche fare cose, come validarsi con clean(). L'entità JPA è più vicina a un semplice contenitore di dati, e le regole di solito stanno altrove.

Poi c'è la questione dello schema del database. In Django lanci python manage.py makemigrations e il framework confronta i modelli con lo stato precedente e scrive da solo un file di migrazione, un pezzo di codice versionato che crea o modifica le tabelle. Spring Boot, con un database in memoria come H2, per comodità fa creare le tabelle da Hibernate a ogni avvio e le butta via alla chiusura. In produzione nessuno lo fa: si aggiunge Flyway o Liquibase e le migrazioni si scrivono in SQL. In Django le migrazioni sono incluse; in Spring sono un mattoncino in più da scegliere.

La query che si scrive con il nome

Ora la regola importante: un campo non può essere prenotato due volte nello stesso momento. Due intervalli di tempo si sovrappongono quando il primo comincia prima che il secondo finisca e finisce dopo che il secondo è cominciato. Se tu hai il campo dalle 18 alle 19:30 e un altro gruppo lo vuole dalle 19 alle 20, le 18 vengono prima delle 20 e le 19:30 dopo le 19: sovrapposizione, rissa evitata.

In Django la regola si scrive così, con i famosi doppi trattini bassi:

sovrapposte = Prenotazione.objects.filter(
    campo=self.campo, inizio__lt=self.fine, fine__gt=self.inizio
).exclude(pk=self.pk)

inizio__lt vuol dire «inizio minore di» (less than), fine__gt «fine maggiore di». Django legge il nome dell'argomento, lo spezza sui doppi trattini e ne ricava la query SQL.

In Spring trovi la stessa idea, portata all'estremo. Non scrivi la query: dichiari un'interfaccia, e Spring Data ricava la query dal nome del metodo:

package it.circolo.padel;

import java.time.OffsetDateTime;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;

public interface PrenotazioneRepository extends JpaRepository<Prenotazione, Long> {

    List<Prenotazione> findByCampoOrderByInizio(Campo campo);

    boolean existsByCampoAndInizioLessThanAndFineGreaterThan(
            Campo campo, OffsetDateTime fine, OffsetDateTime inizio);
}

existsByCampoAndInizioLessThanAndFineGreaterThan. Leggilo ad alta voce: è la query, scritta in inglese, senza spazi. Spring Data non lo implementa nessuno: all'avvio legge il nome, lo scompone in pezzi (exists, By, Campo, And, Inizio, LessThan…) e genera il codice. Scopri così la somiglianza più divertente tra i due mondi: entrambi, per evitarti l'SQL, ti fanno scrivere le query nei nomi. Django nei nomi degli argomenti, Spring nei nomi dei metodi.

E c'è una trappola, che naturalmente trovi. I parametri vanno passati nell'ordine in cui compaiono nel nome: prima il campo, poi il valore per InizioLessThan (che è la fine della nuova prenotazione), poi quello per FineGreaterThan (che è l'inizio). Quindi nel codice scriverai (campo, r.fine(), r.inizio()), con la fine prima dell'inizio. Sembra sbagliato, ma è giusto.

Dove vive la regola

In Django la regola sta nel metodo clean() del modello. Attenzione però: save() non chiama clean() da solo. Lo fanno i form di Django e l'interfaccia di amministrazione, ma se salvi a mano devi chiamare tu full_clean(), come vedrai nella vista.

In Spring la regola va di solito in un service, una classe dedicata alla logica:

package it.circolo.padel;

import static org.springframework.http.HttpStatus.*;

import java.util.List;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.server.ResponseStatusException;

@Service
public class PrenotazioniService {
    private final CampoRepository campi;
    private final PrenotazioneRepository prenotazioni;

    public PrenotazioniService(CampoRepository campi, PrenotazioneRepository prenotazioni) {
        this.campi = campi;
        this.prenotazioni = prenotazioni;
    }

    public List<Prenotazione> elenco(Long campoId) {
        return prenotazioni.findByCampoOrderByInizio(campo(campoId));
    }

    @Transactional
    public Prenotazione prenota(Long campoId, NuovaPrenotazione r) {
        Campo campo = campo(campoId);
        if (!r.fine().isAfter(r.inizio()))
            throw new ResponseStatusException(BAD_REQUEST, "fine prima dell'inizio");
        if (prenotazioni.existsByCampoAndInizioLessThanAndFineGreaterThan(campo, r.fine(), r.inizio()))
            throw new ResponseStatusException(BAD_REQUEST, "campo già occupato");
        return prenotazioni.save(new Prenotazione(campo, r.giocatore(), r.inizio(), r.fine()));
    }

    private Campo campo(Long id) {
        return campi.findById(id).orElseThrow(() -> new ResponseStatusException(NOT_FOUND));
    }
}

Qui vedi la cosa che rende Spring, Spring: la iniezione delle dipendenze. Il service ha bisogno dei due repository, ma non li crea: li riceve nel costruttore. Chi glieli passa? Il container di Spring, che all'avvio trova tutte le classi annotate (@Service, @RestController, i repository), le crea nell'ordine giusto e le collega tra loro. È come un organizzatore di matrimoni che fa sedere ogni invitato accanto a quelli che conosce. Dalla versione 4.3 di Spring, se la classe ha un solo costruttore, non serve nemmeno l'annotazione @Autowired. Il vantaggio si vede nei test, dove puoi passare al costruttore una versione finta dei repository.

Django non ha niente di simile. Gli oggetti si incontrano con un import, come si fa normalmente in Python, e la configurazione globale sta in settings.py. Meno cerimonie, meno indirezione, ma anche meno modi di sostituire un pezzo senza toccare il codice.

Una nota onesta, che vale per entrambi. Controllare se il campo è libero e poi salvare sono due passaggi distinti, e se due persone prenotano nello stesso millisecondo tutte e due possono passare il controllo. Neanche @Transactional ti salva, con il livello di isolamento predefinito. La soluzione seria è un vincolo nel database: PostgreSQL ha i vincoli di esclusione, che Django supporta con ExclusionConstraint. Per un circolo di padel con quattro campi, però, puoi dormire tranquillo.

Le URL: una mappa o dei cartelli

Ora va esposta un'API: GET per vedere le prenotazioni di un campo, POST per farne una nuova. In Django scrivi una vista (che, a dispetto del nome, è quello che altrove si chiama controller; la FAQ spiega che Django preferisce parlare di «MTV», modello-template-vista):

import json

from django.core.exceptions import ValidationError
from django.http import JsonResponse
from django.shortcuts import get_object_or_404
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_http_methods

from .models import Campo, Prenotazione


@csrf_exempt  # API senza sessione: in produzione, token o Django REST framework
@require_http_methods(["GET", "POST"])
def prenotazioni(request, campo_id):
    campo = get_object_or_404(Campo, pk=campo_id)
    if request.method == "GET":
        elenco = campo.prenotazioni.order_by("inizio")
        return JsonResponse([p.as_dict() for p in elenco], safe=False)

    p = Prenotazione(campo=campo, **json.loads(request.body))
    try:
        p.full_clean()
    except ValidationError as e:
        return JsonResponse({"errori": e.messages}, status=400)
    p.save()
    return JsonResponse(p.as_dict(), status=201)

Sì, c'è un if sul metodo HTTP. Ieri abbiamo visto quando un if può restare, e questo può. Con le viste basate su classi di Django o con Django REST framework sparirebbe, ma per due metodi non ne vale la pena. Poi colleghi la vista a un indirizzo, in un file che contiene tutte le URL del sito:

from django.contrib import admin
from django.urls import path

from padel import views

urlpatterns = [
    path("admin/", admin.site.urls),
    path("campi/<int:campo_id>/prenotazioni/", views.prenotazioni),
]

In Spring l'indirizzo non sta in un file centrale ma sopra il metodo, con un'annotazione:

package it.circolo.padel;

import java.util.List;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/campi/{campoId}/prenotazioni")
public class PrenotazioniController {
    private final PrenotazioniService service;

    public PrenotazioniController(PrenotazioniService service) {
        this.service = service;
    }

    @GetMapping
    public List<PrenotazioneDto> elenco(@PathVariable Long campoId) {
        return service.elenco(campoId).stream().map(PrenotazioneDto::da).toList();
    }

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public PrenotazioneDto prenota(@PathVariable Long campoId, @RequestBody NuovaPrenotazione r) {
        return PrenotazioneDto.da(service.prenota(campoId, r));
    }
}

public record NuovaPrenotazione(String giocatore, OffsetDateTime inizio, OffsetDateTime fine) {}

public record PrenotazioneDto(Long id, String giocatore, OffsetDateTime inizio, OffsetDateTime fine) {
    static PrenotazioneDto da(Prenotazione p) {
        return new PrenotazioneDto(p.getId(), p.getGiocatore(), p.getInizio(), p.getFine());
    }
}

Le due scelte hanno conseguenze opposte. In Django, per sapere quali URL esistono apri urls.py e hai la mappa completa, come in una piantina della metropolitana. In Spring ogni metodo porta il suo cartello, e per avere la mappa devi cercare tutte le annotazioni nel progetto (o chiederla all'applicazione mentre gira). In compenso, in Spring quando leggi un metodo sai già a quale URL risponde.

Nota anche i due record Java, NuovaPrenotazione e PrenotazioneDto: sono le forme dei dati in entrata e in uscita. Jackson, la libreria JSON inclusa in Spring Boot, converte da sola il JSON nel record e viceversa. In Django senza librerie aggiuntive il JSON lo leggi e lo scrivi tu (json.loads, as_dict). Chi fa API serie in Django usa Django REST framework, che porta i serializer, l'equivalente di quei record.

Il regalo: l'amministrazione

Ed ecco il momento in cui Django ti fa un regalo. Quattro righe:

from django.contrib import admin
from .models import Campo, Prenotazione

admin.site.register(Campo)
admin.site.register(Prenotazione)

Lanci python manage.py createsuperuser, apri /admin/ nel browser, e c'è un'interfaccia di amministrazione completa: login, elenco dei campi, modifica delle prenotazioni, ricerca, permessi per utente. La segreteria del circolo può gestire i campi da domani mattina. E quando qualcuno ci prova a inserire una prenotazione sovrapposta, l'admin chiama clean() e mostra l'errore in rosso, senza una riga di codice in più.

Non è un caso: è la redazione del Kansas. Il tutorial ufficiale lo spiega così: Django è stato scritto in un ambiente giornalistico, con una netta separazione tra chi pubblica i contenuti e il sito pubblico. I giornalisti dovevano inserire articoli, eventi e risultati sportivi, e un'interfaccia di amministrazione generata in automatico era la risposta. In Spring Boot non c'è niente di equivalente già incluso: o la costruisci tu, o cerchi un progetto esterno.

La trappola del 403

Hai notato il decoratore @csrf_exempt sulla vista Django? Ecco perché. Django protegge di serie dai CSRF (cross-site request forgery, gli attacchi in cui un sito malevolo fa partire richieste a nome tuo usando i cookie del tuo browser). Ogni POST deve portare un token, altrimenti viene rifiutato. Provi a togliere il decoratore e a ripetere la prova:

# POST senza @csrf_exempt
Client(enforce_csrf_checks=True)  ->  403 Forbidden
Client()  # il client di test predefinito  ->  201 Created

Il client di test di Django, per comodità, salta il controllo CSRF, quindi il test passa, ma il primo POST vero viene rifiutato con un 403. Per una API senza sessioni l'eccezione va bene (in produzione si usano i token, di solito con Django REST framework). Ma la cosa da ricordare è un'altra: in Django la sicurezza è accesa di serie.

In Spring Boot è il contrario: finché non aggiungi lo starter di Spring Security, la tua API non ha né login né protezione CSRF. Quando lo aggiungi, però, si chiude tutto: ogni URL richiede l'autenticazione, la protezione CSRF si attiva e Spring stampa nel log una password generata a caso per l'utente user. Una porta che resta aperta finché non decidi di montarla, e quando la monti è blindata.

I test, e l'orologio

Il test è lo stesso nei due mondi: creo un campo, prenoto, provo a prenotare di nuovo alla stessa ora e mi aspetto un rifiuto. In Django:

from django.test import TestCase

from .models import Campo


class PrenotazioniTest(TestCase):
    def test_seconda_prenotazione_sovrapposta_rifiutata(self):
        campo = Campo.objects.create(nome="Centrale", coperto=True)
        url = f"/campi/{campo.pk}/prenotazioni/"
        dati = {"inizio": "2025-09-16T18:00:00+02:00", "fine": "2025-09-16T19:30:00+02:00"}

        r = self.client.post(url, {**dati, "giocatore": "Anna"}, content_type="application/json")
        self.assertEqual(r.status_code, 201)
        r = self.client.post(url, {**dati, "giocatore": "Bruno"}, content_type="application/json")
        self.assertEqual(r.status_code, 400)
        self.assertEqual(len(self.client.get(url).json()), 1)

In Spring:

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;

@SpringBootTest
@AutoConfigureMockMvc
class PrenotazioniTest {
    @Autowired MockMvc mvc;
    @Autowired CampoRepository campi;

    @Test
    void secondaPrenotazioneSovrappostaRifiutata() throws Exception {
        Campo campo = campi.save(new Campo("Centrale", true));
        String url = "/campi/" + campo.getId() + "/prenotazioni";
        String json = """
            {"giocatore": "%s", "inizio": "2025-09-16T18:00:00+02:00", "fine": "2025-09-16T19:30:00+02:00"}""";

        mvc.perform(post(url).contentType(MediaType.APPLICATION_JSON).content(json.formatted("Anna")))
           .andExpect(status().isCreated());
        mvc.perform(post(url).contentType(MediaType.APPLICATION_JSON).content(json.formatted("Bruno")))
           .andExpect(status().isBadRequest());
        mvc.perform(get(url)).andExpect(jsonPath("$.length()").value(1));
    }
}

Si somigliano molto: tutti e due creano un database di prova e simulano le richieste HTTP senza avviare un vero server. La differenza la vedi sull'orologio. Il test Django gira in 18 millesimi di secondo, e l'intero comando, avvio compreso, in 1,3 secondi. Il test Spring dura 6,7 secondi, e 4,9 servono solo ad avviare il contesto dell'applicazione: cercare le classi, creare i bean, collegarli, inizializzare Hibernate. Spring offre test «a fette» (@WebMvcTest, @DataJpaTest) che avviano solo una parte del contesto, e li riusa tra un test e l'altro. Ma su un progetto grande la differenza si sente ogni giorno.

I numeri, con le pinze

Alla fine metti in fila quello che hai misurato. I tempi vengono da una piccola macchina virtuale a 4 core, quindi contano i rapporti più dei valori assoluti.

Django 5.2.6Spring Boot 3.5.5
Righe scritte a mano68 in 5 file (+1 nelle impostazioni)174 in 11 file
Test (avvio compreso)1,3 s per l'intero comando6,7 s, di cui 4,9 per avviare il contesto
Avvio dell'applicazione0,35 s5,6 s
Pesopacchetto Django installato: 49 MBJAR eseguibile: 54 MB
Serve anchePython 3.13 e un server WSGI (es. Gunicorn)una JVM (Java 21)

Quasi tre volte il codice in Java, e circa sedici volte il tempo di avvio. Sembra una sentenza, ma va letta con attenzione. Una buona parte di quelle righe in più sono getter e costruttori che l'IDE scrive per te, e i tipi espliciti che le allungano sono gli stessi che fanno trovare al compilatore gli errori prima che li trovino gli utenti. L'avvio lento, poi, si paga una volta: un server Spring che gira per mesi se ne accorge appena, e la JVM, una volta scaldata, corre molto. Dove l'avvio pesa davvero (funzioni serverless, container che si accendono e si spengono di continuo) Spring può essere compilato in un'immagine nativa con GraalVM. E proprio Java 25, uscito oggi, porta due novità (JEP 514 e JEP 515) che rendono più semplice e più efficace l'avvio anticipato con cache delle classi già caricate e profili dei metodi registrati in un'esecuzione precedente.

L'ultimo dato è quello che ti sorprende di più: il JAR di Spring, con dentro Tomcat, Hibernate e tutto il resto, pesa più o meno quanto il pacchetto Django installato. Batterie incluse in tutti e due i casi, solo confezionate in modo diverso.

Cosa dici al circolo

È sera. Ripensi alla giornata e ti accorgi che le somiglianze sono più delle differenze. Tutti e due hanno un ORM che trasforma le classi in tabelle, scrivono le query a partire dai nomi, preferiscono le convenzioni alla configurazione, generano lo scheletro del progetto con un comando, hanno più di vent'anni e un ecosistema enorme. Le differenze vere sono nelle filosofie: Django ti dà tutto subito, in un unico pacchetto coerente pensato per consegnare in fretta; Spring ti dà un meccanismo, la iniezione delle dipendenze, e un catalogo infinito di pezzi da comporre, pensato per sistemi che vivranno a lungo e cresceranno in direzioni che oggi non conosci.

Per il circolo scegli Django, e il motivo sta in quattro righe: l'amministrazione. La segreteria la userà da domani, e a te non toccherà scrivere una sola pagina di gestione. Se il circolo fosse una catena di trecento centri sportivi, con i pagamenti, la contabilità, le code di messaggi e un team di quaranta sviluppatori Java già assunti, la risposta sarebbe un'altra, e sarebbe giusta anche quella. Il framework migliore, alla fine, è quello che il tuo team sa riparare alle due di notte.

Il quaderno a quadretti va in pensione. Il gruppo WhatsApp no: quello, temi, è eterno.

Fonti