A fronteira entre serviços é onde a maioria dos bugs de sistemas distribuídos nasce. Cada chamada inter-serviço é uma chamada de rede com latência variável, falha possível, timeout configurável e serialização cara. A primeira decisão de arquitetura ao projetar comunicação entre serviços não é "qual protocolo?" — é "qual modelo de comunicação?" Essa decisão acontece antes de REST vs gRPC, antes de fila vs Kafka, antes de qualquer escolha de tecnologia. Acertar o modelo elimina uma classe inteira de problemas; errar o modelo cria problemas que nenhuma otimização de protocolo vai resolver depois.
A taxonomia organiza-se em dois eixos independentes. O primeiro: síncrono vs assíncrono — o chamador bloqueia esperando a resposta ou continua após publicar? O segundo: request-response vs event-driven — a comunicação é uma solicitação com expectativa de resposta, ou a publicação de um fato sem expectativa de retorno? Os dois eixos são ortogonais e independentes. Comunicação síncrona event-driven existe (webhook: o servidor te notifica via HTTP quando algo acontece). Comunicação assíncrona request-response existe (manda mensagem com correlation ID, espera resposta na reply queue). Confundir os eixos leva a escolhas erradas: REST onde fila seria correto, fila onde REST seria correto, e principalmente — usar async/await no código e achar que o sistema ficou assíncrono no sentido arquitetural.
Os 14 conceitos deste módulo se organizam a partir dessa taxonomia. REST (02), gRPC (03), GraphQL (04) são síncrono request-response. Filas (08), Kafka (09), padrões de mensageria (10) são assíncrono event-driven. Reverse proxy (05), API Gateway (06), service mesh (07) são infraestrutura que serve ambos os modelos. Idempotência (11), service discovery (12), rate limiting (13), observabilidade (14) são preocupações que emergem independente do modelo, mas com características distintas em cada um.
Acoplamento temporal — o conceito central
Dois componentes são temporalmente acoplados quando ambos precisam estar disponíveis simultaneamente para que a comunicação seja bem-sucedida. Toda chamada síncrona cria acoplamento temporal por definição: se B está fora quando A chama, A falha. Se B está lento, A fica lento. A disponibilidade de A depende diretamente da disponibilidade de B no momento exato da chamada — não em média, não em geral, mas naquele instante específico.
O impacto em cascata é matemático. Em uma cadeia síncrona A→B→C→D, a disponibilidade composta é o produto das disponibilidades individuais. Quatro serviços de 99.9% cada: 0.999⁴ ≈ 99.6%. Dez serviços: 0.999¹⁰ ≈ 99.0%. O SLA do sistema inteiro é inferior ao SLA de qualquer componente individual e piora a cada serviço adicionado à cadeia. Isso não é teoria: é o motivo pelo qual times que adotam microsserviços sem cuidado com comunicação frequentemente ficam com sistemas menos disponíveis do que o monólito que substituíram.
O acoplamento temporal tem uma segunda dimensão menos discutida: acoplamento de performance. Se B fica lento — não falha, apenas demora — A também fica lento. Em cadeias síncronas longas, a latência do sistema é a soma das latências individuais mais o overhead de rede. Uma operação que toca 8 serviços, cada um com P99 de 50ms, tem P99 composto de pelo menos 400ms — antes de qualquer variância ou retry. Sob carga, a variância se acumula multiplicativamente: o P99 composto de N serviços independentes com P99 p é muito pior do que N × p.
await httpClient.GetAsync(url) é uma chamada síncrona de rede: o fluxo lógico não avança até a resposta chegar. O await apenas libera a thread do OS para outro trabalho — o acoplamento temporal entre os dois serviços permanece intacto. Um serviço com mil async Task que faz chamadas HTTP é tão temporalmente acoplado quanto um serviço bloqueante tradicional.
O eixo síncrono vs assíncrono
Na comunicação síncrona, o chamador envia uma requisição e aguarda a resposta antes de prosseguir com o fluxo atual. O chamador e o callee estão, no momento da comunicação, ambos ativos e coordenados. HTTP, gRPC, consultas ao banco de dados, chamadas TCP síncronas — todos são síncronos no sentido arquitetural, independente de como são implementados internamente.
Na comunicação assíncrona, o chamador envia uma mensagem (evento, comando) e prossegue imediatamente. O callee processa em seu próprio tempo, sem coordenação direta. Os dois sistemas operam com agendas independentes, mediadas por um broker durável. Message queues (RabbitMQ, SQS, Azure Service Bus), log distribuído (Kafka, Kinesis), e até email são assíncronos: nenhum requer que produtor e consumidor estejam ativos simultaneamente.
A diferença prática emerge em três dimensões. Falha do callee: em HTTP síncrono, connection refused ou timeout é uma exceção que se propaga imediatamente para o chamador. Em mensageria assíncrona, a "falha" do consumer não é visível para o produtor — a mensagem persiste no broker, e o consumer processa quando voltar. Carga variável: em HTTP síncrono, picos de carga chegam diretamente ao callee — que deve absorvê-los ou falhar. Em mensageria, o broker funciona como buffer: o producer publica na sua velocidade, o consumer processa na sua velocidade. A fila absorve a diferença. Deploy independente: em async, producer e consumer podem ser deployados, reiniciados e escalados independentemente sem janela de coordenação — o broker garante a continuidade das mensagens.
O eixo request-response vs event-driven
No modelo request-response, a comunicação é direcionada: um sender envia uma requisição para um receiver específico e espera (ou em algum momento recebe) uma resposta. Há um propósito declarado — "faça X" ou "me diga Y" — e uma expectativa de retorno. O sender sabe quem está chamando. Há um loop de feedback explícito. REST, gRPC, SQL queries são request-response. O sender conhece o destinatário em tempo de design.
No modelo event-driven, a comunicação é a publicação de um fato: algo aconteceu no sistema. O publicador não tem interesse em saber quem vai consumir o evento, quantos vão consumir, ou quando. Não há resposta esperada. "OrderCreated", "PaymentProcessed", "InventoryDepleted" são eventos — fatos sobre o mundo que podem interessar a zero, um, ou dez consumidores. O publicador é ignorante sobre os consumidores por design.
Essa ignorância é uma feature, não um bug. Quando um novo serviço de analytics precisa reagir a "OrderCreated", ele simplesmente se inscreve no tópico — sem nenhuma mudança no serviço de pedidos. O acoplamento existe apenas no schema do evento, não na lista de consumidores. Isso permite que o sistema evolua adicionando funcionalidades sem modificar serviços existentes — o que Sam Newman chama de "choreography" em contraposição a "orchestration".
Dentro do modelo event-driven, a distinção entre commands, events e queries (o padrão CQQ/CQRS) é operacionalmente relevante. Um command ("CreateOrder", "CancelPayment") é um pedido de ação direcionado a um destinatário específico — implica autoridade e expectativa de execução. Falha do command precisa ser tratada. Um event ("OrderCreated", "PaymentCancelled") é uma declaração de fato no passado — não implica que ninguém deve fazer nada; cada consumidor decide independentemente. Uma query solicita estado sem efeito colateral — e é sempre síncrona por natureza, pois requer um resultado imediato. Misturar esses três padrões é fonte frequente de contratos ambíguos: um "evento" que falha o produtor se não for consumido é na verdade um command.
Os quatro quadrantes em profundidade
Síncrono + Request-Response
O padrão mais familiar. REST, gRPC, GraphQL. Chamada e resposta em tempo real. O stack trace é linear — debugar é trivial comparado a sistemas assíncronos. Acoplamento temporal é explícito e inevitável. O chamador espera; se o callee não responder em tempo, há timeout com erro claro.
Use quando: o chamador precisa do resultado para prosseguir e a latência de resposta é tolerável pelo usuário ou pelo SLA. "Ver saldo da conta", "buscar produto", "autenticar usuário" — o resultado é necessário agora. Consultas de leitura são quase sempre sync request-response. Operações de escrita com resposta imediata para o usuário também.
Falha característica: cascata. Se C fica lento, B fica lento, A fica lento, e o usuário espera. Sem circuit breaker, timeouts agressivos e bulkhead (Módulo 08), uma lentidão downstream vira indisponibilidade de toda a cadeia. O padrão funciona bem em cadeias curtas (≤3 serviços); em cadeias longas, cada hop adiciona variância de latência e probabilidade de falha.
Não use quando: o callee tem latência variável alta (geração de relatório, transcodificação), quando múltiplos sistemas precisam ser notificados do mesmo evento, ou quando a disponibilidade do chamador não pode depender da disponibilidade do callee.
Síncrono + Event-Driven
Webhooks, Server-Sent Events, HTTP callbacks. O servidor notifica o cliente via chamada HTTP quando algo acontece. A entrega é síncrona (o servidor faz um HTTP POST para a URL registrada pelo cliente), mas o modelo é event-driven (o cliente não pergunta — o servidor avisa quando há algo novo). O cliente não precisa fazer polling; o servidor controla quando notificar.
Use quando: o cliente precisa ser notificado de eventos em tempo (quase) real mas não pode ou não quer manter uma conexão persistente. Webhooks são o padrão da indústria para integrações B2B: Stripe notifica seu sistema de "payment_succeeded", GitHub notifica seu CI de "push", Twilio notifica de "message_delivered". Cada um desses é um evento entregue via HTTP POST síncrono.
Falha característica: o servidor não sabe se o cliente processou com sucesso. O HTTP POST pode retornar 200 OK mas o cliente ter processado parcialmente. Por isso, webhooks bem implementados têm retry com backoff exponencial, idempotency keys para o receptor, e uma UI de "webhook deliveries" para debugging. O servidor precisa lidar com o client estando fora — filas de retry, dead-letter, e TTL de tentativas.
Não use quando: você controla ambos os lados da comunicação (use mensageria interna em vez de webhooks). Webhooks são para integrações externas onde o consumidor não tem acesso ao seu broker.
Assíncrono + Request-Response
Async request-reply via message queue com correlation ID e reply-to queue. O chamador publica um comando com metadados de retorno (correlationId, replyTo) numa fila de entrada. O callee processa e publica a resposta na fila de reply. O chamador, nesse tempo, pode fazer outras coisas — e eventualmente lê a resposta correlacionando pelo ID.
Use quando: a operação tem latência longa ou imprevisível e o chamador eventualmente precisa do resultado — mas pode fazer outras coisas enquanto espera. Geração de relatório pesado: o usuário submete o request, recebe um jobId, e consulta o status periodicamente (ou recebe webhook quando pronto). Também útil para desacoplar picos de carga: o caller não bloqueia threads esperando o callee processar.
Falha característica: complexidade de correlação. O chamador precisa gerenciar o estado pendente (qual correlationId corresponde a qual request), lidar com timeout (e se a resposta nunca chegar?), e tratar respostas fora de ordem ou duplicadas. Dead letter queues e TTL de mensagens são essenciais. O padrão é menos comum exatamente porque essa gestão de estado é difícil — em muitos casos, polling simples ou webhooks são mais práticos.
Assíncrono + Event-Driven
Message queues, Kafka, pub/sub. O modelo mais desacoplado. Publicação de fatos que múltiplos consumidores reagem independentemente. Máxima disponibilidade do produtor (não depende da disponibilidade dos consumers), eventual consistency, e maior complexidade operacional. O broker é a peça central — sua disponibilidade é o novo single point of failure, por isso brokers são projetados com clustering e replicação.
Use quando: múltiplos sistemas precisam reagir ao mesmo evento sem conhecimento mútuo, quando a disponibilidade do produtor não pode depender dos consumers, ou quando o volume de eventos é muito alto para chamadas síncronas (log de auditoria, analytics, stream processing). O modelo de dados natural de muitos domínios é event-driven: pedidos criados, pagamentos processados, usuários registrados são fatos, não solicitações.
Falha característica: estado intermediário e débito de consistência. Entre a publicação do evento e o processamento por todos os consumers, o sistema está em estado intermediário — "OrderCreated" publicado, mas estoque ainda não decrementado, notificação ainda não enviada, analytics ainda não atualizado. Esse intervalo pode ser milissegundos ou horas (se um consumer estava fora). O sistema precisa ser projetado para tolerar e raciocinar sobre esse estado intermediário. Idempotência (conceito 11) e outbox pattern (conceito 10) existem precisamente para dar garantias sobre esse terreno.
Trade-offs em profundidade
Latência
Comunicação síncrona tem latência imediata e determinística: quando a chamada retorna, o resultado está disponível. Em cadeias de serviços, latência se acumula linearmente — A→B→C tem latência ≥ lat(A→B) + lat(B→C) + processamento em cada etapa. Mas a acumulação de variância é o problema real: se cada hop tem P50=10ms e P99=50ms, uma cadeia de 5 serviços tem P99 composto muito acima de 250ms porque as caudas se acumulam de forma não-linear.
Comunicação assíncrona não é "mais rápida" — é uma transferência de quando a latência é sentida. O produtor retorna imediatamente (apenas espera o ack do broker), mas o resultado chega ao consumidor com latência eventual e não determinística. Para o usuário que enviou o request, a experiência é melhor (feedback imediato) mas o processamento pode levar segundos ou minutos. Isso é aceitável para operações de background, inaceitável para consultas de dados que o usuário precisa agora.
Disponibilidade composta
A matemática do acoplamento temporal foi apresentada acima. O ponto adicional: comunicação assíncrona desacopla a disponibilidade do produtor da disponibilidade dos consumers, mas não elimina dependências. O broker se torna a nova dependência crítica. Se o broker cai, tanto produtor quanto consumer são afetados — mas de forma diferente: o producer falha ao publicar (detectável imediatamente), enquanto o consumer para de processar (mensagens acumulam no broker). Brokers são projetados para altíssima disponibilidade com clustering e replicação, mas exigem operação especializada e atenção.
Uma consequência não óbvia: em sistemas event-driven, a disponibilidade do consumer não afeta a disponibilidade do producer — mas afeta o estado do sistema. Se o consumer de inventário fica fora por 2 horas, os pedidos continuam sendo criados (produtor funciona), mas o estoque não é decrementado. Quando o consumer volta, ele tem 2 horas de backlog para processar — potencialmente criando overselling ou inconsistências que precisam de reconciliação. Alta disponibilidade do produtor não garante consistência do sistema.
Observabilidade
Comunicação síncrona é linear: uma requisição tem início, meio e fim em uma cadeia de chamadas representável como árvore de spans de distributed tracing. A correlação é natural — o trace ID se propaga via header HTTP em cada hop. Um único trace no Jaeger mostra toda a jornada da requisição.
Comunicação assíncrona é fragmentada por natureza: produtor e consumer operam em processos, máquinas e momentos distintos. O trace ID precisa ser serializado como header da mensagem e deserializado pelo consumer para que a correlação funcione. Sem essa instrumentação explícita, cada etapa do fluxo é opaca — o debugging se torna caça de mensagens espalhadas entre logs de serviços distintos, sem linha do tempo clara. Com o W3C Trace Context propagado via headers de mensagem, é possível ter um trace que span produtor e consumer — com um gap de tempo visível entre o span de publicação e o span de processamento, que representa o tempo de fila.
Consistência e estado intermediário
Comunicação síncrona facilita consistência forte dentro de uma operação: se B retorna 200 OK, A sabe que B processou. Rollback em caso de erro é possível com base no retorno. A operação é atômica do ponto de vista do chamador — ou funcionou, ou falhou com erro claro.
Comunicação assíncrona é eventual por design, e "eventual" pode significar coisas muito diferentes. Em Kafka com um consumer rápido e sem lag, "eventual" é milissegundos. Com um consumer lento sob carga alta, pode ser minutos ou horas. O sistema não garante quando — apenas que eventualmente acontecerá (assumindo que o consumer está funcionando e sem dead-lettering). Projetar sistemas event-driven requer explicitar quais inconsistências são toleráveis, por quanto tempo, e qual o mecanismo de reconciliação quando o estado intermediário se estende além do esperado.
Acoplamento ao schema
Ambos os modelos têm acoplamento ao schema do contrato de dados, mas de formas distintas. Em REST/gRPC, o cliente e o servidor negociam versão explicitamente — header Accept, versão na URL, campo de versão no .proto. O contrato é bilateral e muitas vezes governado por uma API versioning policy clara.
Em sistemas event-driven, o producer publica um schema e todos os consumers presentes e futuros dependem dele. Mudanças de breaking no schema são muito mais arriscadas: você não tem uma lista completa de consumers para notificar e coordenar. Adicionar um campo obrigatório sem default quebra consumers que não o reconhecem. Renomear um campo quebra todos os consumers. Por isso, contratos de eventos exigem estratégias explícitas de evolução: campos novos são sempre opcionais com default, campos removidos passam por período de deprecação, e formatos como Avro e Protobuf têm modos de compatibilidade (backward, forward, full) que o Schema Registry pode enforçar automaticamente.
Schema coupling e evolução de contratos
A evolução do schema de eventos é o problema de longo prazo mais negligenciado em sistemas event-driven. No início, é fácil: o time é pequeno, todos conhecem todos os consumers, e uma mudança de schema é "só avisar todo mundo". Com 20 serviços consumindo o mesmo tópico Kafka, uma mudança de breaking em "OrderCreated" exige coordenar 20 deploys — ou aceitar que alguns consumers vão quebrar.
A solução padrão é um Schema Registry (Confluent Schema Registry é o mais usado com Kafka) combinado com um formato com suporte nativo a evolução (Avro, Protobuf, JSON Schema). O Schema Registry armazena versões de schema e enforça regras de compatibilidade:
- Backward compatibility: consumers com schema novo podem ler mensagens produzidas com schema antigo. O producer pode adicionar campos opcionais; o consumer ignora campos que não conhece. Esta é a compatibilidade mínima — permite atualizar consumers antes de producers.
- Forward compatibility: consumers com schema antigo podem ler mensagens produzidas com schema novo. Permite atualizar producers antes de consumers sem quebrar consumers existentes.
- Full compatibility: backward + forward. A mais restritiva — só permite adicionar campos opcionais com default e remover campos opcionais. Garante que qualquer combinação de versões de producer e consumer funciona.
# Avro schema — OrderCreated v1
{
"type": "record",
"name": "OrderCreated",
"namespace": "com.example.orders",
"fields": [
{"name": "order_id", "type": "string"},
{"name": "customer_id", "type": "string"},
{"name": "total_cents", "type": "long"},
{"name": "created_at", "type": "string"}
]
}
# OrderCreated v2 — backward compatible: novo campo opcional com default
{
"type": "record",
"name": "OrderCreated",
"namespace": "com.example.orders",
"fields": [
{"name": "order_id", "type": "string"},
{"name": "customer_id", "type": "string"},
{"name": "total_cents", "type": "long"},
{"name": "created_at", "type": "string"},
{"name": "channel", "type": "string", "default": "web"} # novo, com default
]
}
# OrderCreated v3 — BREAKING: renomear campo sem default quebra consumers v1/v2
# NÃO FAZER — use aliases ou crie nova versão do tópico
{
"fields": [
{"name": "order_uuid", ...} # renomeou order_id — quebra todos os consumers
]
}
Framework de decisão
Nenhuma regra funciona em todos os contextos, mas as perguntas abaixo eliminam a maioria das escolhas erradas antes de qualquer debate sobre tecnologia:
O chamador precisa da resposta para continuar o fluxo atual? Se sim, síncrono. O usuário clicou em "ver saldo" e precisa ver o número antes de qualquer outra coisa. Não há alternativa razoável.
A operação é uma notificação ou propagação de fato? Se sim, assíncrono event-driven. "O pedido foi criado" é um fato que estoque, notificação e analytics precisam saber. Nenhum deles precisa responder ao serviço de pedidos. Usar REST aqui cria dependências desnecessárias.
A disponibilidade do chamador pode depender da disponibilidade do callee? Se não pode, assíncrono. Se o serviço de notificação cair, o checkout não deve falhar. A notificação pode chegar com atraso — o pedido não pode deixar de ser criado porque o serviço de email está fora.
A operação tem latência longa ou volume variável de processamento? Se sim, assíncrono. Gerar relatório em PDF, transcodificar vídeo, enviar mil emails — nenhuma dessas operações pertence ao caminho síncrono de uma requisição HTTP com timeout de 30 segundos.
Múltiplos sistemas precisam reagir ao mesmo evento? Se sim, assíncrono event-driven. Fan-out síncrono (chamar 5 serviços via REST) multiplica latência e probabilidade de falha. Event-driven distribui o mesmo evento para N consumers sem custo adicional para o producer.
O contrato de dados pode evoluir independentemente entre producer e consumer? Se não pode coordenar deploys simultâneos, assíncrono com Schema Registry e regras de compatibilidade. Sync REST com versionamento explícito também funciona mas requer disciplina de versioning de API.
Anti-padrões frequentes
REST everywhere. Usar HTTP síncrono para tudo porque "é mais fácil de debugar". O preço emerge com o crescimento: cadeias longas com acoplamento temporal que diminuem disponibilidade composta, timeouts em cascata que transformam lentidão de um serviço em falha de toda a cadeia, e fan-out síncrono (A notifica B, C, D, E via REST sequencialmente) que multiplica latência e risco.
Async everywhere. Tornar tudo assíncrono como princípio de design. O resultado: o usuário clica "comprar" e recebe "seu pedido está sendo processado" — se o processamento falhar silenciosamente, ele não sabe, não tem feedback imediato, e a experiência é pior do que um erro síncrono bem formulado. Async sem necessidade específica é complexidade sem benefício.
Dual-write sem garantia transacional. O serviço persiste no banco E publica um evento em duas operações separadas. Se o banco persiste mas a publicação falha, o evento nunca é publicado. Se o banco falha após a publicação, o evento refere-se a um dado que não existe. A solução é o outbox pattern (conceito 10): escrever o evento na mesma transação do banco, e ter um processo separado publicar do outbox para o broker.
Fan-out síncrono sequencial. Ao processar uma ordem, chamar B (valida cliente), C (verifica estoque), D (reserva estoque), E (analytics), F (invoice) em sequência REST. Latência = soma de seis roundtrips. Qualquer callee falhando falha o pedido inteiro. A solução: fan-out paralelo para operações independentes (B, C e D em paralelo via Task.WhenAll), e async event-driven para operações que não bloqueiam o resultado (E e F via evento "OrderProcessed").
Confundir command com event. Publicar um "evento" que, se não for consumido, representa uma falha de negócio (pedido não processado, pagamento não executado). Isso é um command disfarçado de evento — deve ser tratado como command, com destinatário explícito, confirmação de processamento, e retry com dead-letter. Eventos genuínos são fatos imutáveis do passado; se nenhum consumer os lê, isso é aceitável.
Síncrono vs assíncrono em código
// SÍNCRONO: checkout chama stock-service via REST
// Acoplamento temporal explícito — stock-service fora = checkout falha
public async Task<OrderResult> CreateOrderSync(CreateOrderRequest req)
{
// timeout obrigatório — sem ele, lentidão de stock-service ocupa thread indefinidamente
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
var stockResponse = await _httpClient.PostAsJsonAsync(
"http://stock-service/api/reserve", req.Items, cts.Token);
stockResponse.EnsureSuccessStatusCode(); // 4xx/5xx = exception imediata
var order = new Order(req);
await _db.Orders.AddAsync(order);
await _db.SaveChangesAsync();
return new OrderResult(order.Id, "created");
}
// ASSÍNCRONO: checkout publica evento — consumers processam independentemente
// NotificationService, AnalyticsService, InvoiceService não acoplam o checkout
public async Task<OrderResult> CreateOrderAsync(CreateOrderRequest req)
{
var order = new Order(req);
await _db.Orders.AddAsync(order);
await _db.SaveChangesAsync();
// MassTransit publica para todos os consumers registrados em "OrderCreated"
// Retorna após ack do broker — não espera nenhum consumer processar
await _publishEndpoint.Publish(new OrderCreated(
OrderId: order.Id,
CustomerId: req.CustomerId,
Items: req.Items,
CreatedAt: DateTime.UtcNow));
return new OrderResult(order.Id, "created");
}
// Consumer independente — processa em seu próprio tempo e processo
public class NotificationConsumer : IConsumer<OrderCreated>
{
public async Task Consume(ConsumeContext<OrderCreated> ctx)
{
var msg = ctx.Message;
// Se NotificationService estava fora, MassTransit redelivera via retry policy
// O checkout nunca soube, nunca falhou
await _emailService.SendOrderConfirmation(msg.CustomerId, msg.OrderId);
}
}
O await em ambos os casos é async programming (não bloqueia a thread do OS), mas apenas o segundo é async communication: somente ele desacopla temporalmente o chamador do consumer. Se NotificationService ficar fora por 2 horas, os eventos acumulam no broker e são processados quando ele volta — sem impacto no checkout.
import httpx
import aio_pika
import json
from dataclasses import asdict
# SÍNCRONO: timeout obrigatório em toda chamada de rede entre serviços
# Sem timeout, lentidão de stock-service segura threads da thread pool
async def create_order_sync(req: CreateOrderRequest) -> OrderResult:
async with httpx.AsyncClient(timeout=5.0) as client:
try:
resp = await client.post(
"http://stock-service/api/reserve",
json={"items": req.items},
)
resp.raise_for_status() # 4xx/5xx = exceção imediata
except httpx.HTTPStatusError as e:
raise OrderError(f"stock-service: {e.response.status_code}") from e
except httpx.TimeoutException:
raise OrderError("stock-service timeout após 5s")
order = await Order.create(req)
return OrderResult(order_id=order.id, status="created")
# ASSÍNCRONO: publica evento — retorna após ack do broker
# Todos os consumers (notificação, analytics, invoice) são independentes
async def create_order_async(
req: CreateOrderRequest,
channel: aio_pika.abc.AbstractChannel,
) -> OrderResult:
order = await Order.create(req)
event = {
"order_id": str(order.id),
"customer_id": req.customer_id,
"items": req.items,
"created_at": order.created_at.isoformat(),
}
# delivery_mode=PERSISTENT: mensagem sobrevive a reinício do broker
await channel.default_exchange.publish(
aio_pika.Message(
body=json.dumps(event).encode(),
content_type="application/json",
delivery_mode=aio_pika.DeliveryMode.PERSISTENT,
headers={"traceparent": get_current_traceparent()}, # W3C Trace Context
),
routing_key="order.created",
)
return OrderResult(order_id=order.id, status="created")
# Consumer independente — processa com retry automático via aio_pika
async def on_order_created(message: aio_pika.IncomingMessage):
async with message.process(requeue=False): # NACK sem requeue = dead letter
event = json.loads(message.body)
await notification_service.send_confirmation(
event["customer_id"], event["order_id"]
)
Note o headers={"traceparent": ...} na publicação: sem propagar o W3C Trace Context via headers da mensagem, o trace do checkout e o trace do consumer ficam completamente desconectados no Jaeger — tornando debugging de fluxos assíncronos muito mais difícil.
package orders
import (
"context"
"encoding/json"
"fmt"
"time"
amqp "github.com/rabbitmq/amqp091-go"
"go.opentelemetry.io/otel/propagation"
)
// SÍNCRONO: timeout via context — goroutine liberada após o prazo
// Context cancelado por timeout propaga para o stock client automaticamente
func (s *Service) CreateOrderSync(
ctx context.Context, req CreateOrderRequest,
) (*OrderResult, error) {
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
_, err := s.stockClient.ReserveItems(ctx, req.Items)
if err != nil {
// fmt.Errorf com %w preserva o tipo original para errors.Is/As
return nil, fmt.Errorf("reserva de estoque: %w", err)
}
order, err := s.repo.Create(ctx, req)
if err != nil {
return nil, err
}
return &OrderResult{OrderID: order.ID, Status: "created"}, nil
}
// ASSÍNCRONO: publica evento — retorna após publisher confirm do broker
// A goroutine do checkout não espera nenhum consumer processar
func (s *Service) CreateOrderAsync(
ctx context.Context, req CreateOrderRequest,
) (*OrderResult, error) {
order, err := s.repo.Create(ctx, req)
if err != nil {
return nil, err
}
body, _ := json.Marshal(OrderCreatedEvent{
OrderID: order.ID,
CustomerID: req.CustomerID,
Items: req.Items,
CreatedAt: order.CreatedAt,
})
// Propagar W3C Trace Context via headers AMQP
headers := amqp.Table{}
propagation.TraceContext{}.Inject(ctx,
amqpHeaderCarrier(headers)) // carrier customizado para AMQP headers
err = s.ch.PublishWithContext(ctx,
"domain-events", "order.created",
true, false,
amqp.Publishing{
ContentType: "application/json",
DeliveryMode: amqp.Persistent,
Headers: headers,
Body: body,
},
)
if err != nil {
return nil, fmt.Errorf("publicar OrderCreated: %w", err)
}
return &OrderResult{OrderID: order.ID, Status: "created"}, nil
}
Em Go, a diferença entre sync e async é arquitetural, não sintática — ambos usam context e retornam (T, error). O que muda é a semântica: no sync, a goroutine espera o stock-service responder; no async, a goroutine espera apenas o publisher confirm do broker (milliseconds), e os consumers processam em goroutines completamente separadas.
Decisões de engenharia
Quando o modelo síncrono esconde um problema de design?
Quando A chama B apenas para notificar — não precisa da resposta para continuar. Esse é o sinal mais claro de que o modelo errado foi escolhido: se você usa REST mas ignora o body da resposta (ou verifica apenas o status 200), a operação deveria ser async event-driven. O segundo sinal: quando A chama B, C e D em sequência e qualquer falha desfaz toda a operação — isso é uma saga disfarçada de chamadas síncronas, e a complexidade de rollback aumenta com cada serviço adicionado.
Como decidir entre fila (queue) e tópico (topic/pub-sub)?
Fila (point-to-point): cada mensagem é processada por exatamente um consumer. Use para commands — tarefas de trabalho onde o processamento deve acontecer uma vez. Tópico (pub-sub): cada mensagem é entregue a todos os consumers inscritos. Use para eventos — fatos que múltiplos sistemas precisam saber. A distinção reflete a diferença entre command e event: um "ProcessPayment" é uma fila (um worker processa); um "PaymentProcessed" é um tópico (notificação, analytics e invoice todos recebem).
Como gerenciar versões de contratos em sistemas event-driven?
Schema Registry com Avro ou Protobuf é a solução padrão para sistemas com Kafka em produção. Para sistemas menores sem Schema Registry: adote o princípio de backward compatibility como regra — novos campos sempre opcionais com default, campos removidos nunca (apenas ignorados pelo producer). Se precisar de breaking change, crie um novo tópico com sufixo de versão (order.created.v2) e migre consumers gradualmente antes de desligar o tópico antigo.
O que fazer quando async cria débito de consistência inaceitável?
Primeiro, revisar se o débito é realmente inaceitável ou apenas incomum. "Estoque decrementado 500ms depois do pedido criado" é tolerável em quase todos os casos. "Confirmação de pagamento disponível 5 minutos depois" pode não ser. Se o débito for genuinamente intolerável, considere: (1) mover a operação específica para sync dentro de uma transação local; (2) usar saga com compensações explícitas; (3) aceitar eventual consistency com reconciliação periódica e alertas quando o lag excede um threshold.
Perguntas de entrevista
Qual a diferença entre comunicação síncrona e assíncrona? Por que async/await não torna um sistema assíncrono no sentido arquitetural?
Comunicação síncrona significa que o chamador aguarda a resposta antes de prosseguir, criando acoplamento temporal: ambos os sistemas precisam estar disponíveis simultaneamente. Comunicação assíncrona significa que o chamador publica uma mensagem e continua imediatamente — produtor e consumer operam com agendas independentes, mediados por um broker durável.
Async/await (C#, Python, JavaScript) é um mecanismo de multiplexação de I/O que evita bloquear threads do OS enquanto aguarda operações de rede. Mas do ponto de vista arquitetural, await httpClient.GetAsync(url) ainda espera que o serviço remoto responda antes de prosseguir. O acoplamento temporal persiste — se o serviço remoto está fora, o fluxo falha. A diferença é apenas que a thread do SO fica disponível para outro trabalho durante a espera, o que é uma otimização de throughput, não uma mudança de modelo de comunicação.
Como a disponibilidade composta se degrada em cadeias de chamadas síncronas? Como a comunicação assíncrona muda esse cálculo?
Em uma cadeia síncrona A→B→C→D, a disponibilidade composta é o produto das disponibilidades individuais: 0.999 × 0.999 × 0.999 × 0.999 ≈ 99.6%. Com 10 serviços de 99.9% cada: ≈ 99.0%. O SLA do sistema inteiro é inferior ao SLA de qualquer componente individual e degrada com cada serviço adicionado. Isso acontece porque qualquer componente indisponível no momento da chamada causa falha imediata para o chamador.
Comunicação assíncrona desacopla a disponibilidade do producer da disponibilidade dos consumers. Se o consumer está fora, a mensagem persiste no broker e é processada quando o consumer volta — o producer não falha. A disponibilidade que importa passa a ser a do broker, não dos consumers individuais. Brokers são projetados para 99.99%+ com clustering e replicação. O trade-off: o sistema fica em estado intermediário enquanto o consumer está fora, o que pode ser tolerável (notificação atrasada) ou não (estoque inconsistente).
O que é o problema do dual-write e como o outbox pattern o resolve?
Dual-write é o padrão de persistir no banco de dados E publicar um evento em duas operações separadas. O problema: se a persistência no banco succeed mas a publicação falha (broker temporariamente indisponível, rede particionada, processo morre entre as duas operações), o evento nunca é publicado — mas o dado existe no banco. Consumers nunca saberão que o pedido foi criado. O cenário inverso também é possível: o evento é publicado mas o banco falha, criando um evento que referencia dados inexistentes.
O outbox pattern resolve isso fazendo as duas operações dentro da mesma transação de banco. A tabela "outbox" recebe o evento a ser publicado atomicamente com a entidade. Um processo separado (outbox publisher) lê os eventos pendentes da tabela outbox e os publica no broker, marcando-os como publicados. Se o publisher falha após publicar mas antes de marcar, o evento é republicado — por isso consumers precisam ser idempotentes. O modelo troca consistência entre banco e broker por eventual consistency com garantia de at-least-once delivery.
Como você propagaria trace context em um sistema que mistura chamadas HTTP síncronas e mensagens assíncronas via Kafka?
O W3C Trace Context (RFC 9532) define os headers traceparent e tracestate para propagação. Em chamadas HTTP síncronas, o OTel SDK injeta e extrai esses headers automaticamente via auto-instrumentation. Em mensagens assíncronas via Kafka, a propagação precisa ser explícita: ao produzir, injetar o contexto atual como Kafka record headers; ao consumir, extrair os headers e criar um novo span com aquele context como parent.
No Jaeger, o resultado é um trace com dois spans separados no tempo: o span do producer (publicação da mensagem) e o span do consumer (processamento). O gap de tempo entre eles é visível e representa o tempo que a mensagem ficou no tópico Kafka. Sem essa propagação, cada metade do fluxo aparece como trace independente — impossível correlacionar o processamento de um evento com o request que o originou. O OTel Collector também pode ser configurado para enriquecer os headers de mensagem com atributos de resource automaticamente.
Qual a diferença entre command e event no design de sistemas assíncronos? Por que a distinção importa operacionalmente?
Um command é um pedido de ação direcionado a um destinatário específico — implica autoridade, tem um único consumer esperado, e sua falha de processamento é uma falha de negócio que precisa ser tratada. Um event é uma declaração de fato no passado — não tem destinatário específico, pode ter zero ou N consumers, e se ninguém o consome, isso é aceitável (o fato aconteceu, não é responsabilidade do producer garantir que alguém reagiu).
Operacionalmente: commands precisam de confirmação de processamento, retry com dead-letter para falhas, e geralmente têm um consumer garantido. Events podem ter múltiplos consumers com lógica completamente diferente, e adicionar um novo consumer não requer nenhuma mudança no producer. O erro comum é publicar um "event" que representa um command — por exemplo, "PaymentRequested" que, se não consumido, significa que o pagamento não foi executado. Isso é um command disfarçado, e deve ser tratado com garantias de entrega mais fortes e monitoramento de consumer lag.
Como praticar
- Audite e classifique sua aplicação atual. Liste cada chamada inter-serviço (HTTP, gRPC, query a banco externo, publicação de mensagem). Classifique cada uma nos dois eixos: síncrono/assíncrono e request-response/event-driven. Para cada chamada síncrona, responda: (a) o chamador usa a resposta para continuar o fluxo? (b) a disponibilidade do chamador pode depender da disponibilidade do callee? (c) há timeout configurado? Documente os resultados — a maioria dos times descobre que 30–40% das chamadas síncronas são candidatas a async.
- Calcule disponibilidade composta das cadeias críticas. Para cada fluxo que toca mais de dois serviços via chamadas síncronas, calcule a disponibilidade composta usando os SLOs individuais (ou disponibilidade histórica dos últimos 90 dias). Se o resultado ficar abaixo do SLA do produto para aquele fluxo, identifique quais elos são candidatos a se tornar assíncronos sem comprometer a experiência do usuário. Apresente o trade-off quantitativo: "converter este hop para async aumenta disponibilidade composta de X% para Y%, mas introduz latência eventual de até Z ms".
- Converta uma notificação síncrona para async e meça o impacto. Escolha uma operação onde o chamador notifica outro serviço via REST mas não usa a resposta (ou usa apenas para confirmar recebimento). Redesenhe como publicação de evento com RabbitMQ ou Redis Streams local. Implemente: (a) o producer com outbox pattern para garantia de entrega; (b) o consumer com idempotência (processa a mesma mensagem duas vezes sem efeito colateral); (c) propagação de W3C Trace Context via headers da mensagem. Meça: latência do fluxo antes e depois, e simule o consumer fora por 5 minutos — verifique que o producer continua funcionando.
- Implemente e teste evolução de schema. Crie um evento com schema v1 (3 campos obrigatórios). Suba um consumer v1. Evolua para v2 adicionando um campo opcional com default. Verifique que o consumer v1 ainda processa mensagens v2 sem quebrar (backward compatibility). Em seguida, tente uma mudança breaking (renomear um campo) — observe a falha. Documente as regras de compatibilidade que o time vai adotar como política para todos os eventos do sistema.
- Demonstre o problema do dual-write. Implemente um serviço que salva no banco e publica no broker em duas operações separadas. Simule falha entre as duas operações (kill do processo com SIGKILL após o banco mas antes da publicação). Verifique que o evento nunca chegou ao consumer. Implemente o outbox pattern e repita — verifique que o evento é publicado eventualmente mesmo com falhas entre as operações.
Referências para aprofundar
- livro Building Microservices — Sam Newman (O'Reilly, 2021 — 2ª ed.).
- artigo What do you mean by "Event-Driven"? — Martin Fowler (2017).
- livro Enterprise Integration Patterns — Gregor Hohpe & Bobby Woolf (Addison-Wesley, 2003).
- artigo The Log: What every software engineer should know about real-time data's unifying abstraction — Jay Kreps (2013).
- docs Microservices.io — Patterns — Chris Richardson (2024).
- livro Designing Data-Intensive Applications — Martin Kleppmann (O'Reilly, 2017).
- livro Microservices Patterns — Chris Richardson (Manning, 2018).
- vídeo The Many Meanings of Event-Driven Architecture — Martin Fowler (GOTO 2017).
- docs The Reactive Manifesto — Bonér, Farley, Kuhn, Thompson (2014).
- artigo Fallacies of Distributed Computing — L. Peter Deutsch et al. (Sun Microsystems, 1994).
- artigo Your Coffee Shop Doesn't Use Two-Phase Commit — Gregor Hohpe (IEEE Software, 2005).
- livro Designing Distributed Systems — Brendan Burns (O'Reilly, 2018).