Skip to content
Design Patterns

Design patterns: the developer toolbox 🛠️

Ah, design patterns. Those tiny jewels of software wisdom that make us look smarter than we really are. But what are they, exactly? And why should you care? Imagine building a house without a plan. Sure, you might pull it off, but you will probably end up with a leaking roof and a door that refuses to close. Design patterns are the blueprints for your code: proven solutions to common problems. Not rigid recipes, but adaptable structures.

This guide focuses on code-level design patterns. If you are looking for the larger system view with distributed systems, CQRS, microservices, and their usual drama, that is a different layer of design entirely.


A bit of history 📜

The concept of design patterns became mainstream with Design Patterns: Elements of Reusable Object-Oriented Software, written by the famous Gang of Four: Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides. That book gave software teams a shared vocabulary to describe recurring solutions to recurring problems.

Each pattern identifies a generalized problem and associates it with a reusable solution. The point is not worshipping the pattern. The point is recognizing when a known structure helps you stop reinventing the same wheel with a slightly different wobble.


Why use design patterns? 🤔

  1. Reusability: why reinvent the wheel when a tested solution already exists?
  2. Maintainability: clearer, better organized code causes fewer headaches for you and whoever inherits the project.
  3. Communication: saying “we can use Strategy here” is faster than explaining the full idea from scratch.
  4. Proven solutions: design patterns come from years of real-world practice, not from a particularly inspired whiteboard session.

The three main categories 🧭

The Gang of Four grouped design patterns into three macro categories. The lists below are not exhaustive; they cover the most representative ones.

1. Creational patterns 🏗️

These patterns focus on object creation, isolating construction logic from the rest of the code.

  • Singleton: ensures that a type has a single shared instance.
  • Factory Method: defines an interface for creating objects while leaving the concrete decision elsewhere.
  • Abstract Factory: creates families of related objects without exposing their concrete classes.
  • Builder: assembles complex objects step by step.
  • Prototype: creates new objects by cloning an existing instance.

A note on Singleton in Go

In Go you rarely implement a classical Singleton because there are no classes in the OO sense. Most of the time package-level initialization or sync.Once gives you the same result in a simpler and safer way:

1
2
3
4
5
6
7
8
9
var once sync.Once
var instance *Repo

func GetRepo() *Repo {
  once.Do(func() {
    instance = &Repo{/* ... */}
  })
  return instance
}

Factory Method example in Go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package main

import (
  "errors"
  "fmt"
)

type Product interface { Use() string }

type ConcreteProductA struct{}
func (ConcreteProductA) Use() string { return "Using Product A" }

type ConcreteProductB struct{}
func (ConcreteProductB) Use() string { return "Using Product B" }

func FactoryMethod(productType string) (Product, error) {
  switch productType {
  case "A":
    return ConcreteProductA{}, nil
  case "B":
    return ConcreteProductB{}, nil
  default:
    return nil, errors.New("unknown product type: " + productType)
  }
}

func main() {
  product, err := FactoryMethod("A")
  if err != nil {
    panic(err)
  }
  fmt.Println(product.Use())
}

2. Structural patterns 🧱

These patterns deal with composition, making systems more flexible and easier to evolve.

  • Adapter: lets two incompatible interfaces work together.
  • Bridge: separates an abstraction from its implementation so both can evolve independently.
  • Composite: treats individual and grouped objects uniformly in tree structures.
  • Decorator: adds behavior dynamically to an object.
  • Facade: provides a simplified interface to a more complex subsystem.
  • Flyweight: reduces memory usage by sharing common state.
  • Proxy: controls access to another object through a surrogate.

Decorator example in Go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
package main

import "fmt"

type Coffee interface {
  Cost() int
  Description() string
}

type SimpleCoffee struct{}

func (c SimpleCoffee) Cost() int {
  return 5
}

func (c SimpleCoffee) Description() string {
  return "Simple Coffee"
}

type MilkDecorator struct {
  coffee Coffee
}

func (m MilkDecorator) Cost() int {
  return m.coffee.Cost() + 2
}

func (m MilkDecorator) Description() string {
  return m.coffee.Description() + ", Milk"
}

type SugarDecorator struct {
  coffee Coffee
}

func (s SugarDecorator) Cost() int {
  return s.coffee.Cost() + 1
}

func (s SugarDecorator) Description() string {
  return s.coffee.Description() + ", Sugar"
}

func main() {
  coffee := SimpleCoffee{}
  fmt.Println(coffee.Description(), "Cost:", coffee.Cost())

  coffeeWithMilk := MilkDecorator{coffee: coffee}
  fmt.Println(coffeeWithMilk.Description(), "Cost:", coffeeWithMilk.Cost())

  coffeeWithMilkAndSugar := SugarDecorator{coffee: coffeeWithMilk}
  fmt.Println(coffeeWithMilkAndSugar.Description(), "Cost:", coffeeWithMilkAndSugar.Cost())
}

3. Behavioral patterns 🎭

These patterns focus on interaction, responsibilities, and how objects collaborate.

  • Observer: automatically notifies dependents when state changes.
  • Strategy: swaps algorithms dynamically.
  • Command: wraps a request as an object.
  • State: changes behavior based on internal state.
  • Template Method: defines the shell of an algorithm and delegates some steps.
  • Visitor: separates an algorithm from the data structure it operates on.
  • Chain of Responsibility: passes a request along a chain until someone handles it.
  • Mediator: centralizes communication and reduces mesh-like dependencies.
  • Memento: externalizes and restores state without breaking encapsulation.
  • Iterator: gives you a uniform way to traverse a collection.
  • Interpreter: defines a grammar and an interpreter for simple languages.

Strategy example in Go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package main

import "fmt"

type Strategy interface { Execute(a, b int) int }

type Add struct{}
func (Add) Execute(a, b int) int { return a + b }

type Multiply struct{}
func (Multiply) Execute(a, b int) int { return a * b }

type Calculator struct { strategy Strategy }

func (c *Calculator) SetStrategy(s Strategy) { c.strategy = s }
func (c *Calculator) Compute(a, b int) int { return c.strategy.Execute(a, b) }

func main() {
  calc := &Calculator{}
  calc.SetStrategy(Add{})
  fmt.Println("Add:", calc.Compute(3, 4))

  calc.SetStrategy(Multiply{})
  fmt.Println("Multiply:", calc.Compute(3, 4))
}

The pattern becomes interesting when the Context can really switch strategy at runtime: pricing policies, serializers, compression algorithms, ranking logic, and so on.


Useful resources 📚


When not to use them 🚧

  • Over-engineering: if a 20-line function is enough, do not introduce 5 interfaces just to feel sophisticated.
  • Too early: applying a pattern before the recurring problem actually exists usually makes the design stiffer.
  • Too much abstraction: too many layers make debugging feel like archaeology.
  • Mechanical copying: understand the why before the how.

Golden rule: start simple, then extract the pattern when the recurring problem becomes visible. Pattern literacy is useful. Pattern cosplay is less so.

Last updated on