Skip to content

Repository files navigation

Distributed Wagering Processor

Serviço financeiro distribuído para processar BET, WIN, LOSS, REFUND e ROLLBACK com efeitos monetários idempotentes, ledger imutável, inbox/outbox transacionais e concorrência por wallet.

Stack

  • Bun 1.x e TypeScript strict
  • NestJS 11
  • TypeORM 0.3
  • PostgreSQL 16
  • AWS SQS FIFO via LocalStack
  • Docker Compose

Execução completa

docker compose up --build

A API fica disponível em http://localhost:3000. O PostgreSQL é publicado em localhost:55432 para evitar colisão com instalações locais na porta padrão. Dentro da rede Compose ele continua em postgres:5432.

Serviços iniciados:

  • api: API HTTP;
  • wager-consumer: consumidor da fila de entrada;
  • outbox-publisher: publisher dos eventos confirmados;
  • reference-worker: reprocessamento de referências fora de ordem;
  • postgres e localstack;
  • migrate: job que termina depois de aplicar as migrations.

Para comprovar concorrência entre várias instâncias:

docker compose up -d --scale wager-consumer=3 --scale outbox-publisher=2

Nenhum serviço escalável possui container_name.

Roteiro rápido para avaliação

Pré-requisito: Docker com Compose v2. Em um checkout novo, estes comandos executam as verificações da entrega:

docker compose config --quiet
docker compose up -d --build --scale wager-consumer=3 --scale outbox-publisher=2
docker compose --profile test build integration-tests
docker compose --profile test run --rm --no-deps integration-tests bun run test:all
docker compose --profile test run --rm --no-deps integration-tests bun run typecheck
curl -f http://localhost:3000/health/live
curl -f http://localhost:3000/health/ready
curl -f http://localhost:3000/metrics

Os testes usam banco e filas exclusivos, executam todas as migrations up → down → up e podem rodar com os workers principais ativos. O build separado e --no-deps reutilizam a infraestrutura já iniciada. A entrega foi validada com 78 testes passando (41 unitários e 37 de integração), além do TypeScript strict e dos endpoints de operação da API e dos workers.

O workflow Backend checks executa build, TypeScript e a mesma suíte com PostgreSQL/LocalStack reais em pushes e pull requests no GitHub.

Desenvolvimento e testes

bun install --frozen-lockfile
bun run typecheck
bun run test

bun run test executa somente os testes unitários e não exige infraestrutura. bun run test:all inclui integração e, portanto, exige PostgreSQL e LocalStack ativos.

Para iniciar a API pelo host, copie .env.example para .env, inicie as dependências com docker compose up -d --build postgres localstack, aplique bun run migration:run e execute bun run start:dev. O exemplo usa os endereços locais; o Compose fornece automaticamente os endereços internos aos containers. As credenciais wager e test servem apenas para a infraestrutura local do desafio.

Os testes de integração usam PostgreSQL e LocalStack reais. Eles recriam apenas o schema do banco wagering_test:

docker compose up -d postgres localstack
bun run test:integration

A suíte usa filas *-test.fifo próprias e filas temporárias reliability-*.fifo, removidas ao terminar. Pode rodar com os workers principais ativos. Não execute duas suítes de integração simultaneamente: ambas recriam o schema de wagering_test.

Alternativamente, rode tudo em container:

docker compose up -d postgres localstack
docker compose --profile test build integration-tests
docker compose --profile test run --rm --no-deps integration-tests

A suíte cobre, entre outros casos:

  • 50 entregas paralelas da mesma aposta produzindo um débito;
  • duas apostas de 80.00 disputando saldo 100.00;
  • wallets distintas em paralelo;
  • LOSS, WIN, REFUND e ROLLBACK;
  • reversão que produziria saldo negativo;
  • inbox e redelivery real no SQS;
  • dois publishers concorrentes;
  • mensagem permanente enviada à DLQ;
  • referência entregue fora de ordem;
  • três processos e morte depois do commit, antes do ACK;
  • constraints de saldo e imutabilidade do ledger;
  • ciclo automatizado de migration up → down → up;
  • rollback integral quando a escrita da outbox falha;
  • expiração de referência pendente e evento de rejeição;
  • igualdade final entre saldo e ledger em cada caso de integração;
  • disputa e 50 duplicatas distribuídas entre três processos reais de API;
  • reserva expirada com dois workers de referência concorrentes;
  • reconciliação durante um commit financeiro de outra conexão;
  • retry de conexão PostgreSQL e redrive automático para a DLQ;
  • indisponibilidade SQS, backoff e recuperação da outbox;
  • morte/reinício de publisher após claim e após envio, antes da confirmação;
  • idempotência persistente do consumidor de eventos republicados;
  • SIGTERM real, devolução de visibility e ACK pela próxima instância;
  • falha permanente persistida como FAILED, sem ledger nem alteração de saldo;
  • métricas exportadas pelos workers e lag durante backoff;
  • paginação estável durante novos movimentos e rejeição HTTP 400 de cursores inválidos ou fora do limite do banco.

Além de comparar saldo com ledger, os testes verificam a quantidade de lançamentos por status e a ausência de eventos terminais contraditórios.

Exemplo HTTP

Crie uma wallet usando UUIDs válidos:

curl -X POST http://localhost:3000/wallets \
  -H 'Content-Type: application/json' \
  -d '{
    "playerId":"0192f28f-5dc0-7d58-bdb2-814ad6a0f4a1",
    "initialBalance":{"amount":"100.00","currency":"BRL"}
  }'

Submeta uma aposta substituindo walletId pelo ID retornado:

curl -X POST http://localhost:3000/wagering/transactions \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: provider-a:transaction-123' \
  -d '{
    "providerId":"provider-a",
    "externalTransactionId":"transaction-123",
    "playerId":"0192f28f-5dc0-7d58-bdb2-814ad6a0f4a1",
    "walletId":"WALLET_ID",
    "roundId":"round-987",
    "gameId":"fortune-chimp",
    "kind":"BET",
    "money":{"amount":"25.00","currency":"BRL"}
  }'

Repetir a chamada retorna o mesmo transactionId e o mesmo snapshot de saldo, com idempotentReplay: true.

Endpoints

POST /wallets
GET  /wallets/:walletId
GET  /wallets/:walletId/ledger?cursor=...&limit=50
POST /wallets/:walletId/reconciliation

POST /wagering/transactions
GET  /wagering/transactions/:transactionId
GET  /providers/:providerId/wagering/transactions/:externalTransactionId

GET  /health/live
GET  /health/ready
GET  /metrics

Migrations

docker compose run --rm migrate
docker compose run --rm migrate bun run migration:revert

As migrations não são executadas por cada réplica. No Compose, o job migrate termina antes da API e dos workers iniciarem. Para executá-las pelo host, configure DATABASE_URL=postgres://wager:wager@localhost:55432/wagering antes de usar bun run migration:run ou bun run migration:revert.

SQS

Filas criadas no startup do LocalStack:

wager-transactions.fifo
wager-transactions-dlq.fifo
wager-events.fifo

Envelope de entrada:

{
  "messageId": "msg-123",
  "type": "WagerTransactionRequested",
  "occurredAt": "2026-07-29T15:00:00.000Z",
  "data": {
    "providerId": "provider-a",
    "externalTransactionId": "transaction-123",
    "idempotencyKey": "provider-a:transaction-123",
    "playerId": "0192f28f-5dc0-7d58-bdb2-814ad6a0f4a1",
    "walletId": "0192f291-27dd-7d3f-8071-5f8685deef37",
    "roundId": "round-987",
    "gameId": "fortune-chimp",
    "kind": "BET",
    "money": { "amount": "25.00", "currency": "BRL" }
  }
}

Use MessageGroupId = walletId. Isso reduz contenção, mas as garantias finais permanecem no PostgreSQL.

Observabilidade

  • logs JSON sem payload financeiro completo;
  • x-correlation-id aceito e devolvido pela API;
  • métricas Prometheus em /metrics;
  • liveness separado de readiness;
  • readiness consulta PostgreSQL e SQS.

Os workers também expõem /metrics, /health/live e /health/ready na porta interna 9000 (METRICS_PORT). Cada réplica tem uma porta local aleatória. Descubra o endereço de uma réplica com:

docker compose port --index 1 wager-consumer 9000
docker compose port --index 1 outbox-publisher 9000
docker compose port --index 1 reference-worker 9000

Consulte http://ENDERECO_RETORNADO/metrics. Um coletor deve consultar cada réplica; os contadores dos workers não são agregados pela API. O lag e a profundidade da DLQ são consultados diretamente nas dependências e incluem backoff/redrive automático.

As decisões e garantias estão detalhadas em ARCHITECTURE.md. O mapeamento requisito a requisito está em COMPLIANCE.md.

About

Serviço financeiro distribuído para processar BET, WIN, LOSS, REFUND e ROLLBACK com efeitos monetários idempotentes, ledger imutável, inbox/outbox transacionais e concorrência por wallet.

Resources

Stars

5 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages