Senior 15 min · March 05, 2026

Java Design Patterns — Singleton Lock Contention Fixes

A Singleton with synchronized methods blocked all payment threads on one lock.

N
Naren · Founder
Plain-English first. Then code. Then the interview question.
About
 ● Production Incident 🔎 Debug Guide
Quick Answer
  • Design patterns are reusable solutions to common software design problems, not frameworks or code libraries
  • Three categories: Creational (object creation), Structural (object composition), Behavioral (object interaction)
  • Each pattern solves a specific problem — misapplying a pattern creates more complexity than it solves
  • Production cost: One wrong Singleton can add 50ms per request due to contention; a well-chosen Factory reduces setup time by 30%
  • Biggest mistake: Treating patterns as goals instead of tools — your design should solve the problem, not force a pattern
Plain-English First

Imagine you're building IKEA furniture. You don't invent new tools every time — you follow the instruction sheet. Design patterns are those instruction sheets for software: battle-tested blueprints that solve problems developers keep running into. Just like IKEA uses the same Allen key across thousands of products, a Singleton pattern lets your whole app share one database connection. The blueprint isn't the furniture itself — it's the proven recipe for building it right.

Every production Java codebase you'll ever work on uses design patterns — whether the team named them or not. When a Spring bean is @Scope(\"singleton\"), that's the Singleton pattern. When Jackson deserializes JSON into a POJO, it's using a factory under the hood. When a button click in a UI framework notifies a dozen listeners, that's the Observer pattern firing. Patterns are the invisible skeleton of almost every framework you depend on daily.

Here's the thing: knowing the GoF book cover to cover won't make you a senior engineer. Knowing when NOT to use a pattern will. The real skill is recognizing the problem shape and picking the minimal solution — not the fanciest one. This guide covers the three categories, the patterns you'll actually use in production, and the traps that'll waste your team's sprint.

What Are Design Patterns? — The Three Categories

Design patterns are recurring solutions to common problems in software design. They are not code you copy-paste; they are templates for how to solve a problem. The Gang of Four (GoF) catalogued 23 patterns into three categories:

  • Creational: Deal with object creation mechanisms, trying to create objects in a manner suitable to the situation. Examples: Singleton, Factory Method, Abstract Factory, Builder.
  • Structural: Concern class and object composition. They form large structures from individual parts. Examples: Adapter, Decorator, Proxy, Facade.
  • Behavioral: Focus on communication between objects. Examples: Observer, Strategy, Command, Template Method.

In practice, you'll use maybe 5-8 patterns regularly. The rest are niche. But knowing all 23 helps you recognize the problem shapes faster.

Here's a concrete example: You need to generate different types of reports (PDF, CSV, HTML). Instead of one giant class with switch statements, you can use the Strategy pattern: define a ReportStrategy interface and have each format implement it. Your report generator simply delegates to the strategy. That's it — three interfaces, three implementations, zero branching.

io/thecodeforge/patterns/creational/ReportGenerator.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
package io.thecodeforge.patterns.creational;

import java.util.List;

// Strategy pattern for report generation
interface ReportStrategy {
    String generate(List<String> data);
}

class CsvReportStrategy implements ReportStrategy {
    public String generate(List<String> data) {
        return String.join(",", data);
    }
}

class PdfReportStrategy implements ReportStrategy {
    public String generate(List<String> data) {
        // In real code, use a PDF library
        return "PDF: " + String.join(" | ", data);
    }
}

class ReportGenerator {
    private final ReportStrategy strategy;
    
    public ReportGenerator(ReportStrategy strategy) {
        this.strategy = strategy;
    }
    
    public String buildReport(List<String> data) {
        return strategy.generate(data);
    }
}

// Usage
class Main {
    public static void main(String[] args) {
        List<String> data = List.of("Alice", "Bob", "Charlie");
        ReportGenerator csv = new ReportGenerator(new CsvReportStrategy());
        System.out.println(csv.buildReport(data));
    }
}
Output
Alice,Bob,Charlie
Patterns Are Recipes, Not Ingredients
  • A recipe for cake doesn't tell you to eat cake for every meal — use patterns only when the problem matches.
  • The same pattern can look different in different codebases; the essence is the relationship, not the exact code.
  • Patterns evolve. Modern Java uses lambdas and records to implement patterns that originally required full classes.
  • Overusing patterns is worse than using none — it adds accidental complexity.
Production Insight
The most abused pattern in production is Singleton. Teams add it thoughtlessly for "configuration" or "cache" objects.
But every Singleton becomes a global state that couples every class using it.
Rule: Before making something a Singleton, ask: can I pass it as a dependency instead?
Key Takeaway
Patterns are problem-shapes, not toolboxes.
Learn the problem, then recall the pattern.
The best code uses the fewest patterns.

Creational Patterns — Singleton, Factory, Builder

