Chain of Responsibility em Java: como transformamos um if de 600 linhas em um pipeline de autorização de pagamentos

java dev.to

Chain of Responsibility em Java: como transformamos um if de 600 linhas em um pipeline de autorização de pagamentos

Padrão comportamental · Java 21 · Spring Boot


1. Base Fundamental

Introdução

O Chain of Responsibility (Corrente de Responsabilidade) é um padrão comportamental do catálogo GoF. Padrões comportamentais tratam de como objetos se comunicam e distribuem responsabilidades entre si — diferente dos criacionais (como objetos nascem) e dos estruturais (como objetos se compõem).

A ideia central é simples: em vez de um único objeto decidir tudo, você monta uma fila de tratadores independentes. Cada um recebe a requisição, decide se consegue resolvê-la e, se não conseguir, passa adiante para o próximo. Quem chama não sabe — e não precisa saber — qual elo da corrente vai efetivamente responder.

Se você já escreveu um Filter de Servlet, configurou o SecurityFilterChain do Spring Security ou usou middleware em Express/ASP.NET, você já usou Chain of Responsibility. É um dos padrões mais presentes em frameworks de produção e um dos menos aplicados conscientemente em código de aplicação.

O Problema

O padrão ataca uma dor específica e muito comum: um único método concentra uma sequência de decisões condicionais que cresce indefinidamente.

O sintoma clássico é o método que começou com 20 linhas e hoje tem 600:

public Resultado processar(Transacao t) {
    if (t.getValor() <= 0) { ... }
    else if (!moedasSuportadas.contains(t.getMoeda())) { ... }
    else if (blacklistRepo.existe(t.getClienteId())) { ... }
    else {
        int score = antifraudeClient.consultar(t);   // 400ms, API paga
        if (score > 80) { ... }
        else if (score > 50) { ... }
        else if (t.getPais().equals("DE") && t.getValor() > 30) { ... }
        else if ( /* mais 14 condições */ ) { ... }
    }
}
Enter fullscreen mode Exit fullscreen mode

Os problemas que isso gera não são estéticos:

Sintoma Consequência prática
Toda regra nova altera a mesma classe Violação do Open/Closed Principle; conflito de merge em todo sprint
Regras acopladas dentro de um único método Impossível testar a regra de blacklist sem instanciar todo o resto
Ordem de execução implícita no aninhamento Ninguém sabe dizer, olhando de fora, o que roda antes do quê
Chamadas caras executadas cedo demais Latência e custo pagos mesmo quando uma regra barata já teria negado
Decisão anônima O log diz "transação negada", mas não diz qual regra negou

O último ponto é o que costuma virar incidente às três da manhã. O penúltimo é o que vira fatura no fim do mês.

Conceito

A analogia mais próxima do dia a dia de software é o pipeline de filtros de um servidor web. Uma requisição HTTP entra e atravessa uma sequência: filtro de CORS, filtro de autenticação, filtro de autorização, filtro de rate limit. Cada filtro tem duas opções — interromper a requisição ali mesmo (401, 429) ou deixar passar para o próximo. Nenhum filtro conhece a existência do controller no fim da linha, e nenhum filtro precisa conhecer os outros filtros.

Traduzindo para as peças do padrão:

  • Handler (Tratador): a interface ou classe abstrata que define o contrato trate(requisição) e guarda a referência para o próximo elo.
  • ConcreteHandler: cada tratador específico. Implementa uma regra e decide entre resolver ou delegar.
  • Client: quem monta a corrente e dispara a requisição na primeira ponta.

Duas propriedades merecem destaque porque são o motivo de o padrão existir:

  1. Desacoplamento entre emissor e receptor. O cliente chama corrente.autorizar(t) e recebe uma resposta. Não sabe quantos elos existem nem qual respondeu.
  2. A ordem vira dado, não código. A sequência deixa de estar enterrada em aninhamento de if e passa a ser uma linha explícita e versionada em um arquivo de configuração. Reordenar a corrente é mudar uma linha — e, como veremos, reordenar é exatamente o que resolve o gargalo.

