AlgoMaster Logo

15 Must-Know Design Patterns

High Priority56 min readUpdated August 25, 2026
Listen to this chapter
Unlock Audio

In this chapter, I'll break down 15 of the most important design patterns I've come across in interviews and throughout my 9+ years as a Software Engineer.

If you're new to design patterns, they are proven and reusable approaches to common software design problems. Design patterns were popularized by the classic Gang of Four book, which introduced 23 patterns divided into three main categories: Creational, Structural, and Behavioral.

But you don't need to learn all 23. In practice, around 15 of these patterns come up far more often.

Creational Patterns

Let's start with Creational Patterns. These patterns focus on how objects are created. In this chapter, we'll cover three important creational patterns: Singleton, Builder, and Factory Method.

Let's begin with the Singleton Pattern.

1. Singleton

Sometimes, an application needs exactly one shared instance of a class. Common examples include a configuration manager, logger, cache manager, or thread pool. Creating multiple instances of these objects can waste resources or lead to inconsistent behavior.

The Singleton Pattern solves this by ensuring that only one instance of a class exists, and providing a consistent way to access it.

Code

We can now access the same instance from anywhere in the application:

However, Singleton should be used carefully. Because it is globally accessible, it can hide dependencies and make testing harder.

In many cases, creating the object once and passing it explicitly to the classes that need it is cleaner:

Now the dependency is visible in the constructor, and a test can pass a different AppConfig.

Singleton ensures that only one object of a class is ever created. But what if creating a single object is itself complicated to construct? That brings us to the Builder Pattern.

2. Builder

Consider a UserProfile class. The name and email are required, while fields like age, location, bio, and notification preferences are optional:

For creating a new user profile object, one simple approach is to pass everything through the constructor:

This works, but it is difficult to read. Without checking the constructor definition, it is not obvious what each value represents, and the problem gets worse as more optional fields are added.

You could create multiple overloaded constructors, but that often leads to another problem called the telescoping constructor, where you end up maintaining many constructor variations with different combinations of parameters.

The Builder Pattern solves this by letting us construct the object step by step using clearly named methods:

Now the required fields are provided first, and optional fields are added only when needed.

A simplified implementation looks like this:

Internally, each builder method updates one property and returns the same builder, which allows us to chain multiple calls together. Finally, the build method creates the complete UserProfile object.

Builder helps us construct one complex object. But what if we need to create objects from multiple related classes without exposing their exact creation logic to the client? That brings us to the next pattern: Factory Method.

3. Factory Method

Consider a notification system that supports email, SMS, and push notifications. Each notification follows the same interface:

Each notification channel class provides its own implementation, along with its own setup:

Creating these objects directly would tightly couple the client to a specific notification class:

If the type of notification changes, the client must also change its object creation logic. And if the same creation logic appears in several places, it can quickly become duplicated across the application.

The Factory Method Pattern moves that object creation behind a dedicated method.

We start by creating an abstract notification factory. The sendNotification method defines the overall workflow for creating and sending a notification, but it does not know which concrete notification object will be created. That decision is delegated to the createNotification factory method:

Subclasses extend from this abstract class and decide how and which notification object to return:

The client can now work with the base factory class:

The client uses the common workflow without knowing the internal details of how the notification object is created.

To switch from email to SMS, we can simply provide a different factory:

The rest of the client code remains unchanged.

Structural Patterns

So far, we have focused on how objects are created. But once those objects exist, we also need effective ways to connect, organize, and combine them. That brings us to the next category: Structural Design Patterns.

In this section, we'll cover five important structural patterns: Adapter, Facade, Proxy, Decorator, and Composite.

Let's begin with the Adapter Pattern.

4. Adapter

The Adapter Pattern helps two components work together when their interfaces do not match.

Think of a travel adapter. If your charger and the wall socket use different plugs, the adapter can translate between them. The same idea applies in software.

Consider a checkout system for an e-commerce application. It expects to implement this interface for every payment provider:

But an external third-party gateway that we need to support exposes a different method name, and expects the amount in dollars:

The interfaces do not match. This is where the Adapter Pattern helps.

Instead of changing the gateway or rewriting our checkout system, we can create an adapter that implements the interface our application expects:

The adapter receives the request in the format our application understands. It then converts the amount from cents to dollars and calls the method provided by the legacy gateway.

Now the checkout system can use the external payment provider through the standard PaymentProcessor interface:

It does not need to know that the adapter is translating method names, data formats, or units internally.

The Adapter Pattern helps incompatible components work together. But what if the system works correctly and is simply too complicated to use? That brings us to the Facade pattern.

5. Facade

The Facade Pattern provides a simple interface to a complex subsystem.

Consider building a client for a video publishing platform like YouTube. Publishing a video may involve several steps. Without a facade, the client must coordinate every service itself:

This forces the client to understand the complete workflow, including which services to call and in what order. If this workflow is needed in multiple places, the same coordination logic may also get duplicated across the application.

A facade hides those details behind one simple method provided by the video publishing platform:

Now the client only needs to make one simple call:

The client no longer needs to know how compression, storage, metadata, or notifications work internally.

Facade gives us a simpler way to interact with a complex system. But what if we need to control access to an object instead? That brings us to the Proxy pattern.

6. Proxy

The Proxy Pattern places a substitute object in front of a real object to control access to it. Both the real object and the proxy expose the same interface, so the client can use the proxy just like the original object.

Consider an image gallery application where the high-resolution image class loads the image in the constructor when its object is created:

Creating every image immediately would waste memory and processing time, especially if the client never opens most of them by calling the display method:

If you are not allowed to modify the high-resolution image class, a proxy can delay loading until the image is actually needed by the client.

We create a proxy class called ImageProxy. It wraps the HighResolutionImage class and starts as a lightweight object. It creates and loads the real image only when the display method is called:

The client now uses the proxy class instead of the original high-resolution image class:

Most of the client code stays the same, since both the proxy and the original object implement the same interface.

Proxy controls access, while the next pattern, Decorator, adds new behavior to an object.

7. Decorator

The Decorator Pattern lets us add new behavior to an object dynamically without modifying its original class.

Consider a rich-text editor that starts with a basic component that renders plain text:

We can use it like this:

This works perfectly for simple text without any formatting.

But now we also need to support bold, italic, underline, and different combinations of these styles. Creating a subclass for every combination would quickly lead to a subclass explosion.

Instead, we create small decorators that wrap another TextView and add one style. We first create a base decorator called TextDecorator. It implements the same TextView interface as PlainTextView and wraps another TextView object:

We can then create separate decorators for bold, italic, and underlined text:

Now we can combine these decorators however we need. The client applies multiple formatting styles by wrapping them around one another:

Each decorator adds one responsibility and then delegates the remaining work to the object it wraps. And because every decorator implements the same TextView interface, the client interacts with the text in exactly the same way, regardless of how many decorators have been applied.

A Decorator wraps one object and adds behavior around it. But what if an object needs to contain other objects? That brings us to the next pattern: Composite.

8. Composite

The Composite Pattern lets us treat individual objects and groups of objects through the same interface.

A file system is a perfect example. A file is a single object, while a folder can contain files and other folders. But we still want to perform operations like calculating size or printing details on both.

Without the Composite Pattern, the client may need separate logic for files and folders:

The client needs to check which type of object it received and handle each one differently. This becomes even more complicated because folders can contain other folders, which may contain even more files and folders:

This is where the Composite Pattern helps. We start with a common interface called FileSystemItem that represents anything in the file system:

Now both individual files and folders can implement the same interface. A file is the simplest type of object and simply returns its own size and prints its own name:

A folder contains other file-system items and delegates the operation to them:

When we ask a folder for its size, it asks every child for its size and adds the results together. This recursive structure is what makes the pattern so powerful.

Now we can build an entire directory tree:

The client can now treat the entire folder tree exactly like a single file-system item:

Those last two lines would work identically if item were a single file.

Behavioral Patterns

So far, we have looked at how objects are created and how they are connected. But software is not just about structure. Objects also need to communicate, respond to events, switch behavior, and coordinate workflows. That brings us to the final category: Behavioral Design Patterns.

In this section, we'll cover seven important behavioral patterns: Strategy, Observer, State, Command, Template Method, Iterator, and Chain of Responsibility.

Let's begin with one of the most commonly used patterns: the Strategy Pattern.

9. Strategy

The Strategy Pattern lets us define multiple ways to perform the same task and switch between them when needed.

Consider a navigation app that calculates routes for driving, walking, cycling, or public transport. One way to implement this is to have a RoutePlanner class with a large if-else block that selects the routing logic based on the travel mode:

