O Padrão Fallback: Projetando Degradação Graciosa em Microsserviços
Numa arquitetura de microsserviços, os serviços formam uma rede de chamadas de rede distribuídas. Embora isso permita que as equipes criem e dimensionem serviços de forma independente, também significa que a confiabilidade geral do seu sistema é tão forte quanto o seu elo mais fraco. Se um serviço crítico falhar ou parar de responder, poderá desencadear uma falha em cascata que interromperá todo o aplicativo.
Retornar um “500 Internal Server Error” genérico ou uma página em branco aos usuários no momento em que uma única dependência falha é uma experiência ruim para o usuário. Em vez disso, os sistemas resilientes são construídos para se degradarem normalmente quando as coisas dão errado.
É aqui que entra o Padrão Fallback. Ao definir um caminho de execução alternativo e seguro quando uma chamada de serviço principal falha, você pode manter seu aplicativo funcional, mesmo em um estado degradado.
Neste guia, exploraremos o padrão Fallback, estratégias comuns para implementá-lo, como ele interage com outros padrões de resiliência e como escrever lógica de fallback em Java (Resilience4j) e Go.
A analogia do mundo real: o plano de backup da cafeteria
Imagine que você entra em uma cafeteria local para comprar um café com leite. O barista insere seu pedido, mas quando você vai tocar no seu cartão, o terminal de pagamento mostra um erro de conexão – o provedor de internet da loja está com uma interrupção.
A cafeteria apaga imediatamente as luzes, tranca as portas e manda todos os clientes para casa?
Claro que não. Eles implementam uma estratégia alternativa:
- Se eles tiverem dinheiro em mãos, perguntam se você pode pagar em dinheiro.
- Se você for um cliente regular, o barista pode escrever seu nome e fazer o pedido em um livro-razão e pedir que você pague na próxima visita.
- Eles podem usar um leitor de cartão off-line que armazena o token do cartão localmente e processa o pagamento posteriormente, quando a Internet se recuperar.
Em design de software:
- O Pedido do Café é a solicitação do cliente.
- O Terminal de Cartão é o principal serviço downstream (por exemplo, uma API de gateway de pagamento).
- A interrupção da Internet é um tempo limite de rede ou falha de serviço.
- O Ledger/Leitor offline é o caminho de execução alternativo.
Estratégias comuns de fallback
Dependendo da lógica de negócios e da criticidade do serviço com falha, você pode escolher entre diversas estratégias de fallback:
1. Valores estáticos padrão
A estratégia mais simples é retornar um valor estático seguro e pré-configurado. Isso é altamente eficaz para recursos não críticos onde a exibição de dados em branco ou padrão é aceitável.
- Exemplo: se um serviço de personalização de perfil falhar, retorne uma imagem de avatar padrão e uma saudação genérica.
- Exemplo: se um serviço de recomendação falhar, retorne uma lista vazia ou uma lista codificada de best-sellers universais em vez de gerar um erro.
2. Respostas em cache (obsoleto durante revalidação)
Se os dados ativos não estiverem disponíveis, você poderá recorrer a dados obsoletos somente leitura de um cache local ou de um armazenamento de memória distribuído rápido como o Redis.
- Exemplo: se um serviço de inventário de produtos falhar, exiba a quantidade em estoque armazenada em cache há 5 minutos, junto com uma mensagem sutil na interface do usuário indicando que os dados podem não estar totalmente atualizados.
- Exemplo: se um serviço de configurações do usuário falhar, carregue o perfil do usuário armazenado em cache em vez de bloquear o fluxo de login.
3. Serviço Alternativo (Multi-Provedor)
Ao executar uma operação crítica que deve ser bem-sucedida, você pode configurar um provedor de serviços secundário como backup.
- Exemplo: se o seu gateway de pagamento principal (por exemplo, Stripe) retornar um erro 5xx ou expirar, o mecanismo de fallback redirecionará imediatamente a solicitação de transação para um gateway secundário (por exemplo, PayPal ou Adyen).
- Exemplo: se uma API de geocodificação falhar, volte para um provedor de mapeamento secundário.
4. Fila para mais tarde (buffer assíncrono)
Para operações de gravação que não exigem processamento síncrono imediato, o substituto pode armazenar a solicitação em uma fila local ou banco de dados para ser repetida posteriormente.
- Exemplo: se um serviço de notificação por e-mail estiver inativo, grave a carga útil da notificação em uma fila de mensagens mortas ou em uma tabela de banco de dados local. Um trabalhador em segundo plano lerá essa fila e entregará os e-mails assim que o serviço de notificação estiver íntegro novamente.
O Trio de Resiliência: Nova tentativa vs. Disjuntor vs.
Para construir uma arquitetura altamente resiliente, você precisa combinar o padrão Fallback com os padrões Retry e Circuit Breaker. Eles formam uma linha de defesa de três níveis:
| Padrão | Função | Ação | Cenário |
|---|---|---|---|
| Repetir padrão | Resolve falhas transitórias de curta duração. | Repete a solicitação após um pequeno atraso (backoff + jitter). | Breves quedas de pacotes de rede, redefinições de soquete. |
| Disjuntor | Evita o esgotamento de recursos. | Viagens abertas para bloquear chamadas para um serviço com falha imediatamente (fail-fast). | Tempo de inatividade persistente do serviço, impasses no banco de dados. |
| Padrão substituto | Preserva a experiência do usuário. | Executa uma ação alternativa quando a chamada principal falha ou é bloqueada. | Quando as tentativas se esgotarem ou o disjuntor estiver aberto. |
Como eles funcionam juntos
- Uma solicitação recebida chega ao gateway de serviço.
- A solicitação passa pelo Disjuntor.
- Se o disjuntor estiver fechado, a solicitação vai para o wrapper Retry, que faz a chamada real.
- Se ocorrer um erro transitório, o mecanismo Retry tentará a chamada novamente.
- Se todas as novas tentativas falharem ou se o disjuntor já estiver aberto (falhando rapidamente para economizar recursos), o Manipulador de Fallback interceptará a falha e retornará a resposta degradada/armazenada em cache.
Implementações de código
Vejamos como podemos implementar a lógica de fallback em Java e Go.
1. Java (Resiliência4j)
Em Java, Resilience4j é o padrão da indústria para tolerância a falhas. Podemos definir um método substituto declarativamente usando anotações ou programaticamente.
import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;
import org.springframework.stereotype.Service;
import java.util.Collections;
import java.util.List;
@Service
public class ProductService {
private final InventoryClient inventoryClient;
private final CacheManager cacheManager;
public ProductService(InventoryClient inventoryClient, CacheManager cacheManager) {
this.inventoryClient = inventoryClient;
this.cacheManager = cacheManager;
}
// Bind this method to a circuit breaker. If it fails or is open, route to fallback
@CircuitBreaker(name = "inventoryService", fallbackMethod = "getInventoryFallback")
public List<String> getProductInventory(String category) {
return inventoryClient.fetchStockByCategory(category);
}
// Fallback method must have the same return type and accept the same parameters,
// plus a Throwable parameter containing the error that triggered it
public List<String> getInventoryFallback(String category, Throwable throwable) {
System.err.println("Primary inventory service failed: " + throwable.getMessage());
// Attempt to fetch from local cache (Strategy 2)
List<String> cachedStock = cacheManager.get("inventory:" + category, List.class);
if (cachedStock != null) {
return cachedStock;
}
// Return static empty list if cache is empty (Strategy 1)
return Collections.emptyList();
}
}
2. Vá (Golang)
No Go, podemos escrever decoradores substitutos limpos usando padrões de programação funcional, que nos permitem agrupar qualquer manipulador de execução com um manipulador secundário.
package main
import (
"context"
"errors"
"fmt"
"time"
)
// Request represents a simple payload
type Request struct {
UserID string
}
// Response represents the returned data
type Response struct {
Data string
Status string
}
// ServiceFunc represents our primary function signature
type ServiceFunc func(ctx context.Context, req Request) (Response, error)
// WithFallback wraps a service function with fallback logic
func WithFallback(primary ServiceFunc, fallback ServiceFunc) ServiceFunc {
return func(ctx context.Context, req Request) (Response, error) {
res, err := primary(ctx, req)
if err != nil {
fmt.Printf("[Warning] Primary call failed: %v. Running fallback...\n", err)
return fallback(ctx, req)
}
return res, nil
}
}
func main() {
// 1. Define primary service that occasionally fails
primaryService := func(ctx context.Context, req Request) (Response, error) {
return Response{}, errors.New("database connection timeout (504)")
}
// 2. Define fallback service that retrieves cached data
fallbackService := func(ctx context.Context, req Request) (Response, error) {
// Simulating cached read
return Response{
Data: fmt.Sprintf("Stale cached profile data for user %s", req.UserID),
Status: "DEGRADED (CACHED)",
}, nil
}
// 3. Wrap them together
resilientService := WithFallback(primaryService, fallbackService)
// 4. Execute
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
req := Request{UserID: "user_992"}
response, err := resilientService(ctx, req)
if err != nil {
fmt.Printf("Operation completely failed: %v\n", err)
} else {
fmt.Printf("Response Received:\n - Status: %s\n - Data: %s\n", response.Status, response.Data)
}
}
Melhores práticas para o padrão substituto
- Mantenha o caminho de fallback livre de dependências: A lógica de fallback não deve depender dos mesmos sistemas ou infraestrutura downstream que acabaram de falhar. Se o seu banco de dados estiver inativo, o retorno para outra consulta de banco de dados no mesmo servidor provavelmente também falhará.
- Executar rápido: a lógica de fallback deve ser executada rapidamente. Evite cálculos complexos ou chamadas aninhadas lentas em seus caminhos alternativos. O objetivo é retornar uma resposta ao usuário o mais rápido possível.
- Alerta e monitoramento: sempre registre execuções de fallback e incremente contadores de telemetria. Se o seu sistema estiver executando caminhos alternativos, significa que o sistema está operando em um estado degradado. Você precisa de alertas para saber quando as taxas de fallback excedem os limites normais.
- Tornar a UI resiliente: Trabalhe em estreita colaboração com engenheiros de front-end para garantir que a UI seja projetada para aceitar cargas substitutas (como coleções vazias ou estados parciais) sem quebrar o layout do lado do cliente.
Conclusão
O Padrão Fallback é a apólice de seguro definitiva para confiabilidade de microsserviços. Ao antecipar falhas e fornecer uma lógica alternativa elegante, você transforma falhas graves em interrupções sutis e gerenciáveis. A combinação desse padrão com Repetições e Disjuntores garante que seu aplicativo possa suportar grandes interrupções externas enquanto continua atendendo aos usuários.