2. Desenvolvimento

Cenário do Mundo Real

O contexto é o microsserviço ms-pagamentos de uma plataforma de e-commerce que opera no Brasil e na Europa. Antes de enviar qualquer transação ao adquirente (PSP), o serviço precisa tomar uma decisão de autorização: APROVAR, NEGAR ou mandar para REVISÃO manual.

Cinco regras compõem essa decisão, com custos de execução radicalmente diferentes:

# Regra O que faz Custo
1 Dados cadastrais Valor positivo, moeda suportada ~0 ms (memória)
2 Lista restritiva Cliente bloqueado por chargeback anterior ~2 ms (Redis)
3 Velocidade transacional Mais de 5 tentativas em 10 minutos ~3 ms (Redis)
4 Autenticação forte (SCA/3DS) Emissor na UE e valor acima da isenção ~0 ms (memória)
5 Score antifraude Consulta a bureau externo ~400 ms, cobrado por consulta

No código legado, as cinco regras viviam em um único método de PagamentoService, e o score antifraude era consultado antes das validações baratas — herança de uma implementação em que o score era a única regra existente.

O resultado: 100% das transações pagavam 400 ms e uma consulta faturada, inclusive aquelas com valor negativo ou de clientes já na lista restritiva, que seriam negadas de qualquer forma. O p95 do endpoint de checkout estava em 1,2 s, e a linha do bureau antifraude era a maior despesa variável do time.

Nota de transparência: os números deste estudo de caso são ilustrativos, construídos para representar uma situação realista de engenharia. Se você for reproduzir o experimento, meça os seus.

O objetivo da refatoração tinha dois lados:

  • Arquitetural: permitir adicionar, remover ou desligar regras sem tocar nas existentes.
  • De performance/custo: executar as regras da mais barata para a mais cara, com curto-circuito na primeira decisão.

Chain of Responsibility resolve os dois com a mesma estrutura, e é por isso que ele foi o padrão escolhido em vez de Strategy — Strategy trocaria qual algoritmo roda, mas não resolveria a sequência nem o curto-circuito.

Representação Visual

Diagrama de Classes (UML)

Estrutura do padrão aplicada exatamente ao código apresentado na próxima seção.

classDiagram
    direction LR

    class AutorizacaoService {
        -RegraAutorizacao correnteDeAutorizacao
        -TransacaoRepository transacaoRepository
        +autorizar(Transacao) ResultadoAutorizacao
    }

    class RegraAutorizacao {
        <<abstract>>
        -RegraAutorizacao proxima
        +encadear(RegraAutorizacao) RegraAutorizacao
        +autorizar(Transacao) ResultadoAutorizacao
        #avaliar(Transacao) Optional~ResultadoAutorizacao~
    }

    class RegraDadosCadastrais {
        #avaliar(Transacao) Optional~ResultadoAutorizacao~
    }
    class RegraListaRestritiva {
        -ListaRestritivaRepository repositorio
        #avaliar(Transacao) Optional~ResultadoAutorizacao~
    }
    class RegraVelocidadeTransacional {
        -ContadorTentativas contador
        #avaliar(Transacao) Optional~ResultadoAutorizacao~
    }
    class RegraAutenticacaoForte {
        #avaliar(Transacao) Optional~ResultadoAutorizacao~
    }
    class RegraScoreAntifraude {
        -ClienteAntifraude clienteAntifraude
        #avaliar(Transacao) Optional~ResultadoAutorizacao~
    }

    class Transacao {
        <<record>>
        +String id
        +String clienteId
        +BigDecimal valor
        +String moeda
        +String paisEmissor
    }

    class ResultadoAutorizacao {
        <<record>>
        +Decisao decisao
        +String regra
        +String motivo
    }

    class Decisao {
        <<enumeration>>
        APROVAR
        REVISAR
        NEGAR
    }

    class ListaRestritivaRepository {
        <<interface>>
        +contemCliente(String) boolean
    }
    class ClienteAntifraude {
        <<interface>>
        +consultarScore(Transacao) int
    }

    RegraAutorizacao o-- RegraAutorizacao : proxima
    RegraAutorizacao <|-- RegraDadosCadastrais
    RegraAutorizacao <|-- RegraListaRestritiva
    RegraAutorizacao <|-- RegraVelocidadeTransacional
    RegraAutorizacao <|-- RegraAutenticacaoForte
    RegraAutorizacao <|-- RegraScoreAntifraude

    AutorizacaoService --> RegraAutorizacao : dispara
    RegraAutorizacao ..> Transacao : recebe
    RegraAutorizacao ..> ResultadoAutorizacao : produz
    ResultadoAutorizacao --> Decisao
    RegraListaRestritiva --> ListaRestritivaRepository
    RegraScoreAntifraude --> ClienteAntifraude

