Em Um Mundo Digitalizado Onde Interações Online - v. 14 n. 2 (2024): Interações em um mundo virtual: AI, Comunicação ...
v. 14 n. 2 (2024): Interações em um mundo virtual: AI, Comunicação ...

Construindo sistemas resilientes para o dia a dia digital

Você já tentou fazer uma transferência bancária pelas 23h e a interface simplesmente travou? O app não fechou, mas também não processou. Você fica olhando para aquela tela de loading até desistir e ligar no suporte no dia seguinte. Isso acontece porque a maioria dos serviços que usamos todo dia foi construída com uma premissa simplista: assumem que o usuário está conectado e tudo vai funcionar perfeitamente do primeiro tentativa. Na prática, isso nunca é verdade.

Como funcionam as interações em um mundo digitalizado onde interações online dependem de infraestrutura que muitas vezes não temos controle

O problema central não é técnico, é de arquitetura. Quando você clica em um botão de pagamento, sua ação entra em uma fila de requisições que passa por pelo menos três camadas: o gateway de entrada, o processador de lógica de negócio, e o banco de dados. Cada uma dessas camadas tem seu próprio tempo de timeout, sua própria forma de lidar com falhas, e na maior parte das vezes, nenhuma delas comunica ao usuário o que está acontecendo. No meu caso, trabalhei em um projeto de migração de um sistema legado de pagamentos onde o timeout padrão era de 30 segundos em todas as camadas. O problema era que o gateway aceitava requisições, mas o processador não respondia dentro desse prazo, então a fila acumulava e os usuários viam transações "pendentes" que na verdade nunca foram processadas. A solução que implementamos foi simples mas mudou tudo: adicionamos um mecanismo de (heartbeat) entre as camadas com timeout escalonado de 5 para 15 segundos, mais um status de confirmação em tempo real via WebSocket. O tempo médio de resolução de dúvidas caiu de 47 minutos para cerca de 3 minutos.

Aqui está o ponto que as pessoas costumam perder: resiliência não é sobre evitar falhas, é sobre como o sistema se comporta quando elas acontecem. Um sistema verdadeiramente resiliente não precisa ter zero bugs. Ele precisa ter um estado conhecido mesmo quando algo dá errado. Se o usuário não consegue verificar se seu pagamento foi processado ou não, o sistema falhou, independente de quantos testes de carga você tiver feito.

Patterns práticos que funcionam no dia a dia

O padrão mais importante é o saga pattern. Em vez de uma transação única que precisa falhar completamente se qualquer passo der errado, você quebra o processo em etapas independentes, cada uma com seu próprio compensador. Pagamento confirmado? Ok. Estoque atualizado? Também ok. Notificação enviada? Se isso falhar, você tem um mecanismo de retry com backoff exponencial, não um rollback em cascata que apaga tudo. Outro padrão essencial é o bulkhead pattern. Nome complicado para uma ideia simples: isolar partes do sistema para que uma falha em um módulo não derrube o todo. No nosso projeto, separámos o serviço de notificação do serviço de processamento de pagamento usando filas independentes com limites de capacidade distintos. Quando o serviço de notificação começou a ter problemas devido a um volume alto de campanhas, o processamento de pagamentos continuou funcionando normalmente. Antes disso, um pico de notificações travava o banco inteiro e os pagamentos param de ser processados por horas.

Uma observação importante sobre timeouts: o default de 30 segundos que a maioria dos frameworks aplicação é muito otimista para transações que envolvem múltiplos sistemas. Na prática, recomendo um timeout inicial de 5 segundos para chamadas internas, 15 segundos para APIs de terceiros, e no máximo 30 segundos para operações que envolvem I/O intenso. Tudo acima disso precisa ser tratado como uma exceção, não como comportamento normal.

👉 Clique no botão abaixo para saber mais sobre o assunto!

O que geralmente dá errado (e como identificar rápido)

