Design Patterns in Go: What They Actually Solve, and When to Reach for Them
Design Patterns in Go: What They Actually Solve, and When to Reach for Them
A practical tour of the most commonly used design patterns, with idiomatic Go examples — not just theory.
About the Author
Boovanesh Kumar Backend Developer (Golang) — Microservices | Full-Stack (Vue.js) | Cloud & DevOps (AWS, Docker, Kubernetes)
Backend Developer with hands-on experience building and optimizing Go-based microservices, distributed systems, and REST APIs, complemented by full-stack development in Vue.js and cloud/DevOps exposure across AWS, Docker, and Kubernetes. Skilled in performance tuning, scalability, load balancing, and event-driven architectures, with strong production debugging experience. Experienced in Agile Scrum, working in sprint-based development with story estimation, sprint planning, and cross-functional collaboration.
Why Design Patterns Still Matter in Go
Design patterns are standardized solutions to recurring software design problems — best practices distilled from decades of software development. Go is famously opinionated about simplicity, so it’s fair to ask: do design patterns even belong here?
They do — just not in the class-hierarchy-heavy way they show up in Java or C++. Go doesn’t have inheritance, and that’s exactly why patterns matter more, not less: without inheritance as a crutch, composition, interfaces, and small well-defined contracts are how Go achieves the same flexibility. Patterns like Worker Pool and Producer-Consumer map directly onto goroutines and channels, and classics like Strategy, Observer, and Decorator translate cleanly into Go’s interface-driven style.
A quick distinction worth keeping in mind: patterns are blueprints for organizing code — how components are arranged and how they communicate. Algorithms are recipes for processing data — the steps that produce a result. Patterns shape the “what” and “how” of your system’s structure; algorithms determine how well it performs within that structure. Good architecture makes good algorithms easier to write and maintain — they’re complementary, not competing, concerns.
The Three Families
- Creational patterns control how objects get created — Singleton, Factory Method, Abstract Factory, Builder, Prototype.
- Structural patterns control how objects are composed into larger structures — Adapter, Composite, Decorator, Facade, Flyweight, Proxy.
- Behavioral patterns control how objects communicate and share responsibility — Observer, Strategy, Command, State, Chain of Responsibility, and more.
Below are the ones that come up most often in real Go codebases, with idiomatic examples and a clear read on when each one actually earns its place.
Behavioral Patterns
1. Command Pattern
What it does: Encapsulates a request as an object, decoupling the thing that triggers an action from the thing that performs it.
Real-world analogy: A remote control — each button is a command tied to a device action, and the remote doesn’t need to know how the device actually works internally.
type Command interface {
Execute()
}type Light struct{ Name string }
func (l *Light) On() { fmt.Println(l.Name, "is ON") }
func (l *Light) Off() { fmt.Println(l.Name, "is OFF") }type TurnOnCommand struct{ light *Light }
func (c *TurnOnCommand) Execute() { c.light.On() }type RemoteControl struct{ command Command }
func (r *RemoteControl) Press() { r.command.Execute() }func main() {
light := &Light{Name: "Living Room"}
remote := &RemoteControl{command: &TurnOnCommand{light: light}}
remote.Press() // Living Room is ON
}
When to prefer it: You need to queue, log, or undo/redo operations, or you want to decouple who requests an action from who performs it — for example, a job-processing system where requests get queued and executed later.
2. Observer Pattern
What it does: Defines a one-to-many relationship — when a subject’s state changes, every registered observer is notified automatically.
Real-world analogy: A news subscription service — subscribers get notified of a new story without knowing or caring about each other.
type Observer interface {
Update(price float64)
}type Subject struct {
observers []Observer
price float64
}func (s *Subject) Register(o Observer) {
s.observers = append(s.observers, o)
}func (s *Subject) SetPrice(price float64) {
s.price = price
for _, o := range s.observers {
o.Update(price)
}
}type PriceDisplay struct{ Name string }
func (p *PriceDisplay) Update(price float64) {
fmt.Printf("%s sees new price: %.2f\n", p.Name, price)
}
When to prefer it: Multiple parts of your system need to react to a state change without being tightly wired to the thing that changed — event-driven UIs, pub/sub-style internal notifications, cache invalidation triggers.
3. Strategy Pattern
What it does: Encapsulates a family of interchangeable algorithms behind a common interface, so the behavior can be selected — or swapped — at runtime.
Real-world analogy: A navigation app offering walking, driving, or cycling routes — same destination, different strategy.
type PaymentStrategy interface {
Pay(amount float64) string
}type CreditCard struct{}
func (c CreditCard) Pay(amount float64) string {
return fmt.Sprintf("Paid %.2f via Credit Card", amount)
}type UPI struct{}
func (u UPI) Pay(amount float64) string {
return fmt.Sprintf("Paid %.2f via UPI", amount)
}type Checkout struct{ strategy PaymentStrategy }
func (c *Checkout) SetStrategy(s PaymentStrategy) { c.strategy = s }
func (c *Checkout) Process(amount float64) string { return c.strategy.Pay(amount) }
When to prefer it: You have several ways to do the same job (payment methods, sorting approaches, pricing rules) and want to choose between them without a wall of if/else or switch statements scattered through your business logic.
Creational Patterns
4. Singleton Pattern
What it does: Ensures a class has exactly one instance, with a single global access point.
Real-world analogy: A logging system — every part of the app should write to the same logger, not spin up competing instances.
type Logger struct{}var (
instance *Logger
once sync.Once
)func GetLogger() *Logger {
once.Do(func() {
instance = &Logger{}
})
return instance
}
sync.Once is the idiomatic Go way to do this safely under concurrent access — no manual locking, no double-checked locking bugs.
Get Boovaneshbk’s stories in your inbox
Join Medium for free to get updates from this writer.
When to prefer it: A single shared resource — config manager, logger, connection pool — genuinely needs to be one instance across the app. Worth a note of caution: overusing Singleton introduces global state, which can make testing and reasoning about your code harder. Reach for it deliberately, not by default.
5. Factory Method Pattern
What it does: Defines an interface for creating an object, but lets the specific implementation decide which concrete type gets created.
Real-world analogy: A vehicle manufacturer where CarFactory and TruckFactory each know how to build their own product, behind a shared factory interface.
type Vehicle interface {
Build() string
}type Car struct{}
func (c Car) Build() string { return "Building a car" }type Truck struct{}
func (t Truck) Build() string { return "Building a truck" }func GetVehicleFactory(kind string) Vehicle {
switch kind {
case "car":
return Car{}
case "truck":
return Truck{}
default:
return nil
}
}
When to prefer it: The exact type of object needed isn’t known until runtime, or you want calling code to depend only on an interface, not a concrete struct.
6. Abstract Factory Pattern
What it does: Provides an interface for creating families of related objects without specifying their concrete types.
Real-world analogy: A furniture store where choosing a style (modern, classic) determines a whole matching set — chairs, tables — produced together.
type Button interface{ Render() string }
type Checkbox interface{ Render() string }type DarkButton struct{}
func (d DarkButton) Render() string { return "Dark button" }type DarkCheckbox struct{}
func (d DarkCheckbox) Render() string { return "Dark checkbox" }type UIFactory interface {
CreateButton() Button
CreateCheckbox() Checkbox
}type DarkThemeFactory struct{}
func (f DarkThemeFactory) CreateButton() Button { return DarkButton{} }
func (f DarkThemeFactory) CreateCheckbox() Checkbox { return DarkCheckbox{} }
When to prefer it: Your system needs to support multiple consistent “families” of related components (themes, platform-specific implementations) and switching between families shouldn’t require touching client code.
7. Builder Pattern
What it does: Separates the construction of a complex object from its final representation, so the same construction process can produce different results step-by-step.
Real-world analogy: Building a meal at a fast-food counter — main, side, drink — assembled step-by-step with optional add-ons.
type Burger struct {
Size string
Cheese bool
Bacon bool
}type BurgerBuilder struct{ burger Burger }func NewBurgerBuilder(size string) *BurgerBuilder {
return &BurgerBuilder{burger: Burger{Size: size}}
}
func (b *BurgerBuilder) AddCheese() *BurgerBuilder { b.burger.Cheese = true; return b }
func (b *BurgerBuilder) AddBacon() *BurgerBuilder { b.burger.Bacon = true; return b }
func (b *BurgerBuilder) Build() Burger { return b.burger }// usage:
// burger := NewBurgerBuilder("Large").AddCheese().AddBacon().Build()
When to prefer it: An object has many optional fields or a construction process with several meaningful steps — this keeps constructors from turning into an unreadable pile of parameters.
Structural Patterns
8. Adapter Pattern
What it does: Lets two incompatible interfaces work together by wrapping one so it looks like the other.
Real-world analogy: A plug adapter that lets a device with one plug shape work with a socket of a different shape.
type OldPrinter struct{}
func (p OldPrinter) PrintOld(text string) string { return "[OLD] " + text }type ModernPrinter interface {
Print(text string) string
}type PrinterAdapter struct{ old OldPrinter }
func (a PrinterAdapter) Print(text string) string {
return a.old.PrintOld(text)
}
When to prefer it: You need to integrate an existing type (a legacy component, a third-party library) into code that expects a different interface, without modifying the original source.
9. Facade Pattern
What it does: Provides a simplified, high-level interface over a more complex subsystem.
Real-world analogy: A single home-theater remote standing in for the TV, sound system, and DVD player individually.
type CPU struct{}
func (c CPU) Start() { fmt.Println("CPU starting") }type Memory struct{}
func (m Memory) Load() { fmt.Println("Memory loading") }type ComputerFacade struct {
cpu CPU
mem Memory
}func (c ComputerFacade) Start() {
c.cpu.Start()
c.mem.Load()
fmt.Println("Computer ready")
}// usage: ComputerFacade{}.Start() — caller never touches CPU or Memory directly
When to prefer it: A subsystem has grown complex enough that most callers only need a handful of common operations — give them a clean entry point instead of exposing every internal piece.
10. Decorator Pattern
What it does: Adds new behavior to an individual object dynamically, without altering its structure or creating a new subclass for every combination.
Real-world analogy: Customizing a coffee order with milk, sugar, or whipped cream — no need for a distinct class per combination.
type Coffee interface {
Cost() float64
}type BasicCoffee struct{}
func (c BasicCoffee) Cost() float64 { return 2.0 }type MilkDecorator struct{ Coffee Coffee }
func (m MilkDecorator) Cost() float64 { return m.Coffee.Cost() + 0.5 }type SugarDecorator struct{ Coffee Coffee }
func (s SugarDecorator) Cost() float64 { return s.Coffee.Cost() + 0.25 }// usage:
// order := SugarDecorator{Coffee: MilkDecorator{Coffee: BasicCoffee{}}}
// order.Cost() → 2.75
When to prefer it: You need to add optional, combinable behavior to objects at runtime, and subclassing for every combination would explode into an unmanageable number of types. This favors composition over inheritance — which fits Go naturally, since Go doesn’t have inheritance to reach for in the first place.
Closing Thought
None of these patterns are meant to be applied by default — the goal isn’t to spot a place a pattern could fit and use it there. It’s recognizing the specific problem a pattern solves (decoupling a sender from a receiver, managing object families, adding optional behavior) and reaching for the matching tool only when that exact problem shows up. Used well, they make a Go codebase easier to extend, easier to test, and easier for the next person to reason about — used reflexively, they add indirection nobody asked for.
Code and structure for these examples are collected here: github.com/boovaneshkumar005/Design-pattern. For anyone going deeper on the broader catalog beyond Go specifically, refactoring.guru/design-patterns is a solid reference. Feel free to connect on LinkedIn or check out more of my work on GitHub.








