Amazon SQS: de berichtenwachtrij

Amazon Simple Queue-service (SQS) is een beheerde berichtenwachtrij: producenten sturen berichten naar de wachtrij, consumenten halen ze op via polling. De fundamentele semantiek is punt-tot-punt: Een bericht in de wachtrij het wordt door één consument tegelijk gelezen (de eerste die het pakt, "verbergt" het voor de anderen via de time-out voor zichtbaarheid).

Standaardwachtrij versus FIFO-wachtrij

SQS biedt twee modi:

  • Standaard wachtrij: Onbeperkte doorvoer, minimaal één keer bezorgen (duplicaten mogelijk), best-effort-bestelling (niet gegarandeerd). Voor gebruiksscenario's met grote volumes waar duplicaten beheersbaar zijn aan de consumentenzijde.
  • FIFO-wachtrij: exact éénmalige levering (automatische ontdubbeling via MessageDeduplicationId), gegarandeerde sortering voor berichten groep, doorvoer beperkt tot 3.000 msg/s per verzoek (met batching). Voor gebruiksscenario's waarbij bestellen en duplicaten van cruciaal belang zijn (bijvoorbeeld financiële transacties).
// SQS Producer Java con AWS SDK v2
import software.amazon.awssdk.services.sqs.*;
import software.amazon.awssdk.services.sqs.model.*;

public class SqsProducer {

    private final SqsClient sqsClient = SqsClient.builder()
        .region(Region.EU_WEST_1)
        .build();

    // Standard Queue: invio semplice
    public void sendToStandardQueue(String queueUrl, String messageBody) {
        SendMessageResponse response = sqsClient.sendMessage(
            SendMessageRequest.builder()
                .queueUrl(queueUrl)
                .messageBody(messageBody)
                // Delay di consegna (0-900 secondi)
                .delaySeconds(0)
                // Attributi del messaggio per filtraggio (SNS subscription filter)
                .messageAttributes(Map.of(
                    "eventType", MessageAttributeValue.builder()
                        .stringValue("OrdineCreato")
                        .dataType("String")
                        .build()
                ))
                .build()
        );
        System.out.println("Messaggio inviato: " + response.messageId());
    }

    // FIFO Queue: richiede MessageGroupId e MessageDeduplicationId
    public void sendToFifoQueue(String queueUrl, String ordineId, String payload) {
        sqsClient.sendMessage(
            SendMessageRequest.builder()
                .queueUrl(queueUrl)  // url deve terminare in .fifo
                .messageBody(payload)
                // Group: tutti i messaggi dello stesso ordine vengono elaborati in ordine
                .messageGroupId("ordine-" + ordineId)
                // Deduplication: prevenzione duplicati (valido 5 minuti)
                .messageDeduplicationId(ordineId + "-" + System.currentTimeMillis())
                .build()
        );
    }
}

SQS-consument: time-out voor polling en zichtbaarheid

// SQS Consumer con Long Polling e gestione DLQ
public class SqsConsumer {

    private final SqsClient sqsClient = SqsClient.builder()
        .region(Region.EU_WEST_1).build();

    public void poll(String queueUrl) {
        while (true) {
            // Long polling: aspetta fino a 20 secondi per nuovi messaggi
            // Riduce le chiamate vuote (e i costi) rispetto al short polling
            ReceiveMessageResponse response = sqsClient.receiveMessage(
                ReceiveMessageRequest.builder()
                    .queueUrl(queueUrl)
                    .maxNumberOfMessages(10)        // max 10 per chiamata
                    .waitTimeSeconds(20)             // long polling
                    .visibilityTimeout(30)           // 30s per elaborare
                    .messageAttributeNames("All")
                    .build()
            );

            for (Message message : response.messages()) {
                try {
                    processMessage(message.body());

                    // Successo: elimina dalla coda
                    sqsClient.deleteMessage(
                        DeleteMessageRequest.builder()
                            .queueUrl(queueUrl)
                            .receiptHandle(message.receiptHandle())
                            .build()
                    );

                } catch (Exception e) {
                    // Non eliminare: SQS rirenderà visibile il messaggio
                    // dopo il visibility timeout. Dopo maxReceiveCount tentativi
                    // finisce nella DLQ configurata
                    System.err.println("Errore, il messaggio tornera visibile: " + e.getMessage());
                }
            }
        }
    }
}