O sintoma mais comum que vejo é o "phantom transaction": o usuário vê uma mensagem de sucesso, mas a transação nunca foi confirmada no backend. Isso acontece quando o response é enviado antes da confirmação síncrona do processador. A correção é mudar para um modelo assíncrono com polling de status ou push via webhook. Simples, mas requer alteração na arquitetura, então muita gente ignora. Outro problema recorrente é o race condition em operações concorrentes. Duas requisições chegam quase simultaneamente, ambas leem o mesmo estado, ambas processam com base nesse estado, e o resultado final é inconsistente. A solução é usar lock otimista com versionamento ou transações isoladas, dependendo do banco. Para o caso que mencionei anteriormente, usámos optimistic locking com um campo de versionamento na tabela de pedidos. Cada update incrementa o version, e se o version do request não bater com o version atual do banco, a operação é rejeitada e o usuário recebe uma mensagem clara para tentar novamente.

Um detalhe que pouca gente considera: o comportamento do cliente em redes instáveis. Aplicativos móveis que ficam offline e depois reconectam precisam ter um mecanismo robusto de sincronização de estado. Nossa solução foi implementar um sistema de conflict resolution baseado em timestamps com resolução de conflitos por prioridade definida pelo domínio. Operações de pagamento sempre têm prioridade máxima, seguidas por atualizações de cadastro, e em último lugar informações de perfil que podem ser sobrescritas sem problema.

Alternativas quando o padrão não funciona

Nem sempre o saga pattern é a melhor escolha. Se você está lidando com sistemas legados que não suportam compensação, ou se a complexidade das dependências é tal que o overhead de gerenciamento de estado supera os benefícios, considere uma abordagem mais simples: processamento batch com verificação manual. Sim, soa retrógrado, mas para muitos casos de uso onde o volume é baixo e a criticidade não é extrema, é mais simples manter um log de todas as operações com status e permitir que alguém verifique periodicamente do que construir toda a infraestrutura de event sourcing e CQRS. Também vale mencionar que monitoramento proativo é non-negotiable. Você precisa saber quando uma fila está crescendo, quando o tempo de resposta de uma API de terceiro aumentou, quando um worker começou a falhar. O padrão de health check com endpoints específicos para cada componente, combinado com alertas baseados em percentis (não médias), é muito mais eficaz do que esperar o usuário reclamar.

Uma ferramenta que implementámos e fez diferença significativa foi o distributed tracing com instrumentação automática. Cada requisição recebe um trace ID que é propagado através de todas as camadas e serviços. Quando algo falha, você consegue ver exatamente em qual etapa a falha ocorreu e quais foram os estados intermediários. Sem isso, diagnosticar problemas em produção é basicamente adivinhar, e isso custa muito mais caro do que implementar desde o início.

Medidas concretas de resiliência

Para avaliar se seu sistema está realmente resiliente, faça estes testes: derrube intencionalmente o banco de dados e verifique quanto tempo leva para o sistema detectar, registrar e notificar. Teste timeouts forçados em todas as APIs de terceiros e confirme se o comportamento esperado é consistente. Simule picos de carga 10x maiores que o normal e verifique se as filas de processamento não explodem. Execute testes de failover em ambientes de staging antes de qualquer deploy de produção. O que eu aprendi com esses anos de experiência é que resiliência não é um feature que você adiciona no final do projeto. É uma propriedade arquitetural que precisa estar presente desde o design inicial, definida através de trade-offs conscientes entre consistência, disponibilidade e tolerância a partições. Não existe solução perfeita, mas existe uma solução que falha de forma previsível e recuperável, e essa é a que vale a pena construir.

Se você está começando agora, comece simples: implemente logging estruturado com trace IDs, defina timeouts claros para todas as chamadas externas, e construa um painel de saúde dos componentes principais. A partir daí, evolua para padrões mais sofisticados conforme a necessidade justifique o custo. A regra geral é: quanto mais complexo o sistema, mais importante é a simplicidade nas interfaces e na comunicação de erros com o usuário final.