Pular para o conteúdo

Desafios de entrevista em Go · parte 3 de 11

Nubank - Authorizer

avançadoPublicada em · 5 min de leitura

Tamanho da fonte:
Largura da coluna:

Dados da empresa e da vaga

  • Empresa: Nubank (Nu Holdings Ltd.)
  • Sede: São Paulo, Brasil (com polos em Buenos Aires, Cidade do México, Berlim e contratação remota global)
  • Perfil da vaga: Senior Backend Software Engineer — Core Banking & Authorizer Systems
  • Modelo de trabalho: Remoto internacional / Híbrido
  • Faixa salarial estimada da vaga:
    • Sênior: R$ 26.000 a R$ 38.000/mês / $75.000 a $120.000 USD/ano + Equity (RSU)
    • Staff / Principal: R$ 38.000 a R$ 55.000+/mês / $120.000 a $180.000+ USD/ano
  • Stack principal do time: Go, Clojure, Kafka, AWS, Docker, Kubernetes, Datomic

Contexto do desafio técnico

O Authorizer (Autorizador de Transações) é o desafio prático de arquitetura e código mais representativo do core bancário do Nubank.

O objetivo é implementar uma aplicação que processe um fluxo contínuo de operações via stdin simulando um autorizador de cartão de crédito em tempo real. A aplicação deve manter o estado da conta em memória, validar regras de negócio rígidas (como limite disponível, cartões bloqueados, transações em alta frequência e transações duplicadas) e imprimir o estado atualizado da conta e as eventuais violações no stdout.

O avaliador do Nubank analisa:

  1. Composição e Clean Architecture: Código desacoplado, sem acoplamento entre regras de validação.
  2. Imutabilidade e gestão de estado: Funções puras que transformam o estado de forma segura e determinística.
  3. Modelagem de regras com padrão Strategy / Chain of Responsibility: Facilidade de adicionar novas regras de negócio sem alterar o fluxo central do autorizador.
  4. Testes automatizados exaustivos: Testes unitários para cada regra isolada e testes de integração de ponta a ponta.

Formato de entrada e saída

1. Entrada (stdin)

O programa recebe uma sequência de linhas contendo objetos JSON. Existem dois tipos de operações:

A. Criação de Conta (account)

{"account": {"active-card": true, "available-limit": 1000}}

B. Autorização de Transação (transaction)

{"transaction": {"merchant": "Uber", "amount": 25, "time": "2026-02-13T10:00:00.000Z"}}

2. Saída (stdout)

Para cada linha recebida, o programa deve emitir o estado consolidado da conta e um array com os nomes das violações de regras identificadas:

Sucesso (sem violações):

{"account": {"active-card": true, "available-limit": 975}, "violations": []}

Rejeição (com violações):

{"account": {"active-card": true, "available-limit": 1000}, "violations": ["insufficient-limit"]}

Regras de negócio e validações

Uma transação pode violar múltiplas regras simultaneamente. Nesse caso, a saída deve listar todas as violações ocorridas na ordem de validação, e o limite da conta não deve ser debitado.

1. Inicialização de conta

  • account-not-initialized: Se uma transação for processada antes da inicialização de uma conta.
  • account-already-initialized: Se uma operação de criação de conta for executada quando a conta já foi inicializada anteriormente.

2. Estado do cartão e limite de crédito

  • card-not-active: Uma transação não pode ser aprovada se o cartão estiver inativo ("active-card": false).
  • insufficient-limit: O valor da transação não pode exceder o limite disponível (available-limit) no momento.

3. Detecção de fraudes e frequência (Janela de Tempo Móvel de 2 Minutos)

  • high-frequency-small-interval: Não devem existir mais de 3 transações aprovadas em um intervalo inferior a 2 minutos.
  • doubled-transaction: Não deve existir mais de 1 transação aprovada com o mesmo comerciante (merchant) e o mesmo valor (amount) em um intervalo inferior a 2 minutos.

Observação sobre a janela temporal: Apenas transações aprovadas anteriormente entram no histórico que afeta a contagem de frequência e duplicidade. Transações rejeitadas não afetam as regras futuras.


Casos de teste oficiais da avaliação

Cenário 1: Inicialização e transações bem-sucedidas

Entrada (stdin):

{"account": {"active-card": true, "available-limit": 100}}
{"transaction": {"merchant": "Burger King", "amount": 20, "time": "2026-02-13T10:00:00.000Z"}}
{"transaction": {"merchant": "Hering", "amount": 30, "time": "2026-02-13T10:00:10.000Z"}}

Saída esperada (stdout):

