Desafios de entrevista em Go · parte 5 de 11
Stone - Financial Transfer & Deadlock Prevention
avançadoPublicada em · 5 min de leitura
Dados da empresa e da vaga
- Empresa: StoneCo Ltd. (Stone / Pagar.me)
- Sede: São Paulo / Rio de Janeiro, Brasil (com contratação remota e listada na NASDAQ: STNE)
- Perfil da vaga: Senior Backend Software Engineer — Core Banking, Ledger & Payment Rails
- Modelo de trabalho: Remoto ou híbrido
- Faixa salarial estimada da vaga:
- Sênior: R$ 22.000 a R$ 35.000/mês (CLT/PJ) / $70.000 a $110.000 USD/ano
- Tech Lead / Staff: R$ 35.000 a R$ 50.000+/mês / $110.000 a $150.000+ USD/ano + Bônus anual e stock options
- Stack principal do time: Go, PostgreSQL, Apache Kafka, Docker, Kubernetes, gRPC, Redis, Datadog
Contexto do desafio técnico
A Stone é uma das maiores adquirentes e instituições financeiras do Brasil, processando bilhões de reais em transações diárias. Em seus processos seletivos para engenheiros de backend sêniores em Go, o foco principal é a consistência contábil inegociável, prevenção ativa de deadlocks em transações simultâneas e garantia absoluta de idempotência.
Este desafio técnico exige que o candidato projete e implemente uma API de Transferência Financeira Concorrente e Idempotente entre contas bancárias, simulando a movimentação de saldo em um razão contábil (ledger), garantindo que duas contas possam transferir dinheiro entre si simultaneamente em sentidos opostos sem congelar o sistema em um impasse (deadlock).
Requisitos de negócio e desafios técnicos centrais
1. Prevenção de Deadlock em Transferências Cruzadas
O maior erro cometido por candidatos neste desafio ocorre quando duas goroutines tentam realizar transferências mútuas simultâneas:
- Goroutine 1: Transfere R$ 100 da Conta A para a Conta B.
- Goroutine 2: Transfere R$ 50 da Conta B para a Conta A.
Se a Goroutine 1 travar a Conta A e tentar travar a Conta B, enquanto a Goroutine 2 travou a Conta B e tenta travar a Conta A, o sistema entra em espera circular (circular wait) e congela em um deadlock.
Solução obrigatória (Ordenação Determinística de Travas):
Independentemente de quem é a conta de origem (from) ou de destino (to), o sistema deve adquirir as travas (sejam mutexes em memória ou SELECT ... FOR UPDATE no banco relacional) sempre em uma ordem global e previsível baseada no identificador único da conta (ex.: min(ID_A, ID_B) primeiro, depois max(ID_A, ID_B)).
2. Idempotência com chave única (Idempotency-Key)
Em sistemas financeiros, oscilações de rede e timeouts de clientes HTTP frequentemente causam retentativas automáticas da mesma chamada de transferência.
- O cliente deve enviar um cabeçalho obrigatório:
Idempotency-Key: <UUID>. - Se a chave já tiver sido processada com sucesso anteriormente, a API deve responder imediatamente com o mesmo código HTTP e o mesmo payload da transação original, sem debitar a conta novamente.
- Se a requisição ainda estiver em processamento por outra goroutine, a API deve retornar
HTTP 409 Conflictou aguardar de forma controlada.
3. Integridade contábil e transações ACID
- Não é permitido saldo negativo (a conta pagadora deve ter saldo suficiente).
- O débito da conta de origem e o crédito na conta de destino devem ocorrer dentro da mesma transação atômica (
sql.Tx). - Se qualquer passo falhar, um
ROLLBACKintegral deve ser executado.
Exemplo de implementação da lógica anti-deadlock em Go
package ledger
import (
"context"
"database/sql"
"errors"
"fmt"
)
var (
ErrInsufficientFunds = errors.New("saldo insuficiente")
ErrSameAccount = errors.New("conta de origem e destino devem ser diferentes")
)
type Account struct {
ID string
Balance int64 // Valores monetários representados em centavos (evita float)
}
type TransferService struct {
db *sql.DB
}
func (s *TransferService) ExecuteTransfer(ctx context.Context, fromID, toID string, amountCents int64) error {
if fromID == toID {
return ErrSameAccount
}
if amountCents <= 0 {
return errors.New("o valor da transferência deve ser positivo")
}
tx, err := s.db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelReadCommitted})
if err != nil {
return fmt.Errorf("falha ao iniciar transação: %w", err)
}
defer tx.Rollback()
// ORDENAÇÃO DETERMINÍSTICA DE LOCKS PARA PREVENÇÃO DE DEADLOCK:
// Trava sempre o menor ID primeiro, eliminando qualquer possibilidade de espera circular.
firstID, secondID := fromID, toID
if fromID > toID {
firstID, secondID = toID, fromID
}
// 1. Bloqueia e consulta o primeiro registro
var firstBalance int64
err = tx.QueryRowContext(ctx, "SELECT balance FROM accounts WHERE id = $1 FOR UPDATE", firstID).Scan(&firstBalance)
if err != nil {
return fmt.Errorf("conta %s não encontrada: %w", firstID, err)
}
// 2. Bloqueia e consulta o segundo registro
var secondBalance int64
err = tx.QueryRowContext(ctx, "SELECT balance FROM accounts WHERE id = $1 FOR UPDATE", secondID).Scan(&secondBalance)
if err != nil {
return fmt.Errorf("conta %s não encontrada: %w", secondID, err)
}
// 3. Validação de saldo da conta pagadora
fromBalance := firstBalance
if fromID == secondID {
fromBalance = secondBalance
}
if fromBalance < amountCents {
return ErrInsufficientFunds
}
// 4. Débito e Crédito atômicos
_, err = tx.ExecContext(ctx, "UPDATE accounts SET balance = balance - $1 WHERE id = $2", amountCents, fromID)
if err != nil {
return fmt.Errorf("erro ao debitar conta de origem: %w", err)
}
_, err = tx.ExecContext(ctx, "UPDATE accounts SET balance = balance + $1 WHERE id = $2", amountCents, toID)
if err != nil {
return fmt.Errorf("erro ao creditar conta de destino: %w", err)
}
// 5. Registro contábil no Ledger de auditoria
_, err = tx.ExecContext(ctx,
"INSERT INTO transfers (from_account_id, to_account_id, amount, created_at) VALUES ($1, $2, $3, NOW())",
fromID, toID, amountCents,
)
if err != nil {
return fmt.Errorf("erro ao gravar log de transferência: %w", err)
}
return tx.Commit()
}
O que os avaliadores da Stone analisam
- Uso de inteiros em vez de ponto flutuante: Representar quantias monetárias como inteiros em centavos (
int64/ cents) para evitar perdas cumulativas de arredondamento. - Testes de estresse com concorrência pesada: O candidato deve incluir testes automatizados usando
sync.WaitGroupdisparando 100 ou mais goroutines de transferências cruzadas simultâneas entre as mesmas contas, garantindo:- Zero deadlocks detectados.
- Conservação perfeita da soma total dos saldos ao final da execução ($\text{Saldo Total Final} = \text{Saldo Total Inicial}$).
- Aprovação limpa sob o detector de condições de corrida (
go test -race).
- Mecanismo robusto de idempotência: Tratamento com bloqueio atômico distribuído (Redis) ou tabela de chaves no PostgreSQL com restrição
UNIQUE, garantindo que requisições repetidas não causem movimentações duplicadas.