lunedì 24 agosto 2026

Domain-Driven Design e Clean Code nell'Ecosistema Spring Boot con Java 25

Nello sviluppo enterprise contemporaneo, costruire applicazioni resilienti ed evolvibili richiede l'abbandono di modelli anemici e architetture centrate sul database. L'adozione congiunta di Domain-Driven Design (DDD) e Clean Code (applicati tramite l'Architettura Esagonale o Ports & Adapters) permette di isolare il core di business dai dettagli infrastrutturali.

Con il rilascio di Java 25 (LTS), il linguaggio offre strumenti formidabili per esprimere i costrutti del DDD in modo nativo: Records, Sealed Interfaces, Pattern Matching avanzato e costruttori flessibili permettono di modellare Value Objects, Entità ed Eventi di Dominio con un livello di rigore e pulizia mai visto prima.


1. Fondamenti: Cosa Significa DDD e Clean Code?

Cos'è il Clean Code?

Formalizzato da Robert C. Martin ("Uncle Bob"), il Clean Code è codice progettato per essere compreso immediatamente da altri sviluppatori, prima ancora che dalle macchine:

  • Single Responsibility Principle (SRP): Una classe o un metodo deve avere una sola ragione per cambiare.
  • Indipendenza dai Framework: Il business core non deve dipendere da Spring, Hibernate, Jackson o protocolli di rete.
  • Espressività e Nomi Intenzionali: Il codice deve essere auto-documentante e privo di effetti collaterali oscuri.
  • Testabilità Isolata: Il core di business deve essere verificabile istantaneamente con Unit Test puri su POJO.

Cos'è il Domain-Driven Design (DDD)?

Introdotto da Eric Evans, il DDD pone la complessità del business ("il Dominio") al centro della progettazione software:

  • Ubiquitous Language (Linguaggio Ubiquo): Vocabolario condiviso tra esperti di business e sviluppatori, riflesso in modo trasparente nelle classi e nei metodi.
  • Bounded Context (Contesti Delimitati): Confini espliciti che delimitano la consistenza semantica del modello.
  • Pattern Tattici:
    • Value Objects: Oggetti immutabili definiti solo dal proprio stato/valore, senza ID (perfetti come record in Java 25).
    • Entities: Oggetti definiti dalla loro identità persistente (es. OrderId).
    • Aggregate Root: Unità atomica di consistenza che incapsula entità interne e garantisce le regole e gli invarianti di business.
    • Domain Events: Fatti immutabili accaduti nel dominio, ideali da modellare con sealed interface e record.
    • Repository Port: Astrazione pura Java che simula una collezione in memoria per salvare/caricare l'aggregato.

2. Il Problema: Anemic Domain Model vs Rich Domain Model

Nelle applicazioni Spring tradizionali, è comune incontrare l'anti-pattern dell'Anemic Domain Model, dove le entità sono meri contenitori di dati (getter/setter) e la logica è sparpagliata in classi @Service gigantesche. Ecco come cambia il paradigma con il Rich Domain Model:

Caratteristica Anemic Model (Anti-Pattern) Rich Domain Model (DDD + Java 25)
Invarianti di Business Violati facilmente da setter indiscriminati. Protetti dentro l'Aggregate Root e Value Objects.
Posizione della Logica Dispersa in grossi @Service procedurali. Incapsulata in Entità, Value Objects e Metodi di Business.
Accoppiamento Framework @Entity, @Table, annotazioni Jackson nel core. POJO e Record puri, zero annotazioni esterne.
Velocità dei Test Test lenti con contesti Spring o mock complessi. Test unitari ultra-rapidi eseguiti in pochi millisecondi.

3. Struttura del Bounded Context (Architettura Esagonale)

L'Architettura Esagonale (Ports & Adapters) permette di mantenere il Dominio al centro, completamente isolato.

com.example.ecommerce.order
 ├── domain                     <-- CORE POJO/RECORD (Zero Spring/JPA)
 │    ├── model                 <-- Order (Aggregate), OrderId, Money, OrderLine
 │    ├── event                 <-- OrderDomainEvent (Sealed Interface)
 │    ├── exception             <-- OrderDomainException
 │    └── repository            <-- OrderRepository (Output Port)
 ├── application                <-- CASI D'USO
 │    ├── dto                   <-- CreateOrderCommand, OrderResponseDto
 │    └── service               <-- CreateOrderUseCaseService
 └── infrastructure             <-- DETTAGLI TECNICI E ADAPTERS
      ├── persistence           <-- OrderJpaEntity, SpringDataOrderRepository
      │    ├── adapter          <-- OrderRepositoryJpaAdapter 
      │    └── mapper           <-- OrderPersistenceMapper (MapStruct)
      └── rest                  <-- OrderRestController (Web Adapter)

4. Configurazione Maven: pom.xml per Java 25

Per supportare le ultime feature, configuriamo Spring Boot 3.5+, MapStruct e Testcontainers per Java 25:

<!-- dependencies essenziali nel pom.xml -->
<properties>
    <java.version>25</java.version>
    <org.mapstruct.version>1.6.3</org.mapstruct.version>
</properties>

<dependencies>
    <!-- Spring Boot Web, Data JPA, Validation -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!-- MapStruct per il mapping pulito -->
    <dependency>
        <groupId>org.mapstruct</groupId>
        <artifactId>mapstruct</artifactId>
        <version>${org.mapstruct.version}</version>
    </dependency>
</dependencies>

5. Implementazione del Dominio (Java 25)

5.1 Value Objects Immutabili (Records)

I record di Java sono la traduzione naturale dei Value Objects del DDD: garantiscono immutabilità, comparazione per valore e permettono validazioni compatte.

package com.example.ecommerce.order.domain.model;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Currency;

public record Money(BigDecimal amount, Currency currency) {

    public static final Currency EUR = Currency.getInstance("EUR");

    // Compact constructor per le invarianti
    public Money {
        if (amount == null || amount.compareTo(BigDecimal.ZERO) < 0) {
            throw new IllegalArgumentException("L'importo non può essere negativo");
        }
        if (currency == null) {
            throw new IllegalArgumentException("La valuta è obbligatoria");
        }
        amount = amount.setScale(2, RoundingMode.HALF_UP);
    }

    public static Money ofEuros(double value) {
        return new Money(BigDecimal.valueOf(value), EUR);
    }

    public static Money zero(Currency currency) {
        return new Money(BigDecimal.ZERO, currency);
    }

    public Money add(Money other) {
        if (!this.currency.equals(other.currency)) {
            throw new IllegalArgumentException("Valute incompatibili");
        }
        return new Money(this.amount.add(other.amount), this.currency);
    }

    public Money multiply(int quantity) {
        return new Money(this.amount.multiply(BigDecimal.valueOf(quantity)), this.currency);
    }
}

Gli identificatori fortemente tipizzati prevengono errori comuni (es. scambiare un OrderId con un CustomerId):

public record OrderId(java.util.UUID value) {
    public OrderId {
        if (value == null) throw new IllegalArgumentException("OrderId non può essere nullo");
    }
    public static OrderId generate() { return new OrderId(java.util.UUID.randomUUID()); }
}

5.2 Domain Events con Sealed Interfaces

Le Sealed Interfaces permettono di creare gerarchie chiuse, perfette per garantire un pattern matching esaustivo in fase di compilazione.

package com.example.ecommerce.order.domain.event;

import com.example.ecommerce.order.domain.model.OrderId;
import com.example.ecommerce.order.domain.model.Money;
import java.time.Instant;
import java.util.UUID;

public sealed interface OrderDomainEvent {
    OrderId orderId();
    Instant occurredOn();

    record OrderCreatedEvent(OrderId orderId, UUID customerId, Instant occurredOn) implements OrderDomainEvent {}
    record OrderPaidEvent(OrderId orderId, Money totalPaid, Instant occurredOn) implements OrderDomainEvent {}
    record OrderCancelledEvent(OrderId orderId, String reason, Instant occurredOn) implements OrderDomainEvent {}
}

5.3 L'Aggregate Root: Il Cuore del Dominio

L'Order non ha annotazioni JPA. Espone solo metodi di business espliciti (ubiquitous language) e non espone setter per alterare illegalmente il suo stato interno.

package com.example.ecommerce.order.domain.model;

import com.example.ecommerce.order.domain.event.OrderDomainEvent;
import com.example.ecommerce.order.domain.event.OrderDomainEvent.*;
import java.time.Instant;
import java.util.*;

public class Order {
    private final OrderId id;
    private final UUID customerId;
    private final List<OrderLine> lines;
    private OrderStatus status;
    private final List<OrderDomainEvent> domainEvents = new ArrayList<>();

    public Order(OrderId id, UUID customerId) {
        if (customerId == null) throw new IllegalArgumentException("CustomerId obbligatorio");
        this.id = Objects.requireNonNull(id, "OrderId obbligatorio");
        this.customerId = customerId;
        this.lines = new ArrayList<>();
        this.status = OrderStatus.CREATED;

        recordEvent(new OrderCreatedEvent(this.id, this.customerId, Instant.now()));
    }

    public void addProduct(UUID productId, Money unitPrice, int quantity) {
        if (this.status != OrderStatus.CREATED) {
            throw new IllegalStateException("Impossibile modificare un ordine nello stato " + this.status);
        }
        this.lines.add(new OrderLine(productId, unitPrice, quantity));
    }

    public Money calculateTotal() {
        if (lines.isEmpty()) return Money.zero(Money.EUR);
        return lines.stream()
                .map(OrderLine::calculateSubtotal)
                .reduce(Money.zero(Money.EUR), Money::add);
    }

    public void markAsPaid() {
        if (this.status != OrderStatus.CREATED) {
            throw new IllegalStateException("Solo gli ordini CREATED possono essere pagati");
        }
        if (this.lines.isEmpty()) {
            throw new IllegalStateException("Impossibile saldare un ordine privo di articoli");
        }
        this.status = OrderStatus.PAID;
        recordEvent(new OrderPaidEvent(this.id, calculateTotal(), Instant.now()));
    }

    private void recordEvent(OrderDomainEvent event) {
        this.domainEvents.add(event);
    }

    public List<OrderDomainEvent> pullDomainEvents() {
        var events = List.copyOf(domainEvents);
        domainEvents.clear();
        return events;
    }

    // Solo Getter (stato in sola lettura, collezioni non modificabili)
    public OrderId getId() { return id; }
    public OrderStatus getStatus() { return status; }
    public List<OrderLine> getLines() { return Collections.unmodifiableList(lines); }
}

6. L'Application Layer: Use Cases e Pattern Matching

Il servizio applicativo orchestra il recupero, la mutazione e il salvataggio. Usiamo l'operatore switch potenziato di Java 25 per dispacciare gli eventi in modo type-safe ed esaustivo.

package com.example.ecommerce.order.application.service;

import com.example.ecommerce.order.domain.event.OrderDomainEvent;
import com.example.ecommerce.order.domain.event.OrderDomainEvent.*;
import com.example.ecommerce.order.domain.model.*;
import com.example.ecommerce.order.domain.repository.OrderRepository;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@Transactional
public class CreateOrderUseCaseService {

    private final OrderRepository orderRepository;
    private final ApplicationEventPublisher eventPublisher;

    public CreateOrderUseCaseService(OrderRepository orderRepository, ApplicationEventPublisher eventPublisher) {
        this.orderRepository = orderRepository;
        this.eventPublisher = eventPublisher;
    }

    public OrderResponseDto handle(CreateOrderCommand command) {
        Order order = new Order(OrderId.generate(), command.customerId());

        command.items().forEach(item -> 
            order.addProduct(item.productId(), Money.ofEuros(item.price()), item.quantity())
        );

        Order savedOrder = orderRepository.save(order);

        // Dispatching eventi con Pattern Matching
        savedOrder.pullDomainEvents().forEach(this::dispatchDomainEvent);

        return new OrderResponseDto(savedOrder.getId().value(), savedOrder.getStatus().name());
    }

    private void dispatchDomainEvent(OrderDomainEvent event) {
        // Switch esaustivo su sealed interface (Java 25)
        switch (event) {
            case OrderCreatedEvent created -> eventPublisher.publishEvent(created);
            case OrderPaidEvent paid       -> eventPublisher.publishEvent(paid);
            case OrderCancelledEvent canc  -> eventPublisher.publishEvent(canc);
        }
    }
}

7. L'Infrastructure Layer: L'Adapter JPA

L'infrastruttura implementa il repository (Port) del Dominio, mappando il POJO Order in un'entità JPA (OrderJpaEntity) tramite MapStruct.

@Component
public class OrderRepositoryJpaAdapter implements OrderRepository {

    private final SpringDataOrderRepository springDataRepository;
    private final OrderPersistenceMapper mapper; // Interfaccia MapStruct

    public OrderRepositoryJpaAdapter(SpringDataOrderRepository springRepo, OrderPersistenceMapper mapper) {
        this.springDataRepository = springRepo;
        this.mapper = mapper;
    }

    @Override
    public Order save(Order order) {
        // Dominio -> JPA
        var entity = mapper.toJpaEntity(order);
        var saved = springDataRepository.save(entity);
        // JPA -> Dominio
        return mapper.toDomainEntity(saved);
    }
}

8. Il Superpotere: Testabilità Estrema

Grazie all'isolamento architetturale, i test di business vengono eseguiti senza mock complessi e senza avviare il contesto Spring. Sono test rapidissimi, eseguiti in millisecondi.

class OrderTest {
    @Test
    void shouldCalculateTotalAndMarkAsPaid() {
        // Arrange
        Order order = new Order(OrderId.generate(), UUID.randomUUID());
        order.addProduct(UUID.randomUUID(), Money.ofEuros(49.90), 2);
        
        // Act
        order.markAsPaid();
        
        // Assert
        assertEquals(Money.ofEuros(99.80), order.calculateTotal());
        assertEquals(OrderStatus.PAID, order.getStatus());
        
        var events = order.pullDomainEvents();
        assertTrue(events.stream().anyMatch(e -> e instanceof OrderDomainEvent.OrderPaidEvent));
    }
}

Conclusione

L'integrazione di Java 25, Domain-Driven Design e Clean Code in un'Architettura Esagonale risolve i limiti storici dello sviluppo enterprise monolitico:

  1. Isolamento Totale: Il dominio non conosce i database o il web. Potresti passare da REST a gRPC, o da PostgreSQL a MongoDB, e le classi core non cambierebbero di una virgola.
  2. Sintassi Moderna: Records e Sealed Interfaces rimuovono tonnellate di boilerplate, rendendo il codice elegante e strettamente tipizzato.
  3. Qualità Assicurata: Le regole di business sono centralizzate nell'Aggregate Root e facilmente testabili, azzerando le probabilità di corruzione dello stato interno.

Iniziare un progetto con questo grado di disaccoppiamento richiede un piccolo investimento iniziale in mapping (tramite MapStruct), ma il ritorno in termini di leggibilità, flessibilità e stabilità è incalcolabile.

martedì 27 aprile 2021

Spring - JPA - Monitor Log Slow Query

 

Per eseguire un monitoraggio delle tempistiche di esecuzione delle query è possibile creare un file di log che contenga i tempi in modo da verificare l'eventuale aggiunta di indici.

Definire le tempistiche di monitoraggio di esecuzione della query nel file di properties:

spring.jpa.properties.hibernate.session.events.log.LOG_QUERIES_SLOWER_THAN_MS=50



Aggiungere il logger nel file logback-spring.xml

<!-- query time debug -->

<appender name="query" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>queryLog.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>queryLog.log.%d{yyyy-MM-dd}.%i.gz</fileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>50MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
<maxHistory>3</maxHistory>
</rollingPolicy>
<encoder>
<pattern>%m%n</pattern>
</encoder>
</appender>


<logger name="org.hibernate.SQL_SLOW" level="INFO">
<appender-ref ref="query"/>
</logger>

il file generato contiene:

SlowQuery: 297 milliseconds. SQL: 'HikariProxyPreparedStatement@984805610 wrapping com.mysql.cj.jdbc.ClientPreparedStatement: select a.id as col_0_0_ from abc a where a.id > 10'


venerdì 26 febbraio 2021

HAProxy - Halog


HaProxy possiede un suo log analyzer che si chiama halog

halog -srv -ut -H < haproxy.log | column -t
halog -u -H -q < haproxy.log | column -t | more
halog -u -H -q -hs 404 < haproxy.log | column -t | more

viene estratta questa lista dove vengono evidenziate gli errori le chiamate etc per ogni occorrenza.


domenica 13 dicembre 2020

InfluxDB

 Qualche esperimento veloce con un db time series come Influx DB. Questi db sono specifici per utilizzare dati temporali ed è fornito di alcuni strumenti molto comodi di visualizzazione come Chronograf

Per prima cosa ho installato su una vmware una debian 8, successivamente ho installato influxDB e Chronograf dalla repo seguendo le info sul sito.

Como dataset ho utilizzato il db del meteo sono circa 3M di righe. Importando con un semplice programmino in java.

A questo punto ho iniziato a fare qualche esperimento con Chronograf ottenendo delle visualizzazioni molto esplicative con pochi click.





lunedì 22 luglio 2019

SqlServer - conflict between "Latin1_General_CI_AS" and "SQL_Latin1_General_CP1_CI_AS"



Capita spesso di avere problemi di collate sui db SqlServer quando si utilizza Hibernate. Soprattuto quando cambia la codifica del db o la versione dello stesso:


Caused by: com.microsoft.sqlserver.jdbc.SQLServerException: Cannot resolve the collation conflict between "Latin1_General_CI_AS" and "SQL_Latin1_General_CP1_CI_AS" in the equal to operation.

SELECT SERVERPROPERTY('EditionId') AS EditionId
GO
use master
GO
ALTER DATABASE [dbaname] SET SINGLE_USER WITH ROLLBACK IMMEDIATE
GO
GO
use [dbaname];
GO
USE [master]
GO
ALTER DATABASE [dbaname] COLLATE Latin1_General_CI_AS
GO
ALTER DATABASE [dbaname] SET MULTI_USER
GO

un query che genera uno script di alter potrebbe essere di aiuto per eseguire la modifica sul singolo campo [via]:


SELECT 'ALTER TABLE [' + SYSOBJECTS.Name + '] ALTER COLUMN [' + SYSCOLUMNS.Name + '] ' +
SYSTYPES.name + 
    CASE systypes.NAME
    WHEN 'text' THEN ' '
    ELSE
    '(' + RTRIM(CASE SYSCOLUMNS.length
    WHEN -1 THEN 'MAX'
    ELSE CONVERT(CHAR,SYSCOLUMNS.length)
    END) + ') ' 
    END

    + ' ' + ' COLLATE Latin1_General_CI_AS ' + CASE ISNULLABLE WHEN 0 THEN 'NOT NULL' ELSE 'NULL' END
    FROM SYSCOLUMNS , SYSOBJECTS , SYSTYPES
    WHERE SYSCOLUMNS.ID = SYSOBJECTS.ID
    AND SYSOBJECTS.TYPE = 'U'
    AND SYSTYPES.Xtype = SYSCOLUMNS.xtype
    AND SYSCOLUMNS.COLLATION IS NOT NULL
    AND NOT ( sysobjects.NAME LIKE 'sys%' )
    AND NOT ( SYSTYPES.name LIKE 'sys%' )
    GO

giovedì 13 giugno 2019

SqlServer - Verificare lo stato degli indici

Query di comodità per verificare lo stato di deframmentazione degli indici.

L'ultima colonna contiene il comando di rebuild dell'indice.


domenica 28 ottobre 2018

Comunicazione tra MicroServices in Spring Boot con RabbitMQ


Dopo attenta lettura di documentazione on-line e suggerimenti da amici provo ad eseguire qualche test di comunicazione tra Micro Services utilizzando un gestore di code di messaggi RabbitMQ.

Con una semplice app in Spring Boot, con interfaccia rest,  al richiamo di una path, viene spedito un messaggio al RabbitMQ installato sullo stesso PC in locale.

Per RabbitMQ vi consiglio vivamente di installare il plugin per il management da web

 http://localhost:15672/ guest/guest.





La parte di controller è molto semplice, è un api rest che quando viene richiamata spedisce un messaggio al RabbitMQ e nella stessa classe un metodo che rimane in ascolto sulla stessa coda.


Adesso eseguiamo alcuni stress test:

  1.  creiamo diversi file di configurazione di spring per ogni file cambiamo semplicemente la porta.
  2. eseguiamo la build del progetto e lanciamo n jar per ogni profiles creato nel file di configurazione yaml.

java -jar rabbit-spring-test-0.0.1.jar  --spring.profiles.active=9001
java -jar rabbit-spring-test-0.0.1.jar  --spring.profiles.active=9002
java -jar rabbit-spring-test-0.0.1.jar  --spring.profiles.active=9003
java -jar rabbit-spring-test-0.0.1.jar  --spring.profiles.active=9004


il vostro desktop diventa qualcosa di delirante...

3. Configuro il jMeter per iniziare ad eseguire chiamate sui quattro server 
http://localhost:900{1-4}/sendMessage


C:\dev\apache-jmeter-5.0\bin> .\jmeter -n -t .\sendmessage.jmx -l tmp\jmeter-o.log -o tmp\dashboard

il file sendmessage.jmx



Client web di RabbitMQ



Riassumendo con jMeter abbiamo spedito 48000 richiese di invio di messaggi alle 4 app Spring Boot.
Le app hanno inviato una richiesta in coda al RabbitMQ che a sua volta per ognuno dei suddetti ha reinviato il messaggio alle 4 app.

In ingresso ha processato 48000 messaggi.
In uscita ha processato 192000 messaggi.
Tutto questo in 1 minuto.