Este documento explica as escolhas feitas no projeto e os problemas que resolvi durante o desenvolvimento.
Sistema de votação do BBB que aguenta carga pesada (5-6k req/s). Feito em React + Fastify + Redis + PostgreSQL, com observabilidade via Prometheus/Grafana.
Principais desafios:
- UI fiel ao mockup (ProgressCircle com trigonometria)
- Performance superior ao requisito (100 → 5-6k req/s)
- Arquitetura que escala (cluster mode, load balancer, workers)
- Observabilidade real (não só logs)
Stack: React 18 + Vite + TypeScript + CSS puro
O mockup tinha design muito específico (ProgressCircle customizado, animações, gradientes). CSS puro me deu controle total sem overhead do Tailwind. Para um design único assim, é mais direto.
Precisava de um gráfico circular com:
- Dois arcos crescendo anti-horário
- Labels de % seguindo o fim de cada arco
- Casos extremos (0% e 100%) sem labels ocultos
Solução: Trigonometria básica pra calcular posição dos labels:
const orangeLabelX = centerX + radius * Math.cos(angle);
const orangeLabelY = centerY - radius * Math.sin(angle);Problemas que resolvi:
- Labels sumiam em 100% → Ângulo fixo nesses casos
- Animação travando →
requestAnimationFramesuave - Texto ilegível →
text-shadowmais forte
VotingScreen e VoteResult separados (SRP).
useVoting hook centralizou toda lógica de estado e API, deixando componentes apenas pra UI.
Sprite de imagens: Uma requisição HTTP só pra todas as fotos (técnica antiga mas funciona).
Stack: Fastify + TypeScript + JSON Schema + Redis + PostgreSQL
Performance. Fastify é ~2x mais rápido que Express e tem validação via JSON Schema builtin. Como o requisito era 100 req/s (entreguei 5-6k), precisava de algo rápido.
Aqui foi onde gastei mais tempo pensando:
Voto → API → Redis INCR (<1ms) → Fila → Worker → PostgreSQL (batch 500)
Por que Redis E PostgreSQL?
Redis: Conta em memória, < 1ms, aguenta 100k+ ops/s. Frontend consulta ele (tempo real).
PostgreSQL: Guarda histórico permanente. Workers processam fila em lote (500 votos de uma vez = 1 INSERT ao invés de 500).
Dual-purpose do Redis:
- Contador (INCR/HINCRBY pra stats)
- Fila (LPUSH/BRPOP pra persistência assíncrona)
Isso evita dois sistemas (Redis + RabbitMQ por exemplo).
Decisão: Validação com JSON Schema nativo do Fastify.
Motivações:
- Performance: JSON Schema é compilado e otimizado pelo Fastify (Ajv)
- Integração: Fastify usa JSON Schema nativamente para docs automáticos
- Peso: Zod adiciona ~60KB ao bundle
- Simplicidade: Validações são diretas e rápidas
Exemplo:
const voteSchema = {
body: {
type: 'object',
required: ['paredaoId', 'participantId'],
properties: {
paredaoId: { type: 'string' },
participantId: { type: 'string' },
},
},
};Decisão: Cada API roda em cluster com 4 workers
// cluster.ts
const workers = parseInt(process.env.WEB_CONCURRENCY || '4');
for (let i = 0; i < workers; i++) {
cluster.fork();
}Motivações:
- Aproveita múltiplos cores da CPU
- 8 workers totais (4 por API) = 8x throughput
- Crash de um worker não derruba o serviço
Decisão: Redis serve como cache E fila
Como Cache (Contadores):
await redis.incr(totalKey()); // O(1), <0.1ms
await redis.hincrby(participantHash(), id, 1); // O(1), <0.1msComo Fila:
await redis.lpush(QUEUE_KEY, JSON.stringify(vote)); // AssíncronoMotivações:
- INCR/HINCRBY são operações atômicas extremamente rápidas
- Fila FIFO (LPUSH/BRPOP) garante ordem e confiabilidade
- AOF habilitado = durabilidade mesmo em crash
Por que não PostgreSQL direto?:
- PostgreSQL UPDATE + COUNT(*) = 10-50ms
- Redis INCR = <0.1ms
- 100x mais rápido!
Criei 2 workers dedicados que processam a fila em batches de 500 votos por vez. Isso reduz drasticamente a carga no PostgreSQL - ao invés de 500 INSERTs, faço 1 só com múltiplos VALUES. Com synchronous_commit=off e bulk INSERT, consegui processar milhões de votos sem gargalo.
O PostgreSQL tem duas tabelas: votes_raw (todos os votos) e votes_hourly (agregados por hora). A segunda usa UPSERT para incrementar contadores sem duplicar linhas.
O Nginx distribui requisições entre as 2 instâncias do backend usando least_conn (manda pro menos ocupado). Coloquei rate limiting de 10k req/s pra proteger contra DDoS e keepalive pra reutilizar conexões.
Tudo roda via Docker Compose - make gabriel-globo sobe os 9 serviços (nginx, 2 APIs, 2 workers, redis, postgres, prometheus, grafana, frontend). Funciona em Mac e Linux sem configuração extra.
Criei um Makefile com comandos úteis:
make test- roda todos os testesmake load-test- autocannon progressivo pra validar performancemake check-data- compara Redis vs PostgreSQL pra garantir consistência
Configurei Prometheus pra coletar métricas customizadas do backend (votes_total, votes_by_participant, etc). O Grafana carrega automaticamente um dashboard com 4 painéis: total de votos, distribuição donut, evolução temporal e votos por hora. Queries PromQL agregam dados das 2 instâncias do backend usando sum().
ProgressCircle com 100%/0%: Os labels ficavam escondidos na parte de baixo. Tentei algoritmos genéricos mas acabei hardcoding os ângulos extremos - funciona e fica limpo.
Grafana datasource: Dashboard não achava o Prometheus porque faltava uid: prometheus no YAML. Detalhe pequeno mas crítico.
Agregação multi-instância: Queries do Grafana buscavam uma instância específica, mas tenho 2 backends. Usei sum() com label job="bbb-api" pra consolidar.
PostgreSQL lento: Workers salvavam 1 voto por vez. Mudei pra batch de 500 + bulk INSERT, ficou 50x mais rápido.
Além dos requisitos, implementei:
- Script de consistência (
make check-data): compara Redis vs PostgreSQL pra garantir que tudo tá sincronizado - Load test progressivo: autocannon em etapas (100 → 500 → 1k → 5k conexões) pra validar performance
- Cluster mode: 4 workers por API = 8 cores utilizados = 8x mais throughput
- Makefile: comandos simples (
make test,make load-test, etc) ao invés de decorar docker compose - Dashboard Grafana: 4 painéis profissionais com queries otimizadas e auto-refresh
O sistema aguenta 5-6k req/s sustentado (50-60x o baseline de 100 req/s), com picos de até 38k em bursts. Testado via scripts/load-test.sh com autocannon progressivo.
- Cluster mode multiplica throughput mas exige estado compartilhado (usei Redis como fonte única de verdade)
- Redis INCR é absurdamente rápido (<1ms mesmo a 5k req/s), perfeito pra contadores
- PostgreSQL otimizado com
synchronous_commit=off+ bulk INSERT aguenta 50x mais carga do que parece - Observabilidade via Grafana não é luxo - sem métricas eu não saberia que cheguei a 38k req/s nos picos
- CSS puro foi melhor que Tailwind aqui (design único, sem reutilização massiva)
- CRUD de paredões: pra criar/editar via backend
- Testes E2E completos: Playwright pra fluxo completo
- Circuit breaker: se PostgreSQL/Redis cair, guardar votos em DLQ
- Health checks profundos:
/healthchecando Redis/Postgres - Cache de estatísticas: TTL 5-10s pra reduzir carga
- Logs estruturados: Pino com correlation IDs
- WebSockets: alternativa melhor ao polling do front (mas seria overkill aqui)