Desacoplando sistemas com mensageria no AWS SQS

    Foto do autor Rafael Sotero
    Rafael Sotero
    Compartilhar
    Compartilhar no LinkedInCompartilhar no FacebookCompartilhar no XCompartilhar no WhatsApp
    Imagem banner do post

    Imagine a seguinte situação: seu sistema de acabou de receber um pagamento via Stripe. O dinheiro entrou, a operadora confirmou, e o stripe te avisou, mas bem naquela hora o serviço interno que libera o produto caiu ou ficou sobrecarregado. O que acontece? O cliente pagou e não recebeu nada. Trabalhar com mensageria pode ser a solução.

    Esse cenário é mais comum do que parece. Integrações síncronas, aquelas baseadas em chamadas HTTP diretas entre microsserviços, funcionam bem quando tudo está de pé. Mas basta um serviço ficar indisponível por três segundos para gerar uma cascata de timeouts que congestionam retries e transformam uma falha pontual em indisponibilidade generalizada.

    A solução passa por uma mudança de paradigma: em vez de chamar diretamente o serviço vizinho, você enfileira a intenção em uma fila de mensagens e deixa o consumidor processar no ritmo dele, mesmo que ele esteja temporariamente fora do ar. Ao final deste artigo, teremos dois microsserviços Spring Boot se comunicando por meio do AWS SQS, rodando localmente sem precisar de conta na AWS.

    ℹ️ Este conteúdo é o Episódio 1 de 3 da série Dominando Mensageria na AWS. Nesta série, partimos do padrão básico de filas (este artigo), avançamos para distribuição de eventos com SNS + SQS (Episódio 2) e finalizamos com controle avançado via FIFO, deduplicação e filtros (Episódio 3).

    O que é Mensageria?

    Quando dois sistemas precisam trocar informações, existem basicamente dois caminhos. O primeiro é a comunicação síncrona: o serviço A faz uma requisição HTTP para o serviço B, espera a resposta e só então continua. Se o outro lado não atender, a chamada falha.

    O segundo caminho é a comunicação assíncrona: o serviço A deposita uma mensagem em um intermediário (a fila) e segue com sua vida. O serviço B retira essa mensagem quando puder e processa no seu tempo. Nenhum dos dois precisa estar online ao mesmo tempo.

    Na mensageria, a fila é o componente central. Ela desacopla quem produz de quem consome, criando um amortecedor entre os dois. Mesmo que o consumidor esteja lento, reiniciando ou até fora do ar, a fila segura as mensagens até ele voltar a processar. Esse padrão de comunicação — um produtor para um consumidor — é chamado de Point-to-Point, e é a base de tudo que faremos aqui.

    Como o AWS SQS funciona

    O Amazon SQS (Simple Queue Service) é o serviço de filas gerenciado da AWS. Ele existe desde 2006 e foi literalmente o primeiro serviço público lançado pela Amazon Web Services. A premissa é simples: você cria uma fila, produtores enviam mensagens, consumidores retiram e processam. Toda a infraestrutura por baixo (servidores, replicação, disponibilidade) é responsabilidade da AWS.

    Três conceitos são essenciais:

    1. Polling (Long Polling vs Short Polling)

    O consumidor precisa perguntar à fila se há mensagens novas. No Short Polling, a fila responde imediatamente mesmo se vazia, gerando chamadas desnecessárias. No Long Polling, a fila segura a conexão por até 20 segundos aguardando mensagens, reduzindo custos e latência. É a configuração recomendada pela AWS.

    2. Visibility Timeout

    Quando um consumidor retira uma mensagem, ela fica invisível para outros consumidores durante o Visibility Timeout (padrão: 30 segundos). Se o processamento falhar (crash, timeout, exceção), a mensagem reaparece na fila automaticamente. Nenhuma mensagem se perde por falha do consumidor.

    3. At-Least-Once Delivery

    O SQS garante entrega pelo menos uma vez. Em raras situações, pode entregar mais de uma vez, então seu consumidor precisa ser idempotente: processar a mesma mensagem duas vezes não deve causar efeitos colaterais. No nosso projeto, resolveremos isso com uma verificação no banco de dados.

    O SQS é projetado para alta disponibilidade e durabilidade, armazenando mensagens de forma redundante em múltiplos servidores dentro de uma região da AWS. Cada mensagem pode ter até 256 KB e ser retida por até 14 dias na fila.

    Por que dominar o SQS?

    Mensageria faz parte de grande parte das arquiteturas de microsserviços em produção. Empresas que lidam com grandes volumetrias dependem diretamente de sistemas como o SQS para trafegar eventos de forma confiável.

    O iFood migrou sua plataforma financeira para uma arquitetura orientada a eventos com SQS e EventBridge, reduzindo o tempo de lançamento de funcionalidades em 50% e aumentando a disponibilidade em 30%. O Airbnb construiu o Dynein, um sistema de job queueing distribuído baseado no SQS, escolhendo-o pela escalabilidade e entrega at-least-once. O código é open-source.

    Nosso Projeto

    Para sair da teoria, vamos construir um cenário inspirado em integrações reais com o Stripe, uma das plataformas de pagamento mais utilizadas no mundo. O Stripe utiliza webhooks para notificar seu sistema sempre que um evento relevante acontece (pagamento confirmado, estorno, disputa). O problema? O Stripe espera uma resposta rápida. Se seu endpoint demorar, ele marca o webhook como "Timed out" e entra em ciclo de retries, potencialmente gerando processamento duplicado.

    O microsserviço ms-payment-ingestor recebe o webhook, mas não processa nada. Seu único trabalho é aceitar a requisição e depositar o evento na fila SQS. Resposta rápida para o Stripe, sem risco de timeout.

    O microsserviço ms-billing consome da fila e executa a regra de negócio: converte o valor para dólares consultando a Frankfurter API (API pública com dados do Banco Central Europeu), calcula o imposto sobre a transação e persiste a fatura no banco de dados.

    Com esse desenho, mesmo que o ms-billing caia, os pagamentos continuam sendo aceitos e enfileirados. Quando ele voltar, processa tudo que ficou pendente, sem perder nenhum evento.

    Mão na Massa

    O projeto está disponível no repositório do blog. Vamos usar a seguinte stack:

    • Java 25 como versão da JDK
    • Spring Boot 4.0.5 como base dos microsserviços
    • Spring Cloud AWS 4.0 para integração com SQS
    • Spring Data JPA + H2 para persistência no ms-billing
    • Docker com LocalStack para emular o SQS localmente

    Clone o repositório e entre na pasta do projeto. Execute os comandos via GitBash:

    Copiar
    git clone https://github.com/devsuperior/blog.git
    cd blog/articles/desacoplando-sistemas-com-mensageria-no-aws-sqs/projects

    O que é o LocalStack?

    ⚠️ A partir de março de 2026, o LocalStack exige um LOCALSTACK_AUTH_TOKEN para rodar. Crie uma conta gratuita em app.localstack.cloud, gere seu token e exporte-o como variável de ambiente antes de subir o container. Veja o guia oficial de autenticação.

    O LocalStack é um emulador da AWS que roda em Docker. Ele replica a API de dezenas de serviços (SQS, S3, SNS, DynamoDB), redirecionando chamadas de https://sqs.us-east-1.amazonaws.com para http://localhost:4566. O código que escrevemos aqui funciona identicamente na AWS real — a única diferença será trocar o endpoint e as credenciais.

    Subindo a infraestrutura local

    O docker-compose.yml sobe o LocalStack habilitando apenas o serviço SQS. A variável SERVICES=sqs instrui o LocalStack a inicializar somente o necessário, economizando memória. O volume mapeia nosso script de inicialização para o diretório ready.d do container, que o LocalStack executa automaticamente assim que fica saudável:

    Copiar
    services:
      localstack:
        image: localstack/localstack:latest
        container_name: localstack-sqs
        ports:
          - "4566:4566"
        environment:
          - LOCALSTACK_AUTH_TOKEN=${LOCALSTACK_AUTH_TOKEN}
          - SERVICES=sqs
          - AWS_DEFAULT_REGION=us-east-1
        volumes:
          - ./init-scripts:/etc/localstack/init/ready.d

    O script 01-create-queues.sh usa o awslocal (wrapper do aws cli) para criar a billing-queue na inicialização. Antes de subir, exporte o token gerado em app.localstack.cloud:

    Copiar
    # Exporte seu token (substitua pelo token real gerado no painel do LocalStack)
    export LOCALSTACK_AUTH_TOKEN=ls_xxxxxxxxxxxxxxxxxxxxxxxx
    
    # Credenciais dummy para o aws cli (o LocalStack aceita qualquer valor)
    export AWS_SESSION_TOKEN=
    export AWS_ACCESS_KEY_ID=test
    export AWS_SECRET_ACCESS_KEY=test
    
    # Suba a infraestrutura
    docker-compose up -d
    
    # Confirme que a fila foi criada
    aws --endpoint-url=http://localhost:4566 sqs list-queues --region us-east-1

    A URL retornada (http://sqs.us-east-1.localhost.localstack.cloud:4566/000000000000/billing-queue) é o identificador da fila. Use os comandos abaixo para inspecionar o SQS a qualquer momento:

    Copiar
    # Quantas mensagens estão na fila agora?
    aws --endpoint-url=http://localhost:4566 sqs get-queue-attributes \
      --queue-url http://sqs.us-east-1.localhost.localstack.cloud:4566/000000000000/billing-queue \
      --attribute-names ApproximateNumberOfMessages \
      --region us-east-1
    
    # Espiar uma mensagem sem removê-la da fila (peek)
    aws --endpoint-url=http://localhost:4566 sqs receive-message \
      --queue-url http://sqs.us-east-1.localhost.localstack.cloud:4566/000000000000/billing-queue \
      --visibility-timeout 0 \
      --region us-east-1

    ℹ️ O parâmetro --visibility-timeout 0 no receive-message faz o SQS devolver a mensagem imediatamente para a fila, funcionando como um "peek" sem consumir. Ótimo para debugging.

    Conectando o Spring Boot ao SQS

    A biblioteca Spring Cloud AWS abstrai toda a complexidade do SDK da AWS. Com a dependência spring-cloud-aws-starter-sqs, o Spring Boot auto-configura dois componentes essenciais: o SqsTemplate (para enviar mensagens) e o container de listeners @SqsListener (para consumir). Como boa prática, isolamos as credenciais locais em um Spring Profile (application-local.properties), mantendo o application.properties principal limpo para produção:

    Copiar
    # application-local.properties
    spring.cloud.aws.region.static=us-east-1
    spring.cloud.aws.credentials.access-key=test
    spring.cloud.aws.credentials.secret-key=test
    spring.cloud.aws.sqs.endpoint=http://localhost:4566

    O que é cada propriedade:

    • region.static define a região AWS que o SDK vai usar nas requisições.
    • credentials fornece as chaves de acesso, que no LocalStack podem ser qualquer valor.
    • sqs.endpoint é a mais importante para desenvolvimento local: ela redireciona todas as chamadas do SDK para o container do LocalStack em vez da AWS real.

    Se em algum momento você tentar subir para produção (na AWS), basta iniciar a aplicação sem ativar o profile local. O Spring Cloud AWS ignorará este arquivo e passará a usar automaticamente a autenticação via IAM Roles atrelada ao seu container.

    ms-payment-ingestor: Recebendo e Enfileirando

    💡 Os blocos de código mostram apenas o essencial. Imports e boilerplate foram omitidos. Clone o projeto completo para acompanhar na IDE.

    Este microsserviço recebe a requisição, enfileira o evento e responde 200 OK. O DTO (PaymentEventDTO.java) representa o payload do webhook:

    Copiar
    public record PaymentEventDTO(
            String paymentId,
            BigDecimal amount,
            String currency,
            String status,
            Instant createdAt
    ) {}

    A classe de serviço (service.PaymentQueueService.java) usa o SqsTemplate para enviar a mensagem. Internamente, o SqsTemplate serializa o objeto para JSON via Jackson, empacota como corpo da mensagem SQS e executa a chamada SendMessage na API do SQS. Tudo isso com uma única linha:

    Copiar
    @Service
    public class PaymentQueueService {
    
        private final SqsTemplate sqsTemplate;
    
        @Value("${app.queue.billing}")
        private String billingQueue;
    
        public PaymentQueueService(SqsTemplate sqsTemplate) {
            this.sqsTemplate = sqsTemplate;
        }
    
        public void enqueue(PaymentEventDTO event) {
            log.info("Enfileirando pagamento {} na billing-queue", event.paymentId());
            // Serializa para JSON, envia via API do SQS, aguarda confirmação
            sqsTemplate.send(billingQueue, event);
            log.info("Pagamento {} enfileirado com sucesso", event.paymentId());
        }
    }

    O controller (controller.PaymentWebhookController.java) expõe o endpoint que simula o callback do Stripe. Ele apenas delega para o serviço de enfileiramento:

    Copiar
    @RestController
    @RequestMapping("/api/payments")
    public class PaymentWebhookController {
    
        private final PaymentQueueService queueService;
    
        public PaymentWebhookController(PaymentQueueService queueService) {
            this.queueService = queueService;
        }
    
        @PostMapping("/webhook")
        public ResponseEntity<Map<String, String>> receivePayment(
                @RequestBody PaymentEventDTO event) {
            queueService.enqueue(event);
            return ResponseEntity.accepted().body(Map.of("status", "accepted"));
        }
    }
    }

    Testando o Ingestor Isoladamente

    Com o LocalStack e a fila já criados, vamos subir apenas o ingestor para vermos o enfileiramento acontecer. Abra um terminal e inicie a aplicação ativando o profile local:

    Copiar
    cd ms-payment-ingestor
    SPRING_PROFILES_ACTIVE=local ./mvnw spring-boot:run

    Em outro terminal, simule um webhook do Stripe disparando um POST para a nossa API:

    Copiar
    curl -X POST http://localhost:8081/api/payments/webhook \
      -H "Content-Type: application/json" \
      -d '{
        "paymentId": "pay_1234567890",
        "amount": 299.90,
        "currency": "BRL",
        "status": "succeeded",
        "createdAt": "2026-04-12T10:30:00Z"
      }'

    A resposta deve ser imediata ({"status":"accepted"}). Agora, vamos perguntar à fila se a mensagem chegou:

    Copiar
    # 1. Faça a `autenticação` no localstack novamente
    export AWS_SESSION_TOKEN=
    export AWS_ACCESS_KEY_ID=test
    export AWS_SECRET_ACCESS_KEY=test
    
    # 2. Verifique a fila
    aws --endpoint-url=http://localhost:4566 sqs get-queue-attributes \
      --queue-url http://sqs.us-east-1.localhost.localstack.cloud:4566/000000000000/billing-queue \
      --attribute-names ApproximateNumberOfMessages \
      --region us-east-1

    O retorno mostrará "ApproximateNumberOfMessages": "1", confirmando que o ingestor fez o seu trabalho e desacoplou a requisição. Para ver o conteúdo:

    Copiar
    aws --endpoint-url=http://localhost:4566 sqs receive-message \
      --queue-url http://sqs.us-east-1.localhost.localstack.cloud:4566/000000000000/billing-queue \
      --visibility-timeout 0 \
      --region us-east-1

    Antes de prosseguirmos para o consumidor, limpe a fila para garantirmos um ambiente zerado:

    Copiar
    aws --endpoint-url=http://localhost:4566 sqs purge-queue \
      --queue-url http://sqs.us-east-1.localhost.localstack.cloud:4566/000000000000/billing-queue \
      --region us-east-1

    ms-billing: Consumindo, Processando e Persistindo

    A anotação @SqsListener faz o trabalho pesado. Por trás, ela levanta um message listener container que executa Long Polling contra a fila, desserializa o JSON recebido de volta para PaymentEventDTO e, quando o método termina sem exceção, envia automaticamente o DeleteMessage para o SQS confirmando o processamento. Se uma exceção for lançada, a mensagem não é deletada e retornará à fila após o Visibility Timeout expirar.

    O Spring Cloud AWS gerencia automaticamente o Long Polling por meio do container de listeners. Por padrão (versão 4.x), ele aguarda até 10 segundos por novas mensagens, reduzindo custos e garantindo processamento quase instantâneo.

    O listener (listener.BillingQueueListener.java) recebe a mensagem já convertida:

    Copiar
    @Component
    public class BillingQueueListener {
    
        private final BillingProcessorService processorService;
    
        public BillingQueueListener(BillingProcessorService processorService) {
            this.processorService = processorService;
        }
    
        // O Spring Cloud AWS faz: Long Polling → JSON para DTO → chama este método → DeleteMessage
        @SqsListener("${app.queue.billing}")
        public void onPaymentReceived(PaymentEventDTO event) {
            log.info("Pagamento recebido da fila: {}", event.paymentId());
            processorService.process(event);
            log.info("Pagamento {} processado com sucesso", event.paymentId());
        }
    }

    O serviço (BillingProcessorService.java) executa a regra de negócio em quatro etapas — idempotência, conversão cambial, cálculo fiscal e persistência:

    Copiar
    @Service
    public class BillingProcessorService {
    
        private static final BigDecimal TAX_RATE = new BigDecimal("0.0365");
    
        private final ProcessedPaymentRepository repository;
        private final CurrencyConverter currencyConverter;
    
        // constructor injection
    
        public void process(PaymentEventDTO event) {
            // 1. Checa idempotência do evento recebido
            if (repository.existsByPaymentId(event.paymentId())) {
                log.warn("Pagamento {} já processado, ignorando", event.paymentId());
                return;
            }
    
            // 2. Conversão de moeda via API externa (Frankfurter)
            BigDecimal amountUsd = currencyConverter.toUsd(
                    event.amount(), event.currency());
    
            // 3. Cálculo fiscal sobre o valor convertido
            BigDecimal taxAmount = amountUsd.multiply(TAX_RATE)
                    .setScale(2, RoundingMode.HALF_UP);
            BigDecimal netAmount = amountUsd.subtract(taxAmount)
                    .setScale(2, RoundingMode.HALF_UP);
    
            // 4. Persistência no banco de dados
            var payment = new ProcessedPayment(
                    event.paymentId(), event.amount(), event.currency(),
                    amountUsd, taxAmount, netAmount, "PROCESSED", Instant.now()
            );
            repository.save(payment);
    
            log.info("Fatura persistida: {} — líquido: {} USD",
                    event.paymentId(), netAmount);
        }
    }

    O CurrencyConverter (CurrencyConverter.java) consulta a Frankfurter API via RestClient:

    Copiar
    @Component
    public class CurrencyConverter {
    
        private final RestClient restClient;
    
        public CurrencyConverter(RestClient.Builder restClientBuilder) {
            this.restClient = restClientBuilder
                    .baseUrl("https://api.frankfurter.dev/v1/latest").build();
        }
    
        public BigDecimal toUsd(BigDecimal amount, String fromCurrency) {
            if ("USD".equalsIgnoreCase(fromCurrency)) return amount;
    
            Map<String, Object> response = restClient.get()
                    .uri("?base={base}&symbols=USD", fromCurrency)
                    .retrieve()
                    .body(Map.class);
    
            Map<String, Number> rates = (Map<String, Number>) response.get("rates");
            BigDecimal rate = new BigDecimal(rates.get("USD").toString());
            return amount.multiply(rate);
        }
    }

    A entidade JPA ProcessedPayment armazena no H2 o resultado completo de cada processamento, incluindo valores originais, convertidos e líquidos. O campo paymentId tem restrição unique, garantindo a idempotência em nível de banco.

    Testando o Consumidor Isoladamente

    Da mesma forma que testamos o ingestor, vamos provar que o consumidor trabalha sozinho, sem depender da existência do ingestor. Inicie apenas o ms-billing com o profile local:

    Copiar
    cd ms-billing
    SPRING_PROFILES_ACTIVE=local ./mvnw spring-boot:run

    Em outro terminal, injete uma mensagem diretamente na fila via AWS CLI:

    Copiar
    # 1. Faça a `autenticação` no localstack novamente
    export AWS_SESSION_TOKEN=
    export AWS_ACCESS_KEY_ID=test
    export AWS_SECRET_ACCESS_KEY=test
    
    # 2. Injete uma mensagem a fila
    aws --endpoint-url=http://localhost:4566 sqs send-message \
      --queue-url http://sqs.us-east-1.localhost.localstack.cloud:4566/000000000000/billing-queue \
      --message-body '{"paymentId":"pay_manual_01","amount":500.00,"currency":"BRL","status":"succeeded","createdAt":"2026-04-12T11:00:00Z"}' \
      --region us-east-1

    Olhe para o terminal do ms-billing. Você verá os logs de consumo, conversão cambial via Frankfurter API e a persistência no banco de dados. Isolamento completo comprovado!

    Para conferir no banco, acesse http://localhost:8082/h2-console com as credenciais do application.properties:

    Copiar
    SELECT * FROM PROCESSED_PAYMENTS 

    Pare a aplicação ms-billing (Ctrl+C) antes de seguir para o teste final.

    Testando o Fluxo Completo e a Resiliência

    Como já validamos cada ponta separadamente, agora vamos testar o maior benefício dessa arquitetura: o desacoplamento temporal.

    Suba apenas o Ingestor novamente (SPRING_PROFILES_ACTIVE=local ./mvnw spring-boot:run). Em um segundo terminal, usaremos o curl para enviar vários pagamentos de uma vez, simulando uma rajada enquanto o serviço de faturamento (ms-billing) está fora do ar:

    Copiar
    # Envio do primeiro pagamento
    curl -X POST http://localhost:8081/api/payments/webhook \
      -H "Content-Type: application/json" \
      -d '{"paymentId":"pay_flow_01","amount":299.90,"currency":"BRL","status":"succeeded","createdAt":"2026-04-12T10:30:00Z"}'
    
    # Envio do segundo pagamento
    curl -X POST http://localhost:8081/api/payments/webhook \
      -H "Content-Type: application/json" \
      -d '{"paymentId":"pay_flow_02","amount":299.90,"currency":"BRL","status":"succeeded","createdAt":"2026-04-12T10:30:00Z"}'
    
    # Envio do terceiro pagamento
    curl -X POST http://localhost:8081/api/payments/webhook \
      -H "Content-Type: application/json" \
      -d '{"paymentId":"pay_flow_03","amount":299.90,"currency":"BRL","status":"succeeded","createdAt":"2026-04-12T10:30:00Z"}'

    Com o SQS, garantimos que nenhum pagamento seja perdido. O Amazon SQS é projetado para oferecer alta disponibilidade e durabilidade, armazenando mensagens de forma redundante em múltiplos servidores e zonas de disponibilidade, o que significa que mesmo em cenários de tráfego intenso ou falhas parciais na nuvem, sua fila continuará aceitando mensagens.

    Se o sistema fosse síncrono, a requisição do Stripe teria falhado (timeout/erro 500) porque o serviço de billing não estava rodando. Mas aqui, o Ingestor responde 202 Accepted instantaneamente.

    Para sentir o desacoplamento na prática, inspecione a fila no terminal:

    Copiar
    aws --endpoint-url=http://localhost:4566 sqs get-queue-attributes \
      --queue-url http://sqs.us-east-1.localhost.localstack.cloud:4566/000000000000/billing-queue \
      --attribute-names ApproximateNumberOfMessages \
      --region us-east-1

    Todas as mensagens estarão lá, seguras, esperando. Agora suba o ms-billing (cd ms-billing && ./mvnw spring-boot:run). Observe os logs: ele vai consumir e processar todas as mensagens pendentes uma a uma, sem perder nenhuma. Esse é o poder do desacoplamento temporal.

    Experimente também enviar o mesmo paymentId duas vezes e veja a idempotência em ação: a segunda mensagem será ignorada com um aviso no log. Confira os registros persistidos no console do H2 em http://localhost:8082/h2-console (JDBC URL: jdbc:h2:mem:billingdb).

    Conclusão

    Em poucas linhas de código, saímos de uma integração síncrona frágil para uma arquitetura assíncrona resiliente. Nosso ingestor responde em milissegundos, o billing processa no seu tempo, consulta APIs externas, calcula impostos e persiste resultados no banco. Se ele cair, as mensagens ficam seguras na fila aguardando retorno.

    Ao delegar a gestão das filas para o Amazon SQS, você remove a complexidade operacional de manter brokers de mensagens (como RabbitMQ ou Kafka) e ganha escalabilidade praticamente infinita e automática. Os ganhos são claros:

    • respostas em milissegundos (mitigando timeouts de webhooks)
    • desacoplamento entre serviços
    • resiliência contra falhas
    • escalabilidade independente e idempotência

    Essa é a base do mesmo padrão que sustenta as arquiteturas do iFood e do Airbnb em produção.

    No próximo episódio, vamos expandir o cenário. E se um único evento precisasse notificar vários consumidores ao mesmo tempo? Entraremos no padrão Pub/Sub com AWS SNS + SQS e o poderoso conceito de Fan-out. Até lá! 🚀

    Fontes

    Foto do autor Rafael Sotero
    Rafael Sotero
    Especialista em Engenharia de Software
    Tenho mais de 12 anos de experiência na concepção e implementação de soluções robustas e escaláveis. Atuei em grandes empresas do setor financeiro e telecom levando inovação e simplicidade em ambientes distribuídos com Java, Python, e Cloud AWS.