Creational patterns abstract the instantiation process. They make a system independent of how its objects are created, composed, and represented.

Singleton: Ensures a class has only one instance and provides a global access point. Used for thread pools, caches, device drivers. Pitfall: thread safety and global coupling.

Factory Method: Defines an interface for creating an object, but lets subclasses decide which class to instantiate. Used when a class can't anticipate the class of objects it must create.

Builder: Separates the construction of a complex object from its representation. Used when an object has many optional components (e.g., building a HttpRequest with headers, body, parameters).

Here's a real-world scenario: you're building a notification service that can send emails, SMS, and push notifications. A Factory Method returns the appropriate sender based on the user's preferences. A Builder constructs the notification payload step by step. A Singleton manages the shared connection pool to the messaging provider.

The key insight: Creational patterns let you add new notification channels without touching existing client code. That's the Open/Closed principle in action.

io/thecodeforge/patterns/creational/NotificationFactory.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package io.thecodeforge.patterns.creational;

// Product interface
interface Notification {
    void send(String message);
}

class EmailNotification implements Notification {
    public void send(String message) {
        System.out.println("Sending email: " + message);
    }
}

class SMSNotification implements Notification {
    public void send(String message) {
        System.out.println("Sending SMS: " + message);
    }
}

// Factory Method
class NotificationFactory {
    public static Notification create(String type) {
        return switch (type) {
            case "email" -> new EmailNotification();
            case "sms" -> new SMSNotification();
            default -> throw new IllegalArgumentException("Unknown type: " + type);
        };
    }
}

// Usage in service
class NotificationService {
    public void notifyUser(String userId, String message, String type) {
        Notification notification = NotificationFactory.create(type);
        notification.send(message);
    }
}
Output
Sending email: Hello from TheCodeForge!
Sending SMS: Hello from TheCodeForge!
Factory + Singleton = Hidden Coupling
If your Factory accesses a Singleton directly, every test that calls the Factory becomes dependent on that global state. Instead, inject the Singleton as a parameter to the Factory. Spring's DI container solves this elegantly.
Production Insight
Singletons in a microservice environment cause issues when you have multiple instances of the same service.
Each JVM has its own Singleton, so you can't share state across replicas unless you use an external cache like Redis.
Trade-off: Singleton is fine for stateless objects (like a factory), dangerous for mutable state.
Key Takeaway
Singleton = one instance, but one per JVM only.
Factory = let subclasses decide which object to create.
Builder = construct complex objects step by step.
Don't make global state when dependency injection works.

Structural Patterns — Adapter, Decorator, Proxy

Structural patterns explain how to assemble objects and classes into larger structures while keeping these structures flexible and efficient.

Adapter: Allows incompatible interfaces to work together. Think of a travel adapter that lets your US plug work in a European socket. Used when you want to use an existing class but its interface doesn't match your needs.

Decorator: Attaches additional responsibilities to an object dynamically. Used when subclassing would lead to an explosion of classes. Java's BufferedReader is a classic Decorator.

Proxy: Provides a surrogate or placeholder for another object to control access to it. Lazy loading, access control, logging are common uses. Spring AOP uses dynamic proxies.

In production, you'll see Adapter extensively in legacy integration — wrapping old SOAP APIs into a clean REST-like interface. Decorator is everywhere in stream processing: new BufferedInputStream(new GzipInputStream(new FileInputStream("data.gz"))). Proxy is used by ORM frameworks like Hibernate for lazy loading entities.

Important: Don't confuse Proxy with Decorator. Both have similar structure but different intent. Proxy controls access; Decorator adds behavior.

io/thecodeforge/patterns/structural/AdapterExample.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package io.thecodeforge.patterns.structural;

// Target interface
interface JsonParser {
    String parse(String json);
}

// Adaptee — old XML parser
class LegacyXmlParser {
    String parseXml(String xml) {
        return "Parsed: " + xml;
    }
}

// Adapter
class XmlToJsonAdapter implements JsonParser {
    private final LegacyXmlParser xmlParser;
    
    public XmlToJsonAdapter(LegacyXmlParser xmlParser) {
        this.xmlParser = xmlParser;
    }
    
    @Override
    public String parse(String json) {
        // Convert JSON to XML (fake conversion)
        String xml = json.replace("{", "<root>").replace("}", "</root>");
        return xmlParser.parseXml(xml);
    }
}

