Java Design Patterns — Singleton Lock Contention Fixes
A Singleton with synchronized methods blocked all payment threads on one lock.
20+ years shipping production Java in banking & fintech. Lessons pulled from things that broke in production.
- 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
Design patterns are battle-tested, reusable solutions to common software design problems — not copy-paste code, but templates for structuring object-oriented systems. The Gang of Four (GoF) cataloged 23 patterns into three categories: creational (how objects are created), structural (how classes and objects compose), and behavioral (how objects communicate).
These patterns emerged from real-world pain points in enterprise Java development, where rigid frameworks and boilerplate code made maintainability a nightmare. You use them when you recognize a recurring architectural friction — like needing exactly one database connection (Singleton) or decoupling a notification system from its senders (Observer).
You don't use them when simpler solutions work; patterns add indirection, so applying them prematurely (over-engineering) is a common rookie mistake. The SOLID principles underpin these patterns — for example, the Strategy pattern enforces the Open/Closed Principle by letting you swap algorithms without modifying client code, while the Dependency Inversion Principle is baked into the Factory pattern.
In practice, Java developers lean heavily on creational patterns (Singleton, Factory, Builder) for managing object lifecycles in Spring and Hibernate, structural patterns (Adapter, Proxy) for integrating legacy systems, and behavioral patterns (Observer, Command) for event-driven architectures. The Singleton pattern, however, has a notorious performance pitfall: when multiple threads contend for a lazily-initialized instance, synchronized access creates a bottleneck that can crater throughput in high-concurrency systems — hence the need for double-checked locking, volatile fields, or enum-based singletons to sidestep contention entirely.
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.
Design patterns in Java are battle-tested solutions to recurring architectural problems. Without them, you get tangled inheritance, scattered logic, and brittle code that breaks with every new requirement. This reference covers the 23 GoF patterns, their relationship to SOLID principles, and the anti-patterns that emerge when you misuse or ignore them—so you can write cleaner, more maintainable Java systems from the start.
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.
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)); } }
- 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.
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); } }
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.
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\"}")); } }
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.
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 } }
- 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.
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); } }
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.
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.
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.
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) { ... } }
J2EE Patterns — The Layers You Actually Work With
If you're building enterprise Java — and if you're reading this, you are — the GoF patterns are just the warm-up. The real fight is in the J2EE (now Jakarta EE) stack: servlets, EJBs, JSPs, and the mess of connecting them without creating a god-awful monolith that takes thirty seconds to deploy.
J2EE design patterns emerged because developers kept repeating the same mistakes. The Front Controller pattern keeps request handling centralized instead of scattering it across a hundred servlets. Transfer Object (or Value Object) stops you from serializing entire entity graphs across the wire when all you need is a user name. DAO (Data Access Object) hides persistence logic so you can swap databases without rewriting the service layer.
These aren't academic. They're scars from production. The Business Delegate pattern reduces coupling between the web tier and the EJB tier — because you don't want every JSP to hold a reference to a UserServiceBean. If you've ever debugged a javax.naming.NameNotFoundException at 2 AM, you know why.
Learn the J2EE patterns when your architecture has more than two layers. Before that, they're overkill. After that, they're survival.
// io.thecodeforge — java tutorial // Front Controller: single entry point for all web requests import javax.servlet.*; import javax.servlet.http.*; import java.io.*; public class FrontController extends HttpServlet { protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String path = req.getPathInfo(); // e.g. /users/profile // 1. Authentication check (common concern) if (req.getSession().getAttribute("user") == null) { resp.sendRedirect("/login"); return; } // 2. Dispatch to specific action handler switch (path) { case "/users/profile" -> req.getRequestDispatcher("/WEB-INF/views/profile.jsp").forward(req, resp); case "/orders/history" -> req.getRequestDispatcher("/WEB-INF/views/orderHistory.jsp").forward(req, resp); default -> resp.sendError(HttpServletResponse.SC_NOT_FOUND); } } }
Strategy vs State — Same Shape, Different Intent
Developers with six months of patterns under their belt often confuse Strategy and State because the class diagram looks identical: a context, an interface, a handful of concrete implementations. The difference is why you're swapping them.
Strategy is about selecting an algorithm at runtime. Need different payment gateways depending on the user's country? That's a Strategy. The context owns the selection logic, usually from config or a map. The strategies don't know about each other. You don't change the strategy mid-request — you pick one and execute.
State is about changing behavior when internal conditions change. A TCPConnection behaves differently in Open, Closed, and Listening states. Here, the state objects often transition to other states. The context's own state drives which implementation runs. You don't "choose" a state from outside — the state machine decides.
The concrete difference: in Strategy, the caller picks the implementation. In State, the current implementation picks the next one. A Strategy never calls setStrategy() on the context. A State almost always calls context.setState().
Mixing them up leads to code where the context has a switch statement over the current class name. That's the smell. If you're checking instanceof inside the context, you already lost.
// io.thecodeforge — java tutorial // Strategy: caller picks payment method interface PaymentStrategy { void pay(int amount); } class CreditCardPayment implements PaymentStrategy { public void pay(int amount) { System.out.println("Paid " + amount + " via Credit Card"); } } class CheckoutContext { private PaymentStrategy strategy; CheckoutContext(PaymentStrategy strategy) { this.strategy = strategy; } void processOrder(int total) { strategy.pay(total); } } // State: current state decides what happens next interface ConnectionState { void open(ConnectionContext ctx); void close(ConnectionContext ctx); } class ClosedState implements ConnectionState { public void open(ConnectionContext ctx) { System.out.println("Opening connection"); ctx.setState(new OpenState()); } public void close(ConnectionContext ctx) { System.out.println("Already closed"); } } class OpenState implements ConnectionState { public void open(ConnectionContext ctx) { System.out.println("Already open"); } public void close(ConnectionContext ctx) { System.out.println("Closing connection"); ctx.setState(new ClosedState()); } } class ConnectionContext { private ConnectionState state = new ClosedState(); void setState(ConnectionState state) { this.state = state; } void open() { state.open(this); } void close() { state.close(this); } }
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.
jstack <pid> | grep -A 20 'BLOCKED'jcmd <pid> Thread.printjhat heap.hprofFind instances of listener class — count should match expected. Run: jmap -histo <pid> | grep Listenerjstat -gcutil <pid> 1s 10jmap -histo:live <pid> | head -20| Category | Focus | Examples | When to Use |
|---|---|---|---|
| Creational | Object creation | Singleton, Factory, Builder | When object creation logic is complex or needs flexibility |
| Structural | Object composition | Adapter, Decorator, Proxy | When classes need to work together despite incompatible interfaces |
| Behavioral | Object interaction | Observer, Strategy, Command | When algorithms or responsibilities need to be decoupled |
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?
How does the Decorator pattern differ from inheritance? Provide a Java example.
BufferedFileInputStream, BufferedByteArrayInputStream, CompressedFileInputStream, etc., you compose: new BufferedReader(new FileReader(file)). Each Decorator (wrapping class) adds a behavior (buffering) without needing a subclass for every combination.
This pattern avoids class explosion: with 3 base classes and 3 behaviors, inheritance would require 9 classes; Decorator needs only 3 base + 3 decorator classes.What is the Strategy pattern? Provide a real-world scenario where it solves a production problem.
ShippingStrategy interface with implementations for Standard, Express, and International. The order service receives the strategy via DI. Now adding a new shipping method doesn't touch the order service. Testable: you can test each strategy in isolation.
Java example: PaymentStrategy interface with CreditCardPayment, PayPalPayment, CryptoPayment implementations. The checkout service accepts PaymentStrategy and calls pay(amount).Describe a scenario where the Observer pattern caused a production incident and how you fixed it.
dispose() that explicitly removes the observer. For new code, we used a centralized event bus that automatically unsubscribes when the component is destroyed (similar to React's useEffect cleanup).
Lesson: Always pair attach() with detach(). Use try-finally blocks. Consider using Flow.Publisher/Subscriber from java.util.concurrent for reactive streams.Frequently Asked Questions
Design patterns are proven templates for solving common software design problems. They are not code libraries you import. Instead, they provide a vocabulary and structure for writing better code. In Java, patterns often leverage language features like interfaces, abstract classes, and generics. Think of them as blueprints: the implementation details depend on your context.
No. You'll regularly use about 5-8 patterns (Singleton, Factory, Builder, Adapter, Decorator, Observer, Strategy, Command). The rest are specialized. Focus on recognizing the problem shape first, then recall the pattern. In interviews, showing that you can explain when to use a pattern is more valuable than reciting its UML diagram.
Patterns often implement SOLID principles. For example, the Strategy pattern follows the Open/Closed Principle: you can add new strategies without modifying the client. The Dependency Inversion Principle (DIP) is enabled by the Factory pattern. Understanding SOLID helps you see why patterns work; patterns give you concrete implementations of those principles.
Absolutely. Some patterns become simpler with lambdas. For example, the Strategy pattern can be implemented with a lambda instead of a full class. The Command pattern can use method references. But the underlying intent remains the same. Modern Java hasn't replaced patterns; it's made them more expressive and concise.
Ask yourself: Does this pattern make the code easier to understand and maintain? If the answer is 'no' or 'I'm not sure', don't use it. Write the simplest possible solution first. Refactor into a pattern only when the code screams for it — typically when you have duplication, complex conditionals, or hard-to-test code. Patterns are refactoring destinations, not starting points.
20+ years shipping production Java in banking & fintech. Lessons pulled from things that broke in production.
That's Advanced Java. Mark it forged?
16 min read · try the examples if you haven't