Pular para o conteúdo

Desafios de entrevista em Go · parte 5 de 11

Stone - Financial Transfer & Deadlock Prevention

avançadoPublicada em · 5 min de leitura

Tamanho da fonte:
Largura da coluna:

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 Conflict ou 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 ROLLBACK integral 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

  1. Uso de inteiros em vez de ponto flutuante: Representar quantias monetárias como inteiros em centavos (int64 / cents) para evitar perdas cumulativas de arredondamento.
  2. Testes de estresse com concorrência pesada: O candidato deve incluir testes automatizados usando sync.WaitGroup disparando 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).
  3. 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.

↑ Voltar ao topo

Atalhos de teclado

Abrir busca
/
Alternar tema claro/escuro
t
Aula anterior na série
←
Próxima aula na série
→
Lista de atalhos
?