Add all 23 GoF design pattern implementations

This commit is contained in:
2026-07-25 10:50:29 +05:30
commit f5688a6b32
164 changed files with 4371 additions and 0 deletions

View File

@@ -0,0 +1,30 @@
package abstractfactory;
public class Application {
// The client holds references to the Abstract Factory and Abstract Products.
// It never imports LightButton, DarkButton, or any concrete class.
private final UIFactory factory;
private Button submitButton;
private TextField usernameField;
public Application(UIFactory factory) {
this.factory = factory; // theme is injected; client doesn't decide
}
public void buildUI() {
// Ask the factory — get consistent products from the same family
submitButton = factory.createButton();
usernameField = factory.createTextField();
}
public void render() {
System.out.println("Rendering login screen:");
usernameField.render();
submitButton.render();
}
public void simulateSubmit() {
submitButton.onClick();
}
}

View File

@@ -0,0 +1,7 @@
package abstractfactory;
// Every button, regardless of theme, must be able to render and handle clicks
public interface Button {
void render(); // draw on screen
void onClick(); // respond to a click event
}

View File

@@ -0,0 +1,11 @@
package abstractfactory;
public class DarkButton implements Button {
@Override
public void render() {
System.out.println("[Dark Button] Charcoal background, white text, no border");
}
@Override
public void onClick() {
System.out.println("[Dark Button] Bright highlight ripple effect");
}
}

View File

@@ -0,0 +1,10 @@
package abstractfactory;
public class DarkTextField implements TextField {
private String value = "";
@Override
public void render() {
System.out.println("[Dark TextField] Dark gray background, light placeholder text");
}
@Override
public String getValue() { return value; }
}

View File

@@ -0,0 +1,14 @@
package abstractfactory;
public class DarkThemeFactory implements UIFactory {
@Override
public Button createButton() {
return new DarkButton(); // Only Dark products come from this factory
}
@Override
public TextField createTextField() {
return new DarkTextField();
}
}

View File

@@ -0,0 +1,11 @@
package abstractfactory;
public class LightButton implements Button {
@Override
public void render() {
System.out.println("[Light Button] White background, dark text, 1px gray border");
}
@Override
public void onClick() {
System.out.println("[Light Button] Subtle gray ripple effect");
}
}

View File

@@ -0,0 +1,10 @@
package abstractfactory;
public class LightTextField implements TextField {
private String value = "";
@Override
public void render() {
System.out.println("[Light TextField] White background, dark placeholder text");
}
@Override
public String getValue() { return value; }
}

View File

@@ -0,0 +1,14 @@
package abstractfactory;
public class LightThemeFactory implements UIFactory {
@Override
public Button createButton() {
return new LightButton(); // Only Light products come from this factory
}
@Override
public TextField createTextField() {
return new LightTextField();
}
}

View File

@@ -0,0 +1,16 @@
package abstractfactory;
// Entry point: the ONLY place that knows the theme
public class Main {
public static void main(String[] args) {
// Production: read theme from config or OS preference
String theme = System.getProperty("app.theme", "light");
UIFactory factory = "dark".equals(theme) ? new DarkThemeFactory() : new LightThemeFactory();
Application app = new Application(factory);
app.buildUI();
app.render();
app.simulateSubmit();
}
}

View File

@@ -0,0 +1,33 @@
# Abstract Factory Design Pattern — Java Example
**Pattern:** Creational → Abstract Factory
**Article:** https://ankurm.com/abstract-factory-design-pattern-java/
## What this example shows
A UI toolkit that produces families of related widgets — light theme or dark theme — without the client ever naming a concrete class. `Button` and `TextField` are abstract products; `LightButton`/`LightTextField` and `DarkButton`/`DarkTextField` are the two concrete product families. `UIFactory` declares the creation methods for the whole family; `LightThemeFactory` and `DarkThemeFactory` each produce one consistent family. `Application` is the client — it only depends on `UIFactory` and the abstract products, so swapping the entire theme is a one-line change. `Main` selects the factory via a system property.
## How to run
```bash
javac abstract-factory/*.java -d out/abstract-factory
java -cp out/abstract-factory abstractfactory.Main
# or, for the dark theme family:
java -Dapp.theme=dark -cp out/abstract-factory abstractfactory.Main
```
Requires Java 25.
## Post Section ↔ File Mapping
| Post Section | File(s) |
|---|---|
| Part 1 — The Abstract Products: Button and TextField | `Button.java`, `TextField.java` |
| Part 2 — The Concrete Products (Light family) | `LightButton.java`, `LightTextField.java` |
| Part 2 — The Concrete Products (Dark family) | `DarkButton.java`, `DarkTextField.java` |
| Part 3 — The Abstract Factory: UIFactory Interface | `UIFactory.java` |
| Part 4 — The Concrete Factories | `LightThemeFactory.java`, `DarkThemeFactory.java` |
| Part 5 — The Client: Application | `Application.java`, `Main.java` |
Article: https://ankurm.com/abstract-factory-design-pattern-java/
All patterns: https://ankurm.com/design-patterns-java/

View File

@@ -0,0 +1,7 @@
package abstractfactory;
// Every text field must be able to render and return its value
public interface TextField {
void render();
String getValue();
}

View File

@@ -0,0 +1,13 @@
package abstractfactory;
public interface UIFactory {
// Factory method for buttons — returns the abstract type, not LightButton or DarkButton
Button createButton();
// Factory method for text fields — same principle
TextField createTextField();
// Adding a new product type (e.g., Checkbox) means adding a method here
// AND implementing it in every Concrete Factory. This is the one cost of the pattern.
}

View File

@@ -0,0 +1,27 @@
package builder;
public class HttpClientConfig {
// Director method 1: a health check probe — short timeout, no retries
public HttpRequest buildHealthCheck(String baseUrl) {
return new HttpRequest.Builder()
.url(baseUrl + "/health")
.method("GET")
.timeoutMs(2_000) // fast fail for health checks
.maxRetries(0)
.build();
}
// Director method 2: a resilient POST with standard JSON headers and retries
public HttpRequest buildResilientPost(String url, String jsonBody) {
return new HttpRequest.Builder()
.url(url)
.method("POST")
.body(jsonBody)
.header("Content-Type", "application/json")
.header("Accept", "application/json")
.timeoutMs(15_000)
.maxRetries(3)
.build();
}
}

View File

@@ -0,0 +1,76 @@
package builder;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
public final class HttpRequest {
// All fields are final: the object is immutable after build() returns.
private final String url;
private final String method;
private final String body;
private final Map<String, String> headers;
private final int timeoutMs;
private final int maxRetries;
// Private: the only path to an HttpRequest is through Builder.build()
private HttpRequest(Builder builder) {
this.url = Objects.requireNonNull(builder.url, "URL is required");
this.method = builder.method;
this.body = builder.body;
this.headers = Collections.unmodifiableMap(new HashMap<>(builder.headers));
this.timeoutMs = builder.timeoutMs;
this.maxRetries = builder.maxRetries;
}
// Getters only — no setters, enforcing immutability
public String getUrl() { return url; }
public String getMethod() { return method; }
public String getBody() { return body; }
public Map<String, String> getHeaders() { return headers; }
public int getTimeoutMs() { return timeoutMs; }
public int getMaxRetries() { return maxRetries; }
@Override
public String toString() {
return String.format("%s %s (timeout=%dms, retries=%d, headers=%s, body=%s)",
method, url, timeoutMs, maxRetries, headers, body);
}
// -----------------------------------------------------------------------
// THE BUILDER — static inner class so it can call the private constructor
// -----------------------------------------------------------------------
public static class Builder {
// Required field — no default, must be set
private String url;
// Optional fields with sensible defaults
private String method = "GET";
private String body = null;
private Map<String, String> headers = new HashMap<>();
private int timeoutMs = 30_000; // 30 seconds
private int maxRetries = 0;
// Fluent methods: each returns 'this' so calls can be chained
public Builder url(String url) { this.url = url; return this; }
public Builder method(String method) { this.method = method; return this; }
public Builder body(String body) { this.body = body; return this; }
public Builder header(String key, String v) { headers.put(key, v); return this; }
public Builder timeoutMs(int ms) { this.timeoutMs = ms; return this; }
public Builder maxRetries(int n) { this.maxRetries = n; return this; }
// Terminal method: validates and creates the immutable HttpRequest
public HttpRequest build() {
if (url == null || url.isBlank()) {
throw new IllegalStateException("url() is required before calling build()");
}
if (maxRetries < 0) {
throw new IllegalStateException("maxRetries must be >= 0");
}
return new HttpRequest(this); // calls the private constructor
}
}
}

View File

@@ -0,0 +1,42 @@
package builder;
public class Main {
public static void main(String[] args) {
// Simple GET — only the required field
HttpRequest ping = new HttpRequest.Builder()
.url("https://api.example.com/health")
.build();
System.out.println("Simple GET: " + ping);
// Full POST with authentication and retry
HttpRequest post = new HttpRequest.Builder()
.url("https://api.example.com/orders")
.method("POST")
.body("{\"item\":\"book\",\"qty\":1}")
.header("Content-Type", "application/json")
.header("Authorization", "Bearer eyJ...")
.timeoutMs(10_000) // 10 seconds — name is self-documenting
.maxRetries(3)
.build();
System.out.println("POST: " + post);
// build() throws if url is missing — validation at construction time
try {
HttpRequest bad = new HttpRequest.Builder().build(); // no URL!
} catch (IllegalStateException e) {
System.out.println("Caught: " + e.getMessage()); // url() is required
}
// Using the Director for the two standard presets defined above
HttpClientConfig config = new HttpClientConfig();
HttpRequest healthCheck = config.buildHealthCheck("https://api.example.com");
System.out.println("Health check: " + healthCheck);
HttpRequest order = config.buildResilientPost("https://api.example.com/orders", "{\"qty\":1}");
System.out.println("Order: " + order);
}
}

View File

@@ -0,0 +1,30 @@
# Builder Design Pattern — Java Example
**Pattern:** Creational → Builder
**Article:** https://ankurm.com/builder-design-pattern-java/
## What this example shows
An immutable `HttpRequest` assembled step by step instead of through a constructor with a dozen optional parameters. `HttpRequest` is the product — built once, never mutated afterward. `HttpClientConfig` is a director-style helper that encapsulates common request configurations so callers don't repeat the same builder calls. `Main` wires it together and shows the fluent builder chain in action.
## How to run
```bash
javac builder/*.java -d out/builder
java -cp out/builder builder.Main
```
Requires Java 25.
## Post Section ↔ File Mapping
| Post Section | File(s) |
|---|---|
| Part 1 — The Product: HttpRequest (Immutable) | `HttpRequest.java` |
| Part 2 — The Director: Encapsulating Common Configurations | `HttpClientConfig.java` |
| Part 3 — Using the Builder: Client Code | `Main.java` |
Note: the Lombok `@Builder` example and the JDK standard-library snippets (`HttpRequest.newBuilder()`, `StringBuilder`, `ProcessBuilder`) are illustrative only — they are not part of this repository's runnable example.
Article: https://ankurm.com/builder-design-pattern-java/
All patterns: https://ankurm.com/design-patterns-java/

View File

@@ -0,0 +1,13 @@
package factorymethod;
public class EmailNotification implements Notification {
@Override
public void send(String recipient, String message) {
// In production: use JavaMail or Spring Mail
System.out.printf(" [EMAIL] To: %s | Body: %s%n", recipient, message);
}
@Override
public String getType() { return "EMAIL"; }
}

View File

@@ -0,0 +1,8 @@
package factorymethod;
public class EmailSender extends NotificationSender {
@Override
protected Notification createNotification() {
return new EmailNotification();
}
}

View File

