Java Design Patterns — Singleton Lock Contention Fixes
A Singleton with synchronized methods blocked all payment threads on one lock.
- 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
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.
- 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.
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.
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.
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.
- 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.
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 Name | Category | Problem It Solves |
|---|---|---|
| Abstract Factory | Creational | Create families of related objects without specifying concrete classes |
| Builder | Creational | Construct a complex object step by step, separating construction from representation |
| Factory Method | Creational | Define an interface for creating an object, but let subclasses decide which class to instantiate |
| Prototype | Creational | Create new objects by copying an existing instance (clone) |
| Singleton | Creational | Ensure a class has only one instance and provide a global access point |
| Adapter | Structural | Allow classes with incompatible interfaces to work together |
| Bridge | Structural | Decouple an abstraction from its implementation so both can vary independently |
| Composite | Structural | Compose objects into tree structures to represent part-whole hierarchies |
| Decorator | Structural | Attach additional responsibilities to an object dynamically |
| Facade | Structural | Provide a unified interface to a set of interfaces in a subsystem |
| Flyweight | Structural | Use sharing to support large numbers of fine-grained objects efficiently |
| Proxy | Structural | Provide a surrogate or placeholder for another object to control access |
| Chain of Responsibility | Behavioral | Avoid coupling sender and receiver by giving multiple objects a chance to handle a request |
| Command | Behavioral | Encapsulate a request as an object, allowing parameterization, queuing, and undo |
| Interpreter | Behavioral | Define a grammar for a language and an interpreter to evaluate sentences |
| Iterator | Behavioral | Provide a way to access elements of a collection sequentially without exposing its underlying representation |
| Mediator | Behavioral | Define an object that encapsulates how a set of objects interact |
| Memento | Behavioral | Capture and restore an object's internal state without violating encapsulation |
| Observer | Behavioral | Define a one-to-many dependency so that when one object changes state, all dependents are notified |
| State | Behavioral | Allow an object to alter its behavior when its internal state changes |
| Strategy | Behavioral | Define a family of algorithms, encapsulate each, and make them interchangeable |
| Template Method | Behavioral | Define the skeleton of an algorithm, deferring some steps to subclasses |
| Visitor | Behavioral | Define 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.
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.
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.
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.
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.
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.
The Singleton That Took Down a Payment Gateway
- 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.
Key takeaways
Common mistakes to avoid
3 patternsTreating Singleton as a global variable container
Using Factory pattern for every object creation, even trivial ones
Object()'. Debugging requires navigating multiple layers of indirection for simple instantiation.Observer pattern with strong references causing memory leaks
Interview Questions on This Topic
Explain the difference between Factory Method and Abstract Factory. When would you use each?
Frequently Asked Questions
That's Advanced Java. Mark it forged?
15 min read · try the examples if you haven't