But this approach does not scale well. As new travel modes are added, the conditional logic keeps growing, making the class harder to understand, test, and maintain:

It also means modifying the RoutePlanner every time we introduce a new routing algorithm, which tightly couples the class to all possible routing strategies.

This is where the Strategy Pattern helps. We start by defining a common interface for building routes:

Each routing algorithm becomes a separate strategy class implementing the interface:

Now the route planner no longer needs to know how each algorithm works. It simply delegates the task to the selected strategy:

The client can choose the appropriate strategy when creating the route planner:

And if the user switches from driving to walking, we can change the strategy at runtime:

The RoutePlanner remains unchanged. Only the selected algorithm changes.

Strategy allows one object to choose between different behaviors. But sometimes we need several objects to react when something happens. That brings us to the Observer pattern.

10. Observer

The Observer Pattern is useful when multiple objects need to react to the same event.

Consider an e-commerce system. When an order is shipped, we may need to send an email, update the inventory, and reward the customer. Calling each service directly would tightly couple the order service to all of these actions:

The order service is now responsible not only for shipping the order, but also for knowing everything that should happen afterward.

This is where the Observer Pattern helps. Instead of calling every component directly, the order service simply announces that an event has occurred. Any interested component can subscribe and react to that event.

We begin with a common observer interface:

Each observer handles one specific reaction:

The order service maintains a list of subscribers and notifies them when the event occurs:

The observers are wired up once, and shipOrder no longer names any of them:

Observer allows several objects to react when something happens. But what if one object changes its own behavior based on its current condition? That brings us to the State pattern.

11. State

The State Pattern allows an object to change its behavior when its internal state changes.

Consider an e-commerce system. An order can move through several states: New, Paid, Shipped, Delivered, or Cancelled. The operations allowed on the order depend on its current state. A new order can be paid. A paid order can be shipped. A shipped order can be delivered. But a delivered order cannot be shipped again.

A simple approach is to store the current status and use conditionals throughout the Order class:

This may look manageable with only a few states. But as the order lifecycle grows, every method starts accumulating more if-else or switch statements. Soon, the Order class becomes responsible for the behavior of every possible state and every valid transition between them.

This is where the State Pattern helps. Instead of representing the state using a string or enum alone, we represent each state as a separate object.

We begin with a common interface for order state:

Each concrete state defines which operations are valid and what should happen next:

The Order class now delegates its behavior to the current state object:

The client can use the order without writing any state-specific conditions:

Internally, the same method call behaves differently depending on the current state object. For example, calling ship on a new order produces an error. Calling ship after payment moves the order to the shipped state. And calling it after delivery tells us that the order has already been shipped.

Each state owns its own behavior and controls the valid transitions from that state. This makes it easier to add a new state, such as CancelledOrderState or ReturnedOrderState, without filling the main Order class with even more conditions.

State changes how an object behaves over time. But sometimes, instead of performing an action immediately, we want to represent that action as an object so it can be queued, logged, retried, or undone. That brings us to the next pattern: Command.

12. Command

The Command Pattern turns a request or action into a separate object.

Why is that useful? Because once an action becomes an object, we can store it, queue it, retry it, keep a history, or undo it later.

Consider a text editor with multiple options in the toolbar, where users can add or delete text, copy and paste, and undo the most recent operation. A simple approach is to put all of this logic directly inside the toolbar:

This works for executing actions, but undo becomes difficult. The toolbar knows that a button was clicked, but it does not have enough information to reverse the action. As more actions are added, the toolbar also becomes tightly coupled to every operation supported by the editor.

This is where the Command Pattern helps. Instead of executing actions directly, we represent each action as a command object.

Here is the TextEditor class that performs the actual work of adding and deleting text:

To turn these operations into commands, we first define a common Command interface:

We then create concrete command classes for each type of action. Each command stores the information needed to execute and reverse the operation, and wraps the TextEditor object to perform the actual work:

We also need a way to keep track of executed commands so they can be undone in the correct order. That is the responsibility of the CommandManager class. It maintains the command history, delegates execution to the appropriate command, and handles undo operations:

The client can now use it like this:

Now suppose the user presses Undo. This reverses the most recent command:

Command lets us package an individual action as an object. But sometimes, we have an entire process made up of several steps, where the overall workflow stays the same but some steps need to vary. That brings us to the next pattern: Template Method.

