Boas práticas de gestão de dependências externas com fallback

1. Fundamentos da gestão de dependências externas

Dependências externas são serviços, APIs, bancos de dados ou bibliotecas que um sistema consome para executar suas funcionalidades. Em sistemas distribuídos modernos, é raro encontrar uma aplicação que não dependa de pelo menos um recurso externo — seja um gateway de pagamento, um serviço de autenticação ou uma API de terceiros.

Os riscos associados a essas dependências são variados: indisponibilidade temporária, latência elevada, mudanças inesperadas de contrato de API, versões incompatíveis e throttling. Sem uma estratégia de fallback, qualquer falha em uma dependência externa pode se propagar para o usuário final, resultando em erros 500, timeouts ou degradação total do sistema.

O fallback é uma estratégia de resiliência que define um plano B quando o recurso primário falha. Ele garante continuidade operacional, mesmo que com funcionalidades reduzidas, e é essencial para manter a experiência do usuário em cenários adversos.

2. Padrões de fallback: circuit breaker e retry com backoff

Dois padrões fundamentais para gestão de dependências externas são o Circuit Breaker e o Retry com backoff.

O Circuit Breaker monitora chamadas a um serviço externo e mantém três estados:
- Fechado: operação normal, chamadas são enviadas diretamente.
- Aberto: após um número configurável de falhas consecutivas, o circuito abre e todas as chamadas falham imediatamente sem tentar o serviço.
- Semi-aberto: após um tempo de espera, algumas requisições são permitidas para testar se o serviço se recuperou.

Exemplo conceitual de estados do Circuit Breaker:

Estado: FECHADO
  -> 5 falhas consecutivas
  -> Transição para ABERTO
  -> Espera 30 segundos
  -> Transição para SEMI-ABERTO
  -> 3 requisições de teste
     Se sucesso: volta para FECHADO
     Se falha: volta para ABERTO

O padrão Retry com exponential backoff tenta novamente a operação após um intervalo crescente entre tentativas. O jitter adiciona aleatoriedade ao intervalo para evitar que múltiplos clientes ataquem o serviço ao mesmo tempo:

Tentativa 1: espera 1 segundo
Tentativa 2: espera 2 segundos
Tentativa 3: espera 4 segundos
Tentativa 4: espera 8 segundos + jitter aleatório

A combinação ideal é: primeiro aplicar retry com backoff para erros transitórios (timeouts, 503), e após esgotar as tentativas, ativar o Circuit Breaker para evitar sobrecarga. O fallback deve ser acionado quando o circuito está aberto ou após todas as tentativas falharem.

3. Estratégias de fallback: cache, dados estáticos e degradação funcional

Existem três estratégias principais de fallback para dependências externas:

Fallback via cache: armazena respostas bem-sucedidas anteriores com um TTL (time-to-live) adequado. Se o serviço externo falhar, o sistema retorna o dado em cache, mesmo que ligeiramente desatualizado.

Tentativa: consultar API de preços
  Sucesso -> armazenar em cache por 5 minutos
  Falha -> retornar cache (máximo 30 minutos de idade)

Fallback com dados estáticos: para funcionalidades não críticas, use valores padrão ou dados embutidos no código. Exemplo: se o serviço de cotação de moedas falha, usar uma taxa fixa definida em configuração.

Degradação graciosa: desabilite funcionalidades secundárias mantendo o core. Por exemplo, se o sistema de recomendações falha, o usuário ainda pode comprar produtos, apenas sem sugestões personalizadas.

4. Implementação prática de fallback com timeouts e health checks

Timeouts são cruciais para evitar que uma dependência lenta trave todo o sistema. Configure timeouts por chamada e por componente:

Configurações de timeout:
  API de pagamentos: 5 segundos
  Serviço de notificações: 3 segundos
  Consulta de estoque: 2 segundos
  Timeout total da operação: 10 segundos

Health checks periódicos monitoram a disponibilidade do serviço externo:

Health Check a cada 30 segundos:
  GET /health do serviço externo
  Resposta 200 -> serviço saudável
  Resposta 5xx ou timeout -> serviço indisponível
  Atualizar status interno: "disponível" ou "falha"

Quando o health check indica recuperação, o sistema pode reativar gradualmente as chamadas diretas, primeiro com tráfego reduzido (canary) e depois com carga total.

5. Tratamento de erros e logging estruturado em cenários de fallback

É essencial diferenciar erros transitórios (que podem ser resolvidos com retry) de erros permanentes (que exigem fallback imediato):

Erro transitório: timeout, 503 Service Unavailable, 429 Too Many Requests
  Ação: retry com backoff

Erro permanente: 400 Bad Request, 404 Not Found, 500 Internal Server Error
  Ação: fallback imediato, sem retry

Logs estruturados devem conter contexto suficiente para debug:

Log de fallback ativado:
  {
    "request_id": "abc-123",
    "servico": "api-pagamentos",
    "erro": "timeout após 5s",
    "tentativas": 3,
    "fallback_ativado": "cache",
    "idade_cache": "2 minutos",
    "usuario_id": "user-456",
    "timestamp": "2025-01-15T10:30:00Z"
  }

Métricas importantes: taxa de falhas por serviço, número de ativações de fallback, latência média e tempo de recuperação. Alertas devem ser configurados para quando a taxa de fallback ultrapassar 10% em um período de 5 minutos.

6. Testes e validação de resiliência com fallback

Testes de integração devem simular falhas de dependências externas:

Cenário de teste: API de frete indisponível
  1. Mockar API de frete para retornar 503
  2. Chamar serviço de checkout
  3. Verificar se fallback para cache é ativado
  4. Verificar se log de fallback é gerado
  5. Verificar se usuário vê preço base (não personalizado)

Chaos engineering permite testar resiliência em produção ou staging controlada:

Experimento de caos:
  1. Injetar latência de 10 segundos em 30% das chamadas à API de estoque
  2. Observar comportamento do Circuit Breaker
  3. Verificar tempo de resposta do sistema com fallback
  4. Medir impacto na experiência do usuário

A validação deve garantir que o sistema degrada graciosamente e não quebra completamente em cenários extremos.

7. Exemplo completo: dependência externa com fallback em camadas

Abaixo, um fluxo típico de fallback em três camadas para uma API de preços:

Função obterPrecoProduto(produtoId):

  1. TENTATIVA DIRETA
     Chamar API de preços externa
     Timeout: 3 segundos
     Se sucesso:
       Atualizar cache local com TTL 5 minutos
       Retornar preço obtido
     Se falha (timeout ou erro 5xx):
       Ir para passo 2

  2. RETRY COM BACKOFF
     Para tentativa = 1 até 3:
       Aguardar (2^tentativa) segundos + jitter
       Tentar novamente chamada à API
       Se sucesso:
         Atualizar cache
         Retornar preço obtido
     Se todas as tentativas falharem:
       Ir para passo 3

  3. FALLBACK VIA CACHE
     Verificar cache local
     Se cache existe e idade < 30 minutos:
       Retornar preço do cache
       Registrar log: "fallback_cache"
     Se cache não existe ou expirado:
       Ir para passo 4

  4. FALLBACK ESTÁTICO
     Retornar preço padrão de configuração
     Registrar log: "fallback_estatico"
     Ativar alerta de indisponibilidade

Comparação de cenários:

Sem fallback:
  API falha -> usuário vê erro 500 -> abandona compra

Com fallback:
  API falha -> fallback cache -> usuário vê preço (possivelmente desatualizado)
  Cache expirado -> fallback estático -> usuário vê preço padrão
  Experiência mantida, mesmo com dados imperfeitos

A implementação de fallback em camadas garante que o sistema nunca retorne um erro para o usuário final em funcionalidades não críticas, mantendo a continuidade do negócio mesmo sob falhas de dependências externas.

Referências