Architettura Software 15 min di lettura 5 marzo 2026

Come progettare microservizi scalabili con RabbitMQ e architettura event-driven

Una guida pratica e testata in produzione per decomporre i monoliti, progettare la comunicazione event-driven con RabbitMQ e costruire microservizi che restano affidabili sotto carico reale.

ME

Marco Esposito

Lead Software Architect

Il passaggio da monolito a microservizi è una delle transizioni architetturali più discusse e più spesso mal gestite nell'ingegneria del software. I team partono da un dolore reale — deployment lenti, accoppiamento tra team, impossibilità di scalare componenti specifici — e finiscono con un monolite distribuito più difficile da gestire di quello di partenza. Questa guida documenta ciò che abbiamo imparato costruendo architetture a microservizi event-driven per sistemi in produzione che elaborano centinaia di migliaia di eventi al giorno.

Perché i microservizi? Prerequisiti onesti

I microservizi risolvono problemi specifici. Se non hai questi problemi, un monolite ben strutturato è una scelta migliore. I driver legittimi sono: scalabilità del team (più team che deployano in modo indipendente senza overhead di coordinamento), scaling a livello di componente (una parte del sistema ha un carico 100× superiore a un'altra), requisiti di diversità tecnologica (alcuni componenti hanno bisogno di Python per il ML, altri di Go per le performance), o requisiti di isolamento dei guasti (un failure in un componente non deve propagarsi agli altri).

Se il driver è semplicemente "architettura moderna" o "il team vuole imparare i microservizi", l'overhead organizzativo e operativo non varrà la pena. Abbiamo aiutato tre clienti a migrare di ritorno verso un monolite modulare dopo un'adozione prematura dei microservizi. La lezione è sempre la stessa: la complessità va guadagnata.

La dorsale event-driven

La decisione architetturale più importante in un sistema a microservizi è come i servizi comunicano. La comunicazione HTTP sincrona tra servizi crea accoppiamento temporale stretto: se il Servizio B è down, il Servizio A fallisce. Crea latenza a cascata man mano che le chiamate si impilano. E rende i test molto più difficili.

La comunicazione asincrona event-driven disaccoppia i servizi nel tempo. Il Servizio A emette un evento e va avanti; il Servizio B lo consuma quando è pronto. Questo pattern non è appropriato per ogni interazione — le query in tempo reale necessitano ancora di risposte sincrone — ma per la maggior parte dei flussi di processo business (ordine piazzato → inventario prenotato → fulfillment avviato → notifica inviata), gli eventi sono il primitivo corretto.

RabbitMQ come message broker

RabbitMQ è la nostra scelta predefinita per il brokering di eventi in sistemi di scala media. È maturo, operativamente ben compreso, supporta AMQP con semantica di routing ricca e gestisce throughput fino a centinaia di migliaia di messaggi al secondo su hardware modesto. Per sistemi che richiedono throughput più elevato o retention di lungo termine degli eventi, Apache Kafka è l'alternativa — ma porta con sé una complessità operativa significativa.

Architettura di exchange e code

Il modello degli exchange di RabbitMQ è la sua funzionalità più potente. Invece di produrre direttamente sulle code, i producer pubblicano sugli exchange che instradano i messaggi alle code in base a regole di routing. I tre pattern che usiamo più spesso sono:

Pattern di Exchange RabbitMQ

Direct ExchangeRouting per chiave esatta. Usato per il routing di comandi verso una specifica istanza di servizio.
Topic ExchangeRouting per pattern (ordine.creato, ordine.*). Usato per il fan-out di eventi di dominio verso più consumer.
Fanout ExchangeBroadcast a tutte le code legate. Usato per notifiche e invalidazione della cache.
rabbitmq-config.tstypescript
// Configurazione RabbitMQ per NestJS con exchange multipli
import { RabbitMQModule } from "@golevelup/nestjs-rabbitmq";

@Module({
  imports: [
    RabbitMQModule.forRoot({
      exchanges: [
        {
          name: "ordini.topic",
          type: "topic",
          options: { durable: true },
        },
        {
          name: "notifiche.fanout",
          type: "fanout",
          options: { durable: true },
        },
        {
          name: "comandi.direct",
          type: "direct",
          options: { durable: true },
        },
      ],
      uri: process.env.RABBITMQ_URI,
      connectionInitOptions: { wait: true, reject: true, timeout: 10000 },
      // Dead Letter Exchange per i messaggi falliti
      defaultRpcErrorBehavior: MessageHandlerErrorBehavior.REQUEUE,
    }),
  ],
})
export class MessagingModule {}

Progettare lo schema degli eventi

Gli schemi degli eventi sono il contratto API tra i servizi. Le breaking change agli schemi degli eventi sono l'equivalente distribuito di una breaking change alle API — solo che colpiscono consumer di cui potresti non essere nemmeno a conoscenza. Usiamo un registry degli schemi (Confluent Schema Registry o un semplice repo JSON Schema) e richiediamo solo evoluzione backward-compatible: aggiungere campi opzionali è permesso, rinominare o rimuovere campi richiede un nuovo tipo di evento.

order-events.tstypescript
// Definizioni di eventi versionati e fortemente tipizzati
export interface OrdineCreatoEventoV2 {
  tipoEvento: "ordine.creato";
  versioneEvento: 2;
  idEvento: string;        // UUID per idempotenza
  timestamp: string;       // ISO 8601
  correlationId: string;   // Traccia attraverso i servizi
  payload: {
    idOrdine: string;
    idCliente: string;
    articoli: {
      idProdotto: string;
      sku: string;
      quantita: number;
      prezzoUnitario: number;
    }[];
    totaleImporto: number;
    valuta: string;
    indirizzoSpedizione: Indirizzo;
    // aggiunta v2 (backward compatible)
    metadata?: Record<string, string>;
  };
}

// Unioni discriminate per gestione eventi type-safe
export type EventoOrdine =
  | OrdineCreatoEventoV2
  | OrdineConfermatoEvento
  | OrdineSpeditoEvento
  | OrdineAnnullatoEvento;

Costruire i servizi con NestJS

NestJS è il nostro framework di elezione per i microservizi su Node.js. Il suo sistema di moduli impone confini netti, la dependency injection rende i test semplici e il layer di trasporto per microservizi integrato ha un ottimo supporto per RabbitMQ.

Il servizio ordini

order.service.tstypescript
@Injectable()
export class OrdineService {
  constructor(
    @InjectRepository(Ordine) private readonly ordineRepo: Repository<Ordine>,
    private readonly amqpConnection: AmqpConnection,
    private readonly logger: Logger,
  ) {}

  async creaOrdine(dto: CreaOrdineDto, idCliente: string): Promise<Ordine> {
    // 1. Persiste l'ordine in stato pending
    const ordine = await this.ordineRepo.save({
      ...dto,
      idCliente,
      stato: "in_attesa",
      creatoIl: new Date(),
    });

    // 2. Pubblica l'evento — i servizi sono disaccoppiati da questo punto
    const evento: OrdineCreatoEventoV2 = {
      tipoEvento: "ordine.creato",
      versioneEvento: 2,
      idEvento: randomUUID(),
      timestamp: new Date().toISOString(),
      correlationId: dto.correlationId ?? randomUUID(),
      payload: {
        idOrdine: ordine.id,
        idCliente,
        articoli: ordine.articoli,
        totaleImporto: ordine.totaleImporto,
        valuta: ordine.valuta,
        indirizzoSpedizione: ordine.indirizzoSpedizione,
      },
    };

    await this.amqpConnection.publish("ordini.topic", "ordine.creato", evento);
    this.logger.log(`Ordine ${ordine.id} creato, evento pubblicato`);

    return ordine;
  }
}

Il servizio inventario — Consumer

inventory.consumer.tstypescript
@Injectable()
export class InventarioConsumer {
  constructor(
    private readonly inventarioService: InventarioService,
    private readonly logger: Logger,
  ) {}

  @RabbitSubscribe({
    exchange: "ordini.topic",
    routingKey: "ordine.creato",
    queue: "inventario.prenota",
    queueOptions: {
      durable: true,
      // Dead Letter Queue per i messaggi falliti
      arguments: {
        "x-dead-letter-exchange": "ordini.dlx",
        "x-dead-letter-routing-key": "inventario.prenota.fallito",
        "x-message-ttl": 300000, // TTL di 5 minuti
      },
    },
  })
  async gestisciOrdinCreato(evento: OrdineCreatoEventoV2): Promise<void> {
    this.logger.log(`Prenotazione inventario per ordine ${evento.payload.idOrdine}`);

    try {
      // Controllo idempotenza — elabora ogni idEvento esattamente una volta
      if (await this.inventarioService.isElaborato(evento.idEvento)) {
        this.logger.warn(`Evento duplicato ${evento.idEvento}, saltato`);
        return;
      }

      await this.inventarioService.prenota(evento.payload.articoli);
      await this.inventarioService.marcaElaborato(evento.idEvento);

      // Pubblica l'evento downstream
      await this.amqpConnection.publish(
        "ordini.topic",
        "inventario.prenotato",
        { ...evento, tipoEvento: "inventario.prenotato" }
      );
    } catch (errore) {
      // Lascia che RabbitMQ gestisca il retry tramite DLX
      throw new Nack(false); // false = non rimettere in coda, manda alla DLX
    }
  }
}

Idempotenza: la regola non negoziabile

In qualsiasi sistema distribuito, i messaggi vengono consegnati più di una volta. Partizioni di rete, crash del consumer a metà elaborazione e retry del broker portano tutti a consegne duplicate. Ogni consumer deve essere idempotente: elaborare due volte lo stesso messaggio deve avere lo stesso effetto di elaborarlo una volta. L'approccio standard è memorizzare gli ID degli eventi elaborati in un lookup store veloce (Redis SSET o una tabella di deduplicazione su database) e saltare l'elaborazione se l'ID è già stato visto.

Il pattern Saga per le transazioni distribuite

Le transazioni distribuite — operazioni che attraversano più servizi e devono tutte avere successo o tutte fare rollback — sono uno dei problemi più difficili nei microservizi. Il two-phase commit tradizionale è troppo fragile per i sistemi asincroni. Il pattern Saga risolve questo tramite una sequenza di transazioni locali, ciascuna delle quali pubblica un evento che innesca il servizio successivo. Se un qualsiasi passo fallisce, le transazioni compensanti annullano i passi precedenti.

order-saga.tstypescript
// Saga basata su coreografia per l'elaborazione degli ordini
// Ogni servizio ascolta gli eventi ed emette il proprio esito

// Passo 1: inventario.service.ts — ascolta ordine.creato, emette inventario.prenotato
// Passo 2: pagamento.service.ts  — ascolta inventario.prenotato, emette pagamento.elaborato
// Passo 3: spedizione.service.ts — ascolta pagamento.elaborato, emette spedizione.creata
// Passo 4: ordine.service.ts     — ascolta spedizione.creata, emette ordine.confermato

// Esempio di transazione compensante (fallimento pagamento)
@RabbitSubscribe({
  exchange: "ordini.topic",
  routingKey: "pagamento.fallito",
  queue: "inventario.rilascia",
})
async gestisciPagamentoFallito(evento: PagamentoFallitoEvento): Promise<void> {
  // Transazione compensante: rilascia l'inventario prenotato
  await this.inventarioService.rilascia(evento.payload.idOrdine);
  await this.amqpConnection.publish("ordini.topic", "inventario.rilasciato", {
    ...evento,
    tipoEvento: "inventario.rilasciato",
    motivo: "pagamento_fallito",
  });
}

Pattern di affidabilità

  • Dead Letter Exchange (DLX): i messaggi che falliscono l'elaborazione dopo N retry vengono instradati verso una DLX per ispezione e rielaborazione manuale.
  • Persistenza dei messaggi: imposta deliveryMode: 2 (persistente) su tutti i messaggi business-critical per sopravvivere ai riavvii del broker.
  • Publisher confirm: usa i publisher confirm di RabbitMQ per garantire che i messaggi pubblicati siano stati persistiti dal broker prima di confermare il successo al producer.
  • Prefetch count: imposta channel.prefetch(1) sui consumer per evitare che un singolo consumer lento accumuli tutti i messaggi.
  • Circuit breaker: wrappa le chiamate esterne all'interno dei consumer con un circuit breaker (es. opossum) per prevenire failure a cascata.

Observability: tracing tra i servizi

Il distributed tracing non è opzionale in un'architettura a microservizi. Senza di esso, fare debug di un failure che attraversa quattro servizi è sostanzialmente impossibile. Propaghiamo un campo correlationId in ogni envelope degli eventi e lo iniettamo nel contesto trace OpenTelemetry a ogni confine di servizio. Ogni servizio emette span verso un collettore Jaeger o Tempo centralizzato. Una singola trace mostra il flusso completo dalla richiesta HTTP al consumer dell'evento finale.

Quando usare Kafka invece di RabbitMQ

RabbitMQ è la scelta giusta per la maggior parte dei sistemi a microservizi event-driven. Passa a Kafka quando: hai bisogno di conservare il log completo degli eventi per il replay (Kafka è un log distribuito, non solo una coda); hai un throughput superiore a diversi milioni di messaggi al secondo; hai bisogno di semantica exactly-once con le Kafka Transactions; o hai bisogno che più consumer group indipendenti riproducano da offset differenti. L'overhead operativo di Kafka è significativamente più alto — tienilo in conto nella decisione.

Conclusioni

I microservizi event-driven con RabbitMQ offrono un modello potente per costruire sistemi che scalano in modo indipendente, falliscono con grazia ed evolvono senza accoppiamento. I pattern trattati qui — topic exchange, consumer idempotenti, pattern Saga, gestione dei dead-letter e distributed tracing — rappresentano una baseline per la prontezza alla produzione. L'errore più comune è trattarli come ottimizzazioni opzionali da aggiungere in seguito. Nei sistemi distribuiti, l'affidabilità non è una feature che aggiungi dopo il lancio: è un vincolo per cui progetti dal primo giorno.

Nexora ha costruito e migrato architetture a microservizi event-driven che gestiscono centinaia di migliaia di transazioni giornaliere. Se il tuo team sta valutando una transizione verso i microservizi o ha problemi di affidabilità in un sistema distribuito esistente, possiamo aiutarti.

Tag

MicroserviziRabbitMQEvent-DrivenNestJSArchitetturaSistemi Distribuiti

Prossimo passo

Hai bisogno di implementare questa architettura?

Il nostro team di ingegneria costruisce e scala i sistemi descritti in questo articolo. Dalla discovery alla produzione — con risultati misurabili.