@@ -0,0 +1,23 @@
package factorymethod;
public class Main {
public static void main(String[] args) {
// The client only knows about NotificationSender (the Creator)
NotificationSender sender;
sender = new EmailSender();
sender.notify("alice@example.com", "Your order has shipped!");
sender = new SmsSender();
sender.notify("+1-555-0100", "OTP: 482910");
sender = new PushSender();
sender.notify("device-token-xyz", "New message from Bob");
// Adding Slack: just swap the creator. Client code doesn't change.
sender = new SlackSender(); // new class, nothing else touched
sender.notify("#alerts", "CPU usage above 90%");
}
}

View File

@@ -0,0 +1,11 @@
package factorymethod;
public interface Notification {
// Every notification must know how to deliver itself
void send(String recipient, String message);
// Used by the Creator for logging — it needs to know the type
// without knowing the concrete class
String getType();
}

View File

@@ -0,0 +1,31 @@
package factorymethod;
public abstract class NotificationSender {
// THE factory method — this is the one thing subclasses override.
// It returns a Notification (the Product interface), never a concrete class.
protected abstract Notification createNotification();
// The shared workflow — marked final so subclasses cannot change it.
// It calls createNotification() to get the right notification,
// then runs the same steps every time regardless of which channel was created.
public final void notify(String recipient, String message) {
Notification notification = createNotification(); // delegate creation
System.out.println("Sending " + notification.getType() + " notification...");
notification.send(recipient, message); // use the Product interface
System.out.println("Delivered.");
}
// Shared retry logic — works for all channels without change
public void notifyWithRetry(String recipient, String message, int maxRetries) {
for (int attempt = 1; attempt <= maxRetries; attempt++) {
try {
notify(recipient, message);
return; // success — exit
} catch (Exception e) {
System.err.printf("Attempt %d failed: %s%n", attempt, e.getMessage());
}
}
throw new RuntimeException("All " + maxRetries + " attempts failed");
}
}

View File

@@ -0,0 +1,24 @@
package factorymethod;
public class NotificationService {
public void send(String type, String recipient, String message) {
// Creation logic mixed with business logic
Notification notification;
if ("EMAIL".equals(type)) {
notification = new EmailNotification();
} else if ("SMS".equals(type)) {
notification = new SmsNotification();
} else if ("PUSH".equals(type)) {
notification = new PushNotification();
} else {
throw new IllegalArgumentException("Unknown type: " + type);
}
// Business logic that should never change
System.out.println("Sending " + type + " to " + recipient);
notification.send(recipient, message);
System.out.println("Delivered.");
}
}

View File

@@ -0,0 +1,13 @@
package factorymethod;
public class PushNotification implements Notification {
@Override
public void send(String recipient, String message) {
// In production: use Firebase FCM
System.out.printf(" [PUSH] To: %s | Alert: %s%n", recipient, message);
}
@Override
public String getType() { return "PUSH"; }
}

View File

@@ -0,0 +1,8 @@
package factorymethod;
public class PushSender extends NotificationSender {
@Override
protected Notification createNotification() {
return new PushNotification();
}
}

View File

@@ -0,0 +1,32 @@
# Factory Method Design Pattern — Java Example
**Pattern:** Creational → Factory Method
**Article:** https://ankurm.com/factory-method-design-pattern-java/
## What this example shows
A notification system where the "send" logic decides which channel object to create, instead of the client doing it with a switch statement. `Notification` is the product interface; `EmailNotification`, `SmsNotification`, and `PushNotification` are concrete products. `NotificationSender` declares the factory method and a template `send()` step shared by every channel; `EmailSender`, `SmsSender`, and `PushSender` each override the factory method to produce their own notification type. `NotificationService` is shown separately as the anti-pattern this pattern replaces — it is not called by `Main`. `Main` demonstrates the cost of adding a new channel (Slack) by adding one new product and one new creator, with zero changes to existing classes.
## How to run
```bash
javac factory-method/*.java -d out/factory-method
java -cp out/factory-method factorymethod.Main
```
Requires Java 25.
## Post Section ↔ File Mapping
| Post Section | File(s) |
|---|---|
| The Problem: Object Creation Leaks Into Business Logic | `NotificationService.java` (anti-pattern shown for contrast — not called by `Main`) |
| Part 1 — The Notification Interface | `Notification.java` |
| Part 2 — The Concrete Notification Classes | `EmailNotification.java`, `SmsNotification.java`, `PushNotification.java` |
| Part 3 — The NotificationSender Class | `NotificationSender.java` |
| Part 4 — The Concrete Sender Subclasses | `EmailSender.java`, `SmsSender.java`, `PushSender.java` |
| Part 5 — Running It: Client Code and Output | `Main.java` |
| Adding a New Channel (Slack) | `SlackNotification.java`, `SlackSender.java` |
Article: https://ankurm.com/factory-method-design-pattern-java/
All patterns: https://ankurm.com/design-patterns-java/

View File

@@ -0,0 +1,11 @@
package factorymethod;
// New Concrete Product — knows how to send to Slack
public class SlackNotification implements Notification {
@Override
public void send(String recipient, String message) {
System.out.printf(" [SLACK] Channel: %s | Message: %s%n", recipient, message);
}
@Override
public String getType() { return "SLACK"; }
}

View File

@@ -0,0 +1,9 @@
package factorymethod;
// New Concrete Creator — knows to create a SlackNotification
public class SlackSender extends NotificationSender {
@Override
protected Notification createNotification() {
return new SlackNotification();
}
}

View File

@@ -0,0 +1,13 @@
package factorymethod;
public class SmsNotification implements Notification {
@Override
public void send(String recipient, String message) {
// In production: use Twilio SDK
System.out.printf(" [SMS] To: %s | Text: %s%n", recipient, message);
}
@Override
public String getType() { return "SMS"; }
}

View File

@@ -0,0 +1,8 @@
package factorymethod;
public class SmsSender extends NotificationSender {
@Override
protected Notification createNotification() {
return new SmsNotification();
}
}

View File

@@ -0,0 +1,8 @@
package prototype;
// Our Prototype interface: any class that can clone itself implements this.
// The return type T allows each implementing class to return its own type,
// not the raw interface type, which makes the client code cleaner.
public interface Copyable<T> {
T copy();
}

View File

@@ -0,0 +1,46 @@
package prototype;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class Document implements Copyable<Document> {
private String title; // String: immutable, safe to share on copy
private String content; // String: immutable, safe to share on copy
private List<String> tags; // List: mutable, MUST be deep-copied
private DocumentMetadata metadata; // Mutable object, MUST be deep-copied
public Document(String title, String content, List<String> tags, DocumentMetadata metadata) {
this.title = title;
this.content = content;
this.tags = new ArrayList<>(tags); // defensive copy on construction
this.metadata = metadata;
}
// Copy constructor: the preferred Prototype implementation in modern Java
public Document(Document source) {
this.title = source.title; // immutable: share
this.content = source.content; // immutable: share
this.tags = new ArrayList<>(source.tags); // mutable: new list
this.metadata = source.metadata.copy(); // mutable: deep copy via Copyable
}
@Override
public Document copy() {
return new Document(this); // delegates to copy constructor
}
// Getters and setters
public String getTitle() { return title; }
public void setTitle(String title) { this.title = title; }
public String getContent() { return content; }
public void setContent(String content) { this.content = content; }
public List<String> getTags() { return Collections.unmodifiableList(tags); }
public void addTag(String tag) { tags.add(tag); }
public DocumentMetadata getMetadata() { return metadata; }
@Override public String toString() {
return String.format("Document{title='%s', tags=%s, %s}", title, tags, metadata);
}
}

View File

@@ -0,0 +1,39 @@
package prototype;
import java.time.LocalDate;
public class DocumentMetadata implements Copyable<DocumentMetadata> {
private String author;
private LocalDate createdDate; // LocalDate is immutable — safe to share
private String version;
public DocumentMetadata(String author, LocalDate createdDate, String version) {
this.author = author;
this.createdDate = createdDate; // safe: LocalDate is immutable
this.version = version;
}
// Copy constructor: creates a new instance with the same values
public DocumentMetadata(DocumentMetadata source) {
this.author = source.author; // String: immutable, safe to share
this.createdDate = source.createdDate; // LocalDate: immutable, safe to share
this.version = source.version;
}
@Override
public DocumentMetadata copy() {
return new DocumentMetadata(this); // delegates to copy constructor
}
public String getAuthor() { return author; }
public LocalDate getCreatedDate(){ return createdDate; }
public String getVersion() { return version; }
public void setVersion(String v) { this.version = v; }
public void setAuthor(String a) { this.author = a; }
@Override public String toString() {
return String.format("Metadata{author='%s', date=%s, version='%s'}",
author, createdDate, version);
}
}

View File

@@ -0,0 +1,21 @@
package prototype;
import java.util.HashMap;
import java.util.Map;
public class DocumentRegistry {
private final Map<String, Document> prototypes = new HashMap<>();
// Register a prototype under a name
public void register(String name, Document prototype) {
prototypes.put(name, prototype);
}
// Return a fresh copy of the named prototype
public Document get(String name) {
Document prototype = prototypes.get(name);
if (prototype == null) throw new IllegalArgumentException("No prototype: " + name);
return prototype.copy(); // always return a copy, never the prototype itself
}
}

View File

@@ -0,0 +1,47 @@
package prototype;
import java.time.LocalDate;
import java.util.List;
public class Main {
public static void main(String[] args) {
// Create the original document (imagine this was loaded from a database)
DocumentMetadata metadata = new DocumentMetadata("Alice", LocalDate.of(2025, 1, 15), "1.0");
Document original = new Document(
"Q1 Report",
"Revenue increased by 12%...",
List.of("finance", "quarterly"),
metadata
);
System.out.println("Original: " + original);
// Clone the document and apply only the differences
Document draft = original.copy();
draft.setTitle("Q1 Report — DRAFT");
draft.addTag("draft");
draft.getMetadata().setAuthor("Bob"); // only the copy's metadata changes
draft.getMetadata().setVersion("1.0-draft");
System.out.println("Draft: " + draft);
System.out.println("Original: " + original); // unchanged — deep copy worked
// Verify independence
System.out.println("\nSame object? " + (original == draft)); // false
System.out.println("Same tags? " + (original.getTags() == draft.getTags())); // false
System.out.println("Same meta? " + (original.getMetadata() == draft.getMetadata())); // false
// Registry demo: register a named template once, then hand out independent copies
DocumentRegistry registry = new DocumentRegistry();
registry.register("q-report-template", new Document(
"Quarterly Report Template", "## Executive Summary\n...",
List.of("quarterly", "finance"), metadata));
Document myReport = registry.get("q-report-template"); // a fresh, independent copy
myReport.setTitle("Q2 2025 Report");
myReport.addTag("q2");
System.out.println("\nFrom registry: " + myReport);
}
}

View File

@@ -0,0 +1,32 @@
# Prototype Design Pattern — Java Example
**Pattern:** Creational → Prototype
**Article:** https://ankurm.com/prototype-design-pattern-java/
## What this example shows
Documents cloned from existing instances instead of rebuilt from scratch. `Copyable` declares the cloning contract. `DocumentMetadata` is a nested object that must be copied correctly for a clone to be truly independent of its source. `Document` is the concrete prototype, implementing a real (not shallow) copy. `DocumentRegistry` stores a set of ready-made prototypes that callers clone on demand instead of constructing from raw parameters. `Main` demonstrates cloning from the registry and confirms the clone and original don't share mutable state.
## How to run
```bash
javac prototype/*.java -d out/prototype
java -cp out/prototype prototype.Main
```
Requires Java 25.
## Post Section ↔ File Mapping
| Post Section | File(s) |
|---|---|
| Part 1 — The Prototype Interface: Copyable | `Copyable.java` |
| Part 2 — The Nested Object: DocumentMetadata | `DocumentMetadata.java` |
| Part 3 — The Concrete Prototype: Document | `Document.java` |
| Part 4 — The Prototype Registry: DocumentRegistry | `DocumentRegistry.java` |
| Part 5 — Using the Prototype: Client Code | `Main.java` |
Note: the `ShallowVsDeepDemo` snippet (shallow vs. deep copy) and the `Cloneable`-based example are illustrative only — they are not part of this repository's runnable example.
Article: https://ankurm.com/prototype-design-pattern-java/
All patterns: https://ankurm.com/design-patterns-java/

