Desafio Técnico — Pessoa Desenvolvedora Android Sênior
GOK | Inovação Digital
Visão Geral do Desafio
Você está sendo entrevistado para uma posição Android Sênior em uma consultoria que atende projetos de alta criticidade transacional — incluindo SDKs financeiros, apps de pagamento e sistemas bancários mobile.
O Tech Lead vai simular um cenário real de um dos projetos ativos.
Atenção: Este não é um exercício acadêmico. O problema apresentado representa um bug com impacto financeiro direto em produção. A profundidade da sua resposta será proporcional à senioridade demonstrada.
O Problema
“Temos um app Android de pagamentos que processa operações Pix. O usuário inicia uma transferência, o app chama nossa API via BFF, e a operação é confirmada ou rejeitada. O problema: estamos tendo um aumento de reclamações de usuários dizendo que transferências aparecem como ‘em processamento’ por tempo indeterminado — e em alguns casos o usuário tenta novamente e a operação é processada em duplicata.”
Sua Tarefa
Projete e implemente — ou descreva com profundidade técnica suficiente para que um dev júnior consiga implementar — uma solução Android que:
- Realize a chamada de transferência Pix com resiliência adequada
- Gerencie corretamente o estado da operação do ponto de vista do cliente
- Previna duplicatas no lado cliente, sem depender exclusivamente do backend
- Garanta que a UI reflita sempre um estado consistente, mesmo em falha de rede, timeout ou resposta ambígua do servidor
Restrições Técnicas
| Requisito | Detalhe |
|---|---|
| Linguagem | Kotlin com Coroutines e Flow |
| Arquitetura | Clean Architecture com separação clara de camadas |
| Estado da UI | Exposto via StateFlow no ViewModel |
| Modelagem de estados | Máquina de estados explícita — sem booleanos soltos ou flags implícitas |
| Escopo de coroutines | GlobalScope é proibido |
| Testabilidade | Contratos de interfaces definidos; localização dos testes indicada |
| Camada de rede | Retrofit + OkHttp |
Entrada e Saída Esperadas
Entrada — dados da transferência
data class PixTransferRequest(
val destinationKey: String,
val amount: BigDecimal,
val description: String?,
val idempotencyKey: String // gerado pelo cliente antes do envio
)
Saída — estado exposto para a UI
sealed class PixTransferUiState {
object Idle : PixTransferUiState()
object Loading : PixTransferUiState()
data class Success(val receipt: TransferReceipt) : PixTransferUiState()
data class Error(val type: PixTransferError) : PixTransferUiState()
object AwaitingConfirmation : PixTransferUiState() // estado ambíguo — resposta não chegou
}
sealed class PixTransferError {
object NetworkTimeout : PixTransferError()
object InsufficientFunds : PixTransferError()
object DuplicateOperation : PixTransferError()
data class Unknown(val message: String) : PixTransferError()
}
Atenção: O estado
AwaitingConfirmationé intencional e obrigatório. Candidatos que o omitirem ou o tratarem como erro genérico serão questionados diretamente sobre as implicações para o negócio.
O Que Deve Ser Entregue
Código 100% compilável não é obrigatório. O esperado é:
- [ ] Diagrama mental ou textual da máquina de estados
- [ ] Estrutura das camadas — quais classes existem, em qual camada e qual responsabilidade
- [ ] Implementação do ViewModel com
StateFlowe tratamento dos estados - [ ] Implementação ou contrato do Use Case de transferência
- [ ] Estratégia de idempotência no lado cliente
- [ ] Estratégia de retry — com justificativa clara de quando não fazer retry
- [ ] Comportamento da UI em cada estado, especialmente em
AwaitingConfirmation
Edge Cases — O Que Você Deve Endereçar
| Cenário | Comportamento esperado |
|---|---|
| Timeout sem resposta do servidor | App entra em AwaitingConfirmation. Nenhuma nova tentativa automática. Usuário é informado e tem opção de consultar o status |
| Usuário fecha o app durante a operação | Estado é persistido via Room. Ao reabrir, a operação em curso é recuperada e exibida corretamente |
| Duplo toque no botão “Transferir” | Segunda chamada ignorada no ViewModel — não chega ao Use Case |
| Resposta HTTP 5xx | Sem retry automático. Em operações financeiras, 5xx não é idempotente sem confirmação explícita |
| Resposta HTTP 409 (Conflict) | Mapeado para PixTransferError.DuplicateOperation com mensagem clara ao usuário |
| Perda de conexão durante polling | Coroutines canceladas corretamente, sem vazamentos |
Quando regenerar a idempotencyKey? |
Apenas quando o usuário inicia explicitamente uma nova operação — nunca em retry automático da mesma tentativa |
Critérios de Avaliação
🔴 Eliminatórios — ausência ou erro grave descarta o candidato
| Critério | O que é avaliado |
|---|---|
| Máquina de estados explícita | Estados modelados de forma clara, sem booleanos implícitos ou flags acopladas |
| Idempotência no cliente | idempotencyKey gerada antes da chamada, mantida na mesma tentativa, nunca regenerada em retry automático |
| Ausência de retry automático em timeout financeiro | Compreensão de que retry cego em operações não idempotentes é a causa raiz do bug |
AwaitingConfirmation tratado com seriedade |
Não descartado como erro genérico — implicações para o usuário e para o negócio claramente articuladas |
🟡 Diferenciadores — separa sênior de pleno
| Critério | O que é avaliado |
|---|---|
| Separação de camadas | ViewModel não chama Retrofit; Use Case não conhece ViewModel; Repository é abstrato |
| Persistência de estado | Considera interrupção do processo e reabertura do app — Room ou equivalente utilizado |
| Testabilidade explicitada | Localização dos testes por camada e dependências mockadas com MockK |
| Cancelamento de coroutines | Comportamento correto quando o ViewModel é destruído durante a operação |
| UX no estado ambíguo | Proposta clara: mensagem informativa, opção de consultar status, ausência do botão “Tentar novamente” |
🟢 Excelência — indica candidato acima do esperado
| Critério | O que é avaliado |
|---|---|
Polling ou webhook para AwaitingConfirmation |
Consulta de status com backoff exponencial, cancelamento correto e timeout máximo definido |
| Segurança | idempotencyKey não previsível (UUID v4); estado ambíguo não reexibido como erro para evitar nova tentativa do usuário |
| Observabilidade | Log estruturado dos estados da operação para auditoria — especialmente relevante em contexto fintech |
| Trade-offs explicitados | Candidato menciona o que não está fazendo e por quê, demonstrando consciência das decisões de arquitetura |
Raciocínio Esperado — Padrão de Profundidade
Dica estratégica: Este é o nível de pensamento que o Tech Lead espera ouvir. Não é a única resposta correta — é o padrão mínimo de profundidade para uma posição sênior.
Passo 1 — Identificar o problema real antes de codar
“O bug de duplicata não é um bug de rede — é um bug de estado. O app não distingue ‘a operação falhou’ de ‘a operação foi enviada, mas não recebi confirmação’. Quando o usuário tenta novamente, o app trata os dois casos da mesma forma. Preciso primeiro modelar os estados possíveis da operação.”
Passo 2 — Definir a máquina de estados
“Uma transferência Pix, do ponto de vista do cliente, tem os seguintes estados:
Idle→Loading→Success,ErrorouAwaitingConfirmation. O estado crítico é o terceiro — é quando enviei a requisição, mas não sei o que aconteceu com ela. Esse estado deve persistir, não ser apagado.”
Passo 3 — Decidir sobre retry
“Não farei retry automático em timeout. Se o servidor recebeu a requisição mas o ACK se perdeu, um retry com a mesma
idempotencyKeypode ser seguro — mas somente se o backend implementar idempotência corretamente. Como não tenho essa garantia, deixo o usuário decidir. Retry automático só é seguro quando tenho certeza de que a operação não foi processada.”
Passo 4 — Estratégia de idempotência
“A
idempotencyKeyé gerada uma única vez, antes de qualquer tentativa, e fica associada a essa intenção de transferência. Se o usuário cancelar e iniciar uma nova transferência, gero uma nova chave. Se tentar novamente após um timeout, uso a mesma chave — isso permite que o backend detecte a duplicata e retorne o resultado original.”
Passo 5 — Estrutura das camadas
“O ViewModel expõe
StateFlow<PixTransferUiState>, coordena o Use Case e protege contra double-tap com um flag de operação em andamento ou viastateIn. O Use Case contém a lógica de negócio — chama o Repository, trata os retornos e persiste o estado no banco local antes de retornar ao ViewModel. O Repository abstrai a fonte de dados: API ou banco local, dependendo do estado atual. A camada de dados mantémRemoteDataSource(Retrofit) eLocalDataSource(Room) separados.”
Passo 6 — UX para AwaitingConfirmation
“Quando entro em
AwaitingConfirmation, exibo uma tela específica — não uma tela de erro. O texto seria: ‘Sua transferência está sendo processada. Isso pode levar alguns instantes. Consulte seu extrato para confirmar.’ Ofereço um botão ‘Verificar status’, que dispara uma consulta manual ao backend. Não ofereço ‘Tentar novamente’ — isso aumentaria o risco de duplicata do ponto de vista do usuário.”
Passo 7 — Ciclo de vida e persistência
“Se o usuário fechar o app enquanto está em
LoadingouAwaitingConfirmation, persisto esse estado no Room. Ao reabrir, faço uma consulta de status automática usando aidempotencyKeysalva. Se o servidor confirmar que a operação foi processada, exiboSuccess. Se negar, exiboError. Se ainda estiver processando, mantenhoAwaitingConfirmationcom atualização de status.”
Dica estratégica: O candidato que chega ao Passo 7 de forma natural — sem ser guiado — demonstra maturidade real em ambientes transacionais críticos. É esse nível de pensamento que a GOK precisa em produção.