Concluído Pessoal Node Typescript PostgreSQL Redis NextJS React AWS SQS SNS EC2 Lambda

Rastreamento de pedidos com arquitetura serverless na AWS

Montei uma arquitetura serverless de verdade na AWS para estudar o que acontece fora da função: onde a mensagem se perde, o que ocorre na terceira tentativa e quanto a infra custa parada. Aqui estão as decisões, os trade-offs assumidos e o que eu faria di

Rastreamento de pedidos com arquitetura serverless na AWS

Esse é um projeto de estudo que montei para praticar uma arquitetura serverless de verdade na AWS: uma API que aceita o pedido na hora e joga o processamento para segundo plano, com fila, retry, fila de erros e toda a infraestrutura descrita em código. O código está aberto em github.com/paulodm145/order-tracking-serverless.

A stack é Node 20 com TypeScript em modo estrito, Express empacotado para Lambda com serverless-http, PostgreSQL com Prisma 6, SNS e SQS na mensageria, AWS CDK para a infraestrutura e GitHub Actions com OIDC no deploy.


O caminho de um pedido

A ideia é simples: quem cria o pedido não espera o processamento terminar. A API grava, publica um evento e responde. Outro componente trabalha depois.

Cliente
   |
   v
API Express (Lambda + API Gateway)
   |  1. grava o pedido no Postgres como PENDING
   |  2. publica o evento no SNS
   v
SNS (topico pedido-criado)
   |
   v
SQS (fila principal)
   |
   v
Lambda consumer
   |  3. processa o pedido
   |  4. atualiza para COMPLETED ou FAILED
   |
   +--- falhou 3 vezes? --> DLQ (fila de erros)

O pedido passa por até quatro estados: PENDINGPROCESSINGCOMPLETED, ou FAILED quando o processamento quebra.

A superfície da API é pequena de propósito, só duas rotas:

  • POST /pedidos — persiste o pedido como PENDING, publica no SNS e responde sem esperar nada.
  • GET /pedidos/:id — devolve o status atual, que é onde o cliente descobre se deu certo.

As decisões técnicas e o que cada uma custou

Toda escolha de arquitetura vem com uma conta a pagar. Achei mais honesto listar as duas metades.

Separar a criação do processamento. A escrita não fica refém de uma regra de negócio que pode demorar ou falhar; o tempo de resposta depende só de um insert e de uma publicação. O preço é consistência eventual: o cliente precisa consultar de novo para saber o desfecho, e a interface tem que lidar com um estado intermediário.

SNS na frente do SQS, em vez de só a fila. Quem publica não precisa saber quem consome. Hoje tem uma fila escutando o tópico, amanhã dá para pendurar outra (e-mail, antifraude, analytics) sem tocar no código da API. O custo é um salto a mais no caminho e mais um componente para depurar quando a mensagem some com um único consumidor, é infraestrutura comprada adiantada.

RDS dentro da VPC, sem exposição pública. Banco não deveria ter endereço acessível pela internet. Nas subnets privadas só quem está na rede conversa com ele. Em troca, Lambda em VPC traz junto toda a complexidade de endpoints, rotas e um caminho nada trivial para rodar migration ou abrir um cliente SQL.

VPC Endpoints no lugar do NAT Gateway. As Lambdas só falam com SQS, SNS e Secrets Manager, que são serviços da própria AWS. Endpoints resolvem por uma rota privada e cortam a maior linha da fatura de um ambiente pequeno. A consequência é que não existe saída para a internet pública: qualquer integração externa no futuro obriga a rever isso.

Credencial no Secrets Manager, montada em tempo de execução. A senha não aparece no código nem em variável de ambiente. As funções leem o secret na inicialização e montam a DATABASE_URL ali, então rotacionar não exige deploy novo. O preço é uma chamada a mais no cold start e mais um ponto de falha no boot da função.

DLQ com três tentativas e reportBatchItemFailures. Falha temporária merece retry, falha permanente não pode ficar reciclando e segurando a fila. Depois de três tentativas a mensagem sai para a DLQ. E como o consumo é em lote, só o item que realmente falhou volta para a fila. O que fica em aberto: DLQ precisa de alguém olhando, senão vira um cemitério silencioso de pedidos perdidos.

Express embrulhado com serverless-http. O mesmo código roda local com npm run dev e na Lambda atrás do API Gateway, e os testes de integração batem direto no app Express sem simular evento do API Gateway. Em compensação, é peso extra no bundle e um roteador fazendo o que o API Gateway já faria uma função por rota seria mais enxuto e bem mais chato de testar.

