Creational Patterns — Cheat Sheet
Low Level Design · 8 topics. Download the PDF or the Instagram carousel and share it.
Singleton Pattern
Ensures a class has only one instance and provides a global access point to it.
- ✓volatile is mandatory in Double-Checked Locking; without it the JVM may publish a partially constructed object.
- ✓Enum singleton is the safest approach — immune to reflection and serialization attacks.
- ✓The Initialization-on-Demand Holder idiom is lazy, thread-safe, and has no synchronization overhead.
- ✓Spring @Service/@Component beans are singletons per ApplicationContext, not per JVM.
- ✓Singletons with mutable state require thread-safe data structures (ConcurrentHashMap, AtomicInteger).
- ✓Prefer dependency injection over global Singleton access to improve testability.
// 1. Eager initialization — thread-safe, but instance always created
public class EagerSingleton {
private static final EagerSingleton INSTANCE = new EagerSingleton();
private EagerSingleton() {} // private constructor
public static EagerSingleton getInstance() {
return INSTANCE;
}
}
// 2. Lazy + Double-Checked Locking — thread-safe and lazy
public class LazySingleton {
// volatile prevents instruction reordering (partial construction visibility)
private static volatile LazySingleton instance;
private LazySingleton() {}
public static LazySingleton getInstance() {
if (instance == null) { // first check (no lock)
synchronized (LazySingleton.class) {
if (instance == null) { // second check (with lock)
instance = new LazySingleton();
}
}
}
return instance;
}
}
// 3. Initialization-on-Demand Holder — elegant, lazy, thread-safe
public class HolderSingleton {
private HolderSingleton() {}
private static class Holder {
// Loaded only when Holder is accessed; class loading is thread-safe
private static final HolderSingleton INSTANCE = new HolderSingleton();
}
public static HolderSingleton getInstance() {
return Holder.INSTANCE;
}
}Factory Method Pattern
Defines an interface for creating an object but lets subclasses decide which class to instantiate, decoupling object creation from usage.
- ✓Factory Method decouples the client from concrete classes — client codes against an interface.
- ✓Adding a new product type requires only a new ConcreteCreator subclass (OCP).
- ✓Static factory methods (Effective Java Item 1) are a simpler alternative when subclassing is unnecessary.
- ✓Java standard library examples: Calendar.getInstance(), Collections.unmodifiableList(), Optional.of().
- ✓Factory Method uses inheritance; Abstract Factory uses composition — key distinction.
// Product interface
public interface Notification {
void send(String message);
}
// Concrete products
public class EmailNotification implements Notification {
private final String email;
public EmailNotification(String email) { this.email = email; }
@Override
public void send(String message) {
System.out.println("Email to " + email + ": " + message);
}
}
public class SmsNotification implements Notification {
private final String phone;
public SmsNotification(String phone) { this.phone = phone; }
@Override
public void send(String message) {
System.out.println("SMS to " + phone + ": " + message);
}
}
// Abstract Creator
public abstract class NotificationService {
// Factory Method — subclasses decide what to create
protected abstract Notification createNotification(String recipient);
public void notify(String recipient, String message) {
Notification n = createNotification(recipient); // calls factory method
n.send(message);
}
}
// Concrete Creators
public class EmailNotificationService extends NotificationService {
@Override
protected Notification createNotification(String recipient) {
return new EmailNotification(recipient);
}
}
public class SmsNotificationService extends NotificationService {
@Override
protected Notification createNotification(String recipient) {
return new SmsNotification(recipient);
}
}
// Client
NotificationService service = new EmailNotificationService();
service.notify("user@example.com", "Your order has shipped!");Abstract Factory Pattern
Provides an interface for creating families of related objects without specifying their concrete classes.
- ✓Abstract Factory creates a family of related objects; Factory Method creates one type of object.
- ✓Enforces product family consistency — prevents mixing objects from different families.
- ✓Adding a new product family requires a new concrete factory class (OCP-compliant).
- ✓Adding a new product type requires changing the AbstractFactory interface and ALL concrete factories (costly).
- ✓Commonly implemented with dependency injection — inject the factory, not the products directly.
// Abstract Products
public interface Button {
void render();
void onClick();
}
public interface Checkbox {
void render();
boolean isChecked();
}
// Concrete Products — Windows family
public class WindowsButton implements Button {
@Override public void render() { System.out.println("Rendering Windows Button"); }
@Override public void onClick() { System.out.println("Windows Button clicked"); }
}
public class WindowsCheckbox implements Checkbox {
private boolean checked = false;
@Override public void render() { System.out.println("Rendering Windows Checkbox"); }
@Override public boolean isChecked() { return checked; }
}
// Concrete Products — Mac family
public class MacButton implements Button {
@Override public void render() { System.out.println("Rendering Mac Button"); }
@Override public void onClick() { System.out.println("Mac Button clicked"); }
}
public class MacCheckbox implements Checkbox {
private boolean checked = false;
@Override public void render() { System.out.println("Rendering Mac Checkbox"); }
@Override public boolean isChecked() { return checked; }
}
// Abstract Factory
public interface UIFactory {
Button createButton();
Checkbox createCheckbox();
}
// Concrete Factories
public class WindowsFactory implements UIFactory {
@Override public Button createButton() { return new WindowsButton(); }
@Override public Checkbox createCheckbox() { return new WindowsCheckbox(); }
}
public class MacFactory implements UIFactory {
@Override public Button createButton() { return new MacButton(); }
@Override public Checkbox createCheckbox() { return new MacCheckbox(); }
}
// Client — depends only on interfaces
public class Application {
private final Button button;
private final Checkbox checkbox;
public Application(UIFactory factory) {
this.button = factory.createButton();
this.checkbox = factory.createCheckbox();
}
public void render() {
button.render();
checkbox.render();
}
}
// Wiring at startup
UIFactory factory = System.getProperty("os.name").startsWith("Mac")
? new MacFactory()
: new WindowsFactory();
Application app = new Application(factory);
app.render();Builder Pattern
Separates the construction of a complex object from its representation, enabling the same construction process to create different representations via a fluent API.
- ✓Builder eliminates telescoping constructors and prevents argument-order bugs (e.g. confusing two String params).
- ✓Required fields belong in the Builder constructor; optional fields use fluent setters with defaults.
- ✓The built object should be immutable — no setters, defensive copies on collections.
- ✓Lombok @Builder generates builder boilerplate at compile time; @Builder(toBuilder=true) enables copy-and-modify.
- ✓Director (optional) encapsulates standard build sequences — useful for predefined configurations.
- ✓Validate invariants in build(), not in individual setter methods, for atomic validation.
// Anti-pattern: Telescoping constructor
public class HttpRequest {
public HttpRequest(String url) { ... }
public HttpRequest(String url, String method) { ... }
public HttpRequest(String url, String method, Map<String,String> headers) { ... }
// ... 5 more overloads — unreadable and error-prone
}
// Builder Pattern — immutable HttpRequest
public final class HttpRequest {
private final String url; // required
private final String method; // required
private final Map<String, String> headers;
private final String body;
private final int timeoutMs;
private final boolean followRedirects;
private HttpRequest(Builder builder) {
this.url = builder.url;
this.method = builder.method;
this.headers = Collections.unmodifiableMap(builder.headers);
this.body = builder.body;
this.timeoutMs = builder.timeoutMs;
this.followRedirects = builder.followRedirects;
}
// Getters only — no setters, fully immutable
public String getUrl() { return url; }
public String getMethod() { return method; }
public static class Builder {
private final String url; // required — set in constructor
private final String method;
private Map<String, String> headers = new HashMap<>();
private String body;
private int timeoutMs = 30_000; // default
private boolean followRedirects = true;
public Builder(String url, String method) {
if (url == null || url.isBlank()) throw new IllegalArgumentException("url required");
if (method == null || method.isBlank()) throw new IllegalArgumentException("method required");
this.url = url;
this.method = method;
}
public Builder header(String key, String value) {
this.headers.put(key, value);
return this; // fluent — enables chaining
}
public Builder body(String body) {
this.body = body;
return this;
}
public Builder timeoutMs(int ms) {
if (ms <= 0) throw new IllegalArgumentException("timeout must be positive");
this.timeoutMs = ms;
return this;
}
public Builder followRedirects(boolean follow) {
this.followRedirects = follow;
return this;
}
public HttpRequest build() {
return new HttpRequest(this);
}
}
}
// Usage — readable, order-independent
HttpRequest request = new HttpRequest.Builder("https://api.aicancode.org/courses", "POST")
.header("Authorization", "Bearer token123")
.header("Content-Type", "application/json")
.body("{"title":"LLD Course"}")
.timeoutMs(5_000)
.followRedirects(false)
.build();Prototype Pattern
Creates new objects by copying (cloning) an existing instance, avoiding the cost of creating objects from scratch.
- ✓Prefer copy constructors or static factory copy methods over Cloneable — it is broken by design.
- ✓Shallow copy shares mutable references; deep copy creates fully independent object graphs.
- ✓Prototype Registry caches expensive prototypes and returns clones — combines Prototype + Flyweight.
- ✓Use serialization (serialize + deserialize) for generic deep cloning when copy constructors are impractical.
- ✓Java record types are immutable value objects — cloning is unnecessary for records.
import java.util.ArrayList;
import java.util.List;
public class UserProfile implements Cloneable {
private String name;
private List<String> skills; // mutable reference — clone danger
public UserProfile(String name, List<String> skills) {
this.name = name;
this.skills = skills;
}
// Shallow clone — skills list is SHARED
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone(); // copies reference to skills, not the list itself
}
// Deep clone — skills list is INDEPENDENT
public UserProfile deepClone() {
return new UserProfile(this.name, new ArrayList<>(this.skills));
}
public List<String> getSkills() { return skills; }
public String getName() { return name; }
}
// Demonstrating the difference
UserProfile original = new UserProfile("Alice", new ArrayList<>(List.of("Java", "LLD")));
// Shallow clone
try {
UserProfile shallow = (UserProfile) original.clone();
shallow.getSkills().add("Python"); // modifies original.skills too!
System.out.println(original.getSkills()); // [Java, LLD, Python] — aliased!
} catch (CloneNotSupportedException e) { e.printStackTrace(); }
// Deep clone
UserProfile deep = original.deepClone();
deep.getSkills().add("Python"); // original is unaffected
System.out.println(original.getSkills()); // [Java, LLD] — independentObject Pool Pattern
Manages a set of reusable, pre-initialized objects to avoid the overhead of repeated creation and destruction of expensive resources.
- ✓Object Pool trades memory for speed — pre-allocated objects avoid repeated instantiation overhead.
- ✓Always validate borrowed objects before use — connections can become stale (network timeout, DB restart).
- ✓Return objects to pool in a finally block (or try-with-resources) to prevent pool starvation.
- ✓HikariCP is the Spring Boot default — know its key parameters: maximumPoolSize, minimumIdle, connectionTimeout.
- ✓Pool starvation occurs when all objects are borrowed and no timeout causes callers to block indefinitely.
- ✓Pool size formula: connections = (core_count * 2) + effective_spindle_count (HikariCP documentation).
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;
public class ObjectPool<T> {
private final BlockingQueue<T> pool;
private final PooledObjectFactory<T> factory;
private final int maxSize;
public interface PooledObjectFactory<T> {
T create();
boolean validate(T obj); // check if object is still usable
void destroy(T obj); // cleanup before discard
}
public ObjectPool(PooledObjectFactory<T> factory, int minIdle, int maxSize) {
this.factory = factory;
this.maxSize = maxSize;
this.pool = new ArrayBlockingQueue<>(maxSize);
// Pre-populate with minIdle objects
for (int i = 0; i < minIdle; i++) {
pool.offer(factory.create());
}
}
// Borrow an object — blocks up to timeoutMs
public T borrowObject(long timeoutMs) throws InterruptedException {
T obj = pool.poll(timeoutMs, TimeUnit.MILLISECONDS);
if (obj == null) {
// Pool exhausted — create a new one if below maxSize
obj = factory.create();
} else if (!factory.validate(obj)) {
factory.destroy(obj);
obj = factory.create(); // replace stale object
}
return obj;
}
// Return object to pool
public void returnObject(T obj) {
if (obj != null && factory.validate(obj)) {
if (!pool.offer(obj)) {
factory.destroy(obj); // pool full — discard excess
}
} else if (obj != null) {
factory.destroy(obj);
}
}
public int availableObjects() { return pool.size(); }
}
// Concrete usage: Database Connection Pool
public class ConnectionPool {
private final ObjectPool<java.sql.Connection> pool;
public ConnectionPool(String jdbcUrl, String user, String pass, int min, int max) {
pool = new ObjectPool<>(new ObjectPool.PooledObjectFactory<>() {
@Override
public java.sql.Connection create() {
try { return java.sql.DriverManager.getConnection(jdbcUrl, user, pass); }
catch (Exception e) { throw new RuntimeException("Cannot create connection", e); }
}
@Override
public boolean validate(java.sql.Connection conn) {
try { return !conn.isClosed() && conn.isValid(1); }
catch (Exception e) { return false; }
}
@Override
public void destroy(java.sql.Connection conn) {
try { conn.close(); } catch (Exception ignored) {}
}
}, min, max);
}
public java.sql.Connection borrow() throws InterruptedException {
return pool.borrowObject(5_000);
}
public void release(java.sql.Connection conn) {
pool.returnObject(conn);
}
}Dependency Injection
A technique where an object receives its dependencies from an external source rather than creating them itself, enabling loose coupling and testability.
- ✓Constructor injection is preferred — produces immutable, fully initialized objects without framework dependency.
- ✓Field injection (@Autowired on fields) hides dependencies and requires a Spring context for unit tests.
- ✓DI enables swapping implementations — production uses Stripe, tests use FakePaymentGateway.
- ✓Spring's IoC container manages bean lifecycle: creation, wiring, initialization, and destruction.
- ✓Circular dependencies (A needs B, B needs A) signal a design smell — break the cycle by extracting a third class.
- ✓@Lazy can break circular constructor dependencies as a last resort, but prefer refactoring.
// ❌ Anti-pattern: creating dependencies internally (tight coupling)
public class OrderService {
private final PaymentGateway gateway = new StripePaymentGateway(); // hard dependency
private final EmailClient emailer = new SendGridEmailClient();
// Cannot swap StripePaymentGateway in tests — unit testing is impossible
}
// ✅ Constructor Injection (preferred)
public class OrderService {
private final PaymentGateway gateway;
private final NotificationService notifier;
// Spring auto-detects single constructor — @Autowired optional in Spring 4.3+
public OrderService(PaymentGateway gateway, NotificationService notifier) {
this.gateway = Objects.requireNonNull(gateway, "gateway required");
this.notifier = Objects.requireNonNull(notifier, "notifier required");
}
public Order placeOrder(Cart cart) {
Order order = Order.from(cart);
gateway.charge(order.getTotalAmount()); // depends on interface, not impl
notifier.sendConfirmation(order);
return order;
}
}
// ✅ Setter Injection — for optional dependencies
public class ReportService {
private final DataSource dataSource; // required
private Logger logger; // optional — has a default
public ReportService(DataSource dataSource) { this.dataSource = dataSource; }
@Autowired(required = false)
public void setLogger(Logger logger) { this.logger = logger; }
}
// ❌ Field Injection — avoid in production code
@Service
public class UserService {
@Autowired private UserRepository repo; // hidden dependency, not testable without Spring
}
// Spring wiring (Java config)
@Configuration
public class AppConfig {
@Bean
public PaymentGateway paymentGateway() { return new StripePaymentGateway(); }
@Bean
public OrderService orderService(PaymentGateway gw, NotificationService ns) {
return new OrderService(gw, ns);
}
}Multiton Pattern
Extends Singleton by maintaining a registry of named instances, ensuring one instance per key rather than one instance globally.
- ✓Multiton guarantees one instance per key — expands Singleton scope from global to per-context.
- ✓ConcurrentHashMap.computeIfAbsent() is the idiomatic thread-safe implementation.
- ✓Java Logging framework (java.util.logging.Logger) is the canonical Multiton example.
- ✓Beware of memory leaks — unbounded key sets (e.g. per request-id) cause the registry to grow infinitely.
- ✓EnumMap offers O(1) lookups and is more efficient than HashMap for enum keys.
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
public class Multiton<K, V> {
private final ConcurrentHashMap<K, V> registry = new ConcurrentHashMap<>();
private final Function<K, V> factory;
public Multiton(Function<K, V> factory) {
this.factory = factory;
}
// Thread-safe: computeIfAbsent is atomic per key
public V getInstance(K key) {
return registry.computeIfAbsent(key, factory);
}
public int registeredCount() { return registry.size(); }
}
// Concrete use case: per-tenant database connection pool
public class TenantConnectionPool {
private static final Multiton<String, HikariDataSource> POOLS =
new Multiton<>(tenantId -> {
HikariConfig cfg = new HikariConfig();
cfg.setJdbcUrl("jdbc:postgresql://db/" + tenantId);
cfg.setMaximumPoolSize(10);
return new HikariDataSource(cfg);
});
public static HikariDataSource forTenant(String tenantId) {
return POOLS.getInstance(tenantId);
}
}
// Usage
HikariDataSource tenantAPool = TenantConnectionPool.forTenant("tenant-a");
HikariDataSource tenantAPool2 = TenantConnectionPool.forTenant("tenant-a");
System.out.println(tenantAPool == tenantAPool2); // true — same instance
HikariDataSource tenantBPool = TenantConnectionPool.forTenant("tenant-b");
System.out.println(tenantAPool == tenantBPool); // false — different instances