O ponto mais importante do diagrama é a auto-associação RegraAutorizacao o-- RegraAutorizacao. É ela que caracteriza o padrão: o tratador conhece um tratador do mesmo tipo, o que permite montar uma corrente de tamanho arbitrário sem que nenhuma classe concreta conheça outra.

Arquitetura de Software (alto nível)

Onde a corrente se encaixa no ecossistema.

flowchart TB
    subgraph canais["Canais"]
        WEB["Checkout Web"]
        APP["App Mobile"]
    end

    GW["API Gateway"]

    subgraph pagamentos["ms-pagamentos - Spring Boot"]
        CTRL["PagamentoController"]
        SVC["AutorizacaoService"]
        subgraph corrente["Corrente de Autorizacao"]
            direction TB
            R1["1 - Dados Cadastrais<br/>custo ~0ms"]
            R2["2 - Lista Restritiva<br/>custo ~2ms"]
            R3["3 - Velocidade<br/>custo ~3ms"]
            R4["4 - Autenticacao Forte<br/>custo ~0ms"]
            R5["5 - Score Antifraude<br/>custo ~400ms - pago"]
            R1 -->|"nao decidiu"| R2
            R2 -->|"nao decidiu"| R3
            R3 -->|"nao decidiu"| R4
            R4 -->|"nao decidiu"| R5
        end
    end

    REDIS[("Redis<br/>lista restritiva + contadores")]
    PG[("PostgreSQL<br/>transacoes e decisoes")]
    FRAUDE["API Antifraude<br/>bureau externo"]
    PSP["Adquirente / PSP"]
    KAFKA["Kafka<br/>topico decisoes-autorizacao"]
    NOTIF["ms-notificacoes"]
    BI["Data Lake / BI"]

    WEB --> GW
    APP --> GW
    GW --> CTRL
    CTRL --> SVC
    SVC --> R1
    R2 -.-> REDIS
    R3 -.-> REDIS
    R5 -.-> FRAUDE
    SVC --> PG
    SVC -->|"decisao APROVAR"| PSP
    SVC --> KAFKA
    KAFKA --> NOTIF
    KAFKA --> BI

Observe o efeito de custo: uma transação negada no elo 1 ou 2 nunca chega ao bureau externo. As setas pontilhadas representam I/O — e é justamente por serem I/O que essas regras ficam no fim da corrente.

Implementação em Java

Contratos de domínio

public record Transacao(
        String id,
        String clienteId,
        BigDecimal valor,
        String moeda,
        String paisEmissor,
        Instant criadaEm
) {}
Enter fullscreen mode Exit fullscreen mode
public enum Decisao { APROVAR, REVISAR, NEGAR }
Enter fullscreen mode Exit fullscreen mode
public record ResultadoAutorizacao(Decisao decisao, String regra, String motivo) {

    public static ResultadoAutorizacao aprovar() {
        return new ResultadoAutorizacao(
                Decisao.APROVAR, "CORRENTE_COMPLETA", "Nenhuma regra bloqueou a transacao");
    }

    public static ResultadoAutorizacao negar(String regra, String motivo) {
        return new ResultadoAutorizacao(Decisao.NEGAR, regra, motivo);
    }

    public static ResultadoAutorizacao revisar(String regra, String motivo) {
        return new ResultadoAutorizacao(Decisao.REVISAR, regra, motivo);
    }
}
Enter fullscreen mode Exit fullscreen mode

O campo regra no resultado resolve o problema de observabilidade citado lá no começo: toda decisão carrega o nome de quem a tomou.

O Handler abstrato — o coração do padrão

public abstract class RegraAutorizacao {

    private RegraAutorizacao proxima;

    /** Encadeia e devolve o proximo elo, permitindo montagem fluente. */
    public RegraAutorizacao encadear(RegraAutorizacao proxima) {
        this.proxima = proxima;
        return proxima;
    }

    /** Template Method: fixa o fluxo de delegacao para todas as subclasses. */
    public final ResultadoAutorizacao autorizar(Transacao transacao) {
        return avaliar(transacao)
                .orElseGet(() -> proxima == null
                        ? ResultadoAutorizacao.aprovar()
                        : proxima.autorizar(transacao));
    }

    /**
     * Optional vazio = "nao tenho opiniao, passe adiante".
     * Optional preenchido = "eu decido aqui, a corrente para".
     */
    protected abstract Optional<ResultadoAutorizacao> avaliar(Transacao transacao);
}
Enter fullscreen mode Exit fullscreen mode

Duas decisões de design valem comentário:

  • autorizar é final. A mecânica de delegação é responsabilidade da classe base, não das subclasses. Isso é um Template Method dentro do Chain of Responsibility — a subclasse só implementa a regra, jamais o fluxo.
  • O retorno é Optional em vez de null. Torna explícito no tipo o contrato "não decidi", que é o que faz a corrente andar.

Os elos concretos

public class RegraDadosCadastrais extends RegraAutorizacao {

    private static final Set<String> MOEDAS_SUPORTADAS = Set.of("BRL", "USD", "EUR");

    @Override
    protected Optional<ResultadoAutorizacao> avaliar(Transacao t) {
        if (t.valor() == null || t.valor().compareTo(BigDecimal.ZERO) <= 0) {
            return Optional.of(ResultadoAutorizacao.negar(
                    "DADOS_CADASTRAIS", "Valor invalido"));
        }
        if (!MOEDAS_SUPORTADAS.contains(t.moeda())) {
            return Optional.of(ResultadoAutorizacao.negar(
                    "DADOS_CADASTRAIS", "Moeda nao suportada: " + t.moeda()));
        }
        return Optional.empty();
    }
}
Enter fullscreen mode Exit fullscreen mode
public class RegraListaRestritiva extends RegraAutorizacao {

    private final ListaRestritivaRepository repositorio;

    public RegraListaRestritiva(ListaRestritivaRepository repositorio) {
        this.repositorio = repositorio;
    }

    @Override
    protected Optional<ResultadoAutorizacao> avaliar(Transacao t) {
        if (repositorio.contemCliente(t.clienteId())) {
            return Optional.of(ResultadoAutorizacao.negar(
                    "LISTA_RESTRITIVA", "Cliente bloqueado por historico de chargeback"));
        }
        return Optional.empty();
    }
}
Enter fullscreen mode Exit fullscreen mode
public class RegraVelocidadeTransacional extends RegraAutorizacao {

    private static final int LIMITE_TENTATIVAS = 5;
    private static final Duration JANELA = Duration.ofMinutes(10);

    private final ContadorTentativas contador;

    public RegraVelocidadeTransacional(ContadorTentativas contador) {
        this.contador = contador;
    }

    @Override
    protected Optional<ResultadoAutorizacao> avaliar(Transacao t) {
        long tentativas = contador.tentativasRecentes(t.clienteId(), JANELA);
        if (tentativas > LIMITE_TENTATIVAS) {
            return Optional.of(ResultadoAutorizacao.revisar(
                    "VELOCIDADE", "Excesso de tentativas na janela: " + tentativas));
        }
        return Optional.empty();
    }
}
Enter fullscreen mode Exit fullscreen mode
public class RegraAutenticacaoForte extends RegraAutorizacao {

    private static final Set<String> PAISES_SCA = Set.of("DE", "FR", "ES", "PT", "IT");
    private static final BigDecimal LIMITE_ISENCAO = new BigDecimal("30.00");