class Client {
    public static void main(String[] args) {
        LegacyXmlParser oldParser = new LegacyXmlParser();
        JsonParser adapter = new XmlToJsonAdapter(oldParser);
        System.out.println(adapter.parse("{\"name\":\"Alice\"}"));
    }
}
Output
Parsed: <root>"name":"Alice"</root>
Adapter vs Facade
Adapter changes the interface; Facade provides a simplified interface to a subsystem. Use Adapter when you need to plug something in; use Facade when you want to hide complexity.
Production Insight
Decorator chains can cause unexpected performance overhead if misused. Wrapping an InputStream with 10 decorators adds 10 method calls per read.
Use the Decorator pattern sparingly — prefer composition via constructor injection when possible.
Proxy patterns that use reflection (like Java dynamic proxies) are slower than hand-coded proxies. Measure before using in hot paths.
Key Takeaway
Adapter makes things fit together.
Decorator layers behavior without subclassing.
Proxy controls access — often for good reason.
All three solve composition problems, but each with a different intent.

Behavioral Patterns — Observer, Strategy, Command

Behavioral patterns are concerned with algorithms and the assignment of responsibilities between objects. They describe not just patterns of objects or classes but also the patterns of communication between them.

Observer: Defines a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically. Used in event handling systems, MVC, and reactive programming. Java's PropertyChangeListener is an example.

Strategy: Defines a family of algorithms, encapsulates each one, and makes them interchangeable. Strategy lets the algorithm vary independently from clients that use it. Seen in validation rules, sorting strategies, payment methods.

Command: Encapsulates a request as an object, thereby letting you parameterize clients with different requests, queue or log requests, and support undoable operations. Used in task queues, GUI actions, and transactional behavior.

These patterns are powerful because they decouple the requester of an action from the executor. The Observer pattern is the basis of the reactive streams in Java 9 (Flow API). Strategy is what makes Collections.sort(list, comparator) extensible — you provide the strategy via a comparator. Command is why Undo/Redo works in text editors.

Production warning: The Observer pattern leaks memory if subscribers are not removed. Always use a WeakReference in the subject's list or require explicit unsubscription.

io/thecodeforge/patterns/behavioral/ObserverExample.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
package io.thecodeforge.patterns.behavioral;

import java.util.ArrayList;
import java.util.List;

// Subject interface
interface Subject {
    void attach(Observer o);
    void detach(Observer o);
    void notifyObservers();
}

class WeatherStation implements Subject {
    private List<Observer> observers = new ArrayList<>();
    private int temperature;
    
    void setTemperature(int temp) {
        this.temperature = temp;
        notifyObservers();
    }
    
    @Override
    public void attach(Observer o) { observers.add(o); }
    
    @Override
    public void detach(Observer o) { observers.remove(o); }
    
    @Override
    public void notifyObservers() {
        for (Observer o : observers) o.update(temperature);
    }
}

interface Observer {
    void update(int temperature);
}

class Display implements Observer {
    private String name;
    public Display(String name) { this.name = name; }
    public void update(int temp) {
        System.out.println(name + " shows temperature: " + temp + "°C");
    }
}

class WeatherApp {
    public static void main(String[] args) {
        WeatherStation station = new WeatherStation();
        Display phone = new Display("Phone");
        Display billboard = new Display("Billboard");
        station.attach(phone);
        station.attach(billboard);
        station.setTemperature(22);
        // Both displays update automatically
    }
}
Output
Phone shows temperature: 22°C
Billboard shows temperature: 22°C
Observer as Publisher-Subscriber
  • The subject doesn't know who its observers are — just that they implement the Observer interface.
  • This decoupling means new observers can be added at runtime without changing the subject.
  • But if observers are slow, they block the subject's notification loop — consider async notification.
  • Production: Use an event bus (like Guava EventBus) for complex observer networks.
Production Insight
The Observer pattern often hides circular updates — A changes, B observes A, B's observer updates A again.
Use a flag or version check to break cycles.
Another headache: exceptions thrown in one observer can break notifications for all others. Catch exceptions per observer.
Key Takeaway
Observer — one change, many reactions.
Strategy — pluggable behavior.
Command — action as object.
All three reduce coupling but introduce indirection — measure the cost.
Observer vs Strategy vs Command
IfNeed to notify multiple components when a state changes
UseObserver
IfNeed to swap algorithms at runtime (e.g., different sorting)
UseStrategy
IfNeed to encapsulate an action as an object (undoable, queued)
UseCommand
IfNeed to store a sequence of user actions for playback
UseCommand with history

Complete 23 GoF Patterns Quick Reference

Here is a quick-reference table of all 23 Gang of Four design patterns, organized by category, with the core problem each pattern solves. Use this as a cheat sheet when you're designing a system and need to recall what's available.

Pattern NameCategoryProblem It Solves
Abstract FactoryCreationalCreate families of related objects without specifying concrete classes
BuilderCreationalConstruct a complex object step by step, separating construction from representation
Factory MethodCreationalDefine an interface for creating an object, but let subclasses decide which class to instantiate
PrototypeCreationalCreate new objects by copying an existing instance (clone)
SingletonCreationalEnsure a class has only one instance and provide a global access point
AdapterStructuralAllow classes with incompatible interfaces to work together
BridgeStructuralDecouple an abstraction from its implementation so both can vary independently
CompositeStructuralCompose objects into tree structures to represent part-whole hierarchies
DecoratorStructuralAttach additional responsibilities to an object dynamically
FacadeStructuralProvide a unified interface to a set of interfaces in a subsystem
FlyweightStructuralUse sharing to support large numbers of fine-grained objects efficiently
ProxyStructuralProvide a surrogate or placeholder for another object to control access
Chain of ResponsibilityBehavioralAvoid coupling sender and receiver by giving multiple objects a chance to handle a request
CommandBehavioralEncapsulate a request as an object, allowing parameterization, queuing, and undo
InterpreterBehavioralDefine a grammar for a language and an interpreter to evaluate sentences
IteratorBehavioralProvide a way to access elements of a collection sequentially without exposing its underlying representation
MediatorBehavioralDefine an object that encapsulates how a set of objects interact
MementoBehavioralCapture and restore an object's internal state without violating encapsulation
ObserverBehavioralDefine a one-to-many dependency so that when one object changes state, all dependents are notified
StateBehavioralAllow an object to alter its behavior when its internal state changes
StrategyBehavioralDefine a family of algorithms, encapsulate each, and make them interchangeable
Template MethodBehavioralDefine the skeleton of an algorithm, deferring some steps to subclasses
VisitorBehavioralDefine a new operation on a class hierarchy without changing the classes

This table is your palette. The skill is matching the problem shape to the pattern — not memorizing the list. Keep it handy during whiteboard design sessions.

Don't memorize — recognize
Instead of rote memorization, try this: when you face a design problem, scan the table by category. 'Need to create objects flexibly?' → Creational. 'Need to compose objects?' → Structural. 'Need to vary behavior?' → Behavioral. Over time the patterns become instinctive.
Production Insight
In real projects, you'll use about 8-10 patterns regularly. The rest are specialized. For example, Interpreter is rarely used except in DSLs. Flyweight is useful when you have millions of objects (e.g., rendering text). Don't feel pressured to know every one deeply — but know they exist so you can look them up when needed.
Key Takeaway
The 23 GoF patterns are a catalog, not a checklist. Use the table to jog your memory when a problem shape matches a pattern you haven't used in months.

SOLID Principles — Which Patterns Enforce Which Principle

Design patterns are concrete implementations of the SOLID principles. Understanding this connection helps you choose the right pattern because you're really choosing which principle to enforce. Here's the mapping:

Single Responsibility Principle (SRP): A class should have one reason to change. - Patterns: Command, Strategy, Visitor. Each encapsulates a single responsibility into its own class. Command encapsulates an action; Strategy encapsulates an algorithm; Visitor encapsulates an operation on a structure.

Open/Closed Principle (OCP): Software entities should be open for extension, closed for modification. - Patterns: Factory Method, Strategy, Decorator, Template Method, Observer. Adding new strategies, decorators, or observers doesn't require changing existing code. Factory Method lets you introduce new product types without modifying the creator.

Liskov Substitution Principle (LSP): Subtypes must be substitutable for their base types. - Patterns: Abstract Factory, Factory Method, Strategy. These patterns rely on polymorphism. If your factory returns objects that violate LSP, the pattern breaks. The pattern itself encourages LSP compliance because clients depend on interfaces.

Interface Segregation Principle (ISP): Clients should not be forced to depend on interfaces they don't use. - Patterns: Adapter, Facade, Proxy. Adapter translates an interface to one the client expects. Facade provides a simplified interface, effectively segregating the complex subsystem. Proxy can add access control to segregate interface usage.

Dependency Inversion Principle (DIP): Depend on abstractions, not concretions. - Patterns: Factory Method, Abstract Factory, Strategy, Template Method. All these patterns define abstract interfaces and depend on them rather than concrete classes. Dependency injection (often using Factory or a container) is the practical implementation of DIP.

Here's a practical tip: When you violate SRP, you'll see classes with many methods that change for different reasons. Introducing Command pattern splits those reasons into separate objects. When you violate OCP, you find switch statements on type — replace with Strategy or Factory Method.

The pattern doesn't guarantee the principle — but when applied correctly, it naturally enforces it.

io/thecodeforge/patterns/solid/OpenClosedExample.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package io.thecodeforge.patterns.solid;

// OCP violation: switch on type
class PaymentProcessor {
    void pay(String method, double amount) {
        if (method.equals("credit")) { /* credit logic */ }
        else if (method.equals("paypal")) { /* paypal logic */ }
        // adding new method modifies this class
    }
}

// OCP with Strategy pattern
interface PaymentStrategy {
    void pay(double amount);
}
class CreditCardPayment implements PaymentStrategy {
    public void pay(double amount) { /* credit logic */ }
}
class PayPalPayment implements PaymentStrategy {
    public void pay(double amount) { /* paypal logic */ }
}
class PaymentService {
    private final PaymentStrategy strategy;
    PaymentService(PaymentStrategy strategy) { this.strategy = strategy; }
    void processPayment(double amount) { strategy.pay(amount); }
}
Output
Open for extension: add a CryptoPayment class without touching PaymentService. Closed for modification: PaymentService never changes.
Production Insight
I've seen teams apply patterns without understanding SOLID, then wonder why their code still has coupling. For example, they add a Strategy pattern but make the strategy classes depend on concrete implementations — violating DIP. Always check: does my pattern application actually achieve the principle I'm aiming for? The pattern is a means, not an end.
Key Takeaway
Each pattern is a tool to enforce one or more SOLID principles. When you learn a new pattern, ask: which principle does it enforce? That understanding will help you apply it correctly.

Relations Between Patterns — Common Collaborations

Patterns don't live in isolation. In a well-designed system, patterns work together, each solving a part of the problem. Here are the most common pattern relationships you'll see in production:

Factory + Singleton: A Factory method is often used to return a Singleton instance. The Factory encapsulates the creation logic and ensures only one instance is created. This is cleaner than putting the singleton logic in the class itself. Spring's @Bean with @Scope("singleton") is essentially a Factory that produces a Singleton.

Observer + Strategy: The Observer pattern defines the communication mechanism; the Strategy pattern defines the behavior of the observers. For example, a UI button (subject) notifies observers (listeners), each of which implements a different strategy for handling the click. The combination decouples event sources from event handlers and allows handlers to be swapped independently.

Decorator + Strategy: Decorators can be used to wrap a Strategy object to add pre/post processing. For example, a TimedPaymentStrategy decorator adds logging and timing around any payment strategy. This avoids modifying the strategy classes themselves.

Composite + Visitor: The Composite pattern builds a tree of objects (e.g., file system). The Visitor pattern lets you define operations on that tree without changing the node classes. This is a powerful combination — you can traverse the composite structure with different visitors (size calculator, search engine, backup script) all without touching the tree classes.

Command + Memento: Command stores actions for undo/redo. Memento captures the state before executing the command. This is how text editors support undo. The Command pattern triggers the action, and the Memento pattern captures the state needed to reverse it.

Chain of Responsibility + Composite: The Chain of Responsibility pattern often uses a Composite structure where each node can handle a request or pass it to the next. This is used in middleware pipelines (e.g., servlet filters) where each filter is a node in a chain.

Abstract Factory + Factory Method: Abstract Factory is often implemented using Factory Methods. The abstract factory defines an interface for creating families of products, and concrete factories implement those methods, effectively using Factory Method for each product.

Proxy + Singleton: A Proxy can control access to a Singleton, adding lazy initialization, logging, or security. For example, a SecureSingletonProxy ensures that only authorized threads can call the Singleton's methods.

Understanding these relationships helps you design the architecture, not just pick isolated patterns. When you see a problem that needs multiple patterns, think about how they interact.

Pattern synergy is more powerful than individual patterns
The GoF book itself has a section on pattern relationships. The real power comes when you combine them intentionally — each pattern handles one axis of change.
Production Insight
In a large codebase, the most common collaboration I see is Factory + Singleton for service objects, and Observer + Strategy for event handling. These combinations are so frequent that many frameworks (Spring, Guice) provide built-in support for them. If you're manually wiring patterns, consider using a DI container to manage these relationships.
Key Takeaway
Patterns are building blocks — combine them to solve multi-dimensional problems. The relationships between patterns are as important as the patterns themselves.

Anti-Patterns: God Object, Singleton Abuse, Golden Hammer

While the previous section covered common anti-patterns, these three deserve special attention because they appear in almost every codebase of significant size.

God Object (aka God Class): A class that knows too much or does too much. It centralizes data and logic, often growing over years of adding "one more feature." In a design patterns context, God Object often manifests as a combined Factory + Repository + Service in one class. Symptoms: the class has hundreds of methods, dozens of fields, and is hard to unit test because it depends on many different subsystems. Fix: apply the Single Responsibility Principle. Split into multiple classes — each with one responsibility. Use patterns like Strategy or Command to separate algorithms, and Repository or DAO for data access.

