Java

Padrões de Projeto em Java: Guia Prático para Código Limpo e Manutenível

Descubra como aplicar os principais padrões de projeto em Java (Factory, Adapter, Strategy) para escrever códigos mais limpos, flexíveis e integrados ao Spring Boot.

Marcos Costa
Marcos Costa
11 de julho de 2026 12 min de leitura
Mesa de trabalho de um programador com monitor exibindo código Java estruturado e um caderno ao lado mostrando um diagrama UML de padrões de projeto.

Padrões de projeto em Java são muito mais do que diagramas UML complexos ou conceitos teóricos distantes da realidade. Eles são soluções testadas e comprovadas para problemas recorrentes no design de software, ferramentas poderosas que, quando bem aplicadas, transformam um código caótico em uma estrutura limpa, flexível e fácil de manter.

Se você é um desenvolvedor Java iniciante, está em transição de carreira ou busca aprimorar a qualidade do seu código, este guia prático vai desmistificar os Design Patterns. Vamos explorar como identificar problemas comuns de design e aplicar os padrões mais relevantes no ecossistema Java, com exemplos de código e a relação direta com frameworks modernos como o Spring Boot. Nosso objetivo é que você saia daqui com um entendimento prático de como esses padrões podem elevar a qualidade do seu trabalho e a sua compreensão sobre as relações entre objetos em Java.

A Origem dos Design Patterns e a Divisão Clássica do GoF

A história dos padrões de projeto remonta ao final do século XX, quando quatro engenheiros de software – Erich Gamma, Richard Helm, Ralph Johnson e John Vlissides, conhecidos como o “Gang of Four” (GoF) – publicaram o livro “Design Patterns: Elements of Reusable Object-Oriented Software” (1994). Este livro revolucionou a forma como os desenvolvedores pensavam sobre design de software, catalogando 23 padrões que descreviam soluções elegantes para problemas comuns em sistemas orientados a objetos. Eles perceberam que muitos problemas de design eram recorrentes e que as soluções podiam ser padronizadas e reutilizadas, assim como arquitetos usam padrões em construções civis.

Os padrões do GoF são classicamente divididos em três categorias:

  1. Criacionais (Creational Patterns): Focam na flexibilidade e controle da criação de objetos, desacoplando a forma como os objetos são criados da forma como são usados. Isso é crucial para seguir os princípios de código limpo, como o Single Responsibility Principle (SRP), ao centralizar a lógica de criação.
  2. Estruturais (Structural Patterns): Tratam da composição de classes e objetos para formar estruturas maiores, mantendo-as flexíveis e eficientes. Eles ajudam a garantir que as relações entre objetos em Java sejam bem definidas e que as partes do sistema possam trabalhar juntas, mesmo que tenham interfaces diferentes.
  3. Comportamentais (Behavioral Patterns): Concentram-se na comunicação entre objetos e na atribuição de responsabilidades. Eles definem algoritmos e a forma como os objetos interagem, promovendo um acoplamento fraco e uma maior coesão.

Entender essas categorias é o primeiro passo para saber qual padrão aplicar em cada situação, evitando o famoso “martelo de ouro” onde todo problema vira um prego.

Padrões Criacionais: Flexibilidade na Instanciação com Factory Method

Os padrões criacionais são ideais quando a lógica de criação de objetos se torna complexa ou quando você precisa que a aplicação seja flexível para instanciar diferentes tipos de objetos sem alterar o código cliente. O Factory Method é um excelente exemplo disso.

Imagine que sua aplicação precisa enviar diferentes tipos de notificações (e-mail, SMS, push) e, dependendo de uma configuração ou contexto, você precisa escolher qual tipo de notificação será enviada. Sem um padrão, você poderia ter uma série de if/else para instanciar o objeto correto. Com o Factory Method, a lógica de criação é encapsulada.

Vamos ver um exemplo prático para criar diferentes serviços de notificação:

// 1. Interface comum para as notificações
interface Notificacao {
    void enviar(String mensagem);
}

// 2. Implementações concretas
class EmailNotificacao implements Notificacao {
    @Override
    public void enviar(String mensagem) {
        System.out.println("Enviando E-mail: " + mensagem);
    }
}

class SmsNotificacao implements Notificacao {
    @Override
    public void enviar(String mensagem) {
        System.out.println("Enviando SMS: " + mensagem);
    }
}

class PushNotificacao implements Notificacao {
    @Override
    public void enviar(String mensagem) {
        System.out.println("Enviando Push: " + mensagem);
    }
}