Infraestrutura toda no CDK, em TypeScript. Mesma linguagem da aplicação, tipos de verdade nas propriedades dos recursos e stacks separados por responsabilidade: rede, banco e aplicação. Subir ou destruir o ambiente inteiro vira um comando. A abstração esconde o CloudFormation embaixo, então quando o deploy falha o erro aparece num nível diferente daquele em que você escreveu o código.


Trade-offs que assumi por ser estudo

Algumas escolhas priorizaram aprendizado e custo baixo em vez de robustez. Deixo explícito o que mudaria num ambiente real:

  • Migration com o próprio IP liberado no security group. Como o banco é privado, libero a porta 5432 para o meu IP, rodo e revogo. Em produção seria bastion host ou Session Manager do SSM, com a migration disparada pelo pipeline.
  • Ambiente sobe e desce a cada sessão. RDS e VPC Endpoints cobram enquanto existirem, então o fluxo é subir, testar e rodar cdk destroy --all. Em produção seria ambiente permanente, multi-AZ, com snapshot e janela de manutenção.
  • Local não fecha o ciclo. Sem SNS e SQS de verdade na máquina, a publicação é ignorada e o pedido fica em PENDING. Dava para resolver com LocalStack ou ElasticMQ.
  • Prisma conectando direto ao Postgres a partir da Lambda. Funciona bem nesse volume, mas cada execução concorrente abre sua própria conexão e o Postgres tem um teto. A resposta certa é RDS Proxy.
  • Falha simulada por palavra-chave. Um pedido com "FALHA" no nome do produto quebra o processamento de propósito, só para demonstrar retry e DLQ. Num sistema real o caminho de erro viria das integrações e validações de verdade.


O que eu faria a seguir

Ordenado por quanto pesa numa operação real, não pela dificuldade de implementar:

  1. Idempotência explícita no consumidor. SQS entrega ao menos uma vez e hoje a proteção é a checagem de status. Uma chave de idempotência gravada junto ao pedido tornaria o reprocessamento inofensivo por construção.
  2. Alarme na DLQ. Um alarme no CloudWatch sobre a profundidade da fila de erros, com notificação. Sem isso a falha é silenciosa.
  3. Rastreamento distribuído com X-Ray. Com API, tópico, fila e consumidor no caminho, correlacionar uma requisição ponta a ponta só com log dá trabalho. Um trace id propagado no payload já resolveria boa parte.
  4. RDS Proxy. A partir de algumas dezenas de execuções simultâneas, pooling deixa de ser luxo e vira requisito.
  5. Validação de schema na entrada. Zod ou equivalente no corpo da requisição, com erro descritivo. O tipo do TypeScript não sobrevive ao limite da rede.
  6. Métricas de negócio no CloudWatch. Pedidos por status e tempo entre criação e conclusão, publicados como métricas customizadas.
  7. Domínio próprio no API Gateway. Route 53 e certificado no ACM, para a URL não ser o endereço gerado pela AWS.


Testes e entrega

Os testes cobrem três frentes: unitários do serviço de publicação, isolando o SDK da AWS com aws-sdk-client-mock; integração das rotas contra um Postgres real subido por Docker Compose; e o handler consumidor exercitado com um evento de SQS simulado. Tem ainda um loadtest.mjs para observar o comportamento sob tráfego.

No pipeline, o ci.yml roda os testes em todo PR e push, com Postgres como serviço do job. O deploy.yml executa o cdk deploy no merge para a main, autenticando por OIDC com IAM Role assumida pelo workflow — sem access key guardada no repositório.

Subir o ambiente do zero é isso aqui:

npx cdk bootstrap aws://<conta>/us-east-1
npx cdk deploy --all --require-approval never

# e, ao terminar o estudo:
npx cdk destroy --all


O que ficou de aprendizado

A parte difícil de serverless não é escrever a função. É tudo em volta: decidir onde a mensagem pode se perder, o que acontece na terceira tentativa, como a credencial chega até o processo e quanto a arquitetura custa por dia mesmo parada. Foi por isso que documentei cada decisão junto com o preço dela num projeto de estudo, a escolha consciente vale mais do que a escolha correta.

O repositório tem README com passo a passo para rodar local e na AWS: github.com/paulodm145/order-tracking-serverless.