13. Template Method

The Template Method Pattern is useful when several processes follow the same overall workflow, but a few steps vary.

Consider a data import system that needs to support different file formats, such as CSV and JSON. All formats follow the same sequence: read the file, parse the content, validate the records, save them, and generate a report.

If each importer implements the full workflow separately, most of the code gets duplicated:

The only major difference is how the content is parsed.

This is where the Template Method Pattern helps. We move the common workflow into a base class:

The importData method is the template method. It defines the overall algorithm and fixes the order of the steps, while subclasses customize only what differs.

Now every importer follows the same workflow while providing its own parsing logic:

The base class controls the structure of the algorithm, while subclasses customize specific steps. The client can now use either importer through the same process:

Template Method defines the structure of an algorithm while letting us customize specific steps. But what if we need to traverse a collection without caring how its data is stored? That brings us to the Iterator pattern.

14. Iterator

The Iterator Pattern lets us move through a collection without exposing how its elements are stored.

Consider a music application with a playlist. Internally, the playlist might store songs in an array, a linked list, a database, or even fetch them from an external service.

Initially, the playlist class might expose its internal data structure directly to the client:

The client then traverses it by index:

This works, but it creates a few problems. The client now knows that songs are stored in a List and accessed using an index. If we later update the Playlist and replace the data structure with a linked list, a tree, or a remote data source, the traversal logic may also need to change on the client side.

This is where the Iterator Pattern helps. Instead of exposing the internal collection, the playlist provides an iterator.

An iterator usually supports two basic operations: hasNext, which tells us whether another element is available, and next, which returns that element and moves the iterator forward:

The iterator keeps track of the current position internally:

The playlist provides an iterator without exposing its internal list:

Here is what it looks like in code. The client can now traverse the playlist by repeatedly calling hasNext and next until every song has been visited:

The client no longer needs to manage indexes or know how the playlist stores its songs. And because Playlist implements Iterable, it also works with the enhanced for loop, which is Java's own use of this pattern:

15. Chain of Responsibility

The final pattern in the chain is the Chain of Responsibility Pattern. This pattern lets you pass a request through a sequence of independent handlers. Each handler performs one part of the processing and either stops the request or forwards it to the next handler.

Consider an API request that must pass authentication, authorization, rate limiting, and validation. Putting every check inside one method creates a long conditional that is difficult to reuse or rearrange:

This is where the Chain of Responsibility Pattern helps. We begin with a base handler:

Each handler implements the process method and handles one responsibility. If the handler returns true, the request continues to the next handler. If it returns false, the chain stops immediately:

We can then build the pipeline dynamically by connecting these handlers into a chain:

To process a request, the sender only needs to know where the chain begins:

Another advantage is that the chain can be configured differently for different situations. For example, a public endpoint may only require rate limiting and validation:

An admin endpoint may require the full chain:

We can also change the order of the handlers without modifying their internal logic.

Choosing the Right Pattern

You do not need to remember every class or implementation you saw in this chapter. Instead, focus on the problem each pattern is designed to solve. When you recognize the problem, the right pattern becomes much easier to identify.

PatternUse it when
SingletonThe application genuinely requires one shared instance of a class
BuilderAn object has many optional fields and its constructor is hard to read
Factory MethodSubclasses should decide which concrete object gets created
AdapterTwo components must work together but their interfaces do not match
FacadeA working subsystem is too complicated for clients to use directly
ProxyAccess to an object needs to be controlled, delayed, or checked
DecoratorBehavior must be added to an object without modifying its class
CompositeIndividual objects and groups of them should be treated the same way
StrategyOne task has multiple interchangeable algorithms
ObserverSeveral objects need to react to the same event
StateAn object's behavior depends on the state it is currently in
CommandAn action must be stored, queued, logged, retried, or undone
Template MethodSeveral processes share a workflow but differ in a few steps
IteratorA collection must be traversed without exposing how it stores data
Chain of ResponsibilityA request should pass through a configurable series of handlers

Several of these patterns look similar in a diagram and differ in intent. Adapter and Decorator both wrap an object, but an adapter changes the interface while a decorator keeps it and adds behavior. Proxy also keeps the interface, and controls access rather than extending it. Strategy and State both delegate to an interchangeable object, but a strategy is chosen by the client while a state is chosen by the object itself as part of its own lifecycle.