Back-end · Pleno

Filas e mensageria: processamento assíncrono e desacoplamento

Quando um usuário clica em "finalizar pedido", ele não deveria esperar o e-mail de confirmação ser enviado, a nota fiscal ser gerada e o estoque do fornecedor ser notificado — tudo dentro daquela única request. Processamento assíncrono com filas é a forma de tirar esse peso da request e deixar o sistema responsivo e desacoplado. Entender produtor, consumidor e entrega confiável é esperado de um dev pleno.

Por que processar de forma assíncrona

Uma request HTTP que faz cinco coisas demora o tempo das cinco somadas — e se a terceira falha, o usuário leva erro mesmo que o pedido tenha sido salvo. A ideia do assíncrono é simples: a request faz só o essencial (salvar o pedido) e publica o resto como tarefas a serem feitas depois. O usuário recebe resposta em milissegundos; o trabalho pesado acontece em segundo plano.

A analogia: num restaurante, o garçom não cozinha seu prato na sua frente. Ele anota o pedido (publica), prende a comanda no balcão (fila) e a cozinha vai puxando comandas no seu ritmo (consumidor). O garçom volta a atender outras mesas imediatamente.

Produtor e consumidor

O produtor é quem cria a mensagem e a coloca na fila. O consumidor é quem retira a mensagem e a processa. Eles não se conhecem nem precisam estar online ao mesmo tempo: o produtor publica e segue a vida; o consumidor processa quando puder. A fila (operada por um broker como RabbitMQ, SQS ou Kafka) fica no meio, guardando as mensagens.

// Produtor (na request)
pedido = salvarPedido(dados)
fila.publish("pedidos.criados", { id: pedido.id })
responder(202, "pedido recebido")

// Consumidor (processo separado)
on fila.receive("pedidos.criados") as msg:
    enviarEmail(msg.id)
    msg.ack()        // confirma processamento

O caminho da mensagem

  Produtor          Fila (broker)          Consumidor
 ┌────────┐  publish ┌──────────────┐  pull ┌──────────┐
 │ request│ ───────▶ │ [m3][m2][m1] │ ────▶ │ processa │
 └────────┘          └──────────────┘       └────┬─────┘
                          ▲   buffer              │ ack
                          └───────────────────────┘

A fila funciona como um buffer: se chegam mil pedidos num pico, eles se acumulam e os consumidores drenam no seu ritmo, sem derrubar o sistema. Isso amortece picos e conecta diretamente com escalabilidade — basta subir mais consumidores para processar mais rápido.

Fila vs tópico (pub/sub)

Numa fila tradicional, cada mensagem é entregue a um consumidor — vários consumidores dividem o trabalho. Já no padrão pub/sub (tópico), cada mensagem é entregue a todos os assinantes interessados. A diferença muda o desenho do sistema:

ModeloEntregaUso típico
Fila1 mensagem → 1 consumidorDistribuir trabalho (work queue)
Tópico (pub/sub)1 mensagem → N assinantesNotificar eventos a vários serviços

"Pedido criado" como evento num tópico permite que estoque, faturamento e e-mail reajam de forma independente — cada um com sua própria assinatura.

O que muda ao desacoplar serviços

Sem fila, o serviço de pedidos chama diretamente o de e-mail; se o de e-mail cai, o de pedidos quebra junto. Com a fila no meio, o produtor só depende do broker. O consumidor pode estar fora do ar que as mensagens ficam esperando — quando ele voltar, processa o acúmulo. Esse desacoplamento é uma peça central de resiliência: a falha de um serviço para de derrubar os outros.

Entrega, reprocessamento e dead-letter

Como o consumidor pode falhar no meio do processamento, ele só confirma a mensagem com um ack após terminar. Sem ack, o broker assume que algo deu errado e reentrega a mensagem. Isso garante que nada se perca — mas significa que a mesma mensagem pode chegar mais de uma vez (entrega at-least-once). Uma mensagem que falha repetidamente vai para uma dead-letter queue (DLQ): uma fila separada para inspeção manual, evitando que um "veneno" trave o consumidor para sempre.

Ordenação e idempotência

Duas armadilhas clássicas. Primeira: filas geralmente não garantem ordem global; se a ordem importa, use uma chave de particionamento ou aceite o reordenamento. Segunda, e mais importante: como a mesma mensagem pode ser entregue duas vezes, o consumidor precisa ser idempotente — processar a mesma mensagem N vezes deve ter o mesmo efeito que processá-la uma. Na prática, isso vira "verifique se o e-mail já foi enviado para esse pedido antes de enviar".

A fila promete entregar a mensagem ao menos uma vez — cabe ao consumidor não estragar tudo quando ela chega duas vezes. Idempotência não é um detalhe: é o que separa um sistema de mensageria robusto de um que envia o e-mail três vezes.

Conclusão

Filas tiram trabalho pesado da request, amortecem picos e desacoplam serviços para que a falha de um não derrube os outros. O preço é pensar em entrega (ack e DLQ), ordenação e idempotência. Combinadas com cache, escalabilidade e resiliência, a mensageria é o que permite sistemas crescerem sem que cada parte precise esperar pela outra.

When a user clicks "place order", they shouldn't have to wait for the confirmation email to be sent, the invoice to be generated and the supplier's inventory to be notified — all inside that single request. Asynchronous processing with queues is how you take that weight off the request and keep the system responsive and decoupled. Understanding producer, consumer and reliable delivery is expected of a mid-level dev.

Why process asynchronously

An HTTP request that does five things takes as long as all five added together — and if the third one fails, the user gets an error even though the order was saved. The idea behind async is simple: the request does only the essentials (saving the order) and publishes the rest as tasks to be done later. The user gets a response in milliseconds; the heavy work happens in the background.

The analogy: in a restaurant, the waiter doesn't cook your dish in front of you. He writes down the order (publishes), pins the ticket to the counter (queue) and the kitchen pulls tickets at its own pace (consumer). The waiter immediately goes back to serving other tables.

Producer and consumer

The producer is whoever creates the message and puts it on the queue. The consumer is whoever takes the message off and processes it. They don't know each other and don't need to be online at the same time: the producer publishes and moves on; the consumer processes when it can. The queue (run by a broker such as RabbitMQ, SQS or Kafka) sits in the middle, holding the messages.

// Producer (inside the request)
order = saveOrder(data)
queue.publish("orders.created", { id: order.id })
respond(202, "order received")

// Consumer (separate process)
on queue.receive("orders.created") as msg:
    sendEmail(msg.id)
    msg.ack()        // confirms processing

The message's path

  Producer          Queue (broker)         Consumer
 ┌────────┐ publish ┌──────────────┐  pull ┌──────────┐
 │ request│ ───────▶ │ [m3][m2][m1] │ ────▶ │ process  │
 └────────┘          └──────────────┘       └────┬─────┘
                          ▲   buffer              │ ack
                          └───────────────────────┘

The queue works as a buffer: if a thousand orders arrive in a spike, they pile up and the consumers drain them at their own pace, without taking the system down. This smooths out spikes and connects directly with scalability — just spin up more consumers to process faster.

Queue vs topic (pub/sub)

In a traditional queue, each message is delivered to one consumer — multiple consumers share the work. In the pub/sub pattern (topic), each message is delivered to all interested subscribers. The difference changes the design of the system:

ModelDeliveryTypical use
Queue1 message → 1 consumerDistributing work (work queue)
Topic (pub/sub)1 message → N subscribersNotifying events to several services

"Order created" as an event on a topic lets inventory, billing and email react independently — each with its own subscription.

What changes when you decouple services

Without a queue, the orders service calls the email service directly; if email goes down, orders breaks too. With the queue in the middle, the producer only depends on the broker. The consumer can be offline and the messages just wait — when it comes back, it processes the backlog. This decoupling is a central piece of resilience: one service's failure stops taking down the others.

Delivery, reprocessing and dead-letter

Since the consumer can fail halfway through processing, it only confirms the message with an ack after finishing. Without an ack, the broker assumes something went wrong and redelivers the message. This guarantees nothing gets lost — but it means the same message may arrive more than once (at-least-once delivery). A message that fails repeatedly goes to a dead-letter queue (DLQ): a separate queue for manual inspection, preventing a "poison message" from blocking the consumer forever.

Ordering and idempotency

Two classic pitfalls. First: queues generally do not guarantee global ordering; if order matters, use a partition key or accept reordering. Second, and more important: since the same message can be delivered twice, the consumer needs to be idempotent — processing the same message N times must have the same effect as processing it once. In practice, this becomes "check whether the email was already sent for this order before sending it".

The queue promises to deliver the message at least once — it's up to the consumer not to mess everything up when it arrives twice. Idempotency isn't a detail: it's what separates a robust messaging system from one that sends the email three times.

Conclusion

Queues take heavy work off the request, smooth out spikes and decouple services so that one's failure doesn't bring down the others. The price is having to think about delivery (ack and DLQ), ordering and idempotency. Combined with caching, scalability and resilience, messaging is what lets systems grow without every part having to wait for the others.

Leia também

Voltar à trilha