Cheat SheetsLow Level DesignCreational Patterns

Creational Patterns — Cheat Sheet

Low Level Design · 8 topics. Download the PDF or the Instagram carousel and share it.

Cheat Sheet · AiCanCode.org
Creational Patterns
Low Level Design8 topicsQuick revision reference
1

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.
Java — Eager, Double-Checked Locking, Holder idiom
// 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;
    }
}
2

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.
Java — Classic Factory Method (Notification)
// 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!");
3

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.
Java — Abstract Factory (UI Toolkit example)
// 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();
4

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.
Java — Fluent Builder with immutable object
// 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();
5

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.
Java — Shallow clone vs deep clone comparison
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] — independent
6

Object 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).
Java — Generic Object Pool + Connection Pool
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);
    }
}
7

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.
Java — Constructor, Setter, Field injection comparison
// ❌ 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);
    }
}
8

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.
Java — Generic Multiton + per-tenant connection pool
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
Learn this free with Aria, your AI tutor → AiCanCode.org/learn/lld