Design patterns provide a fundamental foundation to building maintainable and scalable software. Understanding how the patterns work, why they provide a benefit, and when to use them helps to ensure that software is built from reusable object-oriented components. In this Refcard, we will dive into the concepts that underpin design patterns, look at the 23 Gang of Four (GoF) patterns that brought about the proliferation of design patterns, and review some common patterns that have evolved since the GoF patterns were published.
Free PDF for Easy Reference Software Engineer, IBM Table of ContentsDesign patterns are solutions to common problems seen in software development. They are a foundational concept that both novice and advanced developers must continuously study and practice. In this Refcard, we will dive into the concepts that underpin design patterns and look at the 23 Gang of Four (GoF) patterns that brought about the proliferation of design patterns.
We will then look at some common patterns that have evolved over the years since the GoF patterns were published in 1994, as well as some resources that will help us understand the GoF patterns — and design patterns in general — more intuitively.
Before we dive into the GoF patterns, we must first understand the fundamental concepts of object-oriented (OO) design and look at two important considerations that provide a groundwork for all OO software patterns. Understanding the GoF design patterns requires a foundational understanding of OO design. Three of the fundamental concepts shared by all OO design patterns — and the GoF design patterns, in particular — are abstraction, interfaces, and polymorphism.
Abstraction is the removal of unnecessary details and the focus on pertinent details for a given context. For example, when we look at a car, we see doors, body panels, windows, wheels, and the like. If we focus on the wheels, we may go into a deeper level of abstraction and see rubber tires, metal rims, and air. Another level deeper, and we may focus on composites in the rubber compound or the nitrogen and oxygen particles bouncing within the tire that constitute tire pressure. The level of abstraction we select will depend on the particular problem and the problem's context.
Interfaces are the highest form of abstraction in OO design. Interfaces represent what an object can do — through methods — while hiding the details of how it does it. For example, the interface for a car, Car , may be limited to a start() and driveTo(Location) method, neither of which provide any details about how a particular car is started or can be driven to a particular location.
Implementations of this interface (called concrete classes) are then created, which provide these details, but clients interacting with the Car interface are unconcerned with these details. This encapsulates the details and allows clients to focus on the inputs and outputs of the methods rather than the particular details of how they are performed.
Polymorphism is the ability of an object to be treated as multiple types at the same time. There is static polymorphism, which uses method overloading, and dynamic polymorphism, which uses method overriding. Most design patterns focus on dynamic polymorphism, where a client calls the methods of an interface while unaware of the actual implementation being used. For example:
Vehicle vehicleA = new Sedan(); Vehicle vehicleB = new Truck(); vehicleA.drive(); vehicleB.drive();
In this case, when drive() is called on vehcileA , it will use the Sedan , and when drive() is called on vehicleB , it will use the Truck implementation.
In addition to OO design principles, there are two additional points we must consider before we can understand different design patterns: delegation and inheritance vs. aggregation.
The GoF design patterns are divided into three categories: creational, structural, and behavioral.
This type of pattern abstracts the instantiation process of an object.
The abstract factory pattern creates families or groups of objects without knowing their implementation.
Figure 1: Abstract factory pattern
Table 1
Example: An abstract factory can be used when interacting with file systems, where one family of objects pertains to Windows, while another family of objects pertains to Linux. When a client requests the objects used to interact with the file system on a Windows system, the Windows factory is provided; likewise, the Linux factory is provided when interacting with a Linux system.
The builder pattern abstracts the logic for creating an object into a set of steps.
Figure 2: Builder pattern
Table 2
Example: The Apache HttpClient library provides the URIBuilder class, which allows clients to instantiate a builder object, configure the URI components (e.g., protocol, host, port, and query parameters), and then generate a URI object using the build() method.
The factory method abstracts the creation logic for an object in the superclass, allowing subclasses to define the concrete object that will be created.
Figure 3: Factory method pattern
Table 3
Defines an algorithm in a superclass based on the creation of an object, which is represented by a factory method with an interface return value. Concrete classes then override the factory method and return a concrete object.
Delegating the creation logic to subclasses
Example: Factory methods can be used when creating task objects, where a Scheduler used to schedule and execute the logic of the task can be abstracted into a factory method. Subclasses can then be created, such as SingleThreadedTask , where the factory method returns a SingleThreadedScheduler that is used to execute the task logic.
The prototype pattern clones an existing object without requiring a client to know the concrete type.
Figure 4: Prototype pattern
Table 4
Creates a clone method in the interface for a class that returns an object of the same interface. Concrete classes then implement the interface and return a copy of the same object with the same state from the clone method.
Example: The Java Object class includes a clone() method, which allows a client to clone any object that implements the Cloneable interface. There is nuance to the implementation of the clone() method that deviates from a strict prototype pattern implementation, but conceptually, any class that implements Cloneable can be cloned.
The singleton pattern ensures that a class has only one object created, which is accessed through a single entry point.
Figure 5: Singleton pattern
Table 5
Example: The Spring Inversion of Control (IoC) container allows clients to create singleton beans. If a bean is declared as a singleton, then only one instance of the bean will be created per IoC container, and the same object will be used when auto-wiring the bean into dependent objects. Note that Spring does not implement this bean creation using a static method, but it conceptually enforces the singleton pattern.
Structural patterns compose classes and objects to form larger structures.
The adapter pattern allows an object with one interface to be used where another interface is expected.
Figure 6: Adapter pattern
Table 6
Example: The Java InputStreamReader adapter class contains constructors that accept an InputStream and implements the Reader interface. By creating an InputStreamReader using this constructor, an InputStream can be adapted to a Reader and used wherever a Reader is required. Likewise, an OutputStream can be adapted to a Writer using the OutputStreamWriter class.
The bridge pattern splits orthogonal concepts in a single class hierarchy into multiple hierarchies.
Figure 7: Bridge pattern
Table 7
Example: There may be multiple properties that define a vehicle, such as its type (sedan, truck, etc.), its color, and its trim (sport, premium, etc.). Creating a new class for each of these hierarchies, such as RedSportSedan , would introduce a combinatorial explosion as the number of possibilities for each hierarchy grows. Instead, we can create a Sedan implementation for a Vehicle interface that has two properties, Color and Trim , where Red implements the Color interface and Sport implements the Trim interface.
The composite pattern creates a hierarchy of nested objects, where the client does not know if it is performing an operation on a single leaf object or an object representing a collection of sub-objects.
Figure 8: Composite pattern
Table 8
Example: If we want to move files on a file system, we can create a File interface with a moveTo(Path) method, and have two concrete implementations: a composite called Directory and a leaf called TextFile . When a client calls the moveTo(Path) method on an arbitrary File object, a TextFile object will move itself, while a Directory object will move itself as well as all its children (either TextFile or other Directory objects).
The decorator pattern wraps an existing object to add additional responsibility.
Figure 9: Decorator pattern
Table 9
Example: In order to debug issues and track the operation of an application, it is important to log critical events, but logging statements can add clutter to a method definition. Instead of adding logging directly to the method, we can create a decorator that logs the start and completion of the method call, allowing us to supplement the logic contained in the wrapped object without changing the wrapped object itself.
The façade pattern defines a unified interface for a more complex set of sub-interfaces and classes.
Figure 10: Façade pattern
Table 10
Example: The Spring IoC container allows clients to programmatically create beans through the ApplicationContext class. While the Spring IoC container is a complex and intricate system, the ApplicationContext class provides a single entry point that allows clients to focus on bean creation and retrieval, rather than the details of the IoC container.
The flyweight pattern reuses shared components rather than instantiating new objects each time a component is needed.
Figure 11: Flyweight pattern
Table 11
Example: The boxed types in Java each contain a valueOf method that creates a boxed object from its primitive counterpart — for example, Integer.valueOf(1) . The Integer#valueOf(int) method will always return the same Integer object for values between -128 and 127 , while other values may also be cached (e.g., Integer.valueOf(1) == Integer.valueOf(1) will always be true , while Integer.valueOf(1000) == Integer.valueOf(1000) is not guaranteed to be true ). Note: the implementation used by Interger#valueOf(int) may not exactly match the flyweight pattern, but it conceptually implements the pattern.
The proxy pattern wraps an existing object in order to control access to the wrapped object.
Figure 12: Proxy pattern
Table 12
Example: A cache that reduces the number of calls made to a database. If we are trying to access books in a database, we can create a BookRepository interface with a findById(long) method, and we can create a MongoDbBookRepository to find Book objects in our MongoDB database. We can then create a CachedBookRepository that caches Book objects in memory and has a reference to the MongoDbBookRepository to find Book objects that are not already in the cache. When a client calls findById(long) , if the Book object is already in the cache, it is immediately returned without querying the MongoDB database, thus limiting the number of queries to the MongoDB instance.
This category assigns responsibility and defines communication among groups of objects.
The chain of responsibility pattern passes a request to a chain of handlers, where each handler is responsible for handling the request or passing it to the next handler.
Figure 13: Chain of responsibility pattern
Table 13
Example: Although the code is opaque to the application, the Java exception handling uses the chain of responsibility concept. When an exception is thrown, the Java Virtual Machine (JVM) looks to see if the current context on the stack can handle the exception. If it can, the exception handler — defined using a catch block — is called; otherwise, the stack is popped, and the next context is checked to see if it can handle the exception. This process continues until a suitable exception handler is found or the entire stack is popped.
The command pattern stores a request, along with all the information necessary to handle the request, in a single object that can be executed by any client or queued for execution at a later time.
Figure 14: Command pattern
Table 14
Example: Undo operations can be implemented through the command pattern. Each time an action is taken, a command object can be created — with the logic and parameters necessary to undo the action — and pushed onto a stack. If the user presses the undo button, the command objects can be popped from the stack and executed, thus undoing the previous action.
The interpreter creates an object-based representation of a grammar and interprets that representation.
Figure 15: Interpreter pattern
Table 15
Parsing well-defined expressions of a language
Example: Many times, interpreters are used for resolving trees of expressions — mathematical expressions, regular expressions, simple natural language expressions, Boolean expressions, and programming language expressions — into an interpreted result. These sets of expressions are represented as ASTs and then iteratively interpreted until a result is resolved.
The iterator pattern abstracts the traversal of collections without exposing the details of the collection.
Figure 16: Iterator pattern
Table 16
Example: The Java Collection interface includes an iterator() method that returns a concrete implementation of the Iterator interface. This method is specified by the Iterable interface that the Collection interface extends. In Java, any class that implements the Iterable interface can be used in a for-each loop (enhanced for loop), which is made possible by the iterator returned by each implementing class (see the Java Language Specification, JLS, 14.14.2).
The mediator pattern encapsulates the communication between numerous objects into a single object.
Figure 17: Mediator pattern
Table 17
Example: The mediator pattern can be used to create an event or message broker, where each component that sends and receives messages can register with the broker. And as messages are sent to the broker, the broker passes the message onto one or more receivers based on some attribute (e.g., the to field) of the message.
The memento pattern stores a snapshot of the state of an object that can be used to restore that state at a later time.
Figure 18: Memento pattern
Table 18
An originator creates a memento object that contains the state of the originator at the time the memento was instantiated. The memento is then passed to a caretaker object that stores the memento and can use it to restore the state of the originator at a later time.
Example: Similar to the command pattern, a memento can also be used to undo an operation. When an action is taken, a memento is stored in a caretaker and can be used later to undo the action. To undo the operation, a reference to both the originator and the memento is required. The command pattern can be used to create a command object with a reference to both, thus creating a single, self-contained object that can perform the undo operation.
The observer pattern allows clients to subscribe to events that are published when an observed object is changed.
Figure 19: Observer pattern
Table 19
Example: The Java Event Listener framework is an application of the observer pattern, where the listener takes the role of the subscriber, and the object generating the event assumes the role of the subject. Note that there are many publish-subscribe (pub/sub) frameworks — not just in Java, but all languages — but pub/sub is not synonymous with the observer pattern. In some cases, a message broker is used as a mediator to completely decouple the publisher from the subscriber.
The state pattern alters the behavior of an object when its internal state changes.
Figure 20: State pattern
Table 20
Example: The state pattern can be used to lock the internal state of an object without checking a conditional statement, such as isLocked() , before each method call. Instead, the object can delegate its methods to a state object. When the lock() method is called on the context object, it can replace the state object with one that does nothing when the methods are called. When unlock() is called, the state object can be replaced with the original state object that performs the desired logic.
The strategy pattern encapsulates a set of algorithms as objects with a common interface.
Figure 21: Strategy pattern
Table 21
Creates a context class, which has a reference to a strategy object that implements a strategy interface. The method in the context class varies its behavior based on the results of the algorithm method, allowing the behavior of the context class to vary based on the specific strategy object used.
Example: The Java Comparator interface represents a strategy interface, where a Comparator object can be passed to a method, such as Collections#sort(List, Comparator) , to compare one object to another. In the case of Collections#sort , the order that a List is sorted depends on the Comparator strategy that is passed to the method.
The template method pattern provides a structure for an algorithm, while allowing subclasses to define the specific implementation for each step of the algorithm.
Figure 22: Template method pattern
Table 22
Example: The Jakarta EE HttpServlet abstract class implements default behavior for the doGet , doPost , and other basic HTTP methods by returning an HTTP 405 Method Not Allowed response. Clients are expected to override at least one of these methods to change how the servlet handles a request.
The visitor pattern separates an algorithm from the objects on which the algorithm is applied.
Figure 23: Visitor pattern
Table 23
Example: The Java FileVisitor class is an interface that clients can implement to perform a desired action each time a file is visited. The FileVisitor class can be used in conjunction with the Files#walkFileTree to iterate through the files under a desired path and perform the action defined in a concrete FileVisitor object when each file is visited.
While the 23 GoF patterns still stand as the cornerstone of software development design patterns, many patterns have been formalized since 1994. This section describes a few of these patterns commonly seen in our age of web applications and microservices.
In many distributed and cloud systems, events and messages flow from one microservice to another without each microservice knowing who triggered the event or who is listening for its own events. This is possible because these services implement the pub/sub pattern, where publishers generate messages without knowing who is listening for them, and subscribers listen for messages without knowing who is generating them.
The pub/sub pattern is different from the observer pattern because, in many cases, the pub/sub pattern implementations use a message or event broker (e.g., RabbitMQ, Java Messaging Service [JMS]) to handle the forwarding of events. In this case, subscribers do not register directly with a publisher, but instead, both the publishers and subscribers register with a broker that acts like a mediator.
As data flows from a data source to a data sink, it is commonly filtered out. To filter out data, a chain of filters is created, where data that flows out of one filter flows into the next. The Jakarta EE Filter and FilterChain classes implement the filter pattern, where filters are applied to incoming servlet requests. Likewise, the Spring Security Filter Chain is an implementation of the filter pattern.
The model-view-controller (MVC) pattern designates a controller, which is responsible for managing a data model that is then displayed using a desired view. The model acts as a view-agnostic representation of the data being processed, and the controller is responsible for processing the data and designating the view or views that should be used to encode or display the data to the user. The MVC pattern is common among many web frameworks, including Spring Web MVC.
In many concurrent applications, threads are created and then released based on the workload being serviced at any given time. There is non-trivial overhead in creating new threads, which can cause a significant degradation in performance if workloads fluctuate. Instead of creating a new thread object each time a thread is needed, a pool can be created where released threads are returned back to the pool, and subsequent requests for threads can be serviced with threads already created in the pool. The Java ThreadPoolExecutor is an implementation of the thread pool pattern.
The reactor pattern defines a handler service that accepts multiple concurrent incoming requests and dispatches them to request handlers, which each handle a single request. This pattern is common for handling a large number of concurrent requests because the request handlers are usually small and agile enough to work in parallel to numerous service requests. The Spring Reactive framework is an implementation of the reactor pattern.
Design patterns are an essential part of software development, and knowledge of the most effective patterns is a prerequisite for becoming an experienced developer. The GoF patterns are the foundational patterns that have shaped the way software is written for many decades and are still relevant today. In the years since these patterns were enumerated, software engineering has grown and numerous patterns — such as the MVC and reactor patterns — have become ubiquitous.
Regardless of the software being developed, it is important to study a wide range of patterns and continuously practice them in order to gain the experience necessary to apply the right pattern to effectively solve the common problems seen in software development.
Understanding and applying design patterns requires diligent practice. The following resources contain excellent descriptions and examples of design patterns that can help even the most seasoned developer internalize design patterns and instill the experience necessary to apply patterns at the right time and place: