Visualizzazione post con etichetta AWS. Mostra tutti i post
Visualizzazione post con etichetta AWS. Mostra tutti i post

sabato 26 settembre 2026

Architettura Enterprise su AWS: Gestione Attiva della DLQ con Spring Boot 4.1.1, Java 25 e DDD

Nel panorama dello sviluppo software moderno, l'integrazione di sistemi di messaggistica asincrona è fondamentale per garantire scalabilità, resilienza e disaccoppiamento tra i microservizi. Amazon Simple Queue Service (SQS) è lo standard de facto per la gestione delle code in ambiente AWS.

Questo progetto nasce come evoluzione di una semplice integrazione, per trasformarsi in un vero e proprio microservizio production-ready.


Il Progetto: Cosa stiamo costruendo?

Immaginiamo di dover sviluppare un Microservizio di Elaborazione Notifiche per una piattaforma e-commerce. Il sistema riceve richieste per inviare comunicazioni critiche ai clienti. Questi processi richiedono tempo e possono fallire. Un'elaborazione sincrona bloccherebbe l'utente; l'approccio asincrono tramite AWS SQS è la soluzione ideale.

In questo progetto, adotteremo:

  • Java 25 e Spring Boot 4.1.1: Per sfruttare le ultime funzionalità.
  • Clean Architecture e DDD (Domain-Driven Design): La logica di core isolata dai dettagli implementativi di AWS.
  • Gestione Attiva degli Errori tramite DLQ (Dead Letter Queue): Creeremo una coda secondaria di sicurezza per i messaggi falliti.
  • OpenTofu (IaC): Infrastruttura definita tramite codice.

La Struttura del Progetto

src/main/java/com/tuoblog/enterprise/
├── application/           # Livello di Orchestrazione
│   └── usecase/
│       └── PublishMessageUseCase.java
├── domain/                # Il cuore del Business
│   ├── model/
│   │   └── BusinessMessage.java
│   └── port/
│       └── MessagePublisherPort.java
├── infrastructure/        # Connessione ad AWS
│   └── messaging/
│       ├── SqsDlqConsumerAdapter.java      
│       ├── SqsMainConsumerAdapter.java     
│       └── SqsMessagePublisherAdapter.java 
└── presentation/          # Esposizione API REST
    └── api/
        └── MessageController.java

1. L'Infrastruttura come Codice (OpenTofu)

Nel file main.tofu, definiamo la coda principale e la DLQ. La redrive_policy indica che, dopo 3 tentativi falliti, il messaggio deve essere dirottato nella coda di errore.

provider "aws" {
  region = "eu-south-1" 
}

# 1. Creazione della Coda di Errore (DLQ)
resource "aws_sqs_queue" "app_dlq" {
  name = "enterprise-app-dlq"
  message_retention_seconds = 1209600 # 14 giorni
}

# 2. Creazione della Coda Principale
resource "aws_sqs_queue" "app_main_queue" {
  name = "enterprise-app-queue"
  
  redrive_policy = jsonencode({
    deadLetterTargetArn = aws_sqs_queue.app_dlq.arn
    maxReceiveCount     = 3 
  })
}

output "main_queue_url" { value = aws_sqs_queue.app_main_queue.url }
output "dlq_queue_url" { value = aws_sqs_queue.app_dlq.url }

2. Configurazione del Progetto (Maven e YAML)

Il pom.xml definisce il nostro stack tecnologico.

<project xmlns="http://maven.apache.org/POM/4.0.0">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>4.1.1</version>
    </parent>
    <groupId>com.tuoblog.enterprise</groupId>
    <artifactId>sqs-ddd-demo</artifactId>
    <version>1.0.0</version>
    <properties>
        <java.version>25</java.version>
        <spring-cloud-aws.version>3.3.0</spring-cloud-aws.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>io.awspring.cloud</groupId>
            <artifactId>spring-cloud-aws-starter-sqs</artifactId>
            <version>${spring-cloud-aws.version}</version>
        </dependency>
    </dependencies>
</project>

Nel file src/main/resources/application.yml specificheremo i nomi delle code:

spring:
  application:
    name: sqs-ddd-microservice
  cloud:
    aws:
      region:
        static: eu-south-1

aws:
  sqs:
    queue:
      main: enterprise-app-queue
      dlq: enterprise-app-dlq

3. Livello di Dominio e Applicazione (DDD)

Questo livello contiene le regole di business assolute.

package com.tuoblog.enterprise.domain.model;
import java.util.UUID;

public record BusinessMessage(UUID id, String content) {
    public BusinessMessage {
        if (content == null || content.isBlank()) {
            throw new IllegalArgumentException("Il contenuto è obbligatorio");
        }
    }
}
package com.tuoblog.enterprise.domain.port;
import com.tuoblog.enterprise.domain.model.BusinessMessage;

