Creating objects
Singleton, factory, builder and prototype — and why Spring turned the first from a pattern into a scope.
Four patterns about the same question: who decides how this object comes into existence, and what do they need to know to do it?
Three of them are worth knowing. One of them Spring has made almost irrelevant, and that is worth knowing too.
Singleton, and why Spring made it boring
One instance, shared. The textbook version is a class that hides its own constructor:
public class Registry {
private static final Registry INSTANCE = new Registry();
private Registry() {}
public static Registry getInstance() { return INSTANCE; }
}That works, and the reason it has a reputation is not the code — it is what the code does to everything around it:
- It is global mutable state, with every problem global state has ever had.
- It cannot be substituted in a test. A class calling
Registry.getInstance()has a hard dependency nothing can replace. - Initialisation order and threading are subtle enough that "double-checked locking" is a famous bug rather than a technique.
Spring's answer removes all three: a bean is a singleton by default, created once by the container and injected wherever it is needed.
@Service
class Registry { } // one instance, and every user declares its dependencyThe instance is still shared and the sharing is now visible in constructors, which means a test passes a different one and nothing global exists. That is the whole improvement, and it is why singleton in a Spring application is not a pattern you implement — it is a scope you choose.
The enum singleton is still the best hand-written form when you need one — serialisation-safe and reflection-safe, which the private-constructor version is not:
public enum Registry { INSTANCE; }Factory — when the caller should not know the class
A factory decides which implementation to return, so callers do not have to:
PaymentProcessor forMethod(String method) {
switch (method) {
case "card": return new CardProcessor();
case "upi": return new UpiProcessor();
default: throw new IllegalArgumentException(method);
}
}Callers get a PaymentProcessor and never name a concrete class. Adding a method changes one place.
You have met static factories constantly, and they are the more common form in modern Java: List.of, Optional.of, Integer.valueOf, Instant.now. Their advantages over a constructor are worth stating because they explain why the JDK keeps adding them:
- They have names.
Duration.ofMinutes(5)againstnew Duration(5, MINUTES). - They need not return a new instance.
Integer.valueOfreturns a cached object for small values — the primitives lesson's==surprise is a factory doing its job. - They can return a subtype.
List.of(1,2)returns a different class fromList.of(1,2,3), and you never need to know.
In Spring, a @Bean method is a factory method, and the container is a factory. BeanFactory is not a coincidental name.
Builder — when a constructor has too many arguments
The problem it solves is telescoping constructors and, worse, this:
new Order(customerId, null, 5, true, null, false, 3); // what is true?A builder names the arguments and lets the object be immutable once built:
Order order = Order.builder()
.customerId(id)
.quantity(5)
.expedited(true)
.build();Worth knowing about the real version: validate in build(), not in the setters. That is the only point where the object is complete, and it is where "expedited orders need an address" can be checked. A builder whose build() does nothing is a mutable object with extra steps.
Three or four parameters do not need a builder. Seven do, and so do any two adjacent parameters of the same type — new Range(start, end) is fine, new Rectangle(2, 3, 4, 5) is a bug waiting to be transposed.
Lombok's @Builder generates it; a record plus a builder is the common modern shape where your baseline allows records.
Prototype — copy an existing object
Create by copying rather than constructing, when construction is expensive or the configuration is easier to copy than to describe.
Java's built-in support for this is Cloneable and clone(), and the honest advice is: do not use it. Cloneable is a marker interface with no clone method on it, Object.clone is protected, the default is a shallow copy, and the interaction with final fields is broken. It is widely regarded as a design mistake in the library.
What to do instead:
// a copy constructor — explicit, and you control the depth
public Order(Order other) { this.customerId = other.customerId; ... }
// or a static factory that says what it does
static Order copyOf(Order other) { ... }The pattern still appears where it earns its place: Spring's @Scope("prototype") means "a new instance per injection", which is the same idea at the container level, and a cached template object that requests copy and modify is a reasonable design.
But prototype is the lowest-value pattern in this lesson, and a copy constructor is almost always the better expression of it.