What are software design patterns?
Software design patterns are general, reusable solutions to commonly occurring problems in software development.
Design patterns are not libraries or frameworks that you can plug in and use right away. Rather, they are established suggested ways of thinking to use when you're faced with a problem that many developers have solved before.
In essence, design patterns help avoid reinventing the wheel. Along with anti-patterns and architectural patterns, they form an established vocabulary that software engineers often use to discuss common problems and solutions.
It's important to have an understanding of various patterns and know where to use them. However, it's equally important to avoid implementing them all over the place, which may reduce readability and maintainability without providing a noticeable benefit.

Where do design patterns come from?
The idea of design patterns -- best practices to solve similar problems in different contexts -- was originally developed by architects. Adopted and reimagined in software engineering in the late 1980s, design patterns gained popularity when they were collected and codified in Design Patterns: Elements of Reusable Object-Oriented Software, a 1994 book by Gamma, Helm, Johnson and Vlissides. The four authors have since been commonly referred to as the Gang Of Four (GoF).
Design patterns have gained significant traction in Java and C# communities, but they can be used in all object oriented languages.
How many patterns are there?
The original Gang of Four book included 23 patterns divided into 3 categories:
- Creational patterns used to create objects without instantiating them directly. This category includes Abstract Factory, Builder, Factory Method, Prototype, and Singleton.
- Structural patterns that define ways of class and object composition. Adapter, Bridge, Composite, Decorator, Facade, Flyweight, and Proxy are all structural patterns.
- Behavioral patterns that suggest various ways for objects to communicate and/or evolve. Behavioral patterns include Chain of Responsibility, Command, Interpreter, Iterator, Mediator, Memento, Observer, State, Strategy, Template Method, and Visitor.
This is the original list, but it's not set in stone. If you look around, you will find variations where some patterns are omitted and others are introduced.
Selected design patterns
Some software engineers argue that the most common design pattern is The Big Ball of Mud. However, we'll look at a selection of patterns that are arguably more useful. We'll use C# as the language of choice for sample code.
Singleton
One of the most used design patterns, Singleton makes sure that only one instance of a particular class is created, and provides a global point of access to that instance.
Singleton is most useful when you need a single object to coordinate other objects.
A singleton is usually implemented as a class that contains:
- a private constructor (so that only members of the same class can access it),
- a private static member,
- a static public method that returns a reference to the private static member.
Here's a class that represents the president as a singleton. Note the private static property holding the instance, the private constructor that can only be called from inside the class, and the GetInstance()
method that creates a single instance representing the president if it's null, or returns the previously created instance if it's not null:
1class President:
2
3 instance = None
4
5 def __init__(self, firstName, lastName, party):
6 self.FirstName = firstName
7 self.LastName = lastName
8 self.Party = party
9
10 @staticmethod
11 def GetInstance():
12 if President.instance == None:
13 President.instance = President("Donald", "Trump", "Republican")
14 return President.instance
15
16 @property
17 def LastName(self):
18 return self.LastName
19
20 @property
21 def FirstName(self):
22 return self.FirstName
23
24 @property
25 def Party(self):
26 return self.Party
27
28 def __str__(self):
29 return self.FirstName + " " + self.LastName + ", " + self.Party
At call site, we'd call the GetInstance()
method to get the one and only instance. Note how trying to use the constructor directly doesn't compile because the constructor is private.
1# In Python
2constructed = President("Bernie", "Sanders", "Democratic") # Won't work, constructor is private
3one = President.get_instance()
4two = President.get_instance()
5
6print(one)
7print(two)
8print(one is two) # Will return True as both variables point to the same instance
There are multiple legitimate scenarios for using singletons, including caching, logging, and hardware interface access.
Factory
The Factory pattern helps create an instance of an object without exposing the instantiation logic to the code that needs the object. It's best used with objects that are not trivial to create.
Normally, when you create an object, you just call a class constructor directly:
1# In Python
2my_object = MyObject()
Most of the time, there's nothing wrong with creating objects via constructors. However, sometimes you may want to make the parts of your code that need objects independent from the implementation of these objects. That's when Factory can help.
When using Factory, you don't call a constructor. Instead, you call a method in a static factory class and provide information about the type of object that you need. The factory returns an instance of the object you're looking for.
Here's a sample where you have a class representing a tennis ball and a static class for a factory that delivers a tennis ball:
1class ITennisBall:
2 def get_diameter(self):
3 pass
4 def get_make(self):
5 pass
6
7class GrassCourtTennisBall(ITennisBall):
8 def __init__(self):
9 self.color = "Optic yellow"
10 self.diameter = 67
11 self.bounce = 145
12 self.make = "Slazenger Wimbledon"
13
14 def get_diameter(self):
15 return self.diameter
16
17 def get_make(self):
18 return self.make
19
20class TennisBallFactory:
21 @staticmethod
22 def deliver_ball():
23 return GrassCourtTennisBall()
At call site, instead of calling a constructor for TennisBall
, you use TennisBallFactory
to deliver the ball:
1ball = TennisBallFactory.deliver_ball()
2print(f'This is a {ball.get_make()} ball with a diameter of {ball.get_diameter()} mm')
This is a simplified version of the Factory Method pattern. Another related pattern is Abstract Factory.
Builder
The Builder pattern helps... well, build objects! It can be useful in two situations:
- You have an object that could go in a variety of flavors.
- An object uses a non-trivial, multi-step creation process.
When an object could go in a variety of flavors, a common solution is to use a constructor with many parameters that define whether each of these flavors should be used:
1# In Python
2def __init__(self, size, salami, mozzarella, ruccola):
3 pass
An alternative solution is to use the Builder pattern where a separate builder class manages adding flavors to an object and provides methods that make adding flavors explicit.
In the code below, we have two classes: Pizza
represents a pizza but isn't intended to be used to construct pizzas, and PizzaBuilder
is there for the sole purpose of constructing a custom pizza with a set of possible flavors:
1class Pizza:
2 def __init__(self, builder):
3 self.size = builder.size
4 self.salami = builder.salami
5 self.mozzarella = builder.mozzarella
6 self.ruccola = builder.ruccola
7
8 def get_description(self):
9 return f"This is a {self.size} cm pizza."
10
11class PizzaBuilder:
12 def __init__(self, size):
13 self.size = size
14 self.salami = False
15 self.mozzarella = False
16 self.ruccola = False
17
18 def add_salami(self):
19 self.salami = True
20 return self
21
22 def add_mozzarella(self):
23 self.mozzarella = True
24 return self
25
26 def add_ruccola(self):
27 self.ruccola = True
28 return self
29
30 def build(self):
31 return Pizza(self)
At call site, we create a pizza via PizzaBuilder
that makes the customization process very explicit:
1# In Python
2pizza = PizzaBuilder(28)\
3 .add_mozzarella()\
4 .add_salami()\
5 .add_ruccola()\
6 .build()
7
8print(pizza.get_description())
Note that many programming languages allow using named parameters on call sites, which can be a valid alternative to using the Builder pattern in some cases:
1pizza = Pizza(28, True, False, True)
However, if a builder goes beyond adding parameters to an object and applies custom logic on each step of object construction, then named parameters aren't a viable replacement.
Adapter
If two classes cannot work together because their interfaces are different, applying the Adapter pattern helps establish compatibility between them.
For example, if one class has a function that returns XML and another class has a function that takes JSON as input, and you want to take advantage of functionality in both of these classes, you may want to implement the Adapter pattern to enable interaction between them.
It's very similar to how adapters work with consumer electronics:
- To read a memory card from your laptop, you use an adapter.
- To plug your European laptop into a socket in the US, you use an adapter.
Speaking of US sockets, here's an example where we use the USSocketAdapter
class to make the USSocket
class compatible with the ISocket
interface:
1class ISocket:
2 def Charge(self):
3 pass
4
5class GermanSocket(ISocket):
6 def Charge(self):
7 pass
8
9class BritishSocket(ISocket):
10 def Charge(self):
11 pass
12
13class Laptop:
14 def Charge(self, socket):
15 pass
16
17class USSocket:
18 def Magic(self):
19 pass
20
21class USSocketAdapter(ISocket):
22 def __init__(self, socket):
23 self.AmericanSocket = socket
24
25 def Charge(self):
26 self.AmericanSocket.Magic()
At call site, we can now wrap a US socket into the socket adapter and charge our laptop:
1# In Python
2american_socket = USSocket()
3american_socket_adapter = USSocketAdapter(american_socket)
4
5laptop = Laptop()
6laptop.charge(american_socket_adapter)
A related pattern is Facade that provides a simplified interface to a complex subsystem. However, while Facade creates a new, simpler interface, Adapter serves to design to an existing interface.
Strategy
The Strategy pattern helps define a family of related algorithms, and make decisions to use one or another based on user input when the application runs.
One benefit of using Strategy is that it helps avoid complex chains of conditions for switching between algorithms, replacing them with delegation to different Strategy objects.
For example, in an order processing system, prices will probably be calculated in different ways depending on customer type, purchase quantity, and other factors. In this situation, using Strategy can help select the right pricing algorithm at runtime.
Another example would be a tennis game where defence and offence are two distinct strategies:
1from abc import ABC, abstractmethod
2
3class IMatchStrategy(ABC):
4
5 @abstractmethod
6 def hit_the_ball(self):
7 pass
8
9class DefensiveMatchStrategy(IMatchStrategy):
10
11 def hit_the_ball(self):
12 return {
13 "Type": "Slice",
14 "Power": 0.7
15 }
16
17class AggressiveMatchStrategy(IMatchStrategy):
18
19 def hit_the_ball(self):
20 return {
21 "Type": "Flat",
22 "Power": 1.2
23 }
24}
At call site, we can start a match aggressively, but then switch to defense depending on external factors:
1match = Match(AggressiveMatchStrategy())
2
3shot1 = match.hit_the_ball()
4
5print(match.match_strategy)
6print(shot1.__str__())
7
8if random.randint(1, 10) > 5:
9 match.match_strategy = DefensiveMatchStrategy()
10
11shot2 = match.hit_the_ball()
12
13print(match.match_strategy)
14print(shot2.__str__())
A popular technique related to the Strategy pattern is Dependency Injection.
Observer
The Observer pattern helps objects learn about changes to the state of other objects. It does so by establishing a one-to-many dependency between objects. It's used extensively for event management.
You use the pattern when you have a critical object ("the subject"), a group of objects that depend on it ("observers"), and you need to make sure that observers are notified of any changes to the subject -- typically by calling one of their methods. Following this pattern helps circulate updates between objects without unnecessarily coupling them, and makes adding new observers easy.
Have you ever visited a website of a product that was yet to launch? Have you subscribed to receive an e-mail as soon as the product becomes publicly available? If you have, you acted as an observer, and the product acted as the subject.
Here's an example where a carsharing pool is the subject and implements the IObservable
interface from C#'s Base Class Library, and a driver who uses the carsharing service is an observer and implements the IObserver
interface:
1class SharedCar:
2 def __init__(self, model):
3 self._model = model
4 def get_model(self):
5 return self._model
6
7class CarsharingCustomer:
8 def __init__(self, name):
9 self._name = name
10 def update(self, shared_car):
11 print(f"Hey {self._name}, a new car is available: {shared_car.get_model()}")
12
13class CarsharingPool:
14 def __init__(self):
15 self._available_cars = []
16 self._observers = []
17 def add_observer(self, observer):
18 self._observers.append(observer)
19 def add_car(self, shared_car):
20 self._available_cars.append(shared_car)
21 for observer in self._observers:
22 observer.update(shared_car)
(The implementation above also uses a class called Unsubscriber
that is omitted for brevity.)
At call site, the driver is subscribed to updates from the carsharing service, and gets a notification whenever a new shared car becomes available:
1driver = CarsharingCustomer("Louise Sawyer")
2
3available_cars = CarsharingPool()
4available_cars.subscribe(driver)
5available_cars.add_car(SharedCar("Ford Thunderbird"))
Visitor
The Visitor pattern lets you add operations to objects without having to modify these objects -- in other words, it's a way of separating an algorithm from an object structure on which it operates. This pattern is used when:
- you have a complex structure that contains objects of different types,
- similar operations need to be performed on all these objects.
Visitor defines behavior in abstract classes or interfaces. This helps avoid the use of conditional blocks and type checks, which tend to become clunky and hard to maintain.
Below is a code sample where a sports fan (visitor) wants to visit the Olympic Games to watch competitions in various sports (visitees). All visitees have the Accept
method that takes a parameter representing a visitor:
1from abc import ABC, abstractmethod
2
3class SummerOlympicSport(ABC):
4 @abstractmethod
5 def accept(self, visitor): pass
6
7class DivingCompetition(SummerOlympicSport):
8 def jump(self): pass
9 def accept(self, visitor): visitor.watchDiving(self)
10
11class CyclingTrackRace(SummerOlympicSport):
12 def ride(self): pass
13 def accept(self, visitor): visitor.watchCycling(self)
14
15class BadmintonMatch(SummerOlympicSport):
16 def hit(self): pass
17 def accept(self, visitor): visitor.watchBadminton(self)
Here's how the visitor is implemented:
1# In Python
2from abc import ABC, abstractmethod
3
4class ICompetitionVisitor(ABC):
5 @abstractmethod
6 def watch_diving(self, diving_competition): pass
7
8 @abstractmethod
9 def watch_cycling(self, cycling_track_race): pass
10
11 @abstractmethod
12 def watch_badminton(self, badminton_match): pass
13
14class CompetitionVisitor(ICompetitionVisitor):
15 def watch_badminton(self, badminton_match):
16 badminton_match.hit()
17
18 def watch_cycling(self, cycling_track_race):
19 cycling_track_race.ride()
20
21 def watch_diving(self, diving_competition):
22 diving_competition.jump()
At call site, we can now initialize a visitor (competitionVisitor
), and walk them through the list of visitees (competitionsToVisit
) in a loop where we call the Accept()
method that hides dissimilarity of the visitees:
1competitions_to_visit = [
2 DivingCompetition(),
3 BadmintonMatch(),
4 CyclingTrackRace()
5]
6
7competition_visitor = CompetitionVisitor()
8
9for competition in competitions_to_visit:
10 competition.accept(competition_visitor)
There's a related pattern called Iterator. However, Iterator is used on collections that contain objects of the same type. Visitor has wider applicability and can be used on hierarchical and/or composite data structures.
Related concepts
Where there are patterns, there should be anti-patterns. Indeed, a mere year after the original Gang of Four book, another influential book was released, AntiPatterns: Refactoring Software, Architectures, and Projects in Crisis. The book covered bad practices in software development, software architecture, and project management. Some of the most well-known anti-patterns covered in the book include Spaghetti Code, Cut-and-Paste Programming, Vendor Lock-In, Architecture by Implication, Design by Committee, Reinvent the Wheel, Analysis Paralysis, and Death by Planning.
Interestingly, when design patterns get overused, many in the development community start to treat them as anti-patterns. The Singleton pattern is the primary example of such evolution.
In addition to design patterns, there are architectural patterns that are similar in spirit but have a broader scope and apply to the design of an application as a whole. Some of the most popular architectural patterns include Model-View-Controller (MVC), Model-View-ViewModel (MVVM), Enterprise Service Bus (ESB), and Peer-to-Peer (P2P).
Where to learn more?
One Pager Cheat Sheet
- Design Patterns are general and reusable solutions that help developers avoid
reinventing the wheel
, and should beused judiciously
to provide the most benefit. Design patterns were originally developed by architects, and gained popularity when codified by the "Gang of Four" in 1994 as elements of reusable object-oriented software.
- There are 23 patterns from the Gang of Four book, divided into 3 categories (Creational, Structural and Behavioral) with The Big Ball of Mud being one of the most commonly used design patterns.
- The Singleton design pattern ensures that only one instance of a class is created, and provides a global point of access to that instance.
- The Factory Method pattern makes it easy to create instances of objects without exposing the instantiation logic, which is especially useful for non-trivial objects.
- The Builder pattern is a useful solution for when an object needs to be customized through a non-trivial, multi-step process and as an alternative to numerous parameters in the constructor.
- The Adapter pattern helps to bridge the gap between incompatible objects by transforming one object's interface into an interface expected by the other object, similar to physical adapters used in hardware.
- The Strategy pattern allows for a family of related algorithms to be defined, and for a decision of which one to use at runtime to be made based on user input, thereby avoiding
complex chains of conditions
, and making it possible to switch strategies at call site in order to adapt to different situations, while related techniques such asDependency Injection
offer added benefits. - The Observer pattern allows an object (the subject) to let others (the observers) know of any changes to its state by establishing a one-to-many dependency between them.
- The Visitor pattern lets you add operations to objects without modifying them, separating an algorithm from the
object structure
on which it operates, and avoidingconditional blocks
andtype checks
. - The concept of anti-patterns, which can evolve from overused design patterns, as well as architectural patterns such as
MVC
,MVVM
,ESB
andP2P
, are related to patterns in software development. - With resources such as
Awesome Software and Architectural Design Patterns
,Design Patterns for Humans
andLearning JavaScript Design Patterns
by Addy Osmani, as well asOODesign.com
and theGoF Design Patterns Reference
, anyone can deepen their understanding of Design Patterns.