Programação Java
Padrões de projeto criacionais em Java: Singleton, Factory, Builder, Prototype
Intermediário8 min de leituraAtualizado
Esta página foi traduzida do inglês com a ajuda de IA e conferida com o original. Ler a versão em inglês
Os padrões criacionais separam a forma como os objetos são criados da forma como são usados, e é isso que permite trocar implementações, garantir invariantes e manter a lógica de construção em um só lugar. Este guia explica Singleton, Factory Method, Abstract Factory, Builder e Prototype com código Java 21 idiomático, mostra onde cada um aparece no JDK e no Spring e é franco sobre quais são usados em excesso e o que os recursos modernos da linguagem — enums, records, static factories, injeção de dependências — substituem.
O que você vai aprender
- Explicar o problema que cada padrão criacional resolve e reconhecê-lo no JDK e nos frameworks
- Implementar corretamente um Singleton thread-safe e saber quando um contêiner de DI deveria cuidar dele
- Usar static factories e Factory Method para desacoplar quem chama das classes concretas
- Construir objetos imutáveis com muitos parâmetros opcionais usando o padrão Builder
- Avaliar quando um padrão traz clareza e quando só acrescenta cerimônia
Padrões de projeto são soluções com nome para problemas de design recorrentes. Os nomes importam tanto quanto as soluções: dizer "transforme isso num Builder" ou "isso é, na verdade, uma Abstract Factory" comunica uma estrutura inteira em três palavras. Os cinco padrões criacionais do catálogo original atacam o mesmo problema — código que faz new ConcreteThing(...) fica soldado àquela classe concreta e aos detalhes de sua construção — e o resolvem em situações diferentes. Este guia trata de cada um com código Java 21 e, tão importante quanto, aponta onde a linguagem e os frameworks absorveram o padrão a ponto de você não precisar mais escrevê-lo à mão.
Por que a construção precisa de padrões
Uma chamada a new amarra três decisões de uma vez: qual classe, quais argumentos e quando. Às vezes essas decisões pertencem a outro lugar — à configuração, a uma subclasse, a um objeto que conhece as regras para montar uma instância válida. Três sintomas indicam um padrão criacional:
- Uma classe é instanciada em muitos lugares e você precisa trocar a implementação usada, ou adicionar cache, pooling ou logging em volta da criação.
- Um construtor acumulou muitos parâmetros, a maioria opcional, e quem chama passa
nullou0nos que não interessam. - Criar um objeto exige várias etapas, validação entre campos ou conhecimento de outros objetos com os quais quem chama não deveria se preocupar.
Os padrões a seguir são as respostas clássicas. Antes deles, vale mencionar uma construção que serve de base para vários: o static factory method (LocalDate.of, List.of, Executors.newFixedThreadPool). Ao contrário de um construtor, ele tem um nome descritivo, pode devolver uma instância em cache ou já existente, pode devolver um subtipo ou uma interface e pode escolher a classe em tempo de execução. Muitos desses padrões são static factories com um pouco mais de estrutura.
Singleton
Intenção: garantir que uma classe tenha exatamente uma instância e oferecer um ponto de acesso global a ela.
A implementação de livro-texto — construtor privado, instância estática, getter estático — já foi escrita errado tantas vezes que merece um tratamento preciso. Na JVM, as formas corretas mais simples são um enum ou o holder de inicialização sob demanda:
// Enum singleton: serialisation-safe, reflection-safe, thread-safe by the JLS
public enum ClockSource {
INSTANCE;
private final Clock clock = Clock.systemUTC();
public Instant now() { return clock.instant(); }
}
// Holder idiom: lazy, thread-safe without synchronisation, for classes that cannot be enums
public final class Registry {
private Registry() {}
private static final class Holder {
static final Registry INSTANCE = new Registry(); // initialised on first access to Holder
}
public static Registry get() { return Holder.INSTANCE; }
}
As duas se apoiam na garantia de inicialização de classes da JVM: uma classe é inicializada exatamente uma vez, sob um lock gerenciado pela JVM, e o resultado fica visível para todas as threads. A famosa versão com double-checked locking — if (instance == null) synchronized (...) { if (instance == null) instance = new ...} — só é correta se o campo for volatile, e mesmo assim exige mais código do que o holder, que não precisa de nenhum.
São os custos do padrão que o tornam controverso. Um Singleton obtido via Registry.get() dentro de uma classe é uma dependência oculta: nada no construtor da classe diz que ela precisa de um Registry, os testes não conseguem substituí-lo e todos compartilham o estado dele. A solução moderna é manter a propriedade — uma única instância — e abrir mão do acesso global: deixe um contêiner de injeção de dependências criar essa instância única e entregá-la a quem declarar que precisa dela. Um bean do Spring tem escopo singleton por padrão, e é por isso que aplicações Spring raramente têm um método getInstance(). Reserve o padrão escrito à mão para código sem contêiner e para recursos realmente globais ao processo, como um backend de logging.
Factory Method
Intenção: definir uma interface para criar um objeto, mas deixar que as subclasses (ou uma função fornecida) decidam qual classe instanciar.
A forma clássica é um método abstrato numa superclasse, chamado pela lógica de template dela:
public abstract class ReportExporter {
public final Path export(Report report, Path dir) throws IOException {
Writer writer = createWriter(); // the factory method
Path target = dir.resolve(report.name() + "." + writer.extension());
Files.writeString(target, writer.render(report));
return target;
}
protected abstract Writer createWriter();
public interface Writer {
String extension();
String render(Report report);
}
}
public final class CsvExporter extends ReportExporter {
@Override protected Writer createWriter() { return new CsvWriter(); }
}
export é fixo; quem decide com o que ele exporta é a subclasse. java.util.Calendar.getInstance(), NumberFormat.getInstance() e o método Collection.iterator() — cada coleção criando seu próprio tipo de iterador — são exemplos no JDK.
Desde o Java 8, a subclasse muitas vezes é desnecessária: passe a criação como um Supplier<Writer> ou uma Function<Config, Writer> no construtor. É o mesmo padrão, com uma lambda no lugar de uma sobrescrita, e ele se compõe melhor: você pode montar um Map<String, Supplier<Writer>> indexado pelo nome do formato e procurar a factory em tempo de execução, que é como pontos de extensão no estilo plugin costumam ser construídos. ThreadFactory, Collectors.toCollection(ArrayList::new) e Stream.generate(supplier) no JDK recebem factories dessa forma.
Uma forma simples relacionada é a factory parametrizada: um único método estático com um switch que devolve uma implementação diferente para cada argumento. É o primeiro passo certo quando há poucas implementações e quem chama sabe pelo nome qual quer; com uma interface sealed, o compilador verifica se o switch é exaustivo.
Abstract Factory
Intenção: fornecer uma interface para criar famílias de objetos relacionados sem nomear suas classes concretas.
Enquanto Factory Method cria um produto, Abstract Factory cria vários que precisam ser consistentes entre si. O exemplo canônico é um toolkit de interface gráfica — botões, menus e diálogos precisam ser todos do mesmo look-and-feel —, e no backend a mesma forma aparece como um conjunto de componentes de infraestrutura relacionados:
public interface StorageFactory {
BlobStore blobs();
QueueClient queue();
LockProvider locks();
}
public final class AwsStorageFactory implements StorageFactory {
private final Region region;
public AwsStorageFactory(Region region) { this.region = region; }
@Override public BlobStore blobs() { return new S3BlobStore(region); }
@Override public QueueClient queue() { return new SqsQueueClient(region); }
@Override public LockProvider locks() { return new DynamoLockProvider(region); }
}
public final class LocalStorageFactory implements StorageFactory {
@Override public BlobStore blobs() { return new DirectoryBlobStore(Path.of("data")); }
@Override public QueueClient queue() { return new InMemoryQueue(); }
@Override public LockProvider locks() { return new FileLockProvider(); }
}
O código da aplicação recebe uma StorageFactory e nunca menciona o S3 nem o diretório local. Trocar a família inteira é trocar um único objeto. O design DriverManager/Connection do JDBC é uma abstract factory em espírito: uma Connection cria Statements e PreparedStatements compatíveis com o seu banco de dados.
Numa aplicação Spring, os profiles e as classes @Configuration cumprem esse papel — uma configuração @Profile("local") define a família local de beans, outra define a família cloud —, por isso interfaces de factory explícitas aparecem principalmente em bibliotecas e em código sem framework.
Builder
Intenção: separar a construção de um objeto complexo da sua representação, para que o mesmo processo possa produzir representações diferentes — na prática, oferecer uma forma legível, passo a passo, de criar um objeto imutável com muitas partes opcionais.
Em Java, ele assume a forma de uma classe estática aninhada com setters fluentes e um build() que valida e cria o produto imutável:
public final class HttpRequestSpec {
private final URI uri;
private final String method;
private final Map<String, String> headers;
private final Duration timeout;
private final byte[] body;
private HttpRequestSpec(Builder b) {
this.uri = b.uri;
this.method = b.method;
this.headers = Map.copyOf(b.headers); // defensive, immutable
this.timeout = b.timeout;
this.body = b.body == null ? new byte[0] : b.body.clone();
}
public static Builder builder(URI uri) { return new Builder(uri); }
public static final class Builder {
private final URI uri; // required: constructor parameter
private String method = "GET"; // optional: sensible defaults
private final Map<String, String> headers = new LinkedHashMap<>();
private Duration timeout = Duration.ofSeconds(30);
private byte[] body;
private Builder(URI uri) { this.uri = Objects.requireNonNull(uri); }
public Builder method(String method) { this.method = method; return this; }
public Builder header(String name, String v) { headers.put(name, v); return this; }
public Builder timeout(Duration timeout) { this.timeout = timeout; return this; }
public Builder body(byte[] body) { this.body = body; return this; }
public HttpRequestSpec build() {
if (body != null && method.equals("GET")) {
throw new IllegalStateException("GET requests cannot carry a body"); // cross-field rule
}
return new HttpRequestSpec(this);
}
}
}
HttpRequestSpec spec = HttpRequestSpec.builder(URI.create("https://api.example.com/orders"))
.method("POST")
.header("Content-Type", "application/json")
.timeout(Duration.ofSeconds(5))
.body(payload)
.build();
A chamada nomeia cada valor, o produto é imutável, os valores padrão ficam num só lugar e a validação entre campos roda uma única vez em build(). O JDK usa essa forma em HttpRequest.newBuilder(), ProcessBuilder, StringBuilder (um builder de strings) e Stream.Builder; o @Builder do Lombok a gera, e records com muitos componentes costumam vir acompanhados de um builder escrito à mão.
Vale conhecer duas variantes. Um staged builder usa uma cadeia de interfaces para que as etapas obrigatórias sejam chamadas em ordem (.uri(...) devolve um tipo que só oferece .method(...)) — mais seguro, porém com mais código. E um builder sobre um record pode se resumir a uma classe mutável cujo build() chama o construtor canônico do record, mantendo a validação no construtor compacto do record.
Não crie um Builder para um tipo com dois campos obrigatórios; um construtor ou um record é mais claro. O padrão se paga a partir de quatro parâmetros ou quando a opcionalidade é real.
Prototype
Intenção: criar objetos novos copiando uma instância existente em vez de construí-los do zero.
Prototype faz diferença quando construir um objeto é caro (carregou uma configuração grande, compilou um template, abriu um pool de conexões) ou quando quem chama quer "um igual àquele, com uma pequena mudança". O mecanismo nativo do Java é Object.clone() com a interface marcadora Cloneable, e ele é amplamente considerado um erro de design: clone() é protected em Object, faz uma cópia rasa campo a campo, ignora os construtores e não permite atribuir aos campos final valores diferentes dos do original. Prefira cópia explícita:
public final class PipelineConfig {
private final List<String> stages;
private final Map<String, String> options;
public PipelineConfig(List<String> stages, Map<String, String> options) {
this.stages = List.copyOf(stages);
this.options = Map.copyOf(options);
}
public PipelineConfig(PipelineConfig other) { // copy constructor
this(other.stages, other.options);
}
public PipelineConfig withOption(String key, String value) { // "wither": copy with one change
Map<String, String> next = new HashMap<>(options);
next.put(key, value);
return new PipelineConfig(stages, next);
}
}
Com tipos imutáveis o padrão fica quase invisível: um método "wither" devolve uma cópia modificada e o original permanece intacto, o que o torna seguro para compartilhar entre threads. Records tornam isso idiomático (new Config(existing.name(), newTimeout)), e uma sintaxe no estilo with para records já foi discutida para a linguagem, mas não está no Java 21.
Quando cópias profundas de grafos mutáveis são realmente necessárias — um modelo de documento, o estado de um jogo —, um construtor de cópia por classe que copia os filhos é mais previsível que clone(), e serializar para bytes e de volta é um último recurso, lento e frágil.
Como escolher entre eles
| Padrão | Use quando | Substituto moderno |
|---|---|---|
| Singleton | Exatamente uma instância de um recurso global ao processo, sem contêiner | Escopo singleton de um contêiner de DI; enum para objetos do tipo constante |
| Factory Method | Subclasses ou quem chama escolhem o produto concreto | Parâmetro Supplier<T> / Function; static factory com switch |
| Abstract Factory | Vários produtos precisam vir de uma mesma família consistente | Profiles do Spring e classes de configuração |
| Builder | Muitos parâmetros, valores opcionais, validação entre campos, imutabilidade | Record + construtor compacto para tipos pequenos com tudo obrigatório |
| Prototype | Cópia com alterações ou construção cara | Construtores de cópia e withers em tipos imutáveis |
Erros comuns
Singleton como variável global. Estado estático mutável acessível de qualquer lugar, impossível de testar e sujeito a race conditions. Injete-o.
Singleton lazy quebrado. Double-checked locking sem volatile, ou uma simples verificação de null sem sincronização, cria duas instâncias sob carga. Use um enum ou o holder.
Factory que só encapsula o new. Um ThingFactory.create() que devolve new Thing() sem nenhum ponto de variação é pura cerimônia. Adote o padrão quando houver uma segunda implementação ou uma regra de construção a centralizar.
Produto mutável depois do build(). Um builder cujo produto ainda tem setters perde a imutabilidade que o justificava.
Builder sem validação. build() é o único lugar que enxerga todos os campos; verifique ali os invariantes entre campos.
clone() em classes com campos mutáveis. A cópia rasa compartilha as partes mutáveis; cada cópia altera as listas da outra. Copie explicitamente.
Reflection driblando um Singleton. Um construtor privado pode ser chamado via reflection; a forma com enum é a única que a JVM protege, e também a única que sobrevive à serialização como instância única.
Principais pontos
- Padrões criacionais desacoplam o código das classes concretas e centralizam as regras de construção; o static factory method é o bloco básico da maioria deles.
- Implemente um Singleton como enum ou com o holder de inicialização sob demanda, e prefira o escopo singleton de um contêiner de DI para que a dependência fique visível e testável.
- Factory Method delega a escolha do produto a uma subclasse ou, no Java moderno, a um
Supplier/Functionpassado como parâmetro; Abstract Factory faz o mesmo para uma família de produtos que precisam combinar. - Builder é a resposta para muitos parâmetros opcionais: valores nomeados na chamada, imutabilidade, um só lugar para valores padrão e validação.
- Em Java, Prototype significa construtores de cópia e withers em tipos imutáveis, não
Cloneable. - Aplique um padrão quando a variação que ele suporta realmente existir; um padrão com uma única implementação é indireção sem benefício.
Fontes e leituras complementares
Perguntas frequentes
- O padrão Singleton é um antipadrão?
- A versão escrita à mão com acesso global geralmente é: ela esconde dependências, faz os testes compartilharem estado e é difícil de substituir. A ideia de uma única instância por aplicação é válida, e é exatamente o que um bean do Spring com escopo singleton oferece, injetado em vez de buscado. Escreva uma classe Singleton só para recursos realmente globais ao processo, em código que não tenha contêiner.
- Qual é a diferença entre Factory Method e Abstract Factory?
- Factory Method é um único método sobrescrevível que cria um produto e deixa as subclasses decidirem a classe concreta. Abstract Factory é um objeto cujos métodos criam uma família de produtos relacionados que precisam ser usados juntos, como os widgets de um mesmo look-and-feel ou os drivers de um mesmo banco de dados. Abstract factories costumam ser implementadas com factory methods.
- Quando devo usar um Builder em vez de um construtor?
- Quando um tipo tem mais de três ou quatro parâmetros, vários deles opcionais, ou quando a validação precisa considerar vários campos em conjunto. Um builder mantém o resultado imutável, dá nome a cada valor na chamada e evita a explosão de construtores telescópicos. Para um valor pequeno com todos os campos obrigatórios, o construtor canônico de um record é mais simples.
- Devo usar clone() no padrão Prototype?
- Prefira um construtor de cópia ou um método estático de cópia. Object.clone é protected, exige a interface marcadora Cloneable, faz por padrão uma cópia rasa e ignora os construtores, o que complica invariantes e campos final. Um construtor de cópia é explícito, profundo onde você decidir e funciona com campos final.