mercoledì 2 settembre 2026

Serverless Spring + AWS Lambda

Sviluppare funzioni Serverless su AWS (AWS Lambda) è diventato uno standard industriale. Tuttavia, per gli sviluppatori Java abituati all'ecosistema Spring, l'approccio classico può sembrare distante. Fortunatamente, Spring Cloud Function fa da ponte tra questi due mondi, permettendoti di scrivere la logica di business come semplici bean di Spring ed eseguirli in modo trasparente come Lambda.

In questo articolo esploreremo come scrivere una Lambda in Spring (con codice, variabili e commenti in inglese come best practice), come gestire payload JSON, e come effettuare i test locali tramite Maven e Docker prima del deploy su AWS.


1. Configurazione del Progetto (pom.xml)

Per iniziare, crea un progetto Spring Boot (versione 3.x, con Java 17+). Nel tuo file pom.xml dovrai includere le seguenti dipendenze essenziali:

<dependencies>
    <!-- Basic Spring Boot starter -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
    </dependency>

    <!-- Enables exposing functions as HTTP endpoints for local testing -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-function-web</artifactId>
    </dependency>

    <!-- Adapter for AWS Lambda integration -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-function-adapter-aws</artifactId>
    </dependency>
</dependencies>

2. Scrivere la Lambda in Spring (Dati Semplici)

Sfruttando Spring Cloud Function, la tua Lambda diventa semplicemente un Bean Java di tipo java.util.function.Function. Ecco un esempio base che converte una stringa in maiuscolo:

package com.example.lambda;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import java.util.function.Function;

@SpringBootApplication
public class SpringLambdaApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringLambdaApplication.class, args);
    }

    // This is our AWS Lambda defined as a Spring Bean!
    @Bean
    public Function<String, String> uppercase() {
        return input -> {
            System.out.println("Received input: " + input);
            return input.toUpperCase();
        };
    }
}

3. Passare Dati Strutturati (JSON) alla Lambda

Nelle applicazioni reali, la tua Lambda riceverà spesso un payload JSON. Spring Cloud Function gestisce in automatico la serializzazione e deserializzazione JSON usando Jackson. Definiamo i modelli dati (POJO):

package com.example.lambda.model;

public class UserRequest {
    private String name;
    private int age;

    // Default constructor is required for JSON deserialization
    public UserRequest() {}
    
    // Getters and Setters
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    
    public int getAge() { return age; }
    public void setAge(int age) { this.age = age; }
}
package com.example.lambda.model;

public class UserResponse {
    private String message;
    private boolean isAdult;

    public UserResponse(String message, boolean isAdult) {
        this.message = message;
        this.isAdult = isAdult;
    }
    
    // Getters and Setters
    public String getMessage() { return message; }
    public void setMessage(String message) { this.message = message; }
    
    public boolean isAdult() { return isAdult; }
    public void setAdult(boolean adult) { isAdult = adult; }
}

Ora aggiorniamo la classe principale aggiungendo un nuovo Bean che elabora questi oggetti complessi:

    // Lambda for processing complex JSON payloads
    @Bean
    public Function<UserRequest, UserResponse> processUser() {
        return request -> {
            System.out.println("Processing data for user: " + request.getName());
            
            boolean isAdult = request.getAge() >= 18;
            String msg = isAdult ? 
                "Welcome " + request.getName() + ", access granted." :
                "Hello " + request.getName() + ", access denied for minors.";
                
            return new UserResponse(msg, isAdult);
        };
    }

4. Test Locale con Maven (mvn)

Ogni Bean di tipo Function viene mappato su un endpoint HTTP con lo stesso nome del metodo. Puoi avviare l'applicazione direttamente dal terminale con Maven:

mvn spring-boot:run

Spring Boot farà partire un server locale sulla porta 8080. Ora puoi invocare la funzione usando un semplice cURL, passando il JSON:

curl -X POST http://localhost:8080/processUser \
     -H "Content-Type: application/json" \
     -d '{"name": "John Doe", "age": 25}'

Output atteso:

{
  "message": "Welcome John Doe, access granted.",
  "adult": true
}

5. Test Locale con Docker

Per assicurarti che la tua applicazione giri in un ambiente isolato, puoi inserirla in un container Docker. Crea un Dockerfile nella root del progetto:

