Design Patterns in Java
A practical guide to creational, structural, and behavioral design patterns in Java, including Java-specific patterns, the SOLID principles, and anti-patterns to avoid in real-world codebases.
What Are Design Patterns?
A design pattern is a reusable solution to a recurring design problem. Not a library you import, not a snippet you paste, but a template for structuring code that experienced developers have found works well in a particular context. The Gang of Four book, published in 1994, catalogued 23 patterns organized into three categories: creational (how objects are created), structural (how objects are composed), and behavioral (how objects communicate). These categories are still the standard way to discuss patterns.
The value of learning patterns is not that you will mechanically apply them everywhere. It is that patterns give you a shared vocabulary. When a colleague says "we should use an Observer here," you immediately understand the proposed structure without a lengthy explanation. Patterns also help you recognize when code is heading in a direction that has a known better solution.
Creational Patterns
Creational patterns deal with object creation. They abstract or defer the decision of which class to instantiate and how, making the system independent of how its objects are created and composed.
Singleton
Singleton ensures a class has exactly one instance and provides a global access point to it. The classic use cases are configuration managers, logging services, and thread pools: things that make no sense to have duplicated. The implementation must handle thread safety: two threads calling getInstance simultaneously without synchronization can each see the instance as null and create two objects.
The cleanest thread-safe Java implementation uses the enum idiom or the initialization-on-demand holder pattern. The enum approach is serialization-safe and reflection-safe as a bonus. Overuse is the main danger: Singletons are effectively global state. They make unit testing harder (you cannot inject a mock easily), they hide dependencies, and they cause subtle ordering bugs in multi-module systems.
Singleton: Three Approaches
JavaEnum singleton (recommended), initialization-on-demand holder, and double-checked locking.
Factory Method and Abstract Factory
The Factory Method pattern defines an interface for creating an object but lets subclasses decide which class to instantiate. The creator class calls a factory method instead of a constructor directly. Subclasses override the factory method to change the type being created. This is the pattern behind DocumentBuilderFactory.newDocumentBuilder() and countless other JDK and framework APIs.
Abstract Factory goes one level higher: it produces families of related objects. A UIFactory might produce a Button and a TextField that belong together. A DarkUIFactory produces dark-themed versions of both; a LightUIFactory produces light-themed versions. Switching the factory switches the entire family consistently without changing any consuming code.
Factory Method and Abstract Factory
JavaA notification factory that selects the right channel, and a UI component factory that swaps entire themes.
Builder and Prototype
Builder addresses the telescoping constructor problem. When a class has many optional configuration parameters, the alternatives, overloaded constructors or a constructor with ten parameters, are either insufficient or unreadable. Builder provides a fluent API where you set only the fields you need, and a final build() call produces an immutable object. Frameworks like Lombok, OkHttp, and Guava's immutable collections use Builder extensively.
Prototype creates new objects by copying an existing instance rather than constructing from scratch. This is useful when object creation is expensive (database reads, complex initializations) and you need multiple similar objects. Java supports this through the Cloneable interface and the clone() method, though in modern code a copy constructor or a factory method is almost always preferred because clone() has well-known design problems.
Builder and Prototype
JavaA fluent Builder for HTTP requests and a Prototype-based configuration template system.
Structural Patterns
Structural patterns deal with object composition. They describe ways to assemble objects and classes into larger structures while keeping those structures flexible and efficient.
Structural patterns at a glance
Each solves a different composition problem. Adapter and Facade are the most commonly used in everyday Java code.
- Adapter:Converts one interface to another that a client expects. Bridges incompatible interfaces without changing existing code. Java uses this between old and new APIs constantly.
- Decorator:Wraps an object to add behavior at runtime. Java I/O streams are the canonical example: BufferedReader wraps FileReader, which wraps FileInputStream.
- Facade:Provides a simplified interface to a complex subsystem. Reduces the number of objects a client must interact with.
- Proxy:Provides a surrogate for another object to control access. Used for lazy loading, caching, access control, and logging. Java's dynamic proxy and Spring AOP use this.
- Composite:Composes objects into tree structures. Lets clients treat individual objects and compositions uniformly. File systems and UI component trees are classic examples.
- Flyweight:Shares fine-grained objects to reduce memory usage. The JVM's string pool is a flyweight implementation.
Adapter, Decorator, and Facade
JavaAdapting a legacy API, dynamically adding logging and caching via decoration, and simplifying a subsystem with a facade.
Proxy and Composite
JavaA caching proxy for expensive operations, and a file system Composite where files and folders are treated uniformly.
Behavioral Patterns
Behavioral patterns deal with how objects communicate and cooperate. They distribute responsibility between objects in ways that make the system more flexible and easier to extend.
Observer: Event Notification
JavaA stock price feed that notifies multiple independent observers when the price changes, without knowing anything about them.
Strategy, Command, and Template Method
JavaSwappable sorting algorithms, undoable operations via Command, and a fixed algorithm skeleton via Template Method.
Chain of Responsibility and State
JavaA request approval chain where each handler decides to process or pass on the request, and a traffic light whose behavior changes by state.
Java-specific Patterns
Immutable Class, DAO, and Dependency Injection
An immutable class is one whose state cannot change after construction. Every field is private and final, no setters are provided, mutable fields (like lists or arrays) are defensively copied in the constructor and again in any getter. Immutable objects are inherently thread-safe, can be shared freely, and make excellent keys for maps. Java's String, Integer, and all Java 9 List.of() collections are immutable.
The DAO (Data Access Object) pattern separates persistence code from business logic. A DAO interface declares the operations (find, save, delete). The business layer calls the interface. A concrete DAO implements it for a specific storage mechanism: SQL, NoSQL, a file, or an in-memory store. Swapping the data source means writing a new DAO implementation without touching any business code. Testing business logic means injecting an in-memory DAO instead of a real database.
Dependency Injection means that a class receives its dependencies from outside rather than creating them itself. This is the opposite of calling new inside a class body. Constructor injection is the preferred form: dependencies are passed as constructor parameters, making them explicit, mandatory, and easily testable. DI frameworks like Spring automate the wiring, but the pattern works perfectly well without a framework for smaller codebases.
Immutable Class, DAO Pattern, and Dependency Injection
JavaA thread-safe immutable Money class, a pluggable user repository via DAO, and constructor-based dependency injection.
SOLID Principles
SOLID is an acronym for five design principles that together produce code that is easier to maintain, extend, and test. They are principles, not rules: they guide judgment rather than mandate a specific structure.
The five SOLID principles
Each principle addresses a different axis of design quality. Violating one usually makes code harder to change in a specific, predictable way.
- S: Single Responsibility Principle (SRP):A class should have one reason to change. A class that handles both business logic and database access will change when business rules change AND when the database schema changes. Split these concerns.
- O: Open/Closed Principle (OCP):Open for extension, closed for modification. Add behavior by adding new classes (new Strategy implementations, new Decorator wrappers), not by editing existing tested code.
- L: Liskov Substitution Principle (LSP):Objects of a subclass must be usable wherever objects of the superclass are expected, without breaking the program. A classic violation: making Square extend Rectangle but overriding setWidth to also set height, which breaks code that expects width and height to be independent.
- I: Interface Segregation Principle (ISP):Clients should not be forced to depend on methods they do not use. Prefer many small, focused interfaces over one large interface. A Printable and a Scannable are better than one MachineInterface if some machines only print.
- D: Dependency Inversion Principle (DIP):High-level modules should not depend on low-level modules. Both should depend on abstractions. Business logic should depend on a UserRepository interface, not on MySQLUserRepository directly. This is what makes DI work.
Anti-patterns to Avoid
Anti-patterns are recurring design mistakes that look like solutions but introduce problems more serious than the one they appear to solve. Recognizing them by name helps teams communicate clearly about code quality issues.
Common Java anti-patterns
These patterns appear frequently in codebases. Each name describes a real failure mode with a known solution.
- God Class:A single class that does too much and knows too much about the entire system. Every change risks breaking unrelated functionality. Fix: apply SRP and split into focused collaborating classes.
- Spaghetti Code:Tangled, deeply nested logic with no clear flow or structure. Often found in methods that exceed 100 lines. Fix: extract methods, apply design patterns, keep methods focused.
- Premature Optimization:Sacrificing readability and correctness for performance before profiling confirms there is a problem. Most code is not in a hot path. Fix: make it correct first, measure, then optimize where evidence shows it matters.
- Magic Numbers/Strings:Unexplained numeric or string literals scattered through code. 0.05 as a tax rate. "ROLE_ADMIN" as a security string. Fix: declare named constants that make intent explicit.
- Copy-Paste Programming:Duplicating code instead of extracting it. Every copy of a bug must be fixed separately. Fix: extract to a shared method, utility class, or abstract base.
- Inappropriate Intimacy:Two classes that depend too heavily on each other's internal details. Changes to one always require changes in the other. Fix: introduce a cleaner interface boundary and reduce bidirectional coupling.
- Anemic Domain Model:Domain objects (entities) contain no behavior, only fields and getters/setters. All logic lives in separate service classes. The result is procedural code in a class-based disguise. Fix: move behavior that belongs to an entity into the entity itself.
Quiz - Test Your Knowledge
Ten questions covering creational patterns (Singleton, Builder, Factory), structural patterns (Adapter, Decorator, Proxy, Composite), behavioral patterns (Observer, Strategy, Chain of Responsibility), Java-specific patterns (Immutable, DAO, DI), the SOLID principles, and common anti-patterns. Read each option carefully before selecting your answer.
Knowledge Check
1. What problem does the Singleton pattern solve, and what is the main risk of using it?
2. In the Builder pattern, what problem does it solve compared to using a constructor with many parameters?
3. What is the core purpose of the Observer pattern?
4. What distinguishes the Decorator pattern from inheritance?
5. What is the Single Responsibility Principle (SRP) from SOLID?
6. What does the Strategy pattern allow you to do?
7. In the DAO (Data Access Object) pattern, what is the primary benefit?
8. What anti-pattern does "God Class" describe?
9. What is the Open/Closed Principle?
10. What makes the Immutable Class pattern valuable in concurrent programming?