What creational patterns solve
A design pattern is a named, reusable solution to a problem that keeps recurring in object-oriented design. The classic catalogue comes from the 1994 book Design Patterns by Gamma, Helm, Johnson and Vlissides, often called the Gang of Four (GoF) book. It groups 23 patterns into three families:
- Creational — how objects are created.
- Structural — how objects are composed into larger structures (next lesson).
- Behavioural — how objects communicate and share responsibility (lesson after that).
Creational patterns deal with one recurring tension: code that calls new ConcreteClass(...) is tied to that exact class. If the class must change based on configuration, platform or input, or if construction is complex, scattering new across the codebase makes change painful. Creational patterns move the "which class, and how to build it" decision into one place.
Interviewers ask about creational patterns in two ways. First, directly: "Implement a thread-safe singleton" is one of the most commonly asked Java questions, with follow-ups on volatile, reflection and serialisation. Second, inside an LLD problem: "How do you create the right vehicle object from a type string?" or "Your Order has 12 optional fields — how is it constructed?" This lesson covers each pattern in the same format — problem, structure, Java code, when to use, when not to, and an interview question — so you can answer either kind.
Singleton
Problem
Some things should exist exactly once in a process: a configuration registry loaded at start-up, a connection pool, an in-memory cache shared by everyone. You want one instance and a single point of access, and you want to prevent accidental duplicates.
Structure
+----------------------------------+
| Singleton |
+----------------------------------+
| - instance: Singleton {static} |
| - Singleton() (private) |
+----------------------------------+
| + getInstance(): Singleton |
| {static} |
+----------------------------------+
Three ingredients: a private constructor (no one else can call new), a static field holding the one instance, and a static accessor.
Version 1: eager initialisation
final class EagerConfig {
private static final EagerConfig INSTANCE = new EagerConfig();
private EagerConfig() {
// load settings
}
static EagerConfig getInstance() { return INSTANCE; }
}
The instance is created when the class is initialised. The JVM guarantees class initialisation happens once and is thread-safe, so this is correct with no locking. The only downside: the object is created even if never used. For cheap objects that is irrelevant.
Version 2: lazy, not thread-safe
final class LazyUnsafe {
private static LazyUnsafe instance;
private LazyUnsafe() {}
static LazyUnsafe getInstance() {
if (instance == null) { // two threads can both see null...
instance = new LazyUnsafe(); // ...and both create an instance
}
return instance;
}
}
This is a race condition: a bug where the result depends on the timing of threads. Thread A checks null, gets paused, thread B checks null and creates an instance, then A resumes and creates a second one.
Version 3: synchronized method
final class LazySynchronized {
private static LazySynchronized instance;
private LazySynchronized() {}
static synchronized LazySynchronized getInstance() {
if (instance == null) {
instance = new LazySynchronized();
}
return instance;
}
}
Correct, but every call acquires a lock, even long after the instance exists. Under heavy use that is unnecessary contention. Modern JVMs make uncontended locks cheap, so in practice this is often fine — but interviewers will ask for the next version.
Version 4: double-checked locking with volatile
final class DclSingleton {
private static volatile DclSingleton instance;
private DclSingleton() {}
static DclSingleton getInstance() {
DclSingleton local = instance; // one volatile read on the fast path
if (local == null) { // first check, no lock
synchronized (DclSingleton.class) {
local = instance;
if (local == null) { // second check, under the lock
local = new DclSingleton();
instance = local;
}
}
}
return local;
}
}
Why two checks? The first avoids locking once the instance exists. The second is needed because two threads can pass the first check; the one that enters the lock second must see that the first already created the instance.
Why volatile? Creating an object is several steps: allocate memory, run the constructor, assign the reference. Without volatile, the Java memory model allows the compiler or CPU to make the reference visible to another thread before the constructor's writes are visible. A second thread could then see a non-null instance on the first check, skip the lock, and use a half-constructed object. Since Java 5, a volatile write and a later volatile read of the same field create a happens-before relationship: everything the writing thread did before the write (including the constructor) is visible to the reading thread after the read. That closes the hole.
The local variable is a small optimisation: it reads the volatile field once on the common path instead of twice.
Common mistake
Writing double-checked locking without volatile. It compiles, usually works in testing, and fails rarely and unpredictably in production. Interviewers ask "why is volatile needed here?" specifically to catch this. Answer with "safe publication": without it, another thread can see the reference before the constructor's writes.
Version 5: initialisation-on-demand holder
final class HolderSingleton {
private HolderSingleton() {}
private static final class Holder {
static final HolderSingleton INSTANCE = new HolderSingleton();
}
static HolderSingleton getInstance() { return Holder.INSTANCE; }
}
The nested Holder class is not initialised until getInstance first touches it. Class initialisation is lazy and thread-safe by JVM guarantee, so you get lazy creation with no explicit locking and no volatile. This is the cleanest lazy version for ordinary classes.
Version 6: enum singleton
enum IdGenerator {
INSTANCE;
private long next = 1;
synchronized long nextId() { return next++; }
}
class EnumSingletonDemo {
public static void main(String[] args) {
System.out.println(IdGenerator.INSTANCE.nextId()); // 1
System.out.println(IdGenerator.INSTANCE.nextId()); // 2
}
}
Joshua Bloch's Effective Java recommends this form. The JVM guarantees each enum constant is created once. It also handles two attacks that break the other versions:
- Reflection:
Constructor.setAccessible(true)can call a private constructor and create a second instance of a normal class. The JVM refuses to create enum instances reflectively. - Serialisation: deserialising a normal
Serializablesingleton creates a new object unless you add areadResolve()method returning the instance. Enum serialisation always resolves to the existing constant.
Note the synchronized on nextId: a singleton is shared by all threads, so its mutable state still needs protection. The pattern only guarantees one instance, not thread-safe methods. (An AtomicLong would be a lock-free alternative.)
The limitation: an enum cannot extend another class, and it is eagerly created when the enum class is initialised.
| Version | Lazy | Thread-safe | Reflection-safe | Serialisation-safe | Notes |
|---|---|---|---|---|---|
| Eager | No | Yes | No | Needs readResolve | Simplest |
| Lazy unsafe | Yes | No | No | No | Never use |
| Synchronized method | Yes | Yes | No | Needs readResolve | Lock on every call |
| Double-checked + volatile | Yes | Yes | No | Needs readResolve | Classic interview answer |
| Holder idiom | Yes | Yes | No | Needs readResolve | Cleanest lazy form |
| Enum | No (created with enum class) | Yes | Yes | Yes | Recommended when it fits |
Why singletons hurt testability
Singleton is the most criticised GoF pattern, and you should be able to explain why:
- Hidden dependencies. A method calling
PaymentConfig.getInstance()deep inside does not declare that dependency in its constructor or parameters. Readers and tests cannot see it. - Global mutable state. If one test changes the singleton's state, the next test sees it. Tests become order-dependent and flaky.
- Cannot substitute a fake. Code that calls
Database.getInstance()cannot be pointed at an in-memory database for a unit test without hacks. - Tight coupling to a concrete class. Callers depend on the singleton class itself, violating the dependency-inversion principle from the SOLID lesson.
- Lifecycle is fixed to the process. You cannot have "one per tenant" or "one per test" without redesign.
The better approach: "single instance" is a decision for the composition root, not the class. Create one object in main (or let a DI container create it with singleton scope) and pass it to whoever needs it through constructors.
interface Clock {
long now();
}
class SystemClock implements Clock {
public long now() { return System.currentTimeMillis(); }
}
class SessionService {
private final Clock clock;
SessionService(Clock clock) { this.clock = clock; }
boolean isExpired(long createdAt, long ttlMillis) { return clock.now() - createdAt > ttlMillis; }
}
class CompositionRoot {
public static void main(String[] args) {
Clock clock = new SystemClock(); // one instance, chosen here
SessionService sessions = new SessionService(clock);
Clock fixed = () -> 10_000L; // tests pass a fake instead
System.out.println(new SessionService(fixed).isExpired(0, 5_000)); // true
System.out.println(sessions.isExpired(clock.now(), 5_000)); // false
}
}
When to use, when not to
- Use for truly process-wide, stateless or read-mostly resources where a second instance would be a bug: a hardware interface, a JVM-wide registry. In an interview, an enum singleton for an ID generator or a configuration object is acceptable if you mention the testability trade-off.
- Avoid for anything business-related (services, repositories), anything you want to fake in tests, and anything that might later need more than one instance.
Interview question
"Implement a thread-safe lazy singleton and explain each line." Give the double-checked locking version with volatile, explain the two checks and safe publication, then mention the holder idiom as simpler and the enum as reflection- and serialisation-proof. Finish with why you would usually prefer dependency injection.
Factory Method
Problem
A class needs to create objects but should not hard-code which concrete class to instantiate. Example: a notification module creates email, SMS or push senders depending on configuration. If every caller writes if (type == EMAIL) new EmailSender() else if ..., adding WhatsApp touches every caller.
Two related ideas are both called "factory":
- A simple factory (or static factory) is one method that takes a parameter and returns the matching object. It is not a GoF pattern but is the most common form in practice.
- The GoF Factory Method pattern defines an abstract creation method in a base class and lets subclasses decide which product to create.
Structure
+-------------------------+ +-----------------+
| <<abstract>> Creator | creates | <<interface>> |
+-------------------------+- - - - ->| Product |
| + operation() | +--------^--------+
| # createProduct() | :
| {abstract} | +--------+--------+
+-----------^-------------+ : :
| +-------------+ +-------------+
+-------+--------+ | ProductA | | ProductB |
| | +-------------+ +-------------+
+-----------+ +-----------+
| CreatorA | | CreatorB | CreatorA.createProduct()
+-----------+ +-----------+ returns new ProductA()
Java code
First the simple factory, which is what you will use most in LLD rounds:
interface Vehicle {
String type();
}
class Bike implements Vehicle { public String type() { return "bike"; } }
class Car implements Vehicle { public String type() { return "car"; } }
class Truck implements Vehicle { public String type() { return "truck"; } }
enum VehicleKind { BIKE, CAR, TRUCK }
final class VehicleFactory {
private VehicleFactory() {}
static Vehicle create(VehicleKind kind) {
return switch (kind) {
case BIKE -> new Bike();
case CAR -> new Car();
case TRUCK -> new Truck();
};
}
}
class SimpleFactoryDemo {
public static void main(String[] args) {
System.out.println(VehicleFactory.create(VehicleKind.CAR).type()); // car
}
}
Because the switch covers every enum constant with no default, the compiler reports an error if you add a constant and forget a case. That turns a runtime bug into a compile-time one.
Now the GoF Factory Method. A report exporter has a fixed algorithm (gather rows, format, write), and subclasses decide which formatter to create:
import java.util.List;
interface Formatter {
String format(List<String> rows);
}
class CsvFormatter implements Formatter {
public String format(List<String> rows) { return String.join(",", rows); }
}
class JsonFormatter implements Formatter {
public String format(List<String> rows) {
StringBuilder sb = new StringBuilder("[");
for (int i = 0; i < rows.size(); i++) {
if (i > 0) sb.append(',');
sb.append('"').append(rows.get(i)).append('"');
}
return sb.append(']').toString();
}
}
abstract class ReportExporter {
// The template uses the product without knowing its concrete class.
final String export(List<String> rows) {
Formatter f = createFormatter();
return f.format(rows);
}
protected abstract Formatter createFormatter(); // the factory method
}
class CsvExporter extends ReportExporter {
protected Formatter createFormatter() { return new CsvFormatter(); }
}
class JsonExporter extends ReportExporter {
protected Formatter createFormatter() { return new JsonFormatter(); }
}
class FactoryMethodDemo {
public static void main(String[] args) {
List<String> rows = List.of("a", "b");
System.out.println(new CsvExporter().export(rows)); // a,b
System.out.println(new JsonExporter().export(rows)); // ["a","b"]
}
}
Real-world examples in the JDK: Collection.iterator() is a factory method (each collection subclass returns its own iterator type); Calendar.getInstance(), List.of(...) and Integer.valueOf(...) are static factories.
When to use, when not to
- Use a simple factory when callers choose among a family of implementations by a key (enum, string from input), so the choice lives in one place. Use the GoF Factory Method when a base class has a fixed algorithm and subclasses should plug in which product it uses.
- Avoid when there is only one concrete class and no sign of more — just call the constructor. Also avoid a factory that grows into a giant
switchover unrelated types; that suggests a registry map or separate factories.
Interview tip
A factory backed by a map makes registration explicit and keeps the factory closed for modification: Map<VehicleKind, Supplier<Vehicle>> registry. Adding a type means one registry.put(...) line in the wiring code.
Interview question
"What is the difference between a simple factory, Factory Method and Abstract Factory?" A simple factory is one method choosing a class by parameter. Factory Method is an overridable creation method; subclasses decide the product. Abstract Factory creates a family of related products through an interface with several creation methods, and you swap the whole family by swapping the factory object.
Abstract Factory
Problem
You need to create families of related objects that must be used together, and switch whole families at once. Classic example: a UI toolkit with light and dark themes, where a dark button must not be mixed with a light checkbox. In LLD rounds, a common example is a payment integration where each provider needs a matching client, webhook verifier and refund handler.
Structure
+----------------------------+
| <<interface>> UiFactory |
+----------------------------+
| + button(): Button |
| + checkbox(): Checkbox |
+-------------^--------------+
:
+------------+------------+
: :
+-----------------+ +-----------------+
| LightUiFactory | | DarkUiFactory |
+-----------------+ +-----------------+
creates creates
LightButton, DarkButton,
LightCheckbox DarkCheckbox
Java code
interface Button { String render(); }
interface Checkbox { String render(); }
class LightButton implements Button { public String render() { return "[light button]"; } }
class DarkButton implements Button { public String render() { return "[dark button]"; } }
class LightCheckbox implements Checkbox { public String render() { return "[light checkbox]"; } }
class DarkCheckbox implements Checkbox { public String render() { return "[dark checkbox]"; } }
interface UiFactory {
Button button();
Checkbox checkbox();
}
class LightUiFactory implements UiFactory {
public Button button() { return new LightButton(); }
public Checkbox checkbox() { return new LightCheckbox(); }
}
class DarkUiFactory implements UiFactory {
public Button button() { return new DarkButton(); }
public Checkbox checkbox() { return new DarkCheckbox(); }
}
class SettingsScreen {
private final UiFactory ui;
SettingsScreen(UiFactory ui) { this.ui = ui; } // the whole family is injected
String draw() { return ui.button().render() + " " + ui.checkbox().render(); }
}
class AbstractFactoryDemo {
public static void main(String[] args) {
boolean darkMode = true;
UiFactory factory = darkMode ? new DarkUiFactory() : new LightUiFactory();
System.out.println(new SettingsScreen(factory).draw()); // [dark button] [dark checkbox]
}
}
SettingsScreen never names a concrete widget. The one-line choice in main guarantees consistency: it is impossible to get a dark button with a light checkbox.
When to use, when not to
- Use when products come in families that must match, and you switch the family based on environment or configuration (theme, cloud provider, database vendor, test vs production).
- Avoid when products are independent, or there is only one family. Adding a new product type (a
Slider) is painful: every factory implementation must change. Abstract Factory makes adding families easy and adding product types hard — say this trade-off in interviews.
Interview question
"Your app must run on two cloud providers. Storage, queue and secret-manager clients differ per provider. How do you design creation?" An abstract CloudFactory with storage(), queue() and secrets() methods, one implementation per provider, chosen once at start-up and injected. Business code depends only on the Storage, Queue and Secrets interfaces.
Builder
Problem
A class has many fields, several optional, and must be valid and ideally immutable once created. Constructors with many parameters are unreadable and error-prone: in new Pizza("L", true, false, true, false, 2), which boolean means what? Overloading constructors for every combination (the telescoping constructor anti-pattern) explodes. Using a no-argument constructor plus setters leaves the object mutable and possibly half-initialised.
Structure
+-------------------------+ build() +----------------------+
| HttpRequest.Builder |------------>| HttpRequest (final) |
+-------------------------+ +----------------------+
| - method, url (required)| | - method, url |
| - headers, body, | | - headers (copy) |
| timeout (optional) | | - body, timeout |
+-------------------------+ +----------------------+
| + header(n, v): Builder | | - HttpRequest(b) |
| + body(s): Builder | | + getters only |
| + build(): HttpRequest | +----------------------+
+-------------------------+
Java code
import java.util.ArrayList;
import java.util.List;
final class HttpRequest {
private final String method;
private final String url;
private final List<String> headers;
private final String body;
private final int timeoutMillis;
private HttpRequest(Builder b) {
this.method = b.method;
this.url = b.url;
this.headers = List.copyOf(b.headers); // immutable snapshot
this.body = b.body;
this.timeoutMillis = b.timeoutMillis;
}
static Builder builder(String method, String url) { return new Builder(method, url); }
String method() { return method; }
String url() { return url; }
List<String> headers() { return headers; }
String body() { return body; }
int timeoutMillis() { return timeoutMillis; }
static final class Builder {
private final String method; // required
private final String url; // required
private final List<String> headers = new ArrayList<>();
private String body = "";
private int timeoutMillis = 5_000; // sensible default
private Builder(String method, String url) {
this.method = method;
this.url = url;
}
Builder header(String name, String value) {
headers.add(name + ": " + value);
return this; // returning this enables chaining
}
Builder body(String body) { this.body = body; return this; }
Builder timeoutMillis(int t) { this.timeoutMillis = t; return this; }
HttpRequest build() {
if (!url.startsWith("http")) throw new IllegalStateException("bad url");
if (method.equals("GET") && !body.isEmpty()) throw new IllegalStateException("GET has no body");
if (timeoutMillis <= 0) throw new IllegalStateException("timeout must be positive");
return new HttpRequest(this);
}
}
}
class BuilderDemo {
public static void main(String[] args) {
HttpRequest req = HttpRequest.builder("POST", "https://api.example.com/orders")
.header("Content-Type", "application/json")
.body("{\"item\":\"dosa\"}")
.timeoutMillis(2_000)
.build();
System.out.println(req.method() + " " + req.url() + " " + req.headers() + " " + req.timeoutMillis());
}
}
Key points to call out:
- Required parameters go in the builder's constructor, optional ones in chained methods with defaults.
- Validation happens in
build(), across fields, so an invalid object can never exist. - The product is immutable: private constructor,
finalfields, defensive copy of the list. - Builder state is mutable and not thread-safe; that is fine because a builder is normally used by one thread for one construction.
Examples in the JDK: StringBuilder, HttpRequest.newBuilder() in java.net.http, Stream.builder(). Lombok's @Builder generates this code.
The GoF book also describes a Director class that drives a builder through a fixed sequence of steps (for example, "build a standard margherita"). In everyday Java the fluent builder above is far more common; mention the Director only if asked.
In Python
Python's keyword arguments with defaults already solve the readability problem, so builders are rarer. A frozen dataclass gives immutability:
from dataclasses import dataclass, field
@dataclass(frozen=True)
class HttpRequest:
method: str
url: str
headers: tuple = field(default_factory=tuple)
body: str = ""
timeout_ms: int = 5000
def __post_init__(self):
if self.method == "GET" and self.body:
raise ValueError("GET has no body")
r = HttpRequest("POST", "https://api.example.com", headers=(("Accept", "json"),), body="{}")
print(r.timeout_ms) # 5000
When to use, when not to
- Use for objects with roughly four or more parameters, several optional, especially when the result should be immutable and validated as a whole: requests, orders, search queries, configuration.
- Avoid for small classes with two or three required fields — a constructor or a record is clearer. A builder also duplicates every field, so it is more code to maintain.
Interview question
"How does Builder help create immutable objects, and where do you validate?" The builder collects values in mutable fields, then build() passes itself to a private constructor that copies everything into final fields, with defensive copies of collections. Validation that spans fields goes in build() (or the private constructor) so no invalid instance is ever created.
Prototype
Problem
Creating an object from scratch is expensive or complicated (it loaded configuration, ran a calculation, or has many fields set from a template), and you need many similar objects. Prototype says: create new objects by copying an existing one (the prototype) and then adjusting.
Example: a document editor where a user duplicates a slide, or a game spawning many enemies from a configured template.
Structure
+-------------------------+
| <<interface>> Prototype |
+-------------------------+
| + copy(): Prototype |
+-----------^-------------+
:
+---------+---------+
: :
+-------------+ +-------------+
| Shape | | Document |
| copy() { | | copy() { |
| new Shape( | | deep copy |
| this) } | | of pages } |
+-------------+ +-------------+
Shallow vs deep copy
- A shallow copy duplicates the top-level object but shares any referenced objects. Changing a shared list through the copy also changes it in the original.
- A deep copy recursively duplicates referenced mutable objects, so the copy is fully independent.
Shallow copy Deep copy
original --+ original ---> [list A]
+---> [list A] copy ---> [list B] (new)
copy ------+ (shared!)
Java code
Java has Object.clone() and the Cloneable interface, but they are widely considered badly designed: clone is shallow by default, bypasses constructors, throws a checked exception, and Cloneable has no methods. Effective Java recommends a copy constructor or copy factory instead. Here is a copy-constructor prototype that shows both copy kinds:
import java.util.ArrayList;
import java.util.List;
class Slide {
String title;
final List<String> bullets;
Slide(String title, List<String> bullets) {
this.title = title;
this.bullets = bullets;
}
// Shallow: new Slide, same bullets list.
Slide shallowCopy() { return new Slide(title, bullets); }
// Deep: new Slide with its own list (Strings are immutable, so sharing them is safe).
Slide deepCopy() { return new Slide(title, new ArrayList<>(bullets)); }
}
class PrototypeDemo {
public static void main(String[] args) {
Slide template = new Slide("Agenda", new ArrayList<>(List.of("Intro")));
Slide shallow = template.shallowCopy();
shallow.bullets.add("Oops");
System.out.println(template.bullets); // [Intro, Oops] - original changed
Slide deep = template.deepCopy();
deep.bullets.add("Q&A");
deep.title = "Agenda (copy)";
System.out.println(template.bullets); // [Intro, Oops] - original unaffected
System.out.println(deep.bullets); // [Intro, Oops, Q&A]
}
}
How deep to copy is a judgement: copy mutable objects the copy might change; immutable objects (String, Integer, records of immutable values) can be shared safely. A common extension is a prototype registry: a map from name to prototype ("goblin" -> goblinTemplate) from which callers request copies.
Python's copy module makes the distinction explicit:
import copy
template = {"title": "Agenda", "bullets": ["Intro"]}
shallow = copy.copy(template)
deep = copy.deepcopy(template)
shallow["bullets"].append("Oops")
print(template["bullets"]) # ['Intro', 'Oops']
print(deep["bullets"]) # ['Intro']
When to use, when not to
- Use when objects are costly to configure from scratch and many near-identical ones are needed, or when the concrete class is not known to the code that needs a copy (it just calls
copy()). - Avoid for simple objects a constructor builds cheaply, and be careful with object graphs that have cycles or external resources (sockets, file handles), which cannot be meaningfully copied.
Interview question
"What is the difference between shallow and deep copy, and why is clone() discouraged in Java?" Shallow copies share referenced objects; deep copies duplicate mutable ones so the copy is independent. clone() is shallow by default, skips constructors (and their validation), requires the marker Cloneable interface and a checked exception, and interacts awkwardly with final fields. Copy constructors or static copy factories are clearer and safer.
Object Pool (briefly)
Problem
Some objects are expensive to create and can be reused: database connections, threads, large buffers. An object pool keeps a set of ready objects; clients borrow one and return it when done, instead of creating and destroying each time.
Client --borrow()--> +------------------+
| ObjectPool | idle: [c1][c2][c3]
Client <--object---- | max size = N | in use: [c4]
Client --release()-> +------------------+
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.function.Supplier;
class Pool<T> {
private final BlockingQueue<T> idle;
Pool(int size, Supplier<T> factory) {
idle = new ArrayBlockingQueue<>(size);
for (int i = 0; i < size; i++) idle.add(factory.get());
}
T borrow() throws InterruptedException { return idle.take(); } // blocks when empty
void release(T obj) { idle.offer(obj); }
}
class PoolDemo {
public static void main(String[] args) throws InterruptedException {
Pool<StringBuilder> pool = new Pool<>(2, StringBuilder::new);
StringBuilder sb = pool.borrow();
try {
sb.setLength(0); // reset state before use
sb.append("hello");
System.out.println(sb);
} finally {
pool.release(sb); // always return, even on error
}
}
}
The blocking queue makes it thread-safe and makes callers wait when all objects are in use. Real pools (HikariCP for JDBC connections, ThreadPoolExecutor for threads) add validation, timeouts and growth limits.
When to use: objects that are genuinely expensive to create (network connections, threads). When not to: ordinary small objects — modern JVM allocation and garbage collection are fast, and pooling them adds bugs such as leaked objects never returned, or stale state left from the previous user.
Interview question: "What can go wrong with an object pool?" Leaks if a caller forgets to release (use try/finally), state leaking between users if objects are not reset, and exhaustion causing threads to block — so pools need borrow timeouts and monitoring.
Choosing a creational pattern
| Situation | Pattern |
|---|---|
| Exactly one instance should exist process-wide | Singleton (prefer DI with one instance) |
| Pick one implementation from a key or config | Simple factory / registry map |
| Base class algorithm, subclasses choose the product | Factory Method |
| Families of products that must match | Abstract Factory |
| Many optional fields, immutable validated result | Builder |
| Copy an expensive, configured object | Prototype |
| Reuse expensive resources | Object Pool |
In LLD case studies, the most common are the simple factory (vehicles, payment methods, notification senders), Builder (orders, bookings, search queries) and, more rarely, Singleton for a central registry such as the parking lot itself — where you should mention the testability trade-off.
Interview questions
Q1. Why does double-checked locking need volatile?
Object creation involves allocating memory, running the constructor and assigning the reference, and without volatile the memory model allows another thread to see the reference before the constructor's writes. That thread would skip the lock and use a partially built object. A volatile write followed by a volatile read establishes happens-before, guaranteeing the reader sees the fully constructed object.
Q2. Which singleton implementation would you use and why?
For a lazily created singleton I prefer the holder idiom: lazy, thread-safe through class initialisation, no locks. If reflection and serialisation safety matter and the class need not extend another, an enum singleton. In application code, though, I would rather create one instance at the composition root and inject it.
Q3. How can a singleton be broken, and how do you prevent it?
Reflection can call the private constructor; deserialisation can create a new instance; multiple class loaders can each load the class; and cloning, if Cloneable is implemented. Guards include throwing from the constructor if an instance exists, adding readResolve, not implementing Cloneable — or using an enum, which the JVM protects against reflection and serialisation.
Q4. Why are singletons considered an anti-pattern by many?
They introduce global mutable state and hidden dependencies, make tests order-dependent, and prevent substituting fakes. They also couple callers to a concrete class and fix the lifecycle to the whole process. Dependency injection with a single instance gives the "only one" benefit without these costs.
Q5. What is the difference between Factory Method and Abstract Factory?
Factory Method is one overridable method that creates one product, with subclasses choosing the concrete class. Abstract Factory is an object with several creation methods producing a family of related products, swapped as a whole. Abstract Factory often uses factory methods internally.
Q6. When would you choose a Builder over a constructor?
When the class has many parameters, several optional, or parameters of the same type that are easy to mix up, and especially when the result must be immutable and validated across fields. For two or three required fields a constructor or a record is simpler.
Q7. How does a Builder guarantee the built object is immutable?
The product has a private constructor that takes the builder, final fields, no setters, and defensive copies of mutable inputs like lists. Once build() returns, nothing can change the product, even if the builder is reused afterwards.
Q8. Explain shallow vs deep copy with an example.
Copying a slide shallowly creates a new slide object that shares the original's bullet list, so adding a bullet to the copy changes the original. A deep copy creates a new list, so the two are independent. Immutable members like strings can be shared safely in either case.
Q9. Why is Object.clone() discouraged?
It performs a shallow field copy by default, bypasses constructors and their validation, depends on the method-less Cloneable marker interface, throws a checked exception, and conflicts with final fields that need fresh values. Copy constructors or static copy factories are explicit and type-safe.
Q10. How would you create vehicles from user input in a parking lot design?
Parse the input into a VehicleType enum, then use a factory — a switch expression over the enum, or a Map<VehicleType, Function<String, Vehicle>> registry. Callers depend only on Vehicle. Adding a vehicle type then means a new class and one registration, with the compiler flagging an unhandled enum constant in the switch version.
Q11. When is an object pool worth it?
When objects are expensive to create and reusable, such as database connections or threads. For ordinary objects, JVM allocation and garbage collection are fast enough that a pool only adds complexity and bugs like leaks and stale state.
Q12. Is a static factory method a design pattern?
Not one of the GoF 23, but it is a widely recommended idiom (Effective Java item 1). Static factories have names (Money.ofRupees), can return cached instances or subtypes, and can hide implementation classes. List.of, Integer.valueOf and Optional.empty are examples.
Key takeaways
- Creational patterns centralise "which class, and how to build it" so callers stay decoupled from concrete classes.
- Singleton: know eager, synchronized, double-checked with
volatile, holder and enum forms, and why singletons hurt testability. - Prefer creating one instance at the composition root and injecting it over a global singleton.
- Simple factory or a registry map is the everyday tool; Factory Method lets subclasses pick the product.
- Abstract Factory swaps whole families of matching products; adding a new product type is its weak spot.
- Builder gives readable construction with validation in
build()and an immutable result. - Prototype copies configured objects; know shallow vs deep copy and prefer copy constructors over
clone(). - Pool only genuinely expensive resources, and always release in
finally.
Next lesson
Continue with Structural design patterns.

