Pular para o conteúdo
Voltar para projetos
Autoria e desenvolvimento

Distributed Wagering Processor

Serviço financeiro que aplica apostas, vitórias, derrotas, reembolsos e estornos nas carteiras dos jogadores, com saldo e extrato corretos mesmo quando as mensagens chegam duplicadas, fora de ordem ou ao mesmo tempo.

Wallet Console e Flow Explorer mostrando uma aposta e duas requisições disputando a mesma carteira

Resultados-chave

248
testes automatizados

Rodando contra PostgreSQL e LocalStack reais

50×
mesma aposta em paralelo

Resulta em um único débito

3
instâncias disputando uma carteira

Cenário dos testes de concorrência

0
mocks de PostgreSQL ou SQS

A infraestrutura dos testes é real

Contexto e problema

Provedores de jogos enviam transações de aposta (BET, WIN, LOSS, REFUND, ROLLBACK) por HTTP ou por fila SQS. O serviço precisa aplicar cada uma na carteira certa e manter um extrato imutável, sem nunca debitar duas vezes, criar dinheiro do nada ou deixar o saldo negativo, mesmo com várias instâncias processando ao mesmo tempo. Como extensão, a carteira também aceita depósito e saque pelo caixa, com as mesmas garantias.

Solução implementada

Um único serviço NestJS. A rota HTTP e o consumer da fila chamam o mesmo use case; tudo o que muda dinheiro acontece em uma transação SQL, e só a outbox publica eventos.

  • Validação pura, sem abrir transação: dinheiro com exatamente 2 casas, tipo da transação e hash do payload.
  • Busca pela Idempotency-Key: mesmo conteúdo devolve o resultado original; conteúdo diferente é 409.
  • SELECT … FOR UPDATE só na linha da carteira; carteiras diferentes seguem em paralelo.
  • Regras de negócio no domínio, com desfecho PROCESSED, REJECTED ou PENDING_REFERENCE.
  • Na mesma transação: transação, novo saldo, lançamento do extrato, eventos na outbox e, se veio da fila, a inbox. Só depois do commit vem a resposta ou o ack.

Decisões de engenharia

  • Dinheiro nunca é number: centavos em bigint, strings com exatamente 2 casas, numeric(19,2) no banco.
  • Concorrência por carteira, sem lock global: SELECT … FOR UPDATE mais UPDATE condicionado pela version; lock_timeout de 5 s vira 503 com Retry-After.
  • Idempotência persistente: UNIQUE (idempotency_key) e SHA-256 do JSON canônico dos campos de negócio.
  • Uma regra de negócio, três entradas: HTTP, consumer SQS e worker de pendências usam o mesmo decide().
  • Inbox e outbox na transação do dinheiro: a inbox deduplica entregas com ON CONFLICT DO NOTHING, e o ack só sai depois do commit; a outbox é publicada com FOR UPDATE SKIP LOCKED.

Desafios técnicos

  • Mensagens fora de ordem: um estorno que chega antes da aposta fica PENDING_REFERENCE e é reprocessado com backoff exponencial até a referência existir.
  • Corrida real entre instâncias: os testes sobem três processos do serviço disputando a mesma carteira e matam um deles com SIGKILL entre o commit e o ack.
  • Invariantes no banco: constraints e triggers garantem as regras mesmo se o código falhar.

Resultados

248 testes automatizados passando, rodando contra PostgreSQL e LocalStack reais, sem mocks. A mesma aposta enviada 50 vezes em paralelo resulta em um único débito.

Arquitetura

Fluxo de uma transação

HTTP e SQS convergem no mesmo use case; tudo que muda dinheiro acontece numa transação SQL.

erro permanenteuma transaçãoProvedor de jogosPOST/wagering/transactionswager-transactions.fifoConsumer SQS inbox ·ack após o commitwager-transactions-dlq.fifoSubmit use caseidempotência · lockda carteira · regrasPostgreSQL wallets ·ledger ·transactions inbox ·Worker de pendênciasbackoff exponencialOutbox publisher FORUPDATE SKIP LOCKEDwagering-events.fifo

passo a passo

etapa 0/5

Tecnologias utilizadas

Backend

  • BunRuntime do serviço e dos scripts
  • NestJSAPI HTTP, consumer SQS e workers
  • TypeScriptModo estrito em todo o código
  • MikroORMUnit of work e lock pessimista por carteira
  • APIs RESTPOST /wagering/transactions com Idempotency-Key

Dados & Mensageria

  • PostgreSQLCarteiras, extrato, inbox e outbox; constraints e triggers como última barreira
  • AWS SQSFila FIFO de entrada, DLQ e fila de eventos (LocalStack)

Cloud & DevOps

  • DockerCompose com PostgreSQL, LocalStack e backend

Principais funcionalidades

  • Idempotência persistenteIdempotency-Key com hash do payload
  • Lock por carteiraSem lock global; espera máxima de 5 s
  • Ledger append-onlyCada lançamento guarda saldo antes e depois
  • Transactional outboxEventos publicados com SKIP LOCKED

Imagens e demonstrações

  • Wallet Console: apostar, ganhar, perder e acompanhar cada requisição
  • Extrato append-only e conferência de saldo × extrato
  • Flow Explorer: duas apostas disputando o lock da carteira
  • Mesma key com outro conteúdo: 409 e nada gravado
  • Temas claro e escuro

Sobre o projeto

Desafio técnico da Jungle Gaming. Além do backend, o projeto traz dois frontends: o Wallet Console, para usar o sistema como um operador de cassino, e o Flow Explorer, que executa uma rota e anima o caminho que ela fez pelo código, com o SQL, os locks e a fila.

Outros projetos

destaque

Strata

Do planejamento ao código, com o time inteiro na mesma tarefa

Plataforma para times de software que reúne quadro, chat, documentos e um editor de código. Cada tarefa vira um canvas infinito com a especificação, o código e o time, e o contexto não se perde entre ferramentas.

tipos de ambiente no canvas
7
plano Orbit, gratuito
US$ 0
Página inicial do Strata com o quadro de tarefas sobre um fundo espacial
Detalhes Demo
destaque

Voto Aberto

Plataforma de transparência parlamentar: o que cada deputado federal propôs e como votou, direto dos dados abertos da Câmara, com link para conferir cada informação na fonte.

  • Next.js
  • TypeScript
  • SQLite
  • Zod
  • Vitest
  • Node.js
deputados da 57ª legislatura
648
votações nominais
1.597
proposições
94.480
Página inicial do Voto Aberto no computador e o placar de uma votação no celular
Detalhes GitHub
destaque

Crash Game

Um cassino multiplayer em tempo real: a curva sobe, todos veem o mesmo multiplicador ao mesmo tempo e cada jogador decide quando sacar antes do crash.

  • Bun
  • NestJS
  • React
  • Vite
  • TypeScript
  • PostgreSQL
entre atualizações do multiplicador
100 ms
microsserviços NestJS
3
testes automatizados
503
Uma rodada do Crash Game a 3,49x no navegador e a mesma tela no celular
Detalhes GitHub