View File

@@ -0,0 +1,31 @@
package singleton;
public class AppConfigDCL {
// volatile is MANDATORY for DCL to work correctly.
// Without it, the JVM can reorder the writes inside the constructor
// and return a partially-constructed object to a second thread.
private static volatile AppConfigDCL instance;
private AppConfigDCL() { /* load config */ }
public static AppConfigDCL getInstance() {
// First check: no lock, instant return for the 99.99% case
// (instance already created, just return it)
if (instance == null) {
// Lock only when first check says we might need to create
synchronized (AppConfigDCL.class) {
// Second check: re-verify under the lock.
// Another thread might have created the instance between
// our first check and acquiring the lock.
if (instance == null) {
instance = new AppConfigDCL();
}
}
}
return instance;
}
}

View File

@@ -0,0 +1,24 @@
package singleton;
public class AppConfigEager {
// The JVM creates this instance when the class is first loaded.
// static guarantees it belongs to the class, not any instance.
// final guarantees it can never be reassigned.
private static final AppConfigEager INSTANCE = new AppConfigEager();
// Private constructor: prevents any other class from calling new AppConfigEager()
private AppConfigEager() {
System.out.println("Reading configuration from file...");
// load properties, set up values, etc.
}
// The only way to get the instance
public static AppConfigEager getInstance() {
return INSTANCE;
}
public String getProperty(String key) {
return System.getProperty(key, "(not set)");
}
}

View File

@@ -0,0 +1,19 @@
package singleton;
public enum AppConfigEnum {
INSTANCE; // The JVM creates this exactly once, period.
// Enum instances can have fields and methods like any class
private final String configPath;
AppConfigEnum() {
// Enum constructors run once, when the constant is first used
this.configPath = System.getProperty("config.path", "application.properties");
System.out.println("Loading config from: " + configPath);
}
public String getProperty(String key) {
return System.getProperty(key, "(not set)");
}
}

View File

@@ -0,0 +1,19 @@
package singleton;
public class AppConfigHolder {
private AppConfigHolder() { /* load config */ }
// The JVM loads Holder only when getInstance() is first called.
// Class loading is inherently thread-safe — the JVM ensures it happens once.
// No synchronized, no volatile, no locks — just the JVM class loading guarantee.
private static class Holder {
static final AppConfigHolder INSTANCE = new AppConfigHolder();
}
public static AppConfigHolder getInstance() {
// This reference triggers Holder to load (if it hasn't already).
// After first call, Holder is already loaded and this is just a field read.
return Holder.INSTANCE;
}
}

View File

@@ -0,0 +1,16 @@
package singleton;
public class AppConfigNaiveLazy {
private static AppConfigNaiveLazy instance; // null until first request
private AppConfigNaiveLazy() { /* load config */ }
// BROKEN IN MULTITHREADED CODE — see explanation below
public static AppConfigNaiveLazy getInstance() {
if (instance == null) { // Thread A: checks, sees null
instance = new AppConfigNaiveLazy(); // Thread B: also sees null, also enters here
} // Both threads create a new AppConfigNaiveLazy!
return instance;
}
}

View File

@@ -0,0 +1,16 @@
package singleton;
public class AppConfigSynchronized {
private static AppConfigSynchronized instance;
private AppConfigSynchronized() { /* load config */ }
// synchronized: only one thread can execute this method at a time
public static synchronized AppConfigSynchronized getInstance() {
if (instance == null) {
instance = new AppConfigSynchronized(); // safe — no other thread can be here
}
return instance;
}
}

