Stage 6: Design and clean code, lesson 3 of 4

Design patterns: Singleton, Factory and Builder

Intermediate3 min read@since 16Code runs on your Java 25
Explain it forThe essentials plus production detail and pitfalls.

Singleton: exactly one instance. The safest Java versions are an enum or a lazily initialised holder class. In Spring, every bean is a singleton by default, so you rarely write one by hand.

Factory: a method decides which class to create, so callers don't use new and don't know the concrete type. List.of(), Executors.newFixedThreadPool() and Path.of() are factories.

Builder: build a complex object step by step with readable, named methods, then call build() to get an immutable result. It helps when a constructor would need many parameters, some optional. HttpRequest.newBuilder() and Lombok's @Builder are builders.

Use each for a reason: a factory hides a choice, a builder tames many parameters, and a singleton shares one expensive resource.

Example

Java
// Singleton via enum: thread-safe and serialization-safe
public enum AppClock {
    INSTANCE;
    private final Clock clock = Clock.systemUTC();
    public Instant now() { return clock.instant(); }
}

// Factory: choose the implementation from the input
public interface Notifier { void send(String to, String message); }

public final class Notifiers {
    private Notifiers() {}
    public static Notifier forChannel(String channel) {
        return switch (channel) {
            case "email" -> (to, msg) -> System.out.println("Email to " + to + ": " + msg);
            case "sms" -> (to, msg) -> System.out.println("SMS to " + to + ": " + msg);
            default -> throw new IllegalArgumentException("Unknown channel " + channel);
        };
    }
}

// Builder: many optional fields, immutable result
public record Email(String to, String subject, String body, List<String> cc) {
    public static Builder to(String to) { return new Builder(to); }

    public static final class Builder {
        private final String to;
        private String subject = "";
        private String body = "";
        private final List<String> cc = new ArrayList<>();
        private Builder(String to) { this.to = to; }
        public Builder subject(String s) { subject = s; return this; }
        public Builder body(String b) { body = b; return this; }
        public Builder cc(String c) { cc.add(c); return this; }
        public Email build() { return new Email(to, subject, body, List.copyOf(cc)); }
    }
}

Email mail = Email.to("asha@example.com").subject("Welcome").body("Hi Asha").build();
Notifiers.forChannel("sms").send("+919876543210", "Your OTP is 4821");

Common mistake

Writing a lazy singleton with a plain null check. Two threads can both see null and create two instances.

Under the hood

Hand-written singletons with static state make testing hard, because every test shares the same instance; dependency injection gives the same single-instance benefit with swappable implementations. Double-checked locking is only correct with a volatile field; the enum and holder idioms avoid the subtlety entirely.

Check yourself

Which JDK method is an example of the factory pattern?

How this connects

Where this leads

You've reached the end of this thread. Try a learning path for what's next.

Part of Crack the Java interview.

Was this lesson helpful?

Finished reading? Mark it complete to track your progress.