# Use a base image with Java 17
FROM eclipse-temurin:17-jdk-alpine
VOLUME /tmp

# Copy the JAR generated by Maven into the container
COPY target/*.jar app.jar

# Run the application
ENTRYPOINT ["java","-jar","/app.jar"]

Procedi con la compilazione e la creazione dell'immagine Docker:

# Package the application (skipping tests)
mvn clean package -DskipTests

# Build the Docker image
docker build -t spring-lambda-local .

# Run the container mapping port 8080
docker run -p 8080:8080 spring-lambda-local

6. Dal Locale ad AWS Lambda

Quando configuri la tua Lambda sulla console AWS (o tramite Terraform), dovrai indicare un Handler. Devi impostarlo usando l'adapter standard fornito da Spring:

org.springframework.cloud.function.adapter.aws.FunctionInvoker::handleRequest

Al momento del primo avvio (Cold Start), Spring si occuperà di istanziare il contesto applicativo, cercare la tua Lambda e passargli gli eventi in ingresso. Per ovviare ai classici rallentamenti in partenza di Java ed abbattere i tempi di avvio a pochi millisecondi in produzione, è fortemente consigliato l'uso di Spring Native e GraalVM.

venerdì 28 agosto 2026

Pipeline 100% Reattive: Gestire flussi continui con Spring WebFlux, PostgreSQL, R2DBC e Docker

Quando si progetta un'API destinata all'elaborazione di stream continui di dati, l'approccio sincrono tradizionale basato su JDBC e Spring MVC mostra rapidamente i propri limiti architetturali, portando frequentemente alla saturazione dei thread a disposizione del server. L'adozione di Spring WebFlux in sinergia con R2DBC e PostgreSQL consente di superare tale ostacolo, implementando una pipeline end-to-end asincrona e non bloccante. Questo paradigma garantisce un flusso dati ottimizzato tra database e client, riducendo drasticamente l'impatto sulla memoria di sistema.

1. Configurazione dell'Infrastruttura Reattiva

L'implementazione del supporto reattivo richiede l'inclusione delle dipendenze relative a WebFlux, Spring Data R2DBC e il driver asincrono specifico per PostgreSQL all'interno del descrittore di progetto (Maven o Gradle).

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-webflux</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-r2dbc</artifactId>
    </dependency>
    <dependency>
        <groupId>org.postgresql</groupId>
        <artifactId>r2dbc-postgresql</artifactId>
    </dependency>
</dependencies>

Nel file di configurazione application.yml, è imperativo sostituire il prefisso JDBC standard per inizializzare il connection pool reattivo. Di seguito una configurazione production-ready che include la gestione ottimizzata del pool di connessioni e il tracciamento delle query:

spring:
  r2dbc:
    url: ${DB_URL:r2dbc:postgresql://localhost:5432/telemetry_db}
    username: ${DB_USER:postgres}
    password: ${DB_PASSWORD:postgres}
    pool:
      initial-size: 10
      max-size: 50
      max-idle-time: 30m
      validation-query: "SELECT 1"
logging:
  level:
    org.springframework.r2dbc: DEBUG # Espone i log delle query reattive in fase di sviluppo

2. Modellazione del Dominio e Livello di Persistenza

Il framework Spring Data R2DBC si discosta dall'astrazione sincrona tipica di JPA e non utilizza l'Entity Manager. Il mapping oggetto-relazionale avviene tramite annotazioni native e dirette. La seguente classe rappresenta l'entità SensorMetric, strutturata sfruttando Lombok per minimizzare il codice boilerplate:

import org.springframework.data.annotation.Id;
import org.springframework.data.relational.core.mapping.Table;
import org.springframework.data.relational.core.mapping.Column;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import java.time.LocalDateTime;

@Data
@NoArgsConstructor
@AllArgsConstructor
@Table("sensor_metrics")
public class SensorMetric {
    @Id
    private Long id;

    @Column("sensor_id")
    private String sensorId;

    private Double reading;

    @Column("recorded_at")
    private LocalDateTime timestamp;
}

Il livello di persistenza è demandato a un'interfaccia che estende ReactiveCrudRepository, esponendo i tipi reattivi nativi di Project Reactor (Flux per collezioni e Mono per risultati singoli):

import org.springframework.data.repository.reactive.ReactiveCrudRepository;
import org.springframework.stereotype.Repository;
import reactor.core.publisher.Flux;

@Repository
public interface SensorMetricRepository extends ReactiveCrudRepository<SensorMetric, Long> {
    // Il framework genera dinamicamente una query asincrona non bloccante
    Flux<SensorMetric> findBySensorId(String sensorId);
}

3. Implementazione dello Streaming nel Controller

A livello di Controller, l'impiego di TEXT_EVENT_STREAM_VALUE permette di instaurare una comunicazione Server-Sent Events (SSE) verso il client, mentre APPLICATION_NDJSON_VALUE risulta ottimale per l'ingestione efficiente di flussi continui senza caricare l'intero payload in RAM.

import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

@RestController
@RequestMapping("/api/v1/metrics")
public class MetricStreamController {

    private final SensorMetricRepository repository;

    public MetricStreamController(SensorMetricRepository repository) {
        this.repository = repository;
    }

    @GetMapping(value = "/stream/{sensorId}", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Flux<SensorMetric> streamMetricsOut(@PathVariable String sensorId) {
        return repository.findBySensorId(sensorId);
    }

    @PostMapping(value = "/ingest", consumes = MediaType.APPLICATION_NDJSON_VALUE)
    @ResponseStatus(HttpStatus.CREATED)
    public Mono<Void> ingestMetricsIn(@RequestBody Flux<SensorMetric> metricsStream) {
        return repository.saveAll(metricsStream).then();
    }
}

L'aspetto architetturale più rilevante di questa implementazione è il meccanismo di Backpressure. Qualora il client in lettura presenti latenze operative, il server Netty notifica Project Reactor affinché adegui il rate di emissione. Tale segnale viene propagato a cascata fino al driver R2DBC, che sospenderà temporaneamente le richieste di fetch al demone PostgreSQL, prevenendo attivamente l'esaurimento della memoria heap.

4. Orchestrazione Docker e Integrazione dei Dati Spaziali (PostGIS)

Nello sviluppo di moduli software complessi, quali sistemi per l'estrazione di supply plan o l'analisi di telemetria geolocalizzata in formato GeoJSON, l'architettura reattiva dimostra un'elevata scalabilità se integrata con l'estensione PostGIS. Il driver R2DBC per PostgreSQL supporta nativamente i tipi geometrici spaziali, consentendo di elaborare ed esportare grandi volumi di coordinate senza causare il blocco dei thread applicativi.

La configurazione docker-compose.yml seguente illustra come orchestrare agevolmente l'API insieme a un'istanza database dotata di estensione spaziale preconfigurata:

version: '3.8'
services:
  postgres-postgis:
    image: postgis/postgis:15-alpine
    environment:
      POSTGRES_DB: telemetry_db
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
    ports:
      - "5432:5432"

  webflux-api:
    build: .
    ports:
      - "8080:8080"
    environment:
      DB_URL: r2dbc:postgresql://postgres-postgis:5432/telemetry_db

5. Validazione End-to-End (E2E) in Pipeline CI/CD

Il collaudo di endpoint asincroni (SSE e NDJSON) evidenzia spesso i limiti strutturali dei client REST tradizionali. L'approccio ingegneristico più solido consiste nell'implementazione di una suite di test automatizzati tramite Karate DSL. Grazie alla sua sintassi dichiarativa, Karate permette di validare flussi di dati continui ed è facilmente integrabile in pipeline di Continuous Integration (es. Jenkins).

Feature: Validazione E2E dell'API WebFlux Stream

  Background:
    * url 'http://localhost:8080/api/v1/metrics'

  Scenario: Ingestione bulk in NDJSON e verifica stream in lettura SSE
    Given path 'ingest'
    And header Content-Type = 'application/x-ndjson'
    And request '{"sensorId": "S-100", "reading": 22.5}\n{"sensorId": "S-100", "reading": 23.1}'
    When method post
    Then status 201

    Given path 'stream/S-100'
    When method get
    Then status 200
    And match header Content-Type contains 'text/event-stream'

L'adozione sinergica di queste tecnologie definisce un ecosistema software resiliente, progettato per sostenere elevati carichi transazionali e picchi di traffico massicci, pur mantenendo un ridotto e controllato consumo delle risorse infrastrutturali.

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