Question 27 · Section 20

How to implement Singleton pattern with Record

But the simplest way is the Enum Singleton, and Records can be used with a compact constructor.

Language versions: English Russian Ukrainian

🟢 Junior Level

Record is a great option for Singleton! Starting from Java 21 (JEP 445), Records support enhanced serialization, making them ideal for Singleton.

But the simplest way is the Enum Singleton, and Records can be used with a compact constructor.

// Approach 1: Record with static final field
public record Config(String databaseUrl, int maxConnections) {
    public static final Config INSTANCE =
        new Config("jdbc:postgresql://localhost:5432/mydb", 10);
}

// Usage
Config config = Config.INSTANCE;

Why Record is good for Singleton:

  • ✅ Automatically immutable
  • ✅ Serialization is safe (Java 21+)
  • ✅ No reflection attack issues
  • ✅ Minimal code

🟡 Middle Level

Implementation approaches

1. Static field in Record:

public record Logger(String name, LogLevel level) {
    public static final Logger INSTANCE = new Logger("App", LogLevel.INFO);

    public void log(String message) {
        System.out.println("[" + name + "] " + message);
    }
}

// Usage
Logger.INSTANCE.log("Hello");

2. Static factory method:

public record DatabaseConnection(String url) {
    private static final DatabaseConnection INSTANCE =
        new DatabaseConnection("jdbc:postgresql://localhost:5432/db");

    public static DatabaseConnection getInstance() {
        return INSTANCE;
    }

    public void execute(String sql) {
        // execute query
    }
}

// Usage
DatabaseConnection.getInstance().execute("SELECT 1");

3. Enum Singleton (classic, Joshua Bloch):

public enum Config {
    INSTANCE;

    private final String databaseUrl;
    private final int maxConnections;

    Config() {
        this.databaseUrl = "jdbc:postgresql://localhost:5432/db";
        this.maxConnections = 10;
    }

    public String getDatabaseUrl() { return databaseUrl; }
    public int getMaxConnections() { return maxConnections; }
}

Common mistakes

  1. Trying to make mutable Singleton:
    public record Config(String url) {
     // ❌ Cannot — Record is immutable
     public void setUrl(String url) { }  // error
    }
    
  2. Multiple instances: ```java public record Config(String url) { // ⚠️ Can create a new instance! // new Config(“other-url”) — is possible }

// ✅ Solution — private constructor public record Config(String url) { private static final Config INSTANCE = new Config(“default”);

private Config { }  // private compact constructor

public static Config getInstance() {
    return INSTANCE;
} } ```

🔴 Senior Level

Internal Implementation

Why Enum is better than Record for Singleton:

// Enum guarantees singularity at the JVM level
// Record — does not, new instances can be created

public enum EnumSingleton {
    INSTANCE;
    // JVM guarantees single instance
}

public record RecordSingleton(String value) {
    public static final RecordSingleton INSTANCE = new RecordSingleton("default");
    // You can do new RecordSingleton("other") — no guarantee
}

Serialization safety (Java 21+ JEP 445):

// Record uses enhanced deserialization
// Via canonical constructor instead of readObject

public record Config(String url) implements Serializable {
    private static final Config INSTANCE = new Config("default");

    // Deserialization invokes canonical constructor
    // Can be controlled via validation
    public Config {
        // Validation during deserialization
    }
}

Architectural Trade-offs

Enum vs Record vs Class Singleton:

Approach Guarantee Code Serialization Reflection
Enum ✅ JVM guarantee Minimal ✅ Safe ✅ Protected
Record ❌ No guarantee Minimal ✅ Safe (Java 21+) ⚠️ Can create
Class ❌ Must manage Lots ⚠️ Needs readResolve ⚠️ Must protect

Edge Cases

1. Thread-safe initialization:

public record Config(String url) {
    // Static initialization is thread-safe per JLS
    private static final Config INSTANCE = new Config("default");

    // Lazy initialization
    private static class Holder {
        static final Config INSTANCE = new Config("default");
    }

    public static Config getInstance() {
        return Holder.INSTANCE;
    }
}

2. Parameterized Singleton:

public record ServiceRegistry(Map<String, Service> services) {
    private static volatile ServiceRegistry INSTANCE;

    public static synchronized ServiceRegistry getInstance() {
        if (INSTANCE == null) {
            INSTANCE = new ServiceRegistry(new ConcurrentHashMap<>());
        }
        return INSTANCE;
    }

    public void register(String name, Service service) {
        services.put(name, service);
    }
}

3. Reflection protection:

public record Config(String url) {
    private static final Config INSTANCE = new Config("default");

    private Config {
        // This check does NOT protect from reflection attack.
        // Compact constructor runs for EVERY creation, including INSTANCE.
        // First initialization of INSTANCE passes (INSTANCE is still null).
        // But reflection can invoke the constructor directly.
        // Full protection requires a boolean flag or Enum-based approach.
        if (INSTANCE != null) {
            throw new IllegalStateException("Singleton already created");
        }
    }

    public static Config getInstance() {
        return INSTANCE;
    }
}

Performance

Singleton creation:
- Static field: 0 ns (class loading time)
- Lazy initialization: ~1 ns after initialization
- Double-checked locking: ~5 ns

Enum vs Record:
- Enum: JVM guarantee, slightly faster
- Record: Same, but can create new

Production Experience

Configuration Singleton:

public record AppConfig(
    String databaseUrl,
    int maxConnections,
    Duration timeout,
    Map<String, String> properties
) {
    private static final AppConfig INSTANCE;

    static {
        // Load from file/environment
        INSTANCE = loadFromEnvironment();
    }

    private static AppConfig loadFromEnvironment() {
        return new AppConfig(
            System.getenv("DB_URL"),
            Integer.parseInt(System.getenv("MAX_CONNECTIONS")),
            Duration.ofSeconds(30),
            Map.of()
        );
    }

    public static AppConfig getInstance() {
        return INSTANCE;
    }
}

Best practices:

// ✅ Enum for Singleton — best practice
public enum DatabaseConnection {
    INSTANCE;
    private final Connection conn;

    DatabaseConnection() {
        this.conn = createConnection();
    }

    private Connection createConnection() { /* ... */ }
}

// ✅ Record for immutable configuration
public record Config(String url, int timeout) {
    public static final Config INSTANCE = new Config("default", 30);
}

// ❌ Record without protection from multiple instances
// ❌ Class Singleton without readResolve
// ❌ Lazy initialization without synchronization

When NOT to use Singleton

Singleton is a controversial pattern: hidden dependencies, testability issues, global state, threading complications. Consider DI (Spring) as an alternative.


🎯 Interview Cheat Sheet

Must know:

  • Enum — best Singleton in Java (Joshua Bloch): JVM guarantee of singularity
  • Record can be used as Singleton via static final INSTANCE field
  • Record does not guarantee singularity — you can create new Config("other")
  • Private compact constructor does NOT fully protect from reflection
  • Java 21+ enhanced serialization (JEP 445) makes Record serialization safe
  • Singleton is an anti-pattern: hidden dependencies, testability issues, DI is better

Frequent follow-up questions:

  • Why is Enum better than Record for Singleton? — JVM guarantees single instance, reflection protected
  • Can you protect Record from creating new instances? — Not fully, private constructor doesn’t protect from reflection
  • How to make Thread-safe Singleton with Record? — Static final initialization is thread-safe per JLS (class loading)
  • When NOT to use Singleton? — Almost always, DI (Spring, CDI) is better

Red flags (DO NOT say):

  • ❌ “Record guarantees a single instance” — No, you can create new
  • ❌ “Record’s private constructor protects from reflection” — No, reflection can bypass it
  • ❌ “Singleton is best practice” — Controversial pattern, DI is preferred
  • ❌ “Record Singleton is serialization-safe” — Only Java 21+ with JEP 445

Related topics:

  • [[1. What is a Record in Java and since which version]]
  • [[3. Can you extend a Record or have a Record extend another class]]
  • [[6. Can you override a constructor in a Record]]
  • [[8. Can you declare static fields and methods in a Record]]