Design patterns are reusable solutions to common problems in software design.
They provide a shared language for developers and encourage best practices in system architecture.
In this article, we’ll explore some of the most widely used design patterns in Go, grouped into three categories: creational, structural, and behavioral.

๐ง Creational Patterns
1. ๐ Singleton
Ensures a class has only one instance and provides a global point of access to it.
โWhen discussing which pattern to drop, we found that we still love them all. (Not really โ I’m in favor of dropping Singleton. Its use is almost always a design smell.)โ
โ Erich Gamma, Design Patterns: Elements of Reusable Object-Oriented Software
While Singleton often gets a bad reputation, there are still valid use cases in Go:
- โ You only want one component in the system (e.g., database repository, object factory)
- โณ The object is expensive to construct, so you instantiate it only once
- ๐ซ You want to prevent the creation of additional instances
- ๐ค You want lazy instantiation (e.g. load config or connect to DB only when needed)
Go makes this easy and thread-safe with sync.Once
. To stay testable and modular, follow the Dependency Inversion Principle (DIP) โ depend on interfaces, not concrete types.
Hint:
Singleton quite often breaks the Dependency Inversion Principle!
๐งโ๐ป Example:
|
|
๐งช Usage:
|
|
2. ๐ญ Factory
Creates objects without specifying the exact class.
A **factory** helps simplify object creation when:
- ๐ Object creation logic becomes **too convoluted**
- ๐งฑ A struct has **too many fields** that need to be correctly initialized
- ๐ก You want to **delegate creation logic** away from the calling code
There are two flavors of factories in Go:
- ๐ง **Factory function** (also called a `constructor`): a helper function to initialize struct instances
- ๐๏ธ **Factory struct**: a dedicated struct responsible for managing object creation
Unlike the Builder pattern, which is *piecewise*, the Factory creates the object **wholesale** โ usually in one go.
๐งโ๐ป Example:
|
|
๐งช Usage
|
|
3. ๐งฑ Builder
Separates the construction of a complex object from its representation.
Not all objects are created equal:
- โ
Some are simple and can be created with a single constructor call
- โ ๏ธ Others require **a lot of ceremony** to set up
- ๐งฉ Factory functions with **10+ parameters** become hard to use and maintain
When you want more flexibility and readability, use the **Builder pattern**.
- ๐ ๏ธ A **Builder** is a separate component used to construct an object step-by-step
- ๐ It exposes a **fluent API** โ each method returns the receiver (`*Builder`) to enable chaining
- ๐ง In advanced designs, **different builders** can operate on **different facets** of the same object
๐งโ๐ป Example:
|
|
๐งช Usage
|
|
๐งฉ Structural Patterns
1.๐ Adapter
Allows incompatible interfaces to work together.
An Adapter is a design construct that adapts an existing interface SpecificRequest to conform to the required interface Request. It acts as a translator or bridge between two systems that otherwise couldnโt work together.
๐งญ To implement an adapter in Go:
- ๐ Determine the **API you have** (e.g. `Adaptee`)
- ๐ฏ Define the **API you need** (e.g. `Target`)
- ๐งฉ Create an adapter struct that **aggregates** the adaptee (usually via a pointer)
- โก Optimize when needed โ adapters may introduce intermediate representations, so use **caching** or other performance strategies as required
This is especially useful when integrating legacy code or 3rd-party libraries into a new system with different interfaces.
๐งโ๐ป Example:
|
|
๐งช Usage
|
|
2. ๐ Decorator
Adds behavior to objects dynamically by **embedding** and extending existing functionality.
The **Decorator pattern** is used when you want to:
- โ **Augment** an object with additional behavior
- ๐ซ Avoid modifying existing code (โ
Open/Closed Principle โ OCP)
- ๐งผ Keep new functionality **separate** and modular (โ
Single Responsibility Principle โ SRP)
- ๐ Retain the ability to **interact with existing interfaces**
The solution is to **embed** the decorated object and override or extend its behavior. This lets you build **stackable, reusable enhancements** without altering the base struct.
๐งโ๐ป Example: wrapping a basic Coffee
with a MilkDecorator
:
|
|
3. ๐ก Proxy (aka Virtual Proxy)
Provides a surrogate or placeholder shows the โvirtual proxyโ pattern (lazy-loading the real object only when needed).
How it works?
- Image โ the interface clients depend on (Display()).
- RealImage โ the heavy or expensive object to create.
- ProxyImage โ wraps RealImage and delays its creation until the first Display() call.
This is a proxy because clients donโt know if theyโre talking to RealImage or a ProxyImage.
๐งโ๐ป Example:
|
|
๐งช Usage
|
|
4. ๐ณ Composite
Composes objects into tree structures.
Composes objects into tree structures and lets you treat individual and composite objects uniformly.
The Composite pattern is ideal when some components are single objects (like files), and others are containers of other components (like folders). Both should support a common interface so clients donโt need to differentiate between them.
๐งญ To implement a composite in Go:
- ๐งฑ Define a common interface that all components implement.
- ๐ฟ Implement Leaf objects (e.g. File, Button, TextField).
- ๐งบ Implement Composite objects (e.g. Folder, Panel) that aggregate children and delegate behavior to them.
- ๐ Add iteration if you need to traverse or walk the tree (e.g. using the Iterator pattern).
This pattern shines when building hierarchical or nested structures such as UI components, file systems, or organization charts.
๐งโ๐ป Example:
|
|
๐ฆ Example usage:
|
|
โ
When to use Composite:
- You want to treat individual and group objects the same way
- You have recursive or nested structures
- You want to delegate behavior to child components
๐ Bonus: Pair with the Iterator pattern to walk tree structures cleanly without exposing their internal representation.
๐ง Behavioral Patterns
1. ๐งฎ Strategy
Defines a family of algorithms.
Encapsulates a family of algorithms and allows them to be selected and swapped at runtime.
The Strategy pattern is used when you want to:
- ๐ง Separate an algorithm into its skeleton and implementation steps
- ๐งฉ Decompose behavior into high-level workflows and low-level operations
- ๐ Swap logic dynamically without changing the calling code
- โ Adhere to the Open/Closed Principle (OCP) โ new strategies without changing the high-level logic
The solution is to define a high-level algorithm that delegates part of its logic to an injected strategy. This strategy follows a shared interface, so any implementation can be plugged in without breaking the algorithm. ๐ต Analogy: making a hot beverage
Many real-world algorithms follow this structure. Take making tea as an example:
Skeleton algorithm:
Boil water โ Pour into cup โ Add ingredient
Concrete implementation:
Add tea bag, coffee grounds, or cocoa powder
The high-level process is reusable, and the final step is delegated to a drink-specific strategy. This is exactly how Strategy works.
๐งโ๐ป Example: Choosing an operation strategy
|
|
๐งช Usage
|
|
By:
- Defining a common interface (Strategy)
- Creating multiple concrete strategies (Add, Multiply)
- Supporting runtime injection into a reusable context (Context)
You separate the structure of the algorithm from its implementation. Just like boiling water and pouring it into a cup โ what happens next depends on the drink you’re making.
This makes your code modular, extensible, and easy to adapt to new behaviors without touching your existing flow.
2. ๐ Observer
Wants to listen to events and be notified when something happens.
The Observer pattern is used when you want to:
- ๐ฃ Be informed when a particular object changes state, does something, or reacts to an external event
- ๐ Let other objects (observers) subscribe to and react to those changes
- ๐ Decouple the source of truth from those reacting to it
- โ Support dynamic subscription and unsubscription
The solution is to have two participants:
- ๐ข Observable: emits events and holds a list of observers
- ๐ก Observer: subscribes and reacts to events
When the observable changes, it notifies all observers โ sending event data (commonly as interface{} in Go) to each subscriber. This is an intrusive approach since the observable must provide explicit subscription management.
๐งโ๐ป Example:
|
|
๐งช Usage
|
|
With Observer, you give objects the ability to react automatically to changes elsewhere, without tightly coupling them together. This pattern is especially helpful for:
- UIs reacting to data changes
- Logging and monitoring
- Event-based systems
Hint:
This approach is intrusive โ the observable must explicitly support subscriptions and notify logic.
3. ๐ State
Allows an object to alter its behavior when its internal state changes โ effectively changing its class at runtime.
The State pattern is used when you want to:
- ๐ Let an object change behavior dynamically based on its current state
- ๐ฒ Model real-world systems where actions depend on state
- ๐ง Manage complex state logic in a modular, maintainable way
The solution is to encapsulate each state in its own type and let the context object delegate behavior to the current state. When the state changes, so does the object’s behavior โ without conditionals scattered throughout the code.
These transitions are triggered by events (e.g. dialing, picking up, hanging up), and actions vary depending on the state. This is a perfect fit for a state machine โ a formal model that defines:
- ๐ฅ Entry/exit actions for each state
- ๐ Transitions between states, often triggered by events
- โ Guards that control whether a transition is allowed
- โ๏ธ A default behavior if no transition is found
When systems grow in complexity, it pays to define states and transitions explicitly to keep logic clean and modular.
๐งโ๐ป Example:
|
|
๐งช Usage
|
|
With the State pattern:
- You encapsulate each state and its logic in a separate type
- The object transitions explicitly in response to triggers
- Behavior is cleanly modular, without long chains of if or switch
Whether you’re modeling a telephone, a TCP connection, or a video player, state machines help you handle transitions with clarity, flexibility, and control.
โ Conclusion
Design patterns are powerful tools in every Go developerโs toolkit. While Go encourages simplicity, these patterns still applyโespecially in large-scale systems or when writing reusable libraries. Using patterns like Singleton, Adapter, and Strategy can lead to cleaner, more testable, and maintainable code.
Happy Go coding! ๐น
๐ Follow me on norbix.dev for more insights on Go, Python, AI, system design, and engineering wisdom.