// 3. A Factory Method para criar notificações
class NotificacaoFactory {
    public static Notificacao criarNotificacao(String tipo) {
        switch (tipo.toLowerCase()) {
            case "email":
                return new EmailNotificacao();
            case "sms":
                return new SmsNotificacao();
            case "push":
                return new PushNotificacao();
            default:
                throw new IllegalArgumentException("Tipo de notificação desconhecido: " + tipo);
        }
    }
}

// 4. Uso no código cliente
public class ExemploFactoryMethod {
    public static void main(String[] args) {
        Notificacao email = NotificacaoFactory.criarNotificacao("email");
        email.enviar("Seu pedido foi processado!");

        Notificacao sms = NotificacaoFactory.criarNotificacao("SMS");
        sms.enviar("Seu código de verificação é 12345.");

        Notificacao push = NotificacaoFactory.criarNotificacao("Push");
        push.enviar("Nova mensagem na sua caixa de entrada!");
    }
}

Com o Factory Method, o código cliente (ExemploFactoryMethod) não precisa saber os detalhes de como EmailNotificacao, SmsNotificacao ou PushNotificacao são instanciadas. Ele apenas pede uma notificação de um certo tipo à NotificacaoFactory, que se encarrega da criação. Isso facilita a adição de novos tipos de notificação no futuro sem modificar o código que as utiliza.

Padrões Estruturais: Conectando Interfaces Incompatíveis com o Adapter

Os padrões estruturais ajudam a organizar classes e objetos em estruturas maiores, garantindo que eles possam trabalhar juntos de forma eficiente. O padrão Adapter é um dos mais úteis quando você precisa que duas interfaces incompatíveis colaborem.

Imagine que você tem um sistema legado que usa uma OldPaymentGateway com um método processPaymentLegacy(long amount, String currency). Agora, você está desenvolvendo um novo módulo que espera uma interface NewPaymentProcessor com um método process(double value, String paymentMethod). As interfaces são diferentes, mas a funcionalidade é a mesma. O Adapter entra em cena para “traduzir” uma interface para a outra.

// 1. Interface do sistema novo (Target)
interface NewPaymentProcessor {
    void process(double value, String paymentMethod);
}

// 2. Classe do sistema legado (Adaptee)
class OldPaymentGateway {
    public void processPaymentLegacy(long amountInCents, String currencyCode) {
        System.out.println("Processando pagamento legado de " + amountInCents + " " + currencyCode + "...");
        // Lógica de processamento real do gateway antigo
    }
}

// 3. O Adapter: implementa a interface nova e usa a classe legada
class PaymentGatewayAdapter implements NewPaymentProcessor {
    private OldPaymentGateway oldGateway;

    public PaymentGatewayAdapter(OldPaymentGateway oldGateway) {
        this.oldGateway = oldGateway;
    }

    @Override
    public void process(double value, String paymentMethod) {
        // Converte a entrada da nova interface para a antiga
        long amountInCents = (long) (value * 100);
        String currencyCode = "BRL"; // Exemplo: assume BRL

        System.out.println("Adaptando pagamento para gateway antigo: " + paymentMethod);
        oldGateway.processPaymentLegacy(amountInCents, currencyCode);
    }
}

// 4. Uso no código cliente
public class ExemploAdapter {
    public static void main(String[] args) {
        OldPaymentGateway legacyGateway = new OldPaymentGateway();
        NewPaymentProcessor newProcessor = new PaymentGatewayAdapter(legacyGateway);

        // O novo módulo pode usar o adaptador como se fosse um NewPaymentProcessor
        newProcessor.process(150.75, "Cartão de Crédito");
    }
}

O PaymentGatewayAdapter permite que o NewPaymentProcessor interaja com o OldPaymentGateway sem que o NewPaymentProcessor precise saber dos detalhes da interface antiga. Isso é extremamente útil em cenários de integração com sistemas de terceiros ou refatoração de código legado, promovendo o desacoplamento e a reutilização.

Padrões Comportamentais: Eliminando Condicionais Complexas com o Strategy

Os padrões comportamentais lidam com a forma como os objetos interagem e distribuem responsabilidades. O padrão Strategy é um dos mais poderosos para eliminar blocos if/else aninhados e tornar o código mais flexível e extensível.

Considere um sistema de e-commerce que precisa processar pagamentos de diferentes formas: Pix, Cartão de Crédito e Boleto. Sem o Strategy, você teria um método processarPagamento com uma série de condicionais para cada tipo de pagamento. Com o Strategy, cada método de pagamento se torna uma “estratégia” encapsulada em sua própria classe.

// 1. Interface da estratégia (comportamento comum)
interface PaymentStrategy {
    void pagar(double valor);
}

// 2. Implementações concretas das estratégias
class PixStrategy implements PaymentStrategy {
    @Override
    public void pagar(double valor) {
        System.out.println("Pagamento de R$ " + valor + " via Pix.");
        // Lógica específica do Pix
    }
}

