Perché DDD e Architettura Esagonale
Quando un progetto cresce oltre una certa soglia di complessità, l'architettura tradizionale a tre livelli (controller-service-repository) inizia a mostrare i suoi limiti. Il codice di business si mescola con le dipendenze infrastrutturali, i test diventano fragili e le modifiche richiedono interventi a cascata su più livelli.
Per Play The Event, con le sue 86 entità e molteplici domini interconnessi, serviva un'architettura che garantisse separazione delle responsabilità, testabilità e capacità di evolvere nel tempo senza riscritture massicce. La combinazione di Domain-Driven Design, Architettura Esagonale e CQRS si è rivelata la scelta ideale.
Cosa Troverai in Questo Articolo
- La struttura a 4 livelli dell'architettura esagonale
- Il Domain Layer puro senza dipendenze da Spring
- Il pattern CQRS con Command e Query Handler separati
- L'Infrastructure Layer con gli adapter per JPA, Stripe, email e WebSocket
- I design pattern chiave utilizzati nel progetto
- La struttura delle directory del progetto
I Quattro Livelli dell'Architettura
L'architettura di Play The Event è organizzata in quattro livelli concentrici, dove le dipendenze puntano sempre verso l'interno: dall'interfaccia verso il dominio, mai il contrario.
src/main/java/com/playtheevent/
├── domain/ # LIVELLO 1: Dominio Puro
│ ├── model/
│ │ ├── evento/ # Aggregate Root: Evento
│ │ ├── partecipante/ # Aggregate Root: Partecipante
│ │ ├── spesa/ # Aggregate Root: Spesa
│ │ ├── viaggio/ # Aggregate Root: Viaggio
│ │ └── shared/ # Value Objects condivisi
│ ├── event/ # Domain Events
│ ├── repository/ # Interfacce Repository (porte)
│ └── service/ # Domain Services
│
├── application/ # LIVELLO 2: Applicazione
│ ├── command/ # Command Handlers (scrittura)
│ ├── query/ # Query Handlers (lettura)
│ └── service/ # Application Services
│
├── infrastructure/ # LIVELLO 3: Infrastruttura
│ ├── persistence/ # JPA Entities e Repository Impl
│ ├── security/ # JWT, OAuth2, Filtri
│ ├── payment/ # Stripe Adapter
│ ├── email/ # Email Service Adapter
│ ├── websocket/ # WebSocket Adapter
│ └── config/ # Configurazioni Spring
│
└── interfaces/ # LIVELLO 4: Interfacce
├── rest/ # 53 REST Controllers
├── dto/ # Data Transfer Objects
└── mapper/ # MapStruct Mappers
Livello 1: Il Domain Layer
Il cuore dell'architettura è il Domain Layer, che contiene la logica di business pura. La regola fondamentale è: nessuna dipendenza da Spring, JPA o qualsiasi framework esterno. Il dominio conosce solo Java standard.
Questo significa che le entità di dominio non hanno annotazioni @Entity,
@Table o @Column. Non esistono @Autowired
o @Service. Il dominio è puro codice Java che esprime le regole
di business attraverso metodi e validazioni.
public record Money(BigDecimal amount, Currency currency) {
public Money {
if (amount == null) throw new IllegalArgumentException("Amount cannot be null");
if (currency == null) throw new IllegalArgumentException("Currency cannot be null");
if (amount.scale() > 2) {
amount = amount.setScale(2, RoundingMode.HALF_UP);
}
}
public Money add(Money other) {
assertSameCurrency(other);
return new Money(this.amount.add(other.amount), this.currency);
}
public Money subtract(Money other) {
assertSameCurrency(other);
return new Money(this.amount.subtract(other.amount), this.currency);
}
private void assertSameCurrency(Money other) {
if (!this.currency.equals(other.currency)) {
throw new CurrencyMismatchException(this.currency, other.currency);
}
}
}
I Value Objects come Money, Email e UserId
incapsulano la validazione e la logica di business nel punto più vicino ai dati,
eliminando la possibilità di stati inconsistenti.
Le Porte del Dominio
Il dominio definisce le interfacce (porte) che l'infrastruttura deve implementare. Questo è il principio fondamentale dell'architettura esagonale: il dominio dichiara di cosa ha bisogno, senza sapere come verrà fornito.
public interface EventoRepository {
Optional<Evento> findById(EventoId id);
List<Evento> findByOrganizer(UserId organizerId);
Evento save(Evento evento);
void delete(EventoId id);
List<Evento> findPublicEvents(int page, int size);
}
Livello 2: L'Application Layer con CQRS
Il livello applicazione implementa il pattern CQRS, separando nettamente le operazioni di scrittura (Command) da quelle di lettura (Query). Questa separazione consente di ottimizzare indipendentemente i percorsi di lettura e scrittura.
Command Handler: Operazioni di Scrittura
I Command Handler gestiscono le operazioni che modificano lo stato del sistema. Ogni comando rappresenta un'intenzione dell'utente espressa in modo esplicito.
public record CreateEventoCommand(
String nome,
String descrizione,
LocalDateTime dataInizio,
LocalDateTime dataFine,
String luogo,
int maxPartecipanti,
UserId organizerId
) {}
@Component
public class CreateEventoHandler {
private final EventoRepository eventoRepository;
private final EventPublisher eventPublisher;
public EventoId handle(CreateEventoCommand command) {
Evento evento = Evento.create(
command.nome(),
command.descrizione(),
command.dataInizio(),
command.dataFine(),
command.luogo(),
command.maxPartecipanti(),
command.organizerId()
);
eventoRepository.save(evento);
eventPublisher.publish(new EventoCreatedEvent(evento.getId()));
return evento.getId();
}
}
Query Handler: Operazioni di Lettura
I Query Handler sono ottimizzati per la lettura, spesso utilizzando proiezioni specifiche piuttosto che caricare interi aggregati. Questo migliora le prestazioni e riduce il carico sul database.
@Component
public class GetEventoStatisticsHandler {
private final EventoQueryRepository queryRepository;
public EventoStatisticsDto handle(GetEventoStatisticsQuery query) {
return queryRepository.getStatistics(query.eventoId());
}
}
Livello 3: L'Infrastructure Layer
Il livello infrastruttura contiene gli adapter che implementano le porte definite dal dominio. Qui risiedono tutte le dipendenze esterne: JPA, Spring Security, Stripe, servizi email e WebSocket.
Adapter Principali
- JPA Adapter: implementa i repository del dominio traducendo le entità di dominio in entità JPA e viceversa, mantenendo il dominio puro
- Security Adapter: gestisce autenticazione JWT, autorizzazione RBAC, filtri di sicurezza e configurazione OAuth2
- Stripe Adapter: integra il sistema di pagamenti per eventi a pagamento, gestendo checkout, webhook e rimborsi
- Email Adapter: invia notifiche, inviti, reminder e conferme tramite template personalizzabili
- WebSocket Adapter: fornisce aggiornamenti in tempo reale per liste partecipanti, chat e notifiche live
Livello 4: L'Interfaces Layer
Il livello interfacce espone la piattaforma al mondo esterno tramite 53 controller REST che gestiscono oltre 100 endpoint API. Ogni controller utilizza DTO (Data Transfer Objects) per la serializzazione e MapStruct per la conversione automatica tra DTO e oggetti di dominio.
@RestController
@RequestMapping("/api/v1/eventi")
public class EventoController {
private final CreateEventoHandler createHandler;
private final GetEventoHandler getHandler;
private final EventoMapper mapper;
@PostMapping
@PreAuthorize("hasRole('USER')")
public ResponseEntity<EventoResponse> create(
@Valid @RequestBody CreateEventoRequest request) {
CreateEventoCommand command = mapper.toCommand(request);
EventoId id = createHandler.handle(command);
return ResponseEntity.created(URI.create("/api/v1/eventi/" + id))
.body(mapper.toResponse(getHandler.handle(id)));
}
}
Design Pattern Chiave
Oltre ai pattern architetturali principali, il progetto utilizza diversi design pattern a livello di implementazione per gestire la complessità del dominio.
- Aggregate Root: ogni aggregato ha una radice che controlla l'accesso e mantiene l'invarianza.
Evento,ViaggioeSpesasono i principali aggregati - Value Objects:
Money(importo + valuta),Email(con validazione formato),UserId(identificativo tipizzato) garantiscono consistenza e immutabilità - Factory Pattern: le factory centralizzano la creazione di oggetti complessi, applicando validazioni e regole di business alla costruzione
- Repository Pattern: le interfacce nel dominio astraggono la persistenza, permettendo di cambiare database senza toccare la logica di business
- Strategy Pattern: utilizzato per le diverse modalità di split delle spese (equa, percentuale, fissa, personalizzata), permettendo di aggiungere nuove strategie senza modificare il codice esistente
Benefici dell'Architettura
Questa architettura, adottata in Play The Event, porta vantaggi concreti e misurabili nel ciclo di sviluppo.
Vantaggi Principali
- Testabilità: il dominio puro può essere testato con unit test veloci senza contesto Spring, mock di database o container Docker
- Manutenibilità: le modifiche alla logica di business sono isolate nel dominio, senza effetti collaterali sull'infrastruttura o sulle interfacce
- Indipendenza dal framework: il dominio non dipende da Spring Boot. In teoria, potrebbe essere eseguito con qualsiasi framework Java
- Evoluzione incrementale: nuove funzionalità si aggiungono creando nuovi Command/Query Handler senza modificare quelli esistenti
- Chiarezza del codice: la separazione CQRS rende esplicito se un'operazione legge o modifica lo stato, facilitando il ragionamento sul sistema
Nel prossimo articolo esploreremo nel dettaglio il modello di dominio, analizzando le 86 entità che compongono il cuore della piattaforma e come interagiscono tra loro attraverso gli aggregati.
Il codice sorgente è disponibile su GitHub.