Amazon SNS: Fan-Out Pub/Sub

Amazon Simple Notification Service (SNS) implementa il pattern publish/subscribe: un producer pubblica un messaggio su un topic SNS, e SNS lo consegna in parallelo a tutti gli iscritti (subscriber). Un topic SNS può avere migliaia di sottoscrittori: Lambda, SQS, HTTP endpoint, email, SMS, mobile push.

Il pattern SNS + SQS (SNS Fan-Out) è uno degli schemi più comuni in architetture AWS: SNS consegna lo stesso evento a più code SQS in parallelo, e ogni coda serve un consumer diverso (un microservizio diverso).

# SNS + SQS Fan-Out: un evento raggiunge 3 servizi in parallelo

# Struttura:
# SNS Topic "ordini-topic"
#   |--- SQS "inventario-queue" --- Lambda inventario
#   |--- SQS "pagamenti-queue"  --- Lambda pagamenti
#   |--- SQS "notifiche-queue"  --- Lambda notifiche email

# Terraform
resource "aws_sns_topic" "ordini" {
  name = "ordini-topic"
}

resource "aws_sqs_queue" "inventario" {
  name = "inventario-queue"
  visibility_timeout_seconds = 60
  redrive_policy = jsonencode({
    deadLetterTargetArn = aws_sqs_queue.inventario_dlq.arn
    maxReceiveCount     = 3
  })
}

resource "aws_sns_topic_subscription" "ordini_inventario" {
  topic_arn = aws_sns_topic.ordini.arn
  protocol  = "sqs"
  endpoint  = aws_sqs_queue.inventario.arn

  # Filter policy: questa subscription riceve SOLO OrdineCreato
  filter_policy = jsonencode({
    eventType = ["OrdineCreato"]
  })
}
// SNS Publisher Java
import software.amazon.awssdk.services.sns.*;
import software.amazon.awssdk.services.sns.model.*;

public class SnsPublisher {

    private final SnsClient snsClient = SnsClient.builder()
        .region(Region.EU_WEST_1).build();

    public void publishOrdineCreato(String topicArn, OrdineCreato event) throws Exception {
        String messageJson = new ObjectMapper().writeValueAsString(event);

        PublishResponse response = snsClient.publish(
            PublishRequest.builder()
                .topicArn(topicArn)
                .message(messageJson)
                // Message attributes per subscription filter
                .messageAttributes(Map.of(
                    "eventType", MessageAttributeValue.builder()
                        .stringValue("OrdineCreato")
                        .dataType("String")
                        .build()
                ))
                // Subject visibile solo nelle email
                .subject("Nuovo ordine: " + event.getOrdineId())
                .build()
        );
        System.out.println("SNS Message ID: " + response.messageId());
    }
}

Il Confronto Completo

Caratteristica SQS SNS EventBridge
Modello Point-to-point (coda) Fan-out (pub/sub) Content-based routing
Destinatari 1 consumer alla volta Tutti gli iscritti in parallelo Target per regola (1-5)
Filtro Nessuno (prende tutto) Message attributes (limitato) Event pattern su qualsiasi campo JSON
Ordinamento FIFO garantito (FIFO queue) No No
Deduplication Si (FIFO queue) No No
Throughput max Illimitato (Standard) Illimitato 10 milioni eventi/s per region
Latenza Milliseconds (polling) < 1s (push) < 500ms
Schema Registry No No Si (integrato)
Event Archive/Replay No No Si
Partner integration SaaS No No Si (35+ partner)
Costo per milione messaggi ~$0.40 ~$0.50 ~$1.00

Beslissingsgids: wanneer moet u welke gebruiken?