Singleton Abuse: The most common anti-pattern in Java. Developers make every utility class a Singleton because it's "easier." This creates global state that couples the entire codebase. Singleton abuse makes testing impossible (you can't replace the Singleton with a mock) and leads to hidden dependencies. Fix: Use dependency injection. If you need a single instance, let the DI container manage it (Spring's singleton scope). Never make a mutable Singleton — use final fields and initialize them in the constructor. For shared mutable state, use an explicit dependency like a SharedCache passed to the classes that need it, not a global Singleton.

Golden Hammer: The tendency to apply a pattern you've learned to every problem, regardless of fit. A junior developer learns the Strategy pattern and starts wrapping every method call in a strategy interface. This adds unnecessary indirection without solving a real problem. Golden Hammer is dangerous because it adds accidental complexity. The code becomes harder to follow, debug, and maintain. Fix: be honest about the problem. If you're not sure whether a pattern is needed, write the straightforward solution first. Refactor into a pattern only when the simple solution proves insufficient. "Patterns are refactoring destinations, not starting points."

These anti-patterns share a common theme: using a pattern as a goal rather than a tool. Senior engineers learn to recognize when a pattern is making things worse.

io/thecodeforge/patterns/antipatterns/GoldenHammerExample.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package io.thecodeforge.patterns.antipatterns;

// Golden Hammer: Overuse of Strategy pattern for simple logic
// Instead of:
interface Greeter {
    String greet(String name);
}
class EnglishGreeter implements Greeter {
    public String greet(String name) { return "Hello " + name; }
}
class SpanishGreeter implements Greeter {
    public String greet(String name) { return "Hola " + name; }
}

// Just use a method:
class GreeterService {
    String greet(String name, String lang) {
        if (lang.equals("en")) return "Hello " + name;
        if (lang.equals("es")) return "Hola " + name;
        return "Hello " + name;
    }
}
// The strategy pattern is overkill here. Use it only when algorithms are complex or change at runtime.
Output
Keep it simple until you need the flexibility.
Production Insight
The cost of Golden Hammer is not just code complexity — it's cognitive load. Every unnecessary layer of indirection makes debugging harder. I've spent hours tracing through six layers of abstraction only to find a simple if-else that could have been written directly. When debugging a production issue, simple code wins every time.
Concrete example: A team used the Visitor pattern for a simple report generator. The report had two types of rows. After a year, changing the report format required touching 5 classes. A simple template method would have worked better.
Key Takeaway
Anti-patterns are patterns applied thoughtlessly. The best engineers know when to use a pattern and when to write plain code. Simplicity is the ultimate sophistication.

Practice Design Exercises

The best way to internalize design patterns is to solve real-world problems. Here are five exercises that cover different pattern categories and common use cases. Attempt each one, then compare your solution with known pattern-based approaches.

Exercise 1: Design a Notification System A notification system needs to send messages via email, SMS, push notification, and in-app notifications. New channels can be added without modifying existing code. Different notifications have different templates (e.g., welcome email vs password reset). - Required patterns: Strategy (for sending), Factory (for creating notifier based on type), Template Method (for notification template steps). - Bonus: Implement Observer so that when a notification is sent, other components (like analytics) are notified.

Exercise 2: Design a Plugin Architecture Your application should allow third-party developers to write plugins that extend functionality. Each plugin has an initialization phase, a run phase, and a cleanup phase. Plugins are discovered via classpath scanning or configuration. - Required patterns: Command (to encapsulate plugin lifecycle), Factory (to create plugin instances), Chain of Responsibility (to process plugin hooks in order). - Bonus: Use Proxy to sandbox plugins (limiting access to certain APIs).

Exercise 3: Design a Shopping Cart with Discounts Customers can add items to a cart. Discounts are applied based on customer type (regular, premium, VIP) and on total amount thresholds (10% off over $100). Discount rules can change over time. - Required patterns: Strategy (for discount calculation rules), Decorator (to combine multiple discounts), Builder (to construct the cart with optional additives). - Bonus: Use Observer to update the cart UI whenever the cart changes.

Exercise 4: Design a Logging Framework A logging framework that supports multiple log levels (DEBUG, INFO, WARN, ERROR), multiple output destinations (console, file, database), and different formatting (plain text, JSON, XML). New output destinations and formatters should be easy to add. - Required patterns: Chain of Responsibility (log levels propagate down the chain), Strategy (for formatting), Bridge (separate logger abstraction from output implementation). - Bonus: Use Singleton for configuration management (but make it immutable).

Exercise 5: Design a Document Editor with Undo/Redo A document editor that supports text insertion, deletion, formatting (bold, italic), and undo/redo of all operations. The history should be bounded (e.g., last 100 actions). - Required patterns: Command (each action is a command with execute and undo), Memento (to capture document state for complex undo), Prototype (to clone document snapshots). - Bonus: Use Mediator to coordinate commands with the UI (e.g., disable undo button when history is empty).

For each exercise, start with the simplest solution that works. Then refactor to introduce patterns as the need arises (when you have to change the code for a new requirement). This is how patterns are meant to be used: discovered after writing straightforward code, not imposed upfront.

How to approach these exercises
Don't look up the answer. Write the naive solution first (maybe a big switch statement). Then ask: what happens when we add a new notification channel? If the answer is 'modify five files', that's your cue to introduce a pattern. The pattern should reduce the scope of change for future additions.
Production Insight
These exercises mirror real production decisions. For instance, Exercise 1 (notification system) is essentially how many messaging platforms work internally. Exercise 2 (plugin architecture) is how IDEs like Eclipse or VS Code handle extensions. If you can design these systems using patterns, you'll be better prepared to maintain or extend them in a real job.
Remember: the goal is not to use all patterns, but to use the right pattern to keep the code maintainable. Some of these exercises can be solved with just 1-2 patterns. Over-engineering is a mistake.
Key Takeaway
Practice by solving realistic problems. Start simple, then refactor with patterns when the code demands it. That's how you build the intuition to know when a pattern is needed and when it's overhead.

Anti-Patterns — When Patterns Go Wrong

For every pattern there's a corresponding anti-pattern — a common but ineffective solution that looks like the real thing. Senior engineers spot these in code reviews faster than they spot the correct patterns.

Singleton as Global State: Using a Singleton to hold mutable data that many classes depend on. This creates hidden coupling and makes testing a nightmare. Fix: pass dependencies explicitly (Dependency Injection).

God Class: One class that handles everything — creation, business logic, persistence. Often starts as a Singleton or Factory that grows unchecked. Fix: Split into multiple classes using the Single Responsibility Principle.

Listener Overload: Adding an Observer for every minor event without considering if the event matters. This floods the system with no-op notifications, degrading performance. Fix: Use throttling or only notify on meaningful state changes.

Factory for Everything: Creating a Factory for every class, even those with trivial construction. This adds unnecessary indirection and makes code harder to follow. Fix: Use a Factory only when the construction logic is nontrivial or needs to vary.

The golden rule: If a pattern makes your code harder to understand, don't use it. Patterns are tools, not rules.

io/thecodeforge/patterns/antipatterns/GodClassExample.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package io.thecodeforge.patterns.antipatterns;

// Anti-pattern: God Class
class UserManager {
    private Database db;
    private EmailService email;
    private PaymentGateway payment;
    
    public void createUser(String name, String emailAddr) {
        User user = new User(name, emailAddr);
        db.saveUser(user);
        email.sendWelcome(emailAddr);
        payment.charge(10.0);
    }
    
    public void deleteUser(int userId) {
        db.deleteUser(userId);
        email.sendGoodbye(emails);
        payment.refund(userId);
    }
}

// Better: Each responsibility in its own class
class UserCreator {
    private final UserRepository repo;
    private final WelcomeEmailSender sender;
    public UserCreator(UserRepository repo, WelcomeEmailSender sender) { ... }
    public void create(String name, String email) { ... }
}
Output
Avoid God Classes — each class should have one reason to change.
Pattern-for-pattern's-sake
I've seen codebases where every class is wrapped in an interface 'for flexibility'. That's not patterns — that's ceremony. Only abstract when the variability exists or is likely.
Production Insight
The most expensive anti-pattern is the "Golden Hammer": applying a pattern you love to every problem.
A Singleton makes sense for a logging service (and even there, use DI). It doesn't make sense for a cache that needs flushing.
Rule: If you can't explain why a pattern is the right choice in one sentence, it probably isn't.
Key Takeaway
A pattern used wrong is worse than no pattern.
Test-driven design: let the tests tell you if patterns simplify or complicate.
When in doubt, choose simplicity over pattern purity.
● Production incidentPOST-MORTEMseverity: high

The Singleton That Took Down a Payment Gateway

Symptom
Payment processing slowed to a crawl, then failed entirely. Thread dumps showed all worker threads blocked on a single lock.
Assumption
\"Singleton is thread-safe because we added 'synchronized' to the getInstance method.\"
Root cause
The Singleton held a shared counter for transaction IDs without fine-grained synchronization. Threads contended on the monolithic lock, causing effectively single-threaded execution.
Fix
Replace the synchronized method with double-checked locking and a volatile instance. Decouple the counter into an AtomicLong. Add a bounded thread pool for payment processing.
Key lesson
  • Synchronized on a Singleton degrades concurrency to single-threaded — always test under load.
  • Patterns don't guarantee thread safety; the implementation does.
  • If every thread blocks on one lock, you've created a bottleneck worse than the problem you solved.
Production debug guideSymptom-driven approach to identify broken patterns4 entries
Symptom · 01
Application startup fails with class cast exceptions across modules
Fix
Check for Circular Dependency in Singleton or Service Locator. Run a dependency graph analysis (e.g., jdeps) to find cycles.
Symptom · 02
Memory grows indefinitely; objects not garbage collected
Fix
Suspect Observer pattern with strong references. Use a heap dump and search for subscribers that were never removed — likely an event bus leak.
Symptom · 03
Code changes in one place break unrelated features
Fix
Look for excessive coupling through Global State (Singleton anti-pattern) or a misapplied Template Method that couples subclass behavior. Use git bisect to find recent pattern introductions.
Symptom · 04
New features require modifying existing classes far too often
Fix
The system likely missed an abstraction. Consider introducing a Strategy or Command pattern to decouple. Check if existing patterns are closing the wrong change axes (Open/Closed principle violation).
★ Pattern Failure Debugging Cheat SheetQuick commands and checks for the three most common pattern failures in production.
Singleton contention under load (blocked threads)
Immediate action
Grab thread dump: kill -3 <pid> or jstack <pid>
Commands
jstack <pid> | grep -A 20 'BLOCKED'
jcmd <pid> Thread.print
Fix now
Replace synchronized method with double-checked locking (volatile + synchronized block inside) or use enum Singleton. Add AtomicLong for shared counters.
Observer memory leak (listener never removed)+
Immediate action
Take heap dump: jmap -dump:live,format=b,file=heap.hprof <pid>
Commands
jhat heap.hprof
Find instances of listener class — count should match expected. Run: jmap -histo <pid> | grep Listener
Fix now
Use WeakReference in observer lists, or require explicit unsubscribe in a finally block. For event buses, implement a lifecycle callback that removes listeners.
Factory creates too many objects (high GC pressure)+
Immediate action
Monitor GC logs: -Xlog:gc*
Commands
jstat -gcutil <pid> 1s 10
jmap -histo:live <pid> | head -20
Fix now
Cache created objects where appropriate. Use Flyweight pattern inside the factory. Validate that object creation is necessary per request — reuse if possible.
Pattern Category Quick Reference
CategoryFocusExamplesWhen to Use
CreationalObject creationSingleton, Factory, BuilderWhen object creation logic is complex or needs flexibility
StructuralObject compositionAdapter, Decorator, ProxyWhen classes need to work together despite incompatible interfaces
BehavioralObject interactionObserver, Strategy, CommandWhen algorithms or responsibilities need to be decoupled

Key takeaways

1
Design patterns solve specific problems
don't apply them blindly.
2
Creational
Singleton, Factory, Builder control how objects are created.
3
Structural
Adapter, Decorator, Proxy help compose classes and objects.
4
Behavioral
Observer, Strategy, Command decouple communication and algorithms.
5
Anti-patterns like Singleton-as-global-state are worse than no pattern.
6
Test-driven development reveals when a pattern is necessary and when it's overhead.

Common mistakes to avoid

3 patterns
×

Treating Singleton as a global variable container

Symptom
Test failures in unrelated tests due to shared mutable state. Deadlocks under high concurrency when Singleton holds a synchronized map.
Fix
Replace Singleton with dependency injection. If you must use Singleton, make it immutable (fields are final) or use an enum Singleton for serialization safety. For mutable shared state, pass it as a parameter or use a DI container with singleton scope.
×

Using Factory pattern for every object creation, even trivial ones

Symptom
Codebase full of Factory classes that do nothing but 'new Object()'. Debugging requires navigating multiple layers of indirection for simple instantiation.
Fix
Only introduce a Factory when: the creation logic is complex, you need to centralize configuration, or you need to return different implementations based on input. For simple cases, use a static factory method on the class itself or just new directly.
×

Observer pattern with strong references causing memory leaks

Symptom
After many UI interactions, memory grows and objects that should be garbage collected remain in the heap. Heap dumps show listeners that were never removed.
Fix
Use WeakReference in the observer list, or implement a clearly defined lifecycle where listeners are removed in finally blocks or via @PreDestroy. In Java, consider using the Observable/Observer legacy classes only if you need weak references.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
Explain the difference between Factory Method and Abstract Factory. When...
Q02SENIOR
How does the Decorator pattern differ from inheritance? Provide a Java e...
Q03SENIOR
What is the Strategy pattern? Provide a real-world scenario where it sol...
Q04SENIOR
Describe a scenario where the Observer pattern caused a production incid...
Q01 of 04SENIOR

Explain the difference between Factory Method and Abstract Factory. When would you use each?

ANSWER
Factory Method defines an interface for creating a single product, but lets subclasses decide which concrete class to instantiate. Abstract Factory provides an interface for creating families of related or dependent objects without specifying their concrete classes. Use Factory Method when you have a single product to create and want to delegate instantiation to subclasses. Use Abstract Factory when you need to create multiple related products (e.g., a UI toolkit that creates buttons, windows, menus for a specific OS). In production, Abstract Factory often appears in cross-platform libraries; Factory Method is common in frameworks where the framework calls your code to create objects (like Spring's @Bean factory methods).
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
What is Design Patterns in Java in simple terms?
02
Do I need to memorize all 23 GoF patterns?
03
How do design patterns relate to SOLID principles?
04
Are design patterns still relevant in modern Java with Lambdas and Streams?
05
How do I avoid pattern overuse?
🔥

That's Advanced Java. Mark it forged?

15 min read · try the examples if you haven't

Previous
Anonymous Classes in Java
7 / 28 · Advanced Java
Next
Singleton Pattern in Java