    @Override
    protected Optional<ResultadoAutorizacao> avaliar(Transacao t) {
        if (PAISES_SCA.contains(t.paisEmissor())
                && t.valor().compareTo(LIMITE_ISENCAO) > 0) {
            return Optional.of(ResultadoAutorizacao.revisar(
                    "AUTENTICACAO_FORTE", "Transacao exige desafio 3DS"));
        }
        return Optional.empty();
    }
}
Enter fullscreen mode Exit fullscreen mode
public class RegraScoreAntifraude extends RegraAutorizacao {

    private static final int SCORE_BLOQUEIO = 80;
    private static final int SCORE_REVISAO  = 50;

    private final ClienteAntifraude clienteAntifraude;

    public RegraScoreAntifraude(ClienteAntifraude clienteAntifraude) {
        this.clienteAntifraude = clienteAntifraude;
    }

    @Override
    protected Optional<ResultadoAutorizacao> avaliar(Transacao t) {
        int score = clienteAntifraude.consultarScore(t);   // ~400ms, cobrado

        if (score >= SCORE_BLOQUEIO) {
            return Optional.of(ResultadoAutorizacao.negar(
                    "SCORE_ANTIFRAUDE", "Score de risco: " + score));
        }
        if (score >= SCORE_REVISAO) {
            return Optional.of(ResultadoAutorizacao.revisar(
                    "SCORE_ANTIFRAUDE", "Score intermediario: " + score));
        }
        return Optional.empty();
    }
}
Enter fullscreen mode Exit fullscreen mode

Montagem da corrente

Aqui está o ganho arquitetural mais concreto: a ordem de execução de todo o motor de autorização cabe em cinco linhas, em um único arquivo versionado.

@Configuration
public class CorrenteAutorizacaoConfig {

    @Bean
    public RegraAutorizacao correnteDeAutorizacao(
            ListaRestritivaRepository listaRestritivaRepository,
            ContadorTentativas contadorTentativas,
            ClienteAntifraude clienteAntifraude) {

        RegraAutorizacao inicio = new RegraDadosCadastrais();

        inicio.encadear(new RegraListaRestritiva(listaRestritivaRepository))
              .encadear(new RegraVelocidadeTransacional(contadorTentativas))
              .encadear(new RegraAutenticacaoForte())
              .encadear(new RegraScoreAntifraude(clienteAntifraude));

        return inicio;   // devolve a ponta da corrente
    }
}
Enter fullscreen mode Exit fullscreen mode

Repare que as regras não são anotadas com @Component. Elas são instanciadas aqui de propósito: como o elo guarda estado mutável (proxima), transformá-las em singletons gerenciados pelo contêiner criaria uma armadilha no dia em que o sistema precisar de duas correntes diferentes — ambas escreveriam no mesmo objeto.

Ponto de entrada

@Service
public class AutorizacaoService {

    private final RegraAutorizacao correnteDeAutorizacao;
    private final TransacaoRepository transacaoRepository;
    private final PublicadorDeEventos publicador;

    public AutorizacaoService(RegraAutorizacao correnteDeAutorizacao,
                              TransacaoRepository transacaoRepository,
                              PublicadorDeEventos publicador) {
        this.correnteDeAutorizacao = correnteDeAutorizacao;
        this.transacaoRepository = transacaoRepository;
        this.publicador = publicador;
    }

    public ResultadoAutorizacao autorizar(Transacao transacao) {
        ResultadoAutorizacao resultado = correnteDeAutorizacao.autorizar(transacao);

        transacaoRepository.registrarDecisao(transacao, resultado);
        publicador.publicar(new DecisaoTomada(transacao.id(), resultado));

        return resultado;
    }
}
Enter fullscreen mode Exit fullscreen mode

O serviço tem uma linha de regra de negócio e não conhece nenhuma regra concreta. Adicionar uma sexta regra amanhã não toca neste arquivo.

Testabilidade

Este é o benefício que aparece primeiro no dia a dia. Cada elo é testável sozinho, sem contexto Spring, sem mock de framework:

@Test
void deveNegarQuandoClienteEstaEmListaRestritiva() {
    ListaRestritivaRepository repositorio = clienteId -> true;   // lambda, sem Mockito
    RegraAutorizacao regra = new RegraListaRestritiva(repositorio);

    ResultadoAutorizacao resultado = regra.autorizar(umaTransacaoValida());

    assertEquals(Decisao.NEGAR, resultado.decisao());
    assertEquals("LISTA_RESTRITIVA", resultado.regra());
}

@Test
void deveDelegarQuandoClienteNaoEstaEmListaRestritiva() {
    RegraAutorizacao regra = new RegraListaRestritiva(clienteId -> false);
    regra.encadear(new RegraDadosCadastrais());   // corrente montada no teste

    assertEquals(Decisao.APROVAR, regra.autorizar(umaTransacaoValida()).decisao());
}
Enter fullscreen mode Exit fullscreen mode

Prós e Contras

Vantagens

  • Open/Closed na prática. Regra nova é classe nova mais uma linha na configuração. Nenhuma classe existente é editada, o que elimina a categoria de bug "mexi na regra A e quebrei a B".
  • Single Responsibility de verdade. Cada classe tem um motivo para mudar. RegraScoreAntifraude muda quando o bureau muda o contrato; nada mais.
  • A ordem vira decisão explícita e barata de reverter. Foi isso que resolveu o gargalo: mover o elo caro para o fim é mudar a posição de uma linha, não reescrever aninhamento de if.
  • Curto-circuito reduz latência e custo. No cenário descrito, transações negadas por regras baratas param antes do I/O caro.
  • Testabilidade isolada e rápida. Testes unitários sem contexto de aplicação, na casa dos milissegundos.
  • Observabilidade embutida. Toda decisão carrega o nome da regra, o que transforma o log de "negado" em "negado por LISTA_RESTRITIVA".
  • Desligar uma regra é remover uma linha. Combinado com feature flag, permite desativar uma regra problemática em produção sem deploy de código de negócio.

Custos e trade-offs

Ignorar esta parte é o erro mais comum ao adotar padrões. O Chain of Responsibility cobra:

  • Explosão de arquivos. Cinco regras viraram sete classes mais uma configuração. Se o sistema tem duas regras estáveis que não mudam há dois anos, o padrão é overengineering puro — o if era melhor.
  • O fluxo some do código. Você não consegue mais ler a lógica de autorização de cima para baixo em um arquivo. Precisa abrir a configuração para saber a ordem. Debug exige acompanhar a pilha de delegação, e stack traces ficam mais profundos.
  • Nenhuma garantia de tratamento. Se nenhum elo decide, a requisição chega ao fim da corrente sem resposta. Aqui isso foi resolvido com um fallback explícito (aprovar()), mas em muitas implementações o resultado é um null silencioso — um dos bugs mais caros que o padrão pode gerar. Sempre defina o comportamento da ponta final.
  • A ordem vira dependência oculta. O ganho de performance depende da ordem estar correta. Alguém que mova o score antifraude para o topo não quebra nenhum teste funcional — só triplica a latência e a fatura. Vale um teste que assegure a ordem, ou pelo menos uma métrica por elo.
  • Execução estritamente sequencial. Regras independentes poderiam rodar em paralelo, mas o padrão é serial por definição. Se as três regras de I/O fossem obrigatórias e independentes, um CompletableFuture.allOf seria mais rápido que a corrente.
  • Não serve quando você precisa de todos os motivos. Se o requisito for "retorne todas as regras violadas" em vez de "pare na primeira", o curto-circuito trabalha contra você. Nesse caso, o caminho é um pipeline acumulador de validadores ou o padrão Specification.

Sobre variações de implementação: em projetos Spring, é comum ver a corrente expressa como List<RegraAutorizacao> injetada com @Order, iterada por um orquestrador. É mais idiomático e dispensa o campo proxima. Em compensação, deixa de ser uma corrente de verdade — é um laço sobre uma lista, e o elo perde a capacidade de decidir se e como invoca o próximo (fazer trabalho depois da delegação, por exemplo, como um filtro HTTP faz ao manipular a resposta na volta). A versão com proxima apresentada aqui é a canônica e mais flexível; a versão com lista é mais simples. Escolha consciente, não por hábito.