public interface MessagePublisherPort {
    void publish(BusinessMessage message);
}
package com.tuoblog.enterprise.application.usecase;
import com.tuoblog.enterprise.domain.model.BusinessMessage;
import com.tuoblog.enterprise.domain.port.MessagePublisherPort;
import org.springframework.stereotype.Service;
import java.util.UUID;

@Service
public class PublishMessageUseCase {
    private final MessagePublisherPort publisherPort;

    public PublishMessageUseCase(MessagePublisherPort publisherPort) {
        this.publisherPort = publisherPort;
    }

    public BusinessMessage execute(String content) {
        BusinessMessage message = new BusinessMessage(UUID.randomUUID(), content);
        publisherPort.publish(message); // Logica agnostica
        return message;
    }
}

4. L'Infrastruttura: Producer, Consumer e DLQ

Qui è dove il nostro codice si accoppia ad AWS.

package com.tuoblog.enterprise.infrastructure.messaging;
import com.tuoblog.enterprise.domain.model.BusinessMessage;
import com.tuoblog.enterprise.domain.port.MessagePublisherPort;
import io.awspring.cloud.sqs.operations.SqsTemplate;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Component
public class SqsMessagePublisherAdapter implements MessagePublisherPort {
    private final SqsTemplate sqsTemplate;
    private final String queueName;

    public SqsMessagePublisherAdapter(SqsTemplate sqsTemplate, 
                                      @Value("${aws.sqs.queue.main}") String queueName) {
        this.sqsTemplate = sqsTemplate;
        this.queueName = queueName;
    }

    @Override
    public void publish(BusinessMessage message) {
        sqsTemplate.send(queueName, message);
    }
}

Il Listener Principale:

package com.tuoblog.enterprise.infrastructure.messaging;
import com.tuoblog.enterprise.domain.model.BusinessMessage;
import io.awspring.cloud.sqs.annotation.SqsListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

@Component
public class SqsMainConsumerAdapter {
    private static final Logger log = LoggerFactory.getLogger(SqsMainConsumerAdapter.class);

    @SqsListener("${aws.sqs.queue.main}")
    public void consumeMainMessage(BusinessMessage message) {
        log.info("📩 [CODA PRINCIPALE] Ricevuto messaggio ID: {}", message.id());
        
        if ("SIMULATE_FATAL_ERROR".equals(message.content())) {
            log.error("❌ Rilevata richiesta di errore. Simulo un blocco critico.");
            throw new RuntimeException("Errore inaspettato! Scatta il retry...");
        }
        log.info("✅ Elaborazione completata con successo.");
    }
}

Il Listener della DLQ (Gestione Errori):

package com.tuoblog.enterprise.infrastructure.messaging;
import com.tuoblog.enterprise.domain.model.BusinessMessage;
import io.awspring.cloud.sqs.annotation.SqsListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

@Component
public class SqsDlqConsumerAdapter {
    private static final Logger log = LoggerFactory.getLogger(SqsDlqConsumerAdapter.class);

    @SqsListener("${aws.sqs.queue.dlq}")
    public void consumeErrorMessage(BusinessMessage failedMessage) {
        log.warn("⚠️ [DLQ - CODA DI ERRORE] Intercettato messaggio fallito in via definitiva!");
        log.warn("Dettagli -> ID: {}, Contenuto: '{}'", failedMessage.id(), failedMessage.content());
        log.info("🧑‍💻 Salvataggio su database per ispezione manuale...");
    }
}

5. L'API REST (Presentation Layer)

package com.tuoblog.enterprise.presentation.api;
import com.tuoblog.enterprise.application.usecase.PublishMessageUseCase;
import com.tuoblog.enterprise.domain.model.BusinessMessage;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/v1/messages")
public class MessageController {
    private final PublishMessageUseCase publishMessageUseCase;

    public MessageController(PublishMessageUseCase publishMessageUseCase) {
        this.publishMessageUseCase = publishMessageUseCase;
    }

    @PostMapping
    public ResponseEntity<String> createMessage(@RequestBody String payload) {
        BusinessMessage message = publishMessageUseCase.execute(payload);
        return ResponseEntity.ok("Accodato con successo ID: " + message.id());
    }

    @PostMapping("/simulate-error")
    public ResponseEntity<String> triggerErrorSequence() {
        BusinessMessage message = publishMessageUseCase.execute("SIMULATE_FATAL_ERROR");
        return ResponseEntity.status(202).body(
            "Messaggio 'velenoso' inviato (ID: " + message.id() + ")."
        );
    }
}

6. Collaudo Finale

Dopo aver applicato OpenTofu e avviato Spring Boot, usa cURL da terminale:

Test 1: Flusso corretto

curl -X POST http://localhost:8080/api/v1/messages -d "Tutto funziona alla grande!"

Test 2: Flusso di Errore (Intervento della DLQ)

curl -X POST http://localhost:8080/api/v1/messages/simulate-error

Separare le logiche tramite DDD e gestire proattivamente gli errori con una DLQ assicura che il tuo software sia pronto per l'ambiente di produzione, garantendo manutenibilità anche su progetti a lungo termine.

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) { this.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.