🚀AdvancedDesign Patterns

Creational Design Patterns

Implement Singleton, Factory Method, Abstract Factory, and Builder patterns in Java.

Overview

Creational design patterns deal with object creation mechanisms, aiming to create objects in a way that is suitable for the situation. They abstract the instantiation process, making it more flexible and reusable. The four most important creational patterns in Java are: - Singleton: ensures only one instance of a class exists - Factory Method: defines an interface for creating an object but lets subclasses decide which class to instantiate - Abstract Factory: provides an interface for creating families of related objects - Builder: constructs complex objects step by step

Key Points

  • •Singleton: private constructor, static instance, getInstance() method
  • •Thread-safe Singleton uses double-checked locking or enum
  • •Enum Singleton is the simplest thread-safe Singleton in Java
  • •Factory Method delegates object creation to subclasses via an abstract method
  • •Abstract Factory groups factories for families of related products
  • •Builder separates object construction from its representation
  • •Builder is ideal for objects with many optional parameters (avoids telescoping constructors)
  • •Static inner Builder class is the standard Java Builder pattern
  • •Lombok @Builder annotation generates Builder code automatically

Examples

Thread-safe Singleton using enum

public enum DatabaseConnection {
    INSTANCE;

    private final String url;

    DatabaseConnection() {
        this.url = "jdbc:postgresql://localhost:5432/mydb";
        System.out.println("Connection created");
    }

    public String getUrl() { return url; }

    public void query(String sql) {
        System.out.println("Running: " + sql + " on " + url);
    }

    public static void main(String[] args) {
        DatabaseConnection db1 = DatabaseConnection.INSTANCE;
        DatabaseConnection db2 = DatabaseConnection.INSTANCE;
        System.out.println(db1 == db2); // true - same instance

        db1.query("SELECT * FROM users");
    }
}

Enum Singleton is the recommended approach. It is thread-safe by default, serialization-safe, and immune to reflection attacks. "Connection created" prints only once, proving only one instance exists.

Factory Method for pluggable notification channels

public abstract class Notifier {
    // Factory method
    public abstract void send(String message);

    // Template method using the factory method
    public void notifyUser(String user, String message) {
        System.out.println("Notifying " + user);
        send(message);
    }
}

class EmailNotifier extends Notifier {
    @Override
    public void send(String message) {
        System.out.println("Email: " + message);
    }
}

class SmsNotifier extends Notifier {
    @Override
    public void send(String message) {
        System.out.println("SMS: " + message);
    }
}

class NotifierFactory {
    public static Notifier create(String type) {
        return switch (type) {
            case "email" -> new EmailNotifier();
            case "sms"   -> new SmsNotifier();
            default      -> throw new IllegalArgumentException("Unknown type: " + type);
        };
    }
}

public class Main {
    public static void main(String[] args) {
        Notifier n = NotifierFactory.create("email");
        n.notifyUser("alice", "Welcome!");
    }
}

The calling code depends only on the Notifier interface. Adding a new channel (push, Slack) requires a new subclass and one line in the factory, with no changes to existing code.

Builder pattern for complex object construction

public class HttpRequest {
    private final String url;
    private final String method;
    private final String body;
    private final int timeoutMs;
    private final boolean followRedirects;

    private HttpRequest(Builder b) {
        this.url = b.url;
        this.method = b.method;
        this.body = b.body;
        this.timeoutMs = b.timeoutMs;
        this.followRedirects = b.followRedirects;
    }

    @Override
    public String toString() {
        return method + " " + url + " (timeout=" + timeoutMs + "ms, body=" + body + ")";
    }

    public static class Builder {
        private final String url;          // required
        private String method = "GET";     // optional with default
        private String body = null;
        private int timeoutMs = 30_000;
        private boolean followRedirects = true;

        public Builder(String url) { this.url = url; }
        public Builder method(String m) { this.method = m; return this; }
        public Builder body(String b) { this.body = b; return this; }
        public Builder timeout(int ms) { this.timeoutMs = ms; return this; }
        public Builder followRedirects(boolean f) { this.followRedirects = f; return this; }
        public HttpRequest build() { return new HttpRequest(this); }
    }

    public static void main(String[] args) {
        HttpRequest req = new HttpRequest.Builder("https://api.example.com/users")
            .method("POST")
            .body("{"name":"Alice"}")
            .timeout(5000)
            .build();
        System.out.println(req);
    }
}

Builder avoids constructors with many parameters in an arbitrary order. The HttpRequest itself is immutable once built. Each setter returns this for fluent chaining.

Common Mistakes

  • Using double-checked locking for Singleton without declaring the field volatile. Without volatile, the JVM can reorder writes to the instance field, causing another thread to see a partially initialized object.
  • Making the Builder setters return void instead of Builder. Without return this, you cannot chain calls. Always return the Builder from each setter.
  • Exposing the Builder constructor as public but not making HttpRequest's constructor private. If the main class constructor is public, callers can bypass the Builder, defeating its purpose.
  • Confusing Factory Method (subclass decides which class) with Simple Factory (static method switch). Simple Factory is not a GoF pattern but is common and simple enough for most use cases.

Note

If your class has more than 3-4 optional constructor parameters, switch to the Builder pattern. It makes each parameter self-documenting at the call site and eliminates the need to remember parameter order.

Exercises

Write Java code and click Run to execute it instantly. When your output looks correct, mark as done.

Loading editor…

Completed: 0 / 2 exercises

Knowledge Check

Question 1 of 4

Why is enum the recommended way to implement Singleton in Java?