3. Conclusão

Resumo

O Chain of Responsibility resolve um problema que quase todo sistema de médio porte encontra: uma sequência de decisões que cresce sem parar dentro de um único método. Ele quebra essa sequência em tratadores independentes e transforma a ordem de execução — antes implícita no aninhamento de condicionais — em configuração explícita.

No caso apresentado, o valor entregue não foi apenas código mais limpo. Foi um gargalo concreto atacado: a regra mais cara deixou de ser executada em toda transação porque reordenar a corrente passou a custar uma linha. Esse é o teste que vale a pena aplicar antes de adotar qualquer padrão — se ele só deixa o código mais bonito, pense duas vezes; se ele muda o que o sistema é capaz de fazer barato, adote.

Visão Pessoal

Começo por onde deveria começar todo artigo técnico: não usei este padrão em um sistema de pagamentos em produção. Sou estudante de Engenharia de Software e desenvolvedor backend em formação. O cenário deste artigo é um estudo de caso construído para ser realista, e a implementação que apresentei foi escrita e executada por mim justamente para poder falar dela com alguma propriedade. Prefiro dizer isso na cara do que simular experiência que não tenho — o leitor merece saber o peso do que está lendo.

Dito isso, o que aprendi implementando não foi o que eu esperava aprender.

Primeiro: a mecânica é a parte fácil, e quase não é onde estão as decisões. Encadear objetos e delegar é trivial. O que exigiu pensamento foi definir o contrato do elo — o que significa "não tenho opinião sobre esta transação". Minha primeira versão retornava null para dizer isso, e virou um NullPointerException em dez minutos. Trocar por Optional não foi refinamento estético: fez o contrato ficar explícito no tipo, e o erro deixou de ser possível. Foi a primeira vez que senti, na prática, a diferença entre usar Optional porque leram que é bom e usar porque ele resolve um problema concreto de modelagem.

Segundo: descobri que já usava o padrão sem saber o nome dele. Todo Filter de Servlet, todo SecurityFilterChain que configurei em projetos Spring Boot é exatamente isto — uma corrente de responsabilidade. Estudar o padrão formalmente mudou menos o código que escrevo e mais a minha capacidade de ler o código dos frameworks que uso todo dia. Isso me parece o ganho real de estudar padrões: não é ter mais ferramentas, é parar de olhar para o framework como caixa-preta.

Terceiro, e o mais desconfortável: em algum momento eu achei o resultado pior que o if. Depois de quebrar a lógica em sete arquivos, abrir quatro deles para entender um fluxo que antes cabia em uma tela foi frustrante. Levei um tempo para entender que esse desconforto é o preço do padrão, não um sinal de que fiz algo errado — e que ele só compensa quando o número de regras cresce e elas mudam em ritmos diferentes. Com duas ou três regras estáveis, o if ganha. Isso raramente aparece nos artigos sobre design patterns, e talvez seja a coisa mais útil que eu tenha para dizer aqui.

Saio deste estudo com menos vontade de aplicar padrões e mais interesse em entender o custo de cada um. Suspeito que seja o sinal de que aprendi alguma coisa.

Vamos conversar

E na sua stack, onde a decisão de negócio mora hoje? Um método gigante, um motor de regras, um switch, ou já um pipeline de tratadores?

Se você já refatorou um bloco de condicionais para Chain of Responsibility, ficou melhor ou você se arrependeu? Estou especialmente interessado em casos em que o padrão foi a escolha errada — deixa nos comentários o que te fez voltar atrás.


Escrito por **Bruno Candido Salles* · Java 21 · Spring Boot 3.x · Diagramas em Mermaid*

*Estudante de Engenharia de Software na UniEVANGÉLICA · Backend Java/Spring Boot

Source: dev.to

arrow_back Back to Tutorials