Gebruik SQS wanneer:

  • Dat moet taakverdeling tussen meerdere consumenten van hetzelfde type (bijvoorbeeld 5 Lambda's die dezelfde wachtrij verwerken)
  • De boodschap moet worden uitgewerkt precies één keer (FIFO-wachtrij)
  • Dat moet tegendruk: Berichten stapelen zich op in de wachtrij als consumenten traag zijn
  • Wil je de levering vertraging (tot 15 minuten per enkel bericht)
  • Je integreert legacy-systemen die het traditionele wachtrijmodel gebruiken

Gebruik SNS wanneer:

  • Dezelfde gebeurtenis moet bereiken meerdere systemen parallel (uitwaaierpatroon)
  • Je moet sturen meldingen (e-mail, sms, mobiele push, HTTP-webhook)
  • Combineer SNS + SQS om voor elke consument zowel fan-out als buffering te hebben

Gebruik EventBridge wanneer:

  • Dat moet intelligente routering op basis van de inhoud van de lading
  • Integreer met AWS-diensten (EC2, S3, RDS) of SaaS-partners (Zendesk, Shopify, Streep)
  • Je wilt het Schemaregister en schemabeheer
  • Dat moet Evenementarchief en herhaling
  • Bouw een bedrijfsevenementenbus met gecentraliseerde routering

SNS + SQS + Lambda-patroon: de complete stapel

# Architettura tipica per un microservizio event-driven su AWS
#
# Client HTTP
#    |
#    v
# API Gateway / Lambda "ordini-api"
#    | pubblica su
#    v
# SNS Topic "ordini-events"
#    |-- filtra "OrdineCreato" --> SQS "inventario-queue" --> Lambda "inventario-handler"
#    |-- filtra "OrdineCreato" --> SQS "notifiche-queue"  --> Lambda "email-notifier"
#    |-- tutti i tipi         --> SQS "audit-queue"       --> Lambda "audit-logger"
#
# Ogni SQS ha la sua DLQ per messaggi non elaborabili
# Le Lambda usano le SQS come event source (SQS trigger)

# Configurazione Lambda con SQS trigger
resource "aws_lambda_event_source_mapping" "inventario" {
  event_source_arn = aws_sqs_queue.inventario.arn
  function_name    = aws_lambda_function.inventario_handler.arn
  batch_size       = 10         # processa 10 messaggi alla volta
  enabled          = true

  # Bisection on error: in caso di errore batch, divide il batch a meta
  # per identificare il messaggio problematico (evita il poison pill)
  bisect_batch_on_function_error = true

  # Finestra di aggregazione (utile per batch processing)
  maximum_batching_window_in_seconds = 5
}

EventBridge + SQS: het beste van twee werelden

In veel architecturen zijn EventBridge en SQS complementair: EventBridge doet de intelligente routing, SQS doet de buffering en load-balancing. Een evenement arriveert op EventBridge, wordt doorgestuurd naar de juiste SQS-wachtrij, en de consument Lambda verwerkt het vanuit de wachtrij met opnieuw proberen en DLQ.

# EventBridge --> SQS (con transform di input opzionale)
resource "aws_cloudwatch_event_target" "ordini_to_sqs" {
  rule           = aws_cloudwatch_event_rule.ordini_vip.name
  event_bus_name = aws_cloudwatch_event_bus.mioapp.name
  arn            = aws_sqs_queue.vip_orders.arn

  # Input transformer: ristruttura il payload prima di inviarlo a SQS
  # Utile per estrarre solo i campi necessari
  input_transformer {
    input_paths = {
      ordineId  = "$.detail.ordineId"
      clienteId = "$.detail.clienteId"
      totale    = "$.detail.totale"
    }
    input_template = <<-EOF
    {
      "ordineId": "<ordineId>",
      "clienteId": "<clienteId>",
      "totale": "<totale>",
      "processato_da": "eventbridge-vip-rule"
    }
    EOF
  }
}

Volgende stappen in de serie

  • Artikel 8 – Dead Letter-wachtrij en veerkracht: de juiste configuratie van DLQ voor SQS, SNS en EventBridge is van cruciaal belang om berichtverlies te voorkomen. Time-out voor zichtbaarheid, maxReceiveCount en het herverwerkingspatroon.

Link met andere series

  • AWS EventBridge (artikel 6): diepgaande analyse van gebeurtenispatronen, schemaregister en gebeurtenisarchief, alle EventBridge-configuratiedetails.
  • Apache Kafka (serie 38): voor gebruiksscenario's op locatie of met zeer hoge doorvoer (>10 miljoen msg/s), Kafka is superieur aan SQS/SNS. De keuze hangt af van de cloudprovider, door doorvoer en de noodzaak van herhaling van gebeurtenissen.