View File

@@ -0,0 +1,36 @@
package singleton;
public class Main {
public static void main(String[] args) {
System.out.println("--- Implementation 1: Eager ---");
AppConfigEager eager1 = AppConfigEager.getInstance();
AppConfigEager eager2 = AppConfigEager.getInstance();
System.out.println("Same instance? " + (eager1 == eager2));
System.out.println("db.url = " + eager1.getProperty("db.url"));
System.out.println("\n--- Implementation 2: Naive Lazy ---");
AppConfigNaiveLazy lazy1 = AppConfigNaiveLazy.getInstance();
AppConfigNaiveLazy lazy2 = AppConfigNaiveLazy.getInstance();
System.out.println("Same instance? " + (lazy1 == lazy2));
System.out.println("\n--- Implementation 3: Synchronized Method ---");
AppConfigSynchronized sync1 = AppConfigSynchronized.getInstance();
AppConfigSynchronized sync2 = AppConfigSynchronized.getInstance();
System.out.println("Same instance? " + (sync1 == sync2));
System.out.println("\n--- Implementation 4: Double-Checked Locking ---");
AppConfigDCL dcl1 = AppConfigDCL.getInstance();
AppConfigDCL dcl2 = AppConfigDCL.getInstance();
System.out.println("Same instance? " + (dcl1 == dcl2));
System.out.println("\n--- Implementation 5: Holder Idiom ---");
AppConfigHolder holder1 = AppConfigHolder.getInstance();
AppConfigHolder holder2 = AppConfigHolder.getInstance();
System.out.println("Same instance? " + (holder1 == holder2));
System.out.println("\n--- Implementation 6: Enum ---");
AppConfigEnum enumConfig = AppConfigEnum.INSTANCE;
System.out.println("db.url = " + enumConfig.getProperty("db.url"));
}
}

View File

@@ -0,0 +1,34 @@
# Singleton Design Pattern — Java Example
**Pattern:** Creational → Singleton
**Article:** https://ankurm.com/singleton-design-pattern-java/
## What this example shows
Six different ways to implement a singleton, compiled and run side by side so their tradeoffs are directly comparable. `AppConfigEager` initializes at class-load time. `AppConfigNaiveLazy` defers initialization but is not thread-safe. `AppConfigSynchronized` fixes thread-safety at the cost of locking on every call. `AppConfigDCL` uses double-checked locking to avoid that per-call lock. `AppConfigHolder` uses the initialization-on-demand holder idiom for lazy, thread-safe initialization with no locking at all. `AppConfigEnum` uses a single-element enum, which the JVM guarantees is a singleton even against reflection and serialization attacks. `Main` exercises all six in one run.
## How to run
```bash
javac singleton/*.java -d out/singleton
java -cp out/singleton singleton.Main
```
Requires Java 25.
## Post Section ↔ File Mapping
| Post Section | File(s) |
|---|---|
| Implementation 1 — Eager Initialization | `AppConfigEager.java` |
| Implementation 2 — Naïve Lazy Initialization | `AppConfigNaiveLazy.java` |
| Implementation 3 — Synchronized Method | `AppConfigSynchronized.java` |
| Implementation 4 — Double-Checked Locking | `AppConfigDCL.java` |
| Implementation 5 — Initialization-on-Demand Holder | `AppConfigHolder.java` |
| Implementation 6 — Enum Singleton | `AppConfigEnum.java` |
| Running All Six Implementations Together | `Main.java` |
Each implementation is renamed to its own class (e.g. `AppConfigEager`, `AppConfigHolder`) so all six can be compiled together in one package and compared directly. The "Breaking via Reflection," "Breaking via Serialization," and Spring `@Component` snippets are illustrative only — they are not part of this repository's runnable example.
Article: https://ankurm.com/singleton-design-pattern-java/
All patterns: https://ankurm.com/design-patterns-java/