{"account": {"active-card": true, "available-limit": 100}, "violations": []}
{"account": {"active-card": true, "available-limit": 80}, "violations": []}
{"account": {"active-card": true, "available-limit": 50}, "violations": []}

Cenário 2: Cartão inativo e limite insuficiente

Entrada (stdin):

{"account": {"active-card": false, "available-limit": 100}}
{"transaction": {"merchant": "Burger King", "amount": 150, "time": "2026-02-13T10:00:00.000Z"}}

Saída esperada (stdout):

{"account": {"active-card": false, "available-limit": 100}, "violations": []}
{"account": {"active-card": false, "available-limit": 100}, "violations": ["card-not-active", "insufficient-limit"]}

Cenário 3: Alta frequência e transação duplicada

Entrada (stdin):

{"account": {"active-card": true, "available-limit": 500}}
{"transaction": {"merchant": "Uber", "amount": 20, "time": "2026-02-13T10:00:00.000Z"}}
{"transaction": {"merchant": "Uber", "amount": 20, "time": "2026-02-13T10:00:30.000Z"}}
{"transaction": {"merchant": "Uber", "amount": 20, "time": "2026-02-13T10:01:00.000Z"}}
{"transaction": {"merchant": "Uber", "amount": 20, "time": "2026-02-13T10:01:30.000Z"}}

Saída esperada (stdout):

{"account": {"active-card": true, "available-limit": 500}, "violations": []}
{"account": {"active-card": true, "available-limit": 480}, "violations": []}
{"account": {"active-card": true, "available-limit": 480}, "violations": ["doubled-transaction"]}
{"account": {"active-card": true, "available-limit": 480}, "violations": ["doubled-transaction"]}
{"account": {"active-card": true, "available-limit": 480}, "violations": ["high-frequency-small-interval", "doubled-transaction"]}

Arquitetura recomendada em Go

cmd/
  authorizer/
    main.go              # Loop de leitura e escrita JSON
internal/
  domain/
    account.go           # Entidades Account e Transaction
    violations.go        # Constantes tipadas de violação
  authorizer/
    service.go           # Orquestrador de transações
    validator.go         # Interface Validator
    rules/
      initialized.go     # Validação de conta existente
      active_card.go     # Validação de cartão ativo
      limit.go           # Validação de saldo disponível
      frequency.go       # Validação de janela de 2 minutos (alta frequência)
      doubled.go         # Validação de janela de 2 minutos (mesmo merchant/amount)

Design Pattern idiomático de Validações em Go:

package authorizer

import (
	"time"
)

type Violation string

const (
	AccountNotInitialized     Violation = "account-not-initialized"
	AccountAlreadyInitialized Violation = "account-already-initialized"
	CardNotActive             Violation = "card-not-active"
	InsufficientLimit         Violation = "insufficient-limit"
	HighFrequencySmallInterval Violation = "high-frequency-small-interval"
	DoubledTransaction        Violation = "doubled-transaction"
)

type Account struct {
	ActiveCard     bool `json:"active-card"`
	AvailableLimit int  `json:"available-limit"`
}

type Transaction struct {
	Merchant string    `json:"merchant"`
	Amount   int       `json:"amount"`
	Time     time.Time `json:"time"`
}

type Validator interface {
	Validate(account *Account, history []Transaction, current Transaction) *Violation
}

type Service struct {
	account     *Account
	history     []Transaction
	validators  []Validator
	initialized bool
}

func (s *Service) ProcessTransaction(t Transaction) ([]Violation, *Account) {
	if !s.initialized {
		return []Violation{AccountNotInitialized}, nil
	}

	var violations []Violation
	for _, v := range s.validators {
		if violation := v.Validate(s.account, s.history, t); violation != nil {
			violations = append(violations, *violation)
		}
	}

	if len(violations) == 0 {
		s.account.AvailableLimit -= t.Amount
		s.history = append(s.history, t)
	}

	return violations, s.account
}

O que os avaliadores do Nubank procuram no código

  1. Separação de responsabilidades: Não colocar a lógica de cálculo de tempo ou limite dentro do loop de main.go. Cada regra deve ser uma struct que implementa uma interface simples.
  2. Manipulação de datas com time.Time: Uso correto de time.Parse(time.RFC3339Nano, ...) e métodos como t.Sub() para calcular a janela de 2 minutos (2 * time.Minute).
  3. Testes parametrizados: Testar as regras de frequência e duplicação com baterias de transações em milissegundos para garantir que o algoritmo não apresente erros de arredondamento de timestamp.

↑ 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
?