class CreditCardStrategy implements PaymentStrategy {
    @Override
    public void pagar(double valor) {
        System.out.println("Pagamento de R$ " + valor + " via Cartão de Crédito.");
        // Lógica específica do Cartão
    }
}

class BoletoStrategy implements PaymentStrategy {
    @Override
    public void pagar(double valor) {
        System.out.println("Pagamento de R$ " + valor + " via Boleto Bancário.");
        // Lógica específica do Boleto
    }
}

// 3. O Contexto que usa a estratégia
class PaymentService {
    private PaymentStrategy strategy;

    public void setPaymentStrategy(PaymentStrategy strategy) {
        this.strategy = strategy;
    }

    public void executarPagamento(double valor) {
        if (strategy == null) {
            throw new IllegalStateException("Estratégia de pagamento não definida.");
        }
        strategy.pagar(valor);
    }
}

// 4. Uso no código cliente
public class ExemploStrategy {
    public static void main(String[] args) {
        PaymentService service = new PaymentService();

        // Paga com Pix
        service.setPaymentStrategy(new PixStrategy());
        service.executarPagamento(100.00);

        // Paga com Cartão de Crédito
        service.setPaymentStrategy(new CreditCardStrategy());
        service.executarPagamento(250.50);

        // Paga com Boleto
        service.setPaymentStrategy(new BoletoStrategy());
        service.executarPagamento(75.20);
    }
}

O PaymentService (o contexto) não sabe qual estratégia específica está sendo usada; ele apenas invoca o método pagar() da interface PaymentStrategy. Isso torna o sistema extremamente flexível: para adicionar um novo método de pagamento, basta criar uma nova classe que implemente PaymentStrategy, sem modificar o PaymentService existente. Isso é um exemplo clássico do princípio Open/Closed (O de SOLID).

Design Patterns no Mundo Real: Como o Spring Boot usa Singleton e Template Method

Os padrões de projeto não são apenas conceitos acadêmicos; eles estão embutidos em muitos dos frameworks e bibliotecas que usamos diariamente. O Spring Boot, por exemplo, faz uso extensivo de vários padrões sob o capô.

Singleton em Beans do Spring Boot

Um dos padrões mais onipresentes no Spring Boot é o Singleton. Por padrão, todos os Beans gerenciados pelo contêiner IoC do Spring são Singletons. Isso significa que, para uma determinada definição de Bean, o Spring cria apenas uma única instância desse objeto e a reutiliza em toda a aplicação. Isso economiza recursos e garante que componentes que não precisam de estado por requisição sejam eficientes.

// Exemplo de um Bean Singleton no Spring Boot
@Service
public class MySingletonService {

    private int counter = 0; // Exemplo de estado mutável

    public void incrementCounter() {
        counter++;
        System.out.println("Counter: " + counter);
    }

    public int getCounter() {
        return counter;
    }
}

// Em um Controller ou outro componente
@RestController
public class MyController {

    private final MySingletonService service;

    public MyController(MySingletonService service) {
        this.service = service;
    }

    @GetMapping("/increment")
    public String increment() {
        service.incrementCounter();
        return "Counter incrementado para: " + service.getCounter();
    }
}

Impacto no gerenciamento de estado: Como um Bean Singleton é compartilhado por todas as requisições, ele deve ser stateless (sem estado) ou ter seu estado gerenciado de forma thread-safe. Se MySingletonService mantivesse um estado mutável (counter no exemplo) sem sincronização adequada, múltiplas requisições simultâneas poderiam levar a condições de corrida e resultados inconsistentes. O Spring oferece outros escopos (como prototype, request, session) para lidar com Beans que precisam de estado por requisição ou por usuário.

Template Method

O Template Method é outro padrão amplamente utilizado no Spring, especialmente em suas classes utilitárias e de integração. Ele define o esqueleto de um algoritmo em um método, deixando que subclasses implementem ou sobrescrevam algumas etapas específicas sem alterar a estrutura geral do algoritmo.

Por exemplo, classes como JdbcTemplate ou RestTemplate (agora WebClient é mais comum) do Spring utilizam o Template Method. Elas fornecem métodos de alto nível para operações comuns (como executar uma query SQL ou fazer uma requisição HTTP), mas permitem que você forneça callbacks ou implemente interfaces para personalizar partes específicas do processo (como mapear resultados de um ResultSet para um objeto Java).

Ao entender esses padrões, você não apenas escreve código melhor, mas também compreende a arquitetura por trás dos frameworks que utiliza, tornando-se um desenvolvedor mais eficaz.

O Perigo do Overengineering: Quando NÃO Usar um Padrão de Projeto

Embora os padrões de projeto sejam ferramentas valiosas, é crucial entender que eles não são “balas de prata” ou soluções obrigatórias para qualquer trecho de código. O uso excessivo ou inadequado de padrões pode levar ao overengineering, que é a aplicação de uma solução excessivamente complexa para um problema simples. Isso resulta em:

  • Complexidade desnecessária: O código se torna mais difícil de entender, manter e depurar, pois há mais classes e abstrações do que o realmente necessário.
  • Aumento do tempo de desenvolvimento: Implementar um padrão complexo leva mais tempo do que uma solução simples, especialmente se o problema não justifica a complexidade.
  • Acoplamento indesejado: Um padrão mal aplicado pode, ironicamente, aumentar o acoplamento entre componentes, em vez de reduzi-lo.

Quando NÃO usar um padrão de projeto:

  1. Quando uma solução simples é suficiente: Se um if/else resolve o problema de forma clara e legível, não force um Strategy. Se você precisa de apenas uma instância de uma classe e não há lógica complexa de criação ou necessidade de extensibilidade futura, um simples new é perfeitamente aceitável, e um Singleton pode ser um exagero.
  2. Quando o problema ainda não existe: Evite a tentação de adicionar padrões “para o futuro”. Siga o princípio YAGNI (You Ain’t Gonna Need It – Você Não Vai Precisar Disso). Refatore para um padrão quando a necessidade surgir e a complexidade do código justificar.
  3. Quando a curva de aprendizado é muito alta para o benefício: Em equipes menos experientes, introduzir padrões complexos pode gerar mais confusão do que clareza. Comece com padrões mais simples e adicione complexidade gradualmente.
  4. Quando o padrão não se encaixa naturalmente: Não tente encaixar um problema em um padrão à força. Isso geralmente leva a implementações desajeitadas e contraproducentes.

A chave é o equilíbrio. Pense nos padrões como ferramentas em sua caixa de ferramentas: você não usa um martelo para apertar um parafuso. Avalie o problema, considere a simplicidade e a manutenibilidade, e só então decida se um padrão de projeto é a ferramenta mais adequada.

Conclusão

Os padrões de projeto em Java são um conhecimento fundamental para qualquer desenvolvedor que busca escrever código de alta qualidade. Eles oferecem um vocabulário comum para discutir soluções de design e promovem a criação de sistemas mais flexíveis, manuteníveis e escaláveis.

Ao focar em padrões práticos como Factory Method, Adapter e Strategy, e entender como eles se manifestam em frameworks como o Spring Boot, você ganha uma perspectiva valiosa sobre a arquitetura de software moderna. Lembre-se sempre de usar esses padrões com discernimento, evitando o overengineering e priorizando a simplicidade e a clareza do código. Com a prática, você desenvolverá a intuição para aplicar o padrão certo no momento certo, elevando a qualidade do seu trabalho e a sua carreira em tecnologia.

FAQ

Qual a diferença prática entre padrões de projeto e princípios SOLID?

Os princípios SOLID (Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, Dependency Inversion) são diretrizes gerais de design que visam tornar o código mais flexível, robusto e fácil de manter. Já os padrões de projeto são soluções arquiteturais específicas e testadas para problemas recorrentes de design de software. Pense nos princípios SOLID como as “regras de ouro” do bom design, e nos padrões de projeto como “receitas” prontas que aplicam esses princípios para resolver problemas específicos.

O uso de Singleton no Spring Boot pode causar problemas de concorrência?

Sim, se o Bean Singleton mantiver estado mutável sem mecanismos de sincronização adequados. Como os Beans Singleton do Spring Boot atendem a múltiplas requisições simultaneamente, qualquer estado que eles mantiverem será compartilhado entre essas requisições. Para evitar condições de corrida e inconsistências, Beans Singleton devem ser preferencialmente stateless (sem estado) ou ter seu estado gerenciado de forma thread-safe (por exemplo, usando ThreadLocal ou objetos imutáveis).

Preciso aprender todos os 23 padrões do GoF de uma vez?

Não. Tentar aprender todos os 23 padrões de uma vez pode ser esmagador e contraproducente. O ideal é focar nos padrões mais comuns e práticos para o seu dia a dia (como Strategy, Factory Method, Singleton, Adapter e Observer). Aprenda os demais conforme as necessidades de arquitetura dos seus projetos surgirem. A compreensão profunda de alguns padrões e a capacidade de aplicá-los corretamente é muito mais valiosa do que um conhecimento superficial de todos eles.

Marcos Costa

Sobre Marcos Costa

Desenvolvedor backend com foco em arquitetura de software, automação e produtos digitais.

Ver mais artigos