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.
Nessun commento:
Posta un commento