Lab 32 — Síncrono ou assíncrono entre serviços
O problema, e a empresa que o tem
A Cadência, do L31, continuou crescendo: 60 lojas, cerca de 500 pedidos por dia, com picos de campanha que chegam a 4.200 pedidos em três horas. Animada com o resultado de extrair Notificações pela fronteira transacional certa, a equipe extraiu mais dois serviços do mesmo monólito — Catálogo (preço e disponibilidade do item) e Pagamento (autorização de cobrança) — usando o mesmo teste.
O problema não está em ONDE cortaram. Está em COMO ligaram os cortes de volta: cada chamada de método do monólito virou uma chamada HTTP, na mesma ordem, esperando a resposta antes de seguir. Pedidos chama Catálogo; Catálogo, antes de responder, chama Pagamento; Pagamento, antes de responder, chama Notificações. Uma corrente de quatro elos síncronos, onde antes havia quatro chamadas de método no mesmo processo.
O incidente que trouxe o time até aqui: durante uma campanha, o provedor de e-mail transacional (usado por Notificações) teve uma degradação de 40 segundos. Nenhum pedido devia depender disso — mas em 40 segundos, 100% das criações de pedido falharam. O time abriu um incidente de "Pagamento fora do ar" e gastou uma hora olhando métricas de Pagamento, que estava, o tempo todo, respondendo normalmente.
O que este laboratório NÃO é
Não é sobre reduzir o número de serviços, nem sobre desfazer o L31. Os quatro serviços continuam existindo; o que muda é COMO eles conversam entre si. Também não é sobre circuit breaker — mitigar o efeito de um serviço fora do ar sem mudar a topologia é o L36, e ele pressupõe o desenho síncrono/assíncrono correto que este módulo produz.
O que você vai conseguir fazer
Objetivos verificáveis: cada um se prova com uma medição na seção de implantação, não com a sensação de ter entendido.
- Calcular a disponibilidade composta de uma cadeia síncrona como produto, não média, e explicar por que a intuição de "média" está errada.
- Decidir, por chamada e não por sistema, se uma integração deve ser síncrona ou assíncrona, a partir de quem precisa da resposta.
- Configurar timeouts que crescem da ponta da cadeia para a raiz, e explicar o que quebra quando a ordem se inverte.
- Medir, com um serviço do meio derrubado de propósito, quanto de indisponibilidade alheia um serviço saudável reporta como se fosse sua.
- Redesenhar uma corrente de chamadas aninhadas em chamadas irmãs mais um evento assíncrono, e recalcular a disponibilidade resultante.
- Implementar um consumidor assíncrono idempotente diante de reentrega do SQS.
- Configurar uma fila de mensagens não entregues (DLQ) com política de redirecionamento.
- Diagnosticar, pelo sintoma, se uma falha em produção vem de acoplamento temporal ou de indisponibilidade real do serviço apontado.
O que a certificação cobra disto
| Conceito | Certificação | Como aparece aqui | O que dominar |
|---|---|---|---|
| Disponibilidade composta | SAA-C03, SAP-C02 | produto das disponibilidades de uma cadeia síncrona | por que quatro serviços a 99,9% não entregam 99,9% juntos |
| Acoplamento temporal | DVA-C02, SAP-C02 | o chamador fica bloqueado esperando o chamado | reconhecer o sintoma: serviço saudável falhando por dependência |
| Desacoplamento com filas e eventos | SAA-C03, DVA-C02 | EventBridge publica, SQS amortece, o consumidor lê no próprio ritmo | quando um passo do fluxo pode sair do caminho de resposta ao cliente |
| Timeout em cadeia de chamadas | DVA-C02, SAP-C02 | cada elo promete um teto maior que o do vizinho que chama | por que o timeout do topo tem de ser maior que o da base, não igual |
| Fila de mensagens não entregues (DLQ) | DVA-C02, SOA-C02 | redrive policy com maxReceiveCount | a diferença entre mensagem perdida e mensagem isolada para investigação |
| Idempotência em consumidor assíncrono | DVA-C02 | escrita condicional antes de processar | por que SQS padrão entrega pelo menos uma vez, nunca exatamente uma |
| Amazon ECS Service Connect | SOA-C02, DVA-C02 | descoberta de serviço com timeout configurável por hop | a diferença entre o timeout do Service Connect e o do cliente HTTP da aplicação |
| Padrão fire-and-forget vs pergunta-resposta | SAA-C03 | PutEvents não bloqueia por confirmação de processamento | que aceitar o evento é diferente de garantir que ele foi tratado |
Onde isto costuma ser cobrado errado
A pergunta clássica dá quatro serviços a 99,9% de disponibilidade e pergunta a disponibilidade do sistema todo, esperando que o candidato responda 99,9% por "todos têm a mesma". A resposta certa multiplica: 0,999⁴ ≈ 99,60%. Errar aqui não é detalhe de casa decimal — é a diferença entre um SLA que se sustenta e um que não.
Requisitos, e como cada um muda o desenho
Requisito não funcional que não aparece numa linha de configuração é intenção. A coluna da direita é onde cada um deixou marca.
| Requisito | Valor declarado | O que ele decide no desenho |
|---|---|---|
| Cliente sabe se o item existe e o preço, na resposta HTTP | obrigatório | Catálogo permanece síncrono, chamado direto por Pedidos |
| Cliente sabe se a cobrança foi autorizada, na resposta HTTP | obrigatório | Pagamento permanece síncrono, chamado direto por Pedidos |
| Confirmação por e-mail pode chegar minutos depois | herdado do L31 | Notificações sai do caminho de resposta: publicação de evento, não chamada direta |
| Nenhum pedido perdido, mesmo com Notificações fora do ar | zero perda | fila durável (SQS) com retenção de 14 dias entre o evento e o consumo |
| Disponibilidade de criação de pedido | ≥ 99,65% | descarta a corrente de 4 elos (99,60% prevista); exige achatar para 3 elos síncronos irmãos |
| Latência de criação de pedido | p99 abaixo de 4 s | elimina o aninhamento: dois chamados em série sem um terceiro escondido dentro deles |
| Nenhuma mensagem reentregue gera e-mail duplicado | obrigatório | escrita condicional de idempotência antes de qualquer envio |
| Sem infraestrutura nova de peso (servidor dedicado) | restrição de orçamento | EventBridge e SQS são pay-per-use; nenhum broker autogerenciado |
Arquitetura mínima: a corrente de quatro elos
Este é o desenho que a Cadência tem hoje, resultado direto de trocar chamada de método por chamada HTTP sem repensar a ordem. Ele é implantável e funciona no caminho feliz — o defeito só aparece sob medição, e é exatamente essa medição que abre este laboratório.
- → POST /pedidos, aguarda a resposta HTTP
- → encaminha para o alvo saudável
- → GET /produtos/{id}, bloqueia até responder
- → POST /autorizacoes, bloqueia até responder
- → POST /confirmacoes, bloqueia até responder
- Fora da AWS
- Rede e entrega
- Compute
Nenhuma configuração individual está errada — cada serviço, isolado, atinge 99,9%. O defeito é estrutural: encadear quatro chamadas síncronas multiplica as disponibilidades em vez de escolher a pior. Percorra os passos e repare que matar SÓ o Pagamento derruba a resposta de Pedidos e de Catálogo também — sem que nenhum dos dois tenha uma linha de código errada.
- A resposta ao cliente fica pendurada na corrente inteira. O cliente fez UMA chamada, mas a latência que ele sente é a soma dos quatro saltos, e o sucesso que ele recebe depende dos quatro terem funcionado. Nada nisso aparece no contrato que o app consome — só no tempo de resposta e na taxa de erro, que ninguém decompõe até investigar.
- Pedidos não paraleliza: chama Catálogo e espera parado. A chamada é uma requisição HTTP comum, com um `HttpClient` esperando a resposta antes de seguir. Não há nada de errado NELA isoladamente — o problema é o que vem depois dela.
- Catálogo aciona Pagamento antes de responder a quem o chamou. Catálogo não devolve nada a Pedidos enquanto não tem a resposta de Pagamento. Isso é aninhamento: Pedidos depende de Catálogo, que depende de Pagamento — três elos em série, não em paralelo.
- Pagamento aciona Notificações antes de confirmar a si mesmo. O quarto elo. Notificações só precisa enviar um e-mail — mas como a chamada é síncrona, uma lentidão do provedor de e-mail atrasa a confirmação do PAGAMENTO, que é o passo que menos pode esperar de todos.
- A conta que ninguém faz: 0,999⁴, não a média de 0,999. Com os quatro serviços a 99,9% de disponibilidade cada, a probabilidade de os QUATRO responderem dentro do prazo, na mesma requisição, é o produto: 0,999 × 0,999 × 0,999 × 0,999 ≈ 99,60%. Não é uma média (que ficaria em 99,9%) — é uma multiplicação de probabilidades independentes, e cada elo a mais na corrente piora o resultado.
- Mate o serviço do meio, e os extremos relatam uma falha que não é deles. Derrube só o Pagamento. O health check de Catálogo continua verde — ele está de pé, funcionando, escutando requisição. Mesmo assim, toda chamada que chega nele falha, porque ele depende de um vizinho que não responde. Do lado de fora, Pedidos e Catálogo parecem culpados; a causa real está um elo à frente de onde o painel está olhando.
- Por que a equipe encadeou assim. Porque era exatamente essa a ordem das chamadas de método dentro do monólito antes do L31, e trocar `catalogo.Autorizar(pedido)` por um `HttpClient.PostAsync` preserva o comportamento observável em teste local. A extração de serviço (L31) resolveu ONDE cortar; ninguém parou para perguntar se a chamada deveria continuar bloqueante depois do corte.
Antes de mudar qualquer coisa, calcule a previsão e depois meça. Assumindo os quatro serviços a 99,9% de disponibilidade individual — meta comum de SLO interno, não um valor raro — a previsão para a cadeia inteira é:
0,999 × 0,999 × 0,999 × 0,999 ≈ 0,996006 → 99,6006%
Indisponibilidade de um único serviço: 0,1000% do tempo
Indisponibilidade da cadeia de quatro: 0,3994% do tempo
(quase 4× mais)
Em minutos por mês (30 dias = 43.200 min):
um serviço só: ~43 min de indisponibilidade
cadeia de quatro: ~173 min de indisponibilidadeA conta que a maioria erra ao ler "99,9% de disponibilidade" em quatro serviços
A intuição comum é "cada peça é boa, então o sistema é bom" — e trata 99,9% como se fosse uma propriedade do SISTEMA, não de cada peça isolada. A disponibilidade de uma cadeia síncrona é o PRODUTO das disponibilidades dos elos que ela atravessa, porque a requisição só é bem-sucedida se TODOS os elos responderem dentro do prazo. Cada elo a mais multiplica por outro número menor que 1 — e o efeito piora rápido: com oito elos a 99,9%, a disponibilidade cai para cerca de 99,2%, oito vezes pior que um único serviço.
Arquitetura para produção: dois irmãos, um evento
Cada peça deste desenho rastreia a uma linha da tabela de requisitos. Se você não conseguir apontar o requisito, a peça é adorno — e este desenho não tem nenhuma.
- → HTTPS 443, aguarda só a parte síncrona
- → encaminha para o alvo saudável
- → GET /produtos/{id}, chamada irmã 1
- → POST /autorizacoes, chamada irmã 2
- → PutEvents PedidoCriado, dispara e segue
- → regra de roteamento entrega na fila
- → poll assíncrono, no próprio ritmo
- → redrive após esgotar as tentativas
- → profundidade > 0 dispara alarme
- Fora da AWS
- Rede e entrega
- Compute
- Integração de apps
- Gestão e governança
A pergunta deixou de ser "síncrono ou assíncrono" para o sistema inteiro e passou a ser "o chamador precisa da resposta?" — feita uma vez por chamada. Catálogo e Pagamento continuam síncronos porque o cliente precisa saber o resultado; Notificações sai da cadeia porque nada no caminho de resposta depende dela. O aninhamento também sai: Pedidos chama os dois primeiros como irmãos, não como corrente.
- O caminho síncrono tem dois saltos, não três aninhados. Pedidos chama Catálogo e Pagamento — os dois voltam a ser irmãos, respondendo direto a quem os chamou, e nenhum dos dois conhece o outro. A profundidade da árvore de chamada caiu de três para um.
- Os dois que ficaram síncronos ficaram porque o cliente precisa da resposta. Catálogo confirma que o item existe e qual é o preço — sem isso não há pedido para criar. Pagamento confirma que a cobrança passou — sem isso o cliente não sabe se deve considerar a compra feita. As duas respostas moldam o que o app mostra na tela seguinte; por isso continuam bloqueantes.
- A confirmação ao cliente não espera a notificação. Pedidos publica um evento e NÃO aguarda confirmação de entrega — é fire-and-forget do ponto de vista do fluxo de resposta. O `PutEvents` em si é síncrono (Pedidos espera a AWS confirmar que aceitou o evento), mas isso leva milissegundos e não depende de nenhum sistema de e-mail.
- EventBridge desacopla quem publica de quem consome. Pedidos não sabe, e não precisa saber, que existe uma fila ou um serviço de notificação do outro lado. A regra de roteamento é o único lugar que conhece os dois lados — trocar o consumidor não exige tocar em Pedidos.
- Notificações consome no próprio ritmo, e pode cair sem derrubar o pedido. Se Notificações ficar fora do ar, a fila retém a mensagem — ela não se perde, só espera. A criação de pedido, que já respondeu ao cliente antes deste ponto, não é afetada. É a diferença estrutural entre este desenho e o anterior.
- Mensagem que falha repetidamente não trava a fila. Depois de esgotar o número configurado de tentativas, a política de redrive move a mensagem para a DLQ em vez de deixá-la ser reentregue para sempre — o que travaria o processamento das mensagens boas atrás dela.
- A disponibilidade recalculada: três fatores, não quatro. A criação de pedido agora depende de Pedidos, Catálogo e Pagamento — três serviços, não quatro: 0,999³ ≈ 99,70%, contra 99,60% da corrente original. A disponibilidade de Notificações deixou de aparecer nessa conta inteiramente; ela virou uma métrica própria (idade da fila), não um fator do pedido.
A diferença estrutural em relação ao desenho anterior não é "trocar uma chamada por fila". São duas mudanças independentes, e é importante separá-las: (1) tirar Notificações do caminho síncrono, porque nada depende da resposta dela; e (2) achatar o aninhamento entre Catálogo e Pagamento, que continuam síncronos mas deixam de depender um do outro. A segunda mudança sozinha já reduz a corrente de 4 para 3 elos — e a primeira reduz de 3 para 2 o que o cliente efetivamente espera.
Caminho síncrono (Pedidos, Catálogo, Pagamento):
0,999 × 0,999 × 0,999 ≈ 0,997003 → 99,7003%
indisponibilidade: 0,2997% do tempo (~129 min/mês)
Contra a corrente original de 4 elos (99,6006%, ~173 min/mês):
ganho de ~44 minutos de disponibilidade por mês
SEM adicionar redundância nova — só removendo um fator da multiplicação
Notificações: fora da conta de disponibilidade do pedido. Sua própria
disponibilidade agora é medida como atraso de fila, não como causa de erro 5xx.O ganho real não é só disponibilidade — é remover uma classe de incidente
O incidente que abriu este módulo (e-mail lento derrubando pedido) deixa de ser POSSÍVEL neste desenho, não só menos provável. Notificações pode ficar fora do ar por minutos, ter uma dependência externa degradada, ou processar com atraso, e nenhum desses cenários aparece como erro para o cliente que está criando um pedido.
Como funciona ponta a ponta, e onde cada relógio corre
Os números de timeout não são arbitrários — eles seguem uma regra única: o timeout de quem CHAMA tem de ser maior que o timeout que o CHAMADO promete. Se A espera até 30 s por B, e B espera até 30 s por C, o timeout de A tem de ser MAIOR que o de B — senão A desiste no instante em que B ainda poderia estar recebendo a resposta de C, e C continua trabalhando para um chamador que já foi embora.
O que acontece quando a ordem se inverte
Se o timeout de Pedidos ao chamar Catálogo fosse MENOR que o que Catálogo promete (por exemplo, 10 s contra os 13 s prometidos), Pedidos devolveria erro ao cliente enquanto Catálogo ainda está processando — e, pior, ainda esperando a resposta de Pagamento. O trabalho de Pagamento não é cancelado: ele termina, cobra o cliente ou não, e a resposta cai no vazio. É assim que timeout mal ordenado produz cobrança sem pedido confirmado, não só lentidão.
O evento que Pedidos publica no caminho assíncrono é o contrato entre quem cria o pedido e quem consome a notificação. Ele carrega o suficiente para o consumidor agir sem precisar perguntar de volta a nenhum dos outros serviços:
Publicado por Pedidos no EventBridge; roteado por regra para a fila de notificacao. O consumidor NAO volta a chamar Catalogo ou Pagamento para completar a informacao — o evento e autossuficiente.
{
"source": "cadencia.pedidos",
"detail-type": "PedidoCriado",
"detail": {
"pedidoId": "b3f1c2a4-9e21-4a3d-8f2a-1234567890ab",
"clienteId": "c-9",
"email": "cliente@exemplo.com",
"preco": 129.90,
"autorizacaoId": "auth-77213"
}
}As decisões, e o que se perde em cada uma
📋 A Cadência tem 60 lojas, cerca de 500 pedidos por dia, e um pico de campanha que chega a 4.200 pedidos em 3 horas. O time quer decompor os quatro serviços da corrente sem contratar ninguém e sem adicionar infraestrutura cara.
Catálogo e Pagamento respondem "sim": o app não tem o que mostrar ao cliente sem saber se o item existe e se a cobrança passou. Notificações responde "não": nada no caminho de resposta ao cliente depende do e-mail ter sido enviado. Tratar as três chamadas do mesmo jeito — todas síncronas, como estava, ou todas assíncronas, que é o exagero oposto — ignora que elas têm uma resposta diferente para a mesma pergunta. A pergunta feita por chamada é o que faz o desenho ser correto em vez de dogmático.
Alt: Tudo síncrono (o desenho atual) — É este módulo inteiro: disponibilidade composta ruim, timeout aninhado frágil, e um serviço saudável relatando falha alheia. Continua sendo a escolha certa quando a cadeia tem só um ou dois elos — o problema cresce com o número de saltos, não existe em cadeia de um.
Alt: Tudo assíncrono, inclusive Pagamento — Resolveria a disponibilidade composta às custas de quebrar o requisito real: o cliente teria que sair da tela sem saber se a cobrança foi aceita, e descobrir depois — por notificação push ou reconsulta. É a arquitetura certa para pagamento assíncrono por design (boleto, Pix com confirmação tardia), não para cartão com autorização em tempo real.
Alt: Orquestração com Step Functions — Centraliza a lógica de "quem chama quem" fora do código de Pedidos, com retry e tratamento de erro configuráveis por estado — genuinamente melhor para fluxos com mais de três ou quatro passos e ramificação condicional. Para dois chamados irmãos e um evento, é mais infraestrutura do que a corrente atual justifica.
Alt: Manter a corrente e só adicionar circuit breaker — Reduz o dano de um serviço fora do ar (para de bater na porta de quem já está caído), mas não muda a disponibilidade composta do caminho feliz nem remove o aninhamento de timeout. É mitigação, não redesenho — e é exatamente o assunto do L36, que meça este módulo como pré-requisito.
| Decisão | Escolha | Alternativas | Motivo | O que se perde |
|---|---|---|---|---|
| Catálogo: síncrono ou assíncrono | síncrono, chamado direto por Pedidos | assíncrono com resposta via WebSocket/polling | o cliente precisa do preço confirmado antes de a tela seguinte existir | nenhum — é o caso claro de "o chamador precisa da resposta" |
| Pagamento: síncrono ou assíncrono | síncrono, chamado direto por Pedidos | assíncrono com confirmação por notificação push | requisito de negócio: cliente sai da tela sabendo se foi cobrado | nenhum aqui; seria diferente com boleto ou Pix com confirmação tardia |
| Notificações: síncrono ou assíncrono | assíncrono, por evento e fila | síncrono (o desenho original) | nada no caminho de resposta depende do envio do e-mail | o cliente não sabe, em tempo real, se o e-mail já foi enviado — e não precisa saber |
| Aninhamento entre Catálogo e Pagamento | removido: Pedidos chama os dois | manter Catálogo chamando Pagamento internamente | reduz a corrente síncrona de 3 para 2 elos efetivos e remove um nível de timeout aninhado | Pedidos passa a orquestrar duas chamadas em vez de uma; mais lógica no chamador raiz |
| Mecanismo de desacoplamento | EventBridge → SQS | SNS → SQS; SQS direto sem EventBridge; Step Functions | EventBridge já roteia por padrão de evento sem acoplar Pedidos ao nome da fila | uma peça a mais na cadeia de depuração quando um evento não chega ao consumidor |
| Idempotência do consumidor | escrita condicional no DynamoDB antes de processar | fila FIFO com deduplicação nativa; verificação no banco de pedidos | funciona com fila padrão (mais barata) e não acopla o consumidor ao schema de Pedidos | uma tabela e uma chamada extra por mensagem |
A dívida que este desenho cria, e que este módulo não paga
Se Pedidos cair depois de autorizar o pagamento mas antes de publicar o evento, o cliente foi cobrado e nenhuma notificação jamais existirá — o evento nunca foi para a fila. Isto é uma falha de consistência entre dois sistemas que não compartilham transação, e a correção correta é um padrão de outbox transacional, que fica fora do escopo deste laboratório. Por ora, um job de reconciliação que compara autorizações sem evento correspondente é a mitigação mínima.
Construir: o caminho síncrono, com timeout que cresce para fora
O delta em relação ao L03 e ao L31 é pequeno em linhas e grande em consequência: cada serviço declara, no Service Connect, o teto que promete a quem o chama — e o cliente HTTP interno de Pedidos usa exatamente esses tetos, sempre com uma margem maior, nunca igual.
# service-connect.tf — o timeout de cada salto, explícito, e crescendo para fora
#
# Amazon ECS Service Connect usa Cloud Map por baixo, mas o que importa aqui e o
# TIMEOUT: cada servico declara o teto que promete a quem o chama. Sem isto
# configurado, o padrao do Service Connect e generoso demais para esta cadeia
# (idleTimeout de 5 min em HTTP) — ele nao quebra nada sozinho, mas tambem nao
# implementa a regra dos timeouts decrescentes: e voce quem tem de configura-la.
resource "aws_service_discovery_http_namespace" "cadencia" {
name = "cadencia.local"
}
resource "aws_ecs_service" "notificacoes" {
name = "cadencia-notificacoes"
cluster = aws_ecs_cluster.principal.id
task_definition = aws_ecs_task_definition.notificacoes.arn
desired_count = 2
launch_type = "FARGATE"
service_connect_configuration {
enabled = true
namespace = aws_service_discovery_http_namespace.cadencia.arn
service {
port_name = "http"
discovery_name = "notificacoes"
client_alias {
port = 8080
dns_name = "notificacoes"
}
# O elo mais interno da corrente original: 3 s e desiste. Nao ha ninguem
# depois dele para esperar, entao este e o UNICO valor que nao precisa
# ser maior que o de um vizinho.
timeout {
per_request_timeout_seconds = 3
}
}
}
network_configuration {
subnets = aws_subnet.privada[*].id
security_groups = [aws_security_group.task.id]
}
}
resource "aws_ecs_service" "pagamento" {
name = "cadencia-pagamento"
cluster = aws_ecs_cluster.principal.id
task_definition = aws_ecs_task_definition.pagamento.arn
desired_count = 2
launch_type = "FARGATE"
service_connect_configuration {
enabled = true
namespace = aws_service_discovery_http_namespace.cadencia.arn
service {
port_name = "http"
discovery_name = "pagamento"
client_alias {
port = 8080
dns_name = "pagamento"
}
# Promete 8 s a quem o chama: cobre o proprio processamento mais os ate
# 5 s que o cliente HTTP interno espera de Notificacoes (configurado no
# C#, nao aqui — Service Connect nao sabe quem o SEU servico chama).
timeout {
per_request_timeout_seconds = 8
}
}
}
network_configuration {
subnets = aws_subnet.privada[*].id
security_groups = [aws_security_group.task.id]
}
}
resource "aws_ecs_service" "catalogo" {
name = "cadencia-catalogo"
cluster = aws_ecs_cluster.principal.id
task_definition = aws_ecs_task_definition.catalogo.arn
desired_count = 2
launch_type = "FARGATE"
service_connect_configuration {
enabled = true
namespace = aws_service_discovery_http_namespace.cadencia.arn
service {
port_name = "http"
discovery_name = "catalogo"
client_alias {
port = 8080
dns_name = "catalogo"
}
# 13 s: cobre o proprio trabalho mais os ate 10 s que o cliente HTTP
# interno espera de Pagamento.
timeout {
per_request_timeout_seconds = 13
}
}
}
network_configuration {
subnets = aws_subnet.privada[*].id
security_groups = [aws_security_group.task.id]
}
}
# Pedidos NAO precisa de service_connect_configuration para SER chamado — ele so
# CHAMA os outros tres. O timeout que ele pratica ao chamar Catalogo (16 s) e
# Pagamento (8 s, mesmo valor que Pagamento promete) vive no HttpClient do C#,
# porque e ali que a decisao de "quanto esperar" realmente atua.
resource "aws_ecs_service" "pedidos" {
name = "cadencia-pedidos"
cluster = aws_ecs_cluster.principal.id
task_definition = aws_ecs_task_definition.pedidos.arn
desired_count = 2
launch_type = "FARGATE"
service_connect_configuration {
enabled = true
namespace = aws_service_discovery_http_namespace.cadencia.arn
}
load_balancer {
target_group_arn = aws_lb_target_group.pedidos.arn
container_name = "pedidos"
container_port = 8080
}
network_configuration {
subnets = aws_subnet.privada[*].id
security_groups = [aws_security_group.task.id]
}
}
Service Connect não substitui o timeout do cliente HTTP — ele é outra camada
O `per_request_timeout_seconds` do Service Connect é o teto que o PROXY do sidecar aplica; o `HttpClient.Timeout` do C# é o teto que a SUA aplicação aplica. Eles deveriam ser coerentes, mas não são a mesma configuração — setar um sem o outro deixa metade da promessa sem efeito, porque o proxy corta a chamada num tempo e o código da aplicação continua esperando até o dele.
Do lado da aplicação, é aqui que a regra dos timeouts decrescentes vira código: cada `HttpClient` recebe o timeout do VIZINHO que ele chama, com margem, não um valor único copiado em todo lugar.
// PedidosService.cs — dois chamados irmãos, não uma corrente; e o timeout
// prometido por cada vizinho vira o timeout que este serviço PRATICA.
public sealed class PedidosService
{
private readonly HttpClient _catalogo;
private readonly HttpClient _pagamento;
private readonly AmazonEventBridgeClient _eventos;
private readonly ILogger<PedidosService> _log;
public PedidosService(
IHttpClientFactory factory, AmazonEventBridgeClient eventos, ILogger<PedidosService> log)
{
// Cada HttpClient tem o timeout do VIZINHO que ele chama, nao um valor
// generico repetido tres vezes. E a regra dos timeouts decrescentes
// escrita em codigo: se Catalogo promete 13 s, este client espera 16 s
// — sempre MAIOR, nunca igual, para nao desistir no instante em que a
// resposta esta a caminho.
_catalogo = factory.CreateClient("catalogo");
_catalogo.Timeout = TimeSpan.FromSeconds(16);
_catalogo.BaseAddress = new Uri("http://catalogo:8080");
_pagamento = factory.CreateClient("pagamento");
_pagamento.Timeout = TimeSpan.FromSeconds(9); // Pagamento promete 8 s; 9 s de margem
_pagamento.BaseAddress = new Uri("http://pagamento:8080");
_eventos = eventos;
_log = log;
}
public async Task<ResultadoPedido> CriarAsync(NovoPedido novo, CancellationToken ct)
{
// 1) Catalogo primeiro: sem item valido e preco confirmado nao ha o que
// cobrar. Isto e sequencial por NECESSIDADE de dado (o preco entra na
// chamada de Pagamento), nao por aninhamento de servico — a diferenca
// e que Catalogo nunca soube que Pagamento existe.
var item = await _catalogo.GetFromJsonAsync<ItemCatalogo>(
$"/produtos/{novo.ProdutoId}", ct)
?? throw new ItemIndisponivelException(novo.ProdutoId);
// 2) Pagamento em seguida, com o preco que Catalogo confirmou.
var autorizacao = await _pagamento.PostAsJsonAsync(
"/autorizacoes", new { novo.ClienteId, item.Preco }, ct);
autorizacao.EnsureSuccessStatusCode();
var resultado = await autorizacao.Content.ReadFromJsonAsync<Autorizacao>(ct)
?? throw new AutorizacaoInvalidaException();
var pedidoId = Guid.NewGuid();
// 3) So AGORA, com os dois "sim" que o cliente precisa, o pedido existe.
// O evento e disparado e NAO bloqueia a resposta: PutEventsAsync
// confirma em milissegundos que a AWS aceitou o evento, nao que
// alguem o processou.
await _eventos.PutEventsAsync(new PutEventsRequest
{
Entries =
{
new PutEventsRequestEntry
{
Source = "cadencia.pedidos",
DetailType = "PedidoCriado",
EventBusName = "cadencia-eventos",
Detail = JsonSerializer.Serialize(new
{
pedidoId,
novo.ClienteId,
Email = novo.Email,
item.Preco,
AutorizacaoId = resultado.Id,
}),
},
},
}, ct);
return new ResultadoPedido(pedidoId, item.Preco, resultado.Id);
}
}
Por que não usar Polly com retry aqui, e não sem propósito
Retry automático sobre uma chamada sem margem de tempo sobra é o que transforma uma lentidão pontual em sobrecarga — cada tentativa nova soma latência e ainda multiplica a carga no serviço já sob pressão. Este módulo usa timeout fixo e propaga o erro; retry com backoff e jitter, e a decisão de QUANDO ele ajuda em vez de piorar, é o L36.
Construir: o caminho assíncrono, do evento ao consumidor idempotente
O barramento, a fila, a DLQ e a tabela de idempotência — nenhum destes é infraestrutura pesada: os quatro cobram por uso, e o volume da Cadência (500 pedidos/dia, picos de 4.200 em três horas) fica muito abaixo de qualquer patamar que mude o modelo de custo.
# eventos.tf — o caminho que NAO bloqueia a resposta ao cliente
resource "aws_cloudwatch_event_bus" "cadencia" {
name = "cadencia-eventos"
}
resource "aws_sqs_queue" "notificacao_pedido_dlq" {
name = "cadencia-notificacao-pedido-dlq"
message_retention_seconds = 1209600 # 14 dias — o teto do proprio SQS
}
resource "aws_sqs_queue" "notificacao_pedido" {
name = "cadencia-notificacao-pedido"
# Padrao da AWS e 30 s; o processamento (SES + gravacao de idempotencia) leva
# perto de 1 s no exemplo medido — 30 s fica confortavel e NAO foi reduzido,
# porque reduzir aqui nao devolve nada: quem espera este relogio e o proprio
# SQS, nao um cliente HTTP.
visibility_timeout_seconds = 30
redrive_policy = jsonencode({
deadLetterTargetArn = aws_sqs_queue.notificacao_pedido_dlq.arn
# Depois de 5 tentativas sem exclusao bem-sucedida da mensagem, ela sai da
# fila principal — e para de competir por processamento com pedidos novos.
maxReceiveCount = 5
})
}
resource "aws_cloudwatch_event_rule" "roteia_pedido_criado" {
name = "roteia-pedido-criado"
event_bus_name = aws_cloudwatch_event_bus.cadencia.name
event_pattern = jsonencode({
source = ["cadencia.pedidos"]
detail-type = ["PedidoCriado"]
})
}
resource "aws_cloudwatch_event_target" "para_fila_notificacao" {
rule = aws_cloudwatch_event_rule.roteia_pedido_criado.name
event_bus_name = aws_cloudwatch_event_bus.cadencia.name
arn = aws_sqs_queue.notificacao_pedido.arn
}
# So o EventBridge pode publicar nesta fila — nao qualquer principal com
# sqs:SendMessage. Sem isto, um chamador poderia fabricar um "PedidoCriado" e
# jogar direto na fila, contornando Pedidos e a autorizacao que o precede.
resource "aws_sqs_queue_policy" "so_eventbridge_publica" {
queue_url = aws_sqs_queue.notificacao_pedido.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = { Service = "events.amazonaws.com" }
Action = "sqs:SendMessage"
Resource = aws_sqs_queue.notificacao_pedido.arn
Condition = {
ArnEquals = { "aws:SourceArn" = aws_cloudwatch_event_rule.roteia_pedido_criado.arn }
}
}]
})
}
# Idempotencia do consumidor: guarda o pedidoId ja processado, com TTL — nao
# para consultar depois, so para a escrita condicional decidir "e novo ou e
# reentrega" em uma unica chamada atomica.
resource "aws_dynamodb_table" "notificacoes_processadas" {
name = "cadencia-notificacoes-processadas"
billing_mode = "PAY_PER_REQUEST"
hash_key = "pedido_id"
attribute {
name = "pedido_id"
type = "S"
}
ttl {
attribute_name = "expira_em"
enabled = true
}
}
data "aws_iam_policy_document" "notificacoes_consome_fila" {
statement {
effect = "Allow"
actions = [
"sqs:ReceiveMessage",
"sqs:DeleteMessage",
"sqs:GetQueueAttributes",
]
# Restrito a ESTA fila — nao ha motivo para o consumidor de notificacao
# enxergar qualquer outra fila da conta.
resources = [aws_sqs_queue.notificacao_pedido.arn]
}
statement {
effect = "Allow"
actions = ["dynamodb:PutItem", "dynamodb:GetItem"]
resources = [aws_dynamodb_table.notificacoes_processadas.arn]
}
}
# Alarme sobre o sintoma que a corrente sincrona antiga escondia: mensagem
# envelhecendo na fila e mensagem parada na DLQ.
resource "aws_cloudwatch_metric_alarm" "fila_envelhecendo" {
alarm_name = "cadencia-notificacao-fila-envelhecendo"
namespace = "AWS/SQS"
metric_name = "ApproximateAgeOfOldestMessage"
dimensions = { QueueName = aws_sqs_queue.notificacao_pedido.name }
statistic = "Maximum"
period = 60
evaluation_periods = 3
threshold = 300 # 5 min sem consumir e sintoma, nao normalidade
comparison_operator = "GreaterThanThreshold"
alarm_actions = [aws_sns_topic.alertas.arn]
}
Sem a política de fila restrita ao EventBridge, qualquer principal com credencial pode fabricar um pedido
A `aws_sqs_queue_policy` que restringe `sqs:SendMessage` ao serviço `events.amazonaws.com` com a condição de `aws:SourceArn` não é boilerplate — sem ela, qualquer credencial com `sqs:SendMessage` na fila poderia publicar uma mensagem de "PedidoCriado" fabricada, e o consumidor de Notificações a processaria como legítima, sem nunca ter passado pela autorização de Pagamento. É a diferença entre confiar no EventBridge como única porta de entrada e confiar em qualquer coisa que saiba a URL da fila.
O consumidor trata a reentrega do SQS como o caso normal, não como exceção — porque "pelo menos uma vez" é a garantia que o serviço oferece, e nenhuma configuração muda isso para "exatamente uma vez" numa fila padrão.
// NotificacaoWorker.cs — consumidor assíncrono, idempotente por construção
public sealed class NotificacaoWorker : BackgroundService
{
private readonly IAmazonSQS _sqs;
private readonly IAmazonDynamoDB _idempotencia;
private readonly IAmazonSimpleEmailServiceV2 _ses;
private readonly string _filaUrl;
private readonly ILogger<NotificacaoWorker> _log;
protected override async Task ExecuteAsync(CancellationToken ct)
{
while (!ct.IsCancellationRequested)
{
// Long polling de 20 s: reduz chamadas vazias sem exigir nenhum
// ajuste no visibility timeout da fila, que e um relogio separado.
var lote = await _sqs.ReceiveMessageAsync(new ReceiveMessageRequest
{
QueueUrl = _filaUrl,
MaxNumberOfMessages = 10,
WaitTimeSeconds = 20,
}, ct);
foreach (var msg in lote.Messages)
{
var evento = JsonSerializer.Deserialize<PedidoCriadoEvento>(msg.Body)!;
// A escrita condicional E a checagem de idempotencia: se o item
// ja existe, PutItem falha com ConditionalCheckFailedException,
// e isto significa "ja processei este pedido antes" — SQS e
// at-least-once, entao reentrega e o caso normal, nao a excecao.
try
{
await _idempotencia.PutItemAsync(new PutItemRequest
{
TableName = "cadencia-notificacoes-processadas",
Item = new Dictionary<string, AttributeValue>
{
["pedido_id"] = new() { S = evento.PedidoId.ToString() },
["expira_em"] = new() { N = DateTimeOffset.UtcNow.AddDays(7).ToUnixTimeSeconds().ToString() },
},
ConditionExpression = "attribute_not_exists(pedido_id)",
}, ct);
}
catch (ConditionalCheckFailedException)
{
_log.LogInformation("pedido {Id} ja notificado; reentrega ignorada", evento.PedidoId);
await _sqs.DeleteMessageAsync(_filaUrl, msg.ReceiptHandle, ct);
continue;
}
await _ses.SendEmailAsync(MontarEmailConfirmacao(evento), ct);
// So apaga DEPOIS do envio confirmado. Se o processo cair entre
// o PutItem e o DeleteMessage, a mensagem volta a ficar visivel
// apos o visibility timeout — e a proxima tentativa cai no
// ConditionalCheckFailedException acima e so limpa a fila, sem
// reenviar o e-mail. E esse o motivo de a ordem importar aqui.
await _sqs.DeleteMessageAsync(_filaUrl, msg.ReceiptHandle, ct);
}
}
}
}
Por que a escrita condicional acontece ANTES do envio de e-mail, não depois
Se a checagem de idempotência fosse feita depois de enviar o e-mail, uma reentrega ainda enviaria o e-mail duas vezes antes de a segunda tentativa descobrir que já havia processado a primeira. A ordem — checar e reservar antes de agir, agir, só então confirmar — é o que torna o efeito colateral (enviar e-mail) protegido pela mesma garantia que a escrita no banco.
Implantar e provar
Cinco medições. A primeira confirma a previsão matemática da corrente original; as seguintes provam, com número, cada afirmação estrutural feita até aqui — inclusive a de que o serviço saudável no meio da corrente relata falha alheia.
# provas.sh — cinco medições; a disponibilidade composta se prova, não se afirma
PROJETO=cadencia; REGIAO=us-east-1
URL="https://$(terraform output -raw dominio)/api/pedidos"
# ── Prova 1: a linha de base da corrente ORIGINAL (4 elos) ───────────────────
# 2.000 requisições contra a corrente síncrona aninhada, sem matar nada.
for i in $(seq 1 2000); do
curl -s -o /dev/null -w "%{http_code}\n" -X POST "$URL" \
-d '{"produtoId":"p-42","clienteId":"c-9"}'
done | sort | uniq -c
# Esperado: por volta de 99,5–99,6% em 200/201 — perto do 0,999⁴ ≈ 99,60%
# previsto. Ruído normal de rede explica a diferença de décimos.
# ── Prova 2: derrube SÓ Pagamento, e meça quem mais falha ────────────────────
aws ecs update-service --cluster "$PROJETO" --service cadencia-pagamento \
--desired-count 0 --region "$REGIAO"
sleep 20 # tempo para as tasks de Pagamento saírem de RUNNING
for i in $(seq 1 600); do
curl -s -o /dev/null -w "%{http_code}\n" -X POST "$URL" \
-d '{"produtoId":"p-42","clienteId":"c-9"}'
done | sort | uniq -c
# Esperado no desenho ORIGINAL (corrente aninhada): perto de 100% de erro —
# toda requisição que chega em Catálogo falha, porque ele depende de Pagamento.
aws ecs describe-services --cluster "$PROJETO" \
--services cadencia-pedidos cadencia-catalogo --region "$REGIAO" \
--query 'services[].{nome:serviceName,tasksSaudaveis:runningCount}'
# Esperado: Pedidos e Catálogo com runningCount = desiredCount, os dois "de pé"
# — é a prova de que a falha reportada não é deles.
aws ecs update-service --cluster "$PROJETO" --service cadencia-pagamento \
--desired-count 2 --region "$REGIAO"
# ── Prova 3: o mesmo teste, no desenho de PRODUÇÃO (achatado) ────────────────
# Repita a prova 2 depois de aplicar o desenho achatado. Catálogo não conhece
# mais Pagamento, então derrubar Pagamento não deveria mais afetar a chamada a
# Catálogo — só a chamada direta de Pedidos a Pagamento.
# Esperado: a taxa de erro cai para perto de 1/3 da cadeia (só o hop de
# Pagamento falha), não mais toda a cadeia.
# ── Prova 4: Notificações cai por 5 minutos; o pedido continua sendo criado ──
aws ecs update-service --cluster "$PROJETO" --service cadencia-notificacoes \
--desired-count 0 --region "$REGIAO"
for i in $(seq 1 300); do
curl -s -o /dev/null -w "%{http_code}\n" -X POST "$URL" \
-d '{"produtoId":"p-42","clienteId":"c-9"}'
sleep 1
done | sort | uniq -c
# Esperado: 100% em 201, mesmo com Notificações inteiramente fora do ar — é o
# critério de aceite do desenho de produção.
aws sqs get-queue-attributes --queue-url "$(terraform output -raw fila_notificacao_url)" \
--attribute-names ApproximateNumberOfMessages
# Esperado: a contagem cresce ~300 durante a janela, provando que nada se perdeu.
aws ecs update-service --cluster "$PROJETO" --service cadencia-notificacoes \
--desired-count 2 --region "$REGIAO"
# Depois de religar, a fila deve drenar para perto de zero em minutos.
# ── Prova 5: mensagem-veneno vai para a DLQ sem travar as boas ───────────────
aws sqs send-message --queue-url "$(terraform output -raw fila_notificacao_url)" \
--message-body '{"pedidoId": "nao-e-um-guid-valido"}'
# Aguarde 5 tentativas de reentrega (5 × visibility timeout de 30 s ≈ 2,5 min).
sleep 160
aws sqs get-queue-attributes \
--queue-url "$(terraform output -raw fila_notificacao_dlq_url)" \
--attribute-names ApproximateNumberOfMessages
# Esperado: 1 mensagem na DLQ, e nenhum efeito nas mensagens legítimas
# processadas na mesma janela.
O que NÃO foi verificado, e você deve medir no seu ambiente
Os 99,9% de disponibilidade individual usados nas contas deste módulo são um valor de exemplo — três-noves é uma meta comum de SLO interno, não um número medido nesta aplicação específica. Antes de decidir se uma cadeia de dois, três ou quatro elos atende ao seu requisito de disponibilidade, meça a disponibilidade real de CADA serviço em produção durante algumas semanas, e refaça a multiplicação com os números medidos, não com os deste texto.
Quebrar de propósito
Três falhas plantadas de propósito. Reproduza cada uma isoladamente — misturá-las torna o sintoma ambíguo, e o ponto deste exercício é treinar o diagnóstico, não só observar o efeito.
| Falha plantada | Sintoma observado | Onde olhar | Correção |
|---|---|---|---|
| Timeout de Catálogo ao chamar Pagamento (10 s) configurado MENOR que o timeout que Pagamento promete (13 s, por engano trocado) | Catálogo devolve erro a Pedidos enquanto Pagamento ainda processa; ocasionalmente o pagamento é autorizado mesmo assim, sem pedido correspondente | logs de Pagamento mostrando conclusão APÓS o horário do erro relatado por Catálogo | reordenar os timeouts: o de quem chama sempre maior que o prometido por quem é chamado |
| Consumidor de Notificações remove a checagem de idempotência "para simplificar" | clientes recebem dois ou três e-mails de confirmação do mesmo pedido, sem padrão fixo de quantos | CloudWatch Logs Insights filtrando por `pedidoId` repetido nos logs de envio | reintroduzir a escrita condicional antes do envio, tratando reentrega como normal |
| Alarme de `ApproximateAgeOfOldestMessage` nunca foi criado | Notificações trava silenciosamente (exceção não tratada a cada mensagem) e ninguém percebe até um cliente reclamar, dias depois, que nunca recebeu confirmação | idade da mensagem mais antiga na fila principal, e profundidade crescente na DLQ | os dois alarmes do bloco de observabilidade, com ação que notifica alguém |
Segurança
Trocar chamada síncrona por evento e fila abre uma superfície nova: quem pode publicar, quem pode consumir, e o que fica retido numa fila por até 14 dias.
| Risco | Probabilidade | Impacto | Prevenção | Detecção | Resposta |
|---|---|---|---|---|---|
| Principal não autorizado publica evento fabricado direto na fila | Baixa | Alto — pedido de notificação sem autorização de pagamento correspondente | política de fila restrita a `events.amazonaws.com` com `aws:SourceArn` | CloudTrail em `sqs:SendMessage` fora do padrão esperado | revogar a policy alterada e auditar mensagens processadas na janela |
| Task role de Notificações com permissão sobre filas ou tabelas de outros serviços | Baixa | Médio — leitura ou escrita cruzada entre domínios | IAM restrito ao ARN exato da fila e da tabela deste serviço | IAM Access Analyzer sinalizando acesso não utilizado | reduzir a policy ao uso real observado |
| Payload do evento carrega dado sensível além do necessário (ex.: dado de cartão) | Baixa | Alto — exposição em log de fila ou DLQ retida por 14 dias | o evento carrega `autorizacaoId`, nunca o dado de pagamento em si | revisão de schema do evento em code review | purgar a fila/DLQ e rotacionar a autorização exposta |
| DLQ acumula mensagens indefinidamente e vira repositório não gerenciado | Média | Baixo a médio — dado retido além do necessário, sem dono | retenção de 14 dias (o teto do próprio SQS) e alarme de profundidade | alarme de `ApproximateNumberOfMessagesVisible` na DLQ acima de zero por mais de 1 dia | investigar e reprocessar ou descartar deliberadamente |
| Consumidor sem idempotência processa mensagem maliciosa reenviada em loop | Baixa | Médio — e-mail duplicado em volume, custo de SES e reputação de envio | escrita condicional antes de qualquer efeito colateral | métrica de envios de SES por `pedidoId` acima de 1 | corrigir o consumidor e revisar o volume de envio do período |
Quatro serviços em uma cadeia síncrona têm, cada um, 99,9% de disponibilidade medida isoladamente. Qual é a disponibilidade aproximada da cadeia inteira, e por quê?
Observabilidade
A corrente síncrona original escondia dois sintomas atrás de um só: "erro 5xx em Pedidos" tanto podia ser Pedidos quanto qualquer um dos três serviços atrás dele. O painel de produção precisa responder a perguntas específicas, não a uma pergunta genérica de "está tudo bem?".
| Pergunta que o painel responde | Métrica | Limiar inicial |
|---|---|---|
| A criação de pedido está mais lenta que o normal? | latência p99 de `POST /pedidos` (X-Ray ou CloudWatch) | > 1,5× a mediana de 7 dias |
| Qual hop síncrono está contribuindo mais para a latência? | segmento X-Ray por chamada (Catálogo vs Pagamento) | nenhum hop deveria dominar sozinho >70% |
| Algum hop está estourando timeout mais do que o esperado? | contagem de exceções de timeout por serviço, nos logs estruturados | > 1% das chamadas em 5 min |
| Notificações está processando no ritmo em que os eventos chegam? | `ApproximateAgeOfOldestMessage` na fila principal | > 300 s |
| Mensagens estão sendo descartadas para investigação? | `ApproximateNumberOfMessagesVisible` na DLQ | > 0 por mais de 15 min |
| A disponibilidade composta prevista se sustenta na prática? | taxa de sucesso de `POST /pedidos` (SLO) | < 99,65% em janela móvel de 1 h |
Por que o alarme de DLQ é sobre profundidade, não sobre idade
Uma mensagem pode chegar à DLQ minutos depois de publicada (falha imediata e repetida) ou dias depois (degradação lenta que só cruza o limiar de tentativas aos poucos). O que importa operacionalmente é que existe algo, não há quanto tempo — por isso o limiar é `> 0`, não um tempo de espera.
Escala
| Ordem de grandeza | O que muda no caminho síncrono | O que muda no caminho assíncrono |
|---|---|---|
| 10 pedidos/dia | nada digno de nota; a corrente de 4 elos original também "funcionaria" sem incidente perceptível | a fila fica ociosa quase sempre; nenhum ajuste de capacidade necessário |
| 500 pedidos/dia (volume atual) | a diferença entre 99,60% e 99,70% de disponibilidade já representa dezenas de minutos de indisponibilidade evitada por mês | SQS e EventBridge absorvem o volume sem configuração de capacidade — são serviços gerenciados sem provisionamento de throughput |
| 4.200 pedidos em 3 horas (pico de campanha) | o número de tasks de Catálogo e Pagamento precisa acompanhar o pico — auto scaling do ECS baseado em `RequestCountPerTarget` | a fila absorve o burst sem que Notificações precise escalar no mesmo instante; ela drena no próprio ritmo depois do pico |
| 1 milhão de pedidos/dia (fora do horizonte atual da Cadência) | a corrente síncrona de 3 elos passa a exigir revisão: mesmo em 99,70%, isso são cerca de 3.000 pedidos falhos por dia só de indisponibilidade — hora de reavaliar redundância por elo, não só a topologia | fila única pode virar gargalo de throughput por partição; considerar múltiplas filas por região de cliente ou FIFO com grupos de mensagem |
| Falha de uma AZ inteira | com duas tasks por serviço distribuídas em duas AZs, a corrente síncrona perde metade da capacidade, não a disponibilidade inteira — desde que o auto scaling reponha na AZ sobrevivente | SQS e EventBridge são serviços regionais, não amarrados a uma AZ; o consumidor perde capacidade na mesma proporção que o ECS, mas a fila não perde mensagem |
Custo
As dimensões que geram custo aqui: requisições no barramento do EventBridge, requisições na fila do SQS, e o cômputo do ECS que fica ocupado enquanto uma chamada síncrona espera resposta. Nenhum valor absoluto aparece abaixo — os modelos de cobrança mudam menos que os preços, e o AWS Pricing Calculator tem os números do dia com a região certa.
| Cenário | Volume | O que domina o custo |
|---|---|---|
| Protótipo | ~500 pedidos/dia, 1 task por serviço | compute do ECS ocioso a maior parte do tempo; SQS e EventBridge próximos do nível gratuito |
| Produção pequena (volume atual) | ~500 pedidos/dia com picos de 4.200/3h | compute do ECS dimensionado para o pico; SQS/EventBridge seguem marginais no total |
| Alta escala | 1 milhão de pedidos/dia | volume de mensagens no SQS e eventos no EventBridge passa a aparecer na fatura; ainda assim tende a ser pequeno comparado ao compute dos três serviços síncronos |
O custo oculto: não é a fila, é a conexão presa esperando rede
O custo mais fácil de ignorar não está no SQS nem no EventBridge — está no compute do caminho SÍNCRONO. Cada requisição que Pedidos mantém aberta esperando Catálogo e Pagamento consome uma thread/conexão pelo tempo inteiro da espera, não só pelo tempo de processamento real. Quanto mais aninhada e mais lenta a cadeia, mais tasks (mais CPU e memória provisionados) são necessárias para sustentar o mesmo throughput — é custo de computação parada esperando rede, e ele não aparece em nenhuma linha de fatura chamada "espera".
Well-Architected nos seis pilares
| Pilar | Situação | Risco | Melhoria | Prioridade |
|---|---|---|---|---|
| Excelência operacional | sintoma de falha aparece no serviço errado do painel | diagnóstico lento; incidente atribuído ao serviço saudável | alarmes por hop (X-Ray) e por fila (idade, profundidade) | Alta |
| Segurança | fila aceita mensagem de qualquer principal com permissão | evento fabricado sem ter passado pela autorização de pagamento | policy de fila restrita ao EventBridge com `aws:SourceArn` | Alta |
| Confiabilidade | disponibilidade composta de 4 elos síncronos aninhados | indisponibilidade quase 4× maior que um único serviço | achatar para elos síncronos irmãos e mover o que pode para assíncrono | Alta |
| Eficiência de desempenho | timeout copiado sem relação com a posição na cadeia | chamador desiste antes do chamado terminar, ou espera além do necessário | timeout derivado da posição real de cada serviço na cadeia | Média |
| Otimização de custo | compute dimensionado para segurar conexão em espera | mais tasks provisionadas do que o trabalho real exige | reduzir profundidade da cadeia síncrona antes de escalar horizontalmente | Média |
| Sustentabilidade | reentrega de mensagem sem idempotência reprocessa trabalho | e-mail duplicado é trabalho e energia gastos duas vezes pelo mesmo resultado | escrita condicional de idempotência antes de qualquer efeito colateral | Baixa |
Evolução em níveis
Seis níveis, do monólito de método chamando método até um sistema que usa sinal em tempo real para decidir. Cada nível declara o que muda, o novo risco que ele assume, e o impacto de custo — não é uma escada de "mais serviços", é uma escada de decisões sobre acoplamento.
| Nível | O que muda | Novo risco | Impacto de custo |
|---|---|---|---|
| 1. Protótipo — monólito, chamada de método | Pedidos, Catálogo, Pagamento e Notificações são métodos no mesmo processo | nenhum isolamento: um bug em Notificações pode derrubar o processo inteiro | mínimo — um único deployable |
| 2. Aplicação básica — corrente síncrona aninhada (este módulo, mínima) | os quatro viram serviços, ligados na mesma ordem, agora por HTTP | disponibilidade composta ruim; falha em cascata sem culpa direta | baixo — mais compute, mas sem infraestrutura de mensageria |
| 3. Produção — síncronos irmãos + evento assíncrono (este módulo, produção) | Notificações sai do caminho; Catálogo e Pagamento deixam de estar aninhados | consistência entre autorização e evento não é transacional (dívida registrada na seção 8) | baixo a médio — EventBridge, SQS e DynamoDB de idempotência, todos pay-per-use |
| 4. Alta escala — circuit breaker e retry com jitter (L36) | chamadas síncronas restantes ganham disjuntor que corta ANTES do timeout expirar | disjuntor mal calibrado esconde degradação real ou dispara por falso positivo | médio — observabilidade adicional para calibrar o limiar do disjuntor |
| 5. Plataforma — malha de serviço com timeout e retry centralizados (L33) | a política de timeout deixa de viver em cada `HttpClient` e passa a ser configuração de malha, com mTLS entre serviços | complexidade operacional da própria malha; um novo componente que também pode falhar | médio a alto — sidecar por task, e a curva de aprendizado da equipe |
| 6. Dados e IA — decisão de rota guiada por sinal em tempo real | um modelo prevê degradação do gateway de pagamento a partir de métricas históricas e antecipa a abertura do disjuntor, em vez de reagir depois do primeiro erro | decisão automática errada (abrir o disjuntor sem necessidade) derruba um caminho saudável; exige fallback determinístico quando o modelo diverge | alto — pipeline de treino, monitoramento de drift, e o custo de manter os dois caminhos (com e sem IA) operáveis |
Onde IA entra, e onde não entra
Aqui, IA não resolve o problema central — e forçá-la seria o antipadrão que esta série evita
O defeito deste módulo é aritmética de disponibilidade e uma escolha de desenho (síncrono aninhado vs. assíncrono por chamada), não um padrão que precise ser aprendido a partir de dados. Nenhum modelo decide melhor do que a pergunta "o chamador precisa da resposta?" — ela tem resposta determinística, conhecida no momento do design, e não muda com o tempo nem com o tráfego.
O nível 6 da evolução em níveis aponta onde IA eventualmente agrega: prever, a partir de métricas históricas, QUANDO um serviço tende a degradar, para abrir um disjuntor preventivamente em vez de reagir ao primeiro erro. Mesmo aí, a decisão de origem — síncrono ou assíncrono, por chamada — continua sendo feita por quem desenha o sistema, não pelo modelo. O modelo ajusta UM parâmetro (quando abrir o disjuntor) dentro de um desenho que já foi decidido sem ele.
Anti-padrões
| Anti-padrão | Por que alguém faz | Sintoma em produção | Forma correta |
|---|---|---|---|
| Encadear serviços extraídos na mesma ordem das chamadas de método do monólito | é a tradução mais direta do código existente; passa nos testes locais sem mudar nenhuma lógica de negócio | disponibilidade composta pior que a soma das partes sugere; serviço saudável reportando falha alheia | perguntar, por chamada, se o resultado é necessário para responder ao cliente |
| Depois de aprender sobre desacoplamento, jogar TUDO para fila — inclusive Pagamento | zelo mal direcionado: "assíncrono é a lição aprendida", aplicada sem distinguir os casos | cliente sai da tela sem saber se a cobrança passou; suporte lotado de "meu pagamento sumiu" | manter síncrono o que o cliente precisa saber antes de continuar |
| Copiar o mesmo valor de timeout para todos os `HttpClient`, em todos os serviços | é o caminho de menor esforço — um valor, colado em todo lugar, "por garantia" | chamador desiste antes do chamado terminar, ou espera bem além do necessário | derivar o timeout da posição de cada serviço na cadeia, sempre maior que o do vizinho chamado |
| Consumidor assíncrono sem checagem de idempotência | funciona perfeitamente no caminho feliz, e reentrega do SQS não aparece em teste manual | e-mail duplicado, cobrança duplicada se o padrão se repetir em um consumidor de pagamento | escrita condicional que reserva o efeito colateral antes de executá-lo |
| Fila sem alarme de profundidade ou idade | SQS "parece" autogerenciado, e a ausência de erro imediato passa a impressão de que nada precisa de observação | consumidor travado silenciosamente por dias, descoberto só pela reclamação do cliente | alarme de idade da mensagem mais antiga e de profundidade da DLQ, com ação que notifica alguém |
Quando algo não funciona
| Sintoma | Causa provável | Como investigar | Onde olhar | Correção |
|---|---|---|---|---|
| Erro 5xx em Pedidos, mas Pedidos e Catálogo passam no próprio health check | um elo mais à frente da cadeia síncrona está fora do ar ou degradado | trace de X-Ray da requisição que falhou, ponta a ponta | segmento do serviço mais interno na cadeia, não o mais externo | confirmar disponibilidade do elo apontado pelo trace, não do serviço que reportou o erro |
| Cliente recebe dois ou mais e-mails de confirmação para o mesmo pedido | reentrega do SQS processada sem checagem de idempotência | CloudWatch Logs Insights agrupando envios por `pedidoId` | logs do `NotificacaoWorker` e a tabela de idempotência | reintroduzir a escrita condicional antes do envio |
| Fila de notificação crescendo sem consumo perceptível | consumidor travado (exceção não tratada em loop) ou desligado (`desired_count = 0`) | `ApproximateAgeOfOldestMessage` combinado com `describe-services` | logs do worker no instante em que a idade começou a crescer | corrigir a exceção não tratada ou restaurar o `desired_count` |
| DLQ acumulando mensagens sem que ninguém tenha sido avisado | alarme de profundidade da DLQ nunca foi criado ou está sem ação associada | conferir se o alarme existe e se `alarm_actions` aponta para um destino válido | configuração do `aws_cloudwatch_metric_alarm` da DLQ | criar ou corrigir o alarme com uma ação que efetivamente notifica alguém |
| Latência de criação de pedido alta, mas nenhum serviço individualmente lento | timeout mal ordenado fazendo Pedidos esperar o teto de Catálogo mesmo quando a resposta real chegaria antes, por margem insuficiente entre os valores | comparar o tempo de resposta real de cada hop com o timeout configurado para ele | os valores de `per_request_timeout_seconds` e de `HttpClient.Timeout` | ajustar as margens seguindo a regra: timeout do chamador sempre maior que o do chamado |
Limpeza
# A ordem importa: remova o que consome antes do que produz, para não deixar
# mensagem presa apontando para um alvo que já não existe.
terraform destroy \
-target=aws_ecs_service.notificacoes \
-target=aws_cloudwatch_event_target.para_fila_notificacao \
-target=aws_cloudwatch_event_rule.roteia_pedido_criado
terraform destroy # o restante: Pedidos, Catálogo, Pagamento, fila, DLQ, tabela| Recurso | O `destroy` remove? | Observação |
|---|---|---|
| Mensagens na fila principal e na DLQ | Sim, a fila inteira é removida | se houver mensagem não processada, o conteúdo se perde — confirme fila vazia antes |
| Itens na tabela DynamoDB de idempotência | Sim, a tabela inteira é removida | nenhum custo residual: `PAY_PER_REQUEST` não cobra por capacidade parada |
| Log groups do ECS e do worker | Não, por padrão | log group sem política de retenção definida guarda para sempre e cobra por isso |
| Barramento customizado do EventBridge | Sim, se nenhuma regra de outro módulo o referenciar | confira se nenhum outro laboratório publica no mesmo `cadencia-eventos` antes de remover |
O que o `destroy` não leva: log group sem retenção
Assim como no L03, log group criado sem `retention_in_days` sobrevive ao `destroy` do serviço e continua sendo cobrado por armazenamento. Confira com `aws logs describe-log-groups` depois de destruir.
Resumo
| Problema | Peça | Motivo |
|---|---|---|
| Corrente de 4 chamadas síncronas aninhadas | achatar Pedidos→Catálogo e Pedidos→Pagamento | remove um nível de aninhamento e de timeout dependente |
| Cliente não precisa saber se o e-mail foi enviado | EventBridge + SQS | tira Notificações do cálculo de disponibilidade do pedido |
| SQS entrega pelo menos uma vez, nunca exatamente uma | escrita condicional no DynamoDB | idempotência antes de qualquer efeito colateral |
| Mensagem que falha sempre trava a fila em loop | DLQ com `maxReceiveCount` | isola o problema sem impedir o processamento das mensagens boas |
| Timeout copiado sem relação com a posição na cadeia | timeout crescente da ponta para a raiz | garante que quem espera sempre espera mais que quem é esperado |
| Falha | Proteção |
|---|---|
| Um serviço do meio da cadeia cai | achatamento reduz quantos vizinhos dependem dele diretamente |
| Timeout mal ordenado | regra única aplicada em todos os hops: chamador > chamado |
| Reentrega de mensagem | escrita condicional de idempotência |
| Mensagem-veneno | redrive policy para DLQ após N tentativas |
| Fila crescendo sem ninguém notar | alarme de idade e de profundidade de DLQ |
- Cliente chama Pedidos.
- Pedidos chama Catálogo, síncrono, aguarda item e preço.
- Pedidos chama Pagamento, síncrono, aguarda autorização.
- Pedidos publica evento PedidoCriado no EventBridge — não aguarda processamento.
- Pedidos responde ao cliente: pedido criado.
- EventBridge roteia o evento para a fila de notificação.
- Notificações consome a fila no próprio ritmo.
- Notificações checa idempotência por escrita condicional.
- Notificações envia o e-mail e apaga a mensagem da fila.
- Mensagem que falha 5 vezes vai para a DLQ, sem travar as seguintes.
Perguntas frequentes
❓ Por que quatro serviços a 99,9% de disponibilidade não formam um sistema a 99,9%?
❓ Notificações precisa ficar fora do caminho síncrono mesmo sendo rápida hoje?
❓ Qual timeout devo configurar primeiro numa cadeia de chamadas síncronas?
❓ SQS garante que uma mensagem é processada só uma vez?
❓ Circuit breaker resolve o problema de disponibilidade composta deste módulo?
❓ Por que o pagamento continua síncrono se assíncrono resolveria a disponibilidade?
❓ O que muda na disponibilidade se eu adicionar redundância em vez de achatar a cadeia?
❓ Por que checar idempotência no DynamoDB em vez de usar fila FIFO?
Fixando
Numa cadeia A→B→C, A espera até 20 s por B, e B espera até 15 s por C. O que acontece quando C está genuinamente lento e leva 18 s para responder?
Depois de migrar Notificações para consumir uma fila SQS, clientes começaram a receber o e-mail de confirmação duas vezes para o mesmo pedido, de forma esporádica. Qual é a causa mais provável?
Conhecimentos, próximo módulo e documentação
| Item | Conteúdo |
|---|---|
| Conhecimentos anteriores necessários | L03 (rollout confiável) e L31 (fronteira transacional) no ar, com Catálogo e Pagamento também extraídos pela mesma equipe |
| Conhecimentos adquiridos | disponibilidade composta como produto, não média; a regra dos timeouts decrescentes numa cadeia aninhada; a pergunta "o chamador precisa da resposta?" aplicada por chamada; idempotência de consumidor assíncrono; DLQ e redrive policy |
| Limitação que fica | não há transação entre autorizar o pagamento e publicar o evento — uma queda de Pedidos entre os dois deixa uma cobrança sem notificação correspondente, dívida registrada na seção de decisões |
| Próximo exemplo recomendado | L36 — circuit breaker para as chamadas síncronas que permaneceram na cadeia. Reutiliza os dois serviços irmãos deste módulo (Catálogo e Pagamento) e mede o efeito do disjuntor sobre a mesma carga usada aqui |
| Também habilitado por este módulo | L33 (Service Connect ou VPC Lattice: quando a malha de serviço substitui timeout e retry centralizados por sidecar) parte do desenho de produção construído aqui |
| Data da última validação técnica | 7 de agosto de 2026 |
Documentação oficial consultada: Amazon ECS Service Connect — configuração de timeout — os padrões de `idleTimeout` (5 min em HTTP, 1 h em TCP) e a existência de `perRequestTimeout` como configuração separada do cliente HTTP da aplicação; e Amazon SQS — visibility timeout e filas de mensagens não entregues — o padrão de 30 s de visibility timeout e o mecanismo de redrive policy para DLQ, que fundamentam a seção de construção do caminho assíncrono. Nenhum valor de preço aparece neste módulo por decisão: use o AWS Pricing Calculator, porque preço varia por região e envelhece mais rápido que o conteúdo.
O que não foi verificado, e você deve medir no seu ambiente
As disponibilidades de 99,9% usadas nas contas deste módulo são um valor de exemplo — meta comum de SLO interno, não medição desta aplicação específica. Os valores de timeout (3 s, 5 s, 8 s, 10 s, 13 s, 16 s) seguem a REGRA correta (crescente da ponta para a raiz), mas os números exatos dependem da latência real de cada serviço no seu ambiente — derive-os do p99 medido de cada hop, não copie os valores deste texto. E o número de tentativas antes de mover uma mensagem para a DLQ (5, neste módulo) deve refletir quanto tempo um erro transitório do seu provedor de e-mail costuma durar, não um valor padrão.
Terminou de ler?
Marcar como concluído registra o XP, mantém sua sequência e coloca 3 cartas deste módulo na fila de revisão espaçada.
Próximos passos sugeridos
Temas deste módulo
Discussão
Carregando comentários…