O Padrão Fallback: Projetando Degradação Graciosa em Microsserviços

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.
Diagrama de arquitetura de padrão de fallback mostrando falha de serviço primário e roteamento para um manipulador de fallback para recuperar dados armazenados em cache/backup

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

  1. Uma solicitação recebida chega ao gateway de serviço.
  2. A solicitação passa pelo Disjuntor.
  3. Se o disjuntor estiver fechado, a solicitação vai para o wrapper Retry, que faz a chamada real.
  4. Se ocorrer um erro transitório, o mecanismo Retry tentará a chamada novamente.
  5. 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

  1. 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á.
  2. 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.
  3. 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.
  4. 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.