Sitemap
9 min readJul 26, 2026

--

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.

Press enter or click to view image in full size
Press enter or click to view image in full size
Press enter or click to view image in full size

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.

📧 boovaneshbk123@gmail.com · 🔗 LinkedIn · 💻 GitHub

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.

--

--

Boovaneshbk
Boovaneshbk

Written by Boovaneshbk

Backend Developer with hands-on experience building and optimizing Go-based microservices, distributed systems, and REST APIs