Minha primeira participação na Rinha de Backend
A Rinha de Backend, idealizada pelo Zanfranceschi, acontece desde 2023 e é um evento que movimenta a comunidade dev brasileira com o…
Minha primeira participação na Rinha de Backend

A Rinha de Backend, idealizada pelo Zanfranceschi, acontece desde 2023 e é um evento que movimenta a comunidade dev brasileira com o objetivo de fomentar o aprendizado e o compartilhamento de conhecimento.
Eu conheci a Rinha no ano passado (2024) mas só fiquei sabendo depois que já havia terminado. Ainda assim, acabei aprendendo muuuuita coisa acompanhando vídeos no youtube, como os do Fábio Akita, com a análise das soluções submetidas para a competição.
E agora, em 2025, descobri, depois de 1 semana do lançamento, que a Rinha estava acontecendo e ainda restavam mais 3 semanas para implementar e enviar uma solução! Fiquei super empolgado em poder participar e aí começou a minha odisséia, dedicando cada tempo livre disponível ao desenvolvimento de uma arquitetura para solucionar o problema da Rinha.
O Problema da Rinha de Backend 2025
Nesta terceira edição da Rinha o objetivo era implementar um sistema para encaminhar solicitações de pagamentos para o serviço processador de pagamentos (payment-processor).
Existem 2 instâncias de payment-processor: default e fallback. Pagamentos encaminhados para o processador fallback terão uma taxa maior dos que as requisições para o default, no entanto, para deixar as coisas interessantes, ambas instâncias passam por instabilidades variando consideravelmente o tempo de resposta de cada processor ou até mesmo ficando fora do ar por algum tempo.

Em suma, o objetivo era conseguir encaminhar o maior número de pagamentos, preferencialmente pagando uma taxa menor. O campeão da Rinha (olhaí o Node.js! 🚀) foi o backend que obteve o maior Lucro ao final dos testes.

Ranking dos 10 melhores colocados
Outro ponto super importante era a penalização por inconsistências. Durante o teste também eram feitas requisições de auditoria em que o backend deveria retornar a quantidade total de pagamentos encaminhadas para cada instância de payment-processor em um dado intervalo de tempo.
O teste também enviava a mesma requisição diretamente aos processors e caso os valores retornados pelo backend fossem diferente dos retornados pelos processadores de pagamento você teria uma multa pesadíssima de 35% sobre o valor total, derrubando consideravelmente seu lucro e, consequentemente, sua posição no ranking da Rinha.
No repositório oficial você pode conferir as instruções mais detalhadamente e também o resultado final completo.
Construindo minha solução

Diagrama da Arquitetura
Disclaimer: não consegui participar dos testes finais pois cometi um erro fatal, em uma refatoração literalmente na última hora, que derrubava o meu serviço com a requisição feita, ironicamente, para verificar se o backend havia iniciado (errei rude 🥲). Mas independente disso, eu aprendi muito com todo o processo e gostaria de compartilhar alguns desses aprendizados, por isso aqui estou escrevendo este relato.
A primeira coisa na qual foquei não foi em como estruturar a arquitetura do sistema ou quais ferramentas eu utilizaria, até porque tentar responder essas questões antes mesmo de refletir conceitualmente sobre o problema seria um grande tiro no pé. Então me concentrei em resolver um único problema: qual estratégia utilizar para escolher a instância de payment-processor que deverá receber a requisição de pagamento, levando em consideração a latência variável e o custo mais alto do processador fallback?
Comecei a pesquisar diferentes algoritmos de balanceamento de carga, mas precisava de algo mais elaborado do que apenas um simples roud-robin ou least connections. Eu queria desenvolver um balanceamento de carga dinâmico que levasse em consideração o menor tempo de resposta mas também considerasse o custo mais alto no caso do fallback. Eis que me deparei com o primeiro grande aprendizado dessa jornada: o problema do multi-armed bandit.
Balanceamento entre default e fallback
O *multi-armed bandit problem* é utilizado em teoria da probabilidade e também no aprendizado de máquina como abordagem de aprendizagem por reforço. Em linhas gerais, consiste em tomar decisões repetidas vezes entre várias opções de resultados inicialmente incertos, cujas propriedades vão sendo melhor compreendidas ao longo do tempo. Outro ponto fundamental é que escolher uma opção não altera as propriedades da opção escolhida e nem das outras opções possíveis.
Eureka! Me parece exatamente o tipo de problema que quero resolver. As opções são as instâncias do processador de pagamentos (default ou fallback) e as propriedades são latência e o custo. E nesse problema específico, ter apenas 2 opções tornaria essa abordagem possível mesmo considerando as limitações de recursos (pois é, as regras da rinha definem 1.5 CPU e 350 MB de memória como o total a ser distribuído entre todos os serviços do seu backend 👍).
Dentre as diversas formas de lidar com o problema do multi-armed bandit, escolhi utilizar **Thompson Sampling com [distribuições beta](https://pt.wikipedia.org/wiki/Distribui%C3%A7%C3%A3o_beta)* para resolver o dilema entre continuar enviando as requisições para a instância escolhida por parecer ser a mais atrativa (fase de exploitation) ou testar outra opção para descobrir se vale a pena enviar as requisições para outra instância (fase de exploration*).
Uma característica que achei muito interessante é que o Thompson Sampling tem a capacidade de auto-correção, ou seja, quando comete um erro ao explorar uma opção ruim, ele corrige sua estratégia de forma imediata, reduzindo a chance de repetir a mesma escolha equivocada. Portanto ideal para o cenário da Rinha em que durante o teste as condições seriam imprevisíveis e altamente dinâmicas, necessitando de um balanceamento que se adaptasse constantemente às condições do momento.
A forma como isso funciona, sem me aprofundar muito na matemática envolvida, é que cada instância de payment-processor terá dois valores associados (alpha e beta) que servem para definir uma distribuição Beta.
Para escolher uma das duas opções, é sorteado aleatoriamente um valor pertencente à distribuição Beta de cada instância, este valor é o score e a opção com o maior score será a escolhida.
Feita a escolha, a requisição é enviada e a distribuição beta da instância escolhida é atualizada da seguinte forma: em caso de sucesso incrementamos alpha, em caso de falha incrementamos beta. Quanto maior o valor de alpha em relação à beta, maior a probabilidade do próximo score sorteado ser alto.
O algoritmo que implementei para calcular o score trata casos em que o processor escolhido respondeu com sucesso mas com uma latência maior do que um limite configurado (latencyThreshold) e mesmo quando a latência está abaixo do limite também incremento o valor de beta para equilibrar a distribuição, visto que ao longo do teste são feitas dezenas de milhares de requisições e a cada requisição a distribuição é atualizada.
Mas como diria Linus Torvalds: “Talk is cheap. Show me the code”! Então aqui está o código da minha implementação em Go para a atualização da distribuição:
func (lb *LoadBalancer) UpdateLatency(stats *ReplicaStats, responseTime int64) {
if responseTime < 0 || responseTime > lb.latencyThreshold {
// ^ responseTime negativo indica que a requisição falhou
inc := 1.0
if responseTime > lb.latencyThreshold {
// requisições processadas com sucesso mas com latência
// maior que o limite configurado são interpretadas como "falha"
// incremento proporcional à latencia (min: 0.5, max: ~1.5)
inc = float64((responseTime-lb.latencyThreshold)/responseTime) + 0.5
}
stats.Lock()
stats.LatencyBeta += inc // Em caso de "falha" apenas beta é incrementado
stats.Unlock()
return
}
// Em caso de sucesso:
// score normalizado, quanto mais perto de 1.0 melhor (min: 0.0, max: ~0.99)
latencyScore := math.Max(0, float64(lb.latencyThreshold-responseTime)) / float64(lb.latencyThreshold)
// incremento proporcional ao latencyScore
// maior o latencyScore, maior o incremento (min: 0.1, max: ~0.99)
weightedAlphaIncrement := 0.1 + 0.9*latencyScore
// também incrementa beta para equilibrar a distribuição
// maior o latencyScore, menor o incremento (min: 0.1, max: ~0.5)
weightedBetaIncrement := 0.1 + 0.4*(1-latencyScore)
stats.Lock()
stats.LatencyAlpha += weightedAlphaIncrement
stats.LatencyBeta += weightedBetaIncrement
stats.Unlock()
}
E a forma como o score é calculado ficou assim:
// Este é apenas um trecho da função que seleciona qual instância utilizar,
// omiti o resto pra simplificar
lb.DefaultReplica.Stats.RLock()
betaDefault := distuv.Beta{
Alpha: lb.DefaultReplica.Stats.LatencyAlpha,
Beta: lb.DefaultReplica.Stats.LatencyBeta,
}
lb.DefaultReplica.Stats.RUnlock()
lb.FallbackReplica.Stats.RLock()
betaFallback := distuv.Beta{
Alpha: lb.FallbackReplica.Stats.LatencyAlpha,
Beta: lb.FallbackReplica.Stats.LatencyBeta,
}
lb.FallbackReplica.Stats.RUnlock()
// sorteia valor aleatório na distribuição Beta
scoreDefault := betaDefault.Rand()
// score do fallback sempre será penalizado por conta do custo
// CostWeight pertence ao intervalo [0.0, 1.0]
scoreFallback := betaFallback.Rand() * lb.CostWeight
A respeito do lb.CostWeight aplicado ao fallback, este valor é definido por CostWeight: 1.0 - env.COST_WEIGHT a partir da configuração da variável de ambiente:
## Mais perto de 1.0 maior é a penalidade pelo custo
COST_WEIGHT=0.25
E com essa implementação pude observar durante a execução dos testes que, com valores mais baixos de COST_WEIGHT, o balanceamento reage corretamente às mudanças.
Em estágios do teste em que o default possui uma latência mais alta, após algumas requisições o balanceador “aprende” que é preferível passar a encaminhar as requisições para o fallback e a partir do momento que o default passa a ter uma latência menor a ponto do custo do fallback não valer mais a pena, novamente o balanceamento passa a encaminhar as requisições para o default!
O valor configurado em COST_WEIGHT será determinante nessa dinâmica pois é esta configuração que especifica se vale mais a pena (ou não) pagar uma taxa mais cara para conseguir processar um volume maior de pagamentos.

Na maioria dos estágios deste teste o fallback tem menor latência. Com 0.25 de peso, o balanceador encaminhou mais requisições pro fallback, processando mais pagamentos.

Na maioria dos estágios deste teste o fallback tem menor latência. Mas com 0.75 de peso, o balanceador ainda encaminhou a maioria das requisições para o default, processando menos pagamentos no total devido à alta latência.
To be continued …
Depois de um tanto de matemática e código, acho que esse artigo já está um tanto extenso e olha que até agora comentei apenas sobre o balanceamento das requsições 😅 então por enquanto vou parando por aqui. Mas se você achou interessante o que compartilhei até agora pode deixar que vou continuar escrevendo sobre essa minha primeira experiência na Rinha de Backend!
O código completo da minha solução tá no meu Github, pra quem tiver interesse.
Agradecimentos
Aproveito pra agradecer de coração ao Zan e à toda a comunidade que torna esse evento possível! Para mim foi uma experiência muito enriquecedora participar da Rinha de Backend e é uma excelente forma de evoluir tecnicamente e aprender junto com uma comunidade de desenvolvedores admiráveis.
Recomendo demais a todas as pessoas desenvolvedoras que participem da Rinha. Mesmo pra quem tá iniciando na área, analisar as soluções propostas já é um aprendizado gigantesco. E mesmo se você não conseguir uma solução super performática ou que nem chega a rodar os testes (sei como é) tenho certeza que vai ter contribuído para tua evolução como desenvolvedor e como arquiteto de soluções.
메타데이터
- post_id
- 624197c5ae7a
- slug
- minha-primeira-participação-na-rinha-de-backend-624197c5ae7a
- url
- https://medium.com/@igorms.dev/minha-primeira-participa%C3%A7%C3%A3o-na-rinha-de-backend-624197c5ae7a
- canonical_url
- https://medium.com/@igorms.dev/minha-primeira-participa%C3%A7%C3%A3o-na-rinha-de-backend-624197c5ae7a
- author_url
- https://medium.com/@igorms.dev
- status
- ok
- fetched_at
- 2026-06-09 15:37:30