Go Design Patterns — Complete Deep Guide

12 Essential Go Design
Patterns Mastered

Creational · Structural · Behavioral · Concurrency · Architecture — Every pattern explained with real-world Go code, diagrams, use cases, and production context.

① Singleton
② Builder
③ Factory
④ Adapter
⑤ Observer
⑥ Strategy
⑦ Decorator
⑧ Worker Pool
⑨ Pipeline
⑩ Fan-Out/In
⑪ Repository
⑫ Middleware
Pattern Categories & Overview
Creational
Object creation & instantiation
Singleton Builder Factory
Structural
Object composition & interfaces
Adapter Decorator Repository
Behavioral
Algorithms, communication & responsibilities
Observer Strategy Middleware
Concurrency Patterns — Go-Specific 🐹
Goroutines, channels & concurrent coordination
Worker Pool Pipeline Fan-Out / Fan-In
🔒
Singleton
One instance, global access, thread-safe
01
🏗️
Builder
Step-by-step complex object construction
02
🏭
Factory
Object creation without specifying type
03
🔌
Adapter
Interface compatibility between incompatible types
04
👁️
Observer
Event notification — publish & subscribe
05
♟️
Strategy
Swap algorithms at runtime
06
🎁
Decorator
Add behavior without modifying struct
07
⚙️
Worker Pool
Bounded goroutine concurrency
08
🔗
Pipeline
Channel-chained processing stages
09
📡
Fan-Out/In
Parallel work distribution & result aggregation
10
🗄️
Repository
Data access abstraction layer
11
🔧
Middleware
Chain of request/response handlers
12
Singleton Pattern
🔒
Singleton
Creational Pattern · GoF Classic
Ensures a type has only one instance throughout the entire application lifetime, providing a global access point — implemented in Go using sync.Once for goroutine safety.
Category: Creational Thread-safe: sync.Once Go idiom: ✓✓ Industry use: Very High

What is Singleton?

The Singleton pattern restricts instantiation of a type to a single object. Every part of your application accesses the same shared instance. In Go, the idiomatic implementation uses sync.Once which guarantees the initializer runs exactly once even under concurrent goroutine access — unlike many other languages that rely on double-checked locking.

How It Works

  • sync.Once: A zero-value struct with a Do(f func()) method
  • First call to Do() executes the function; subsequent calls are no-ops
  • Internally uses atomic operations + mutex — goroutine-safe by design
  • The instance is stored in a package-level variable
  • Exported accessor function returns the singleton

Rules & Requirements

  • Never export the struct directly — force access through the function
  • Always use sync.Once, not init() or var instance = &T{} alone
  • The constructor function must be idempotent
  • Avoid storing mutable state without proper internal synchronization
  • Consider if you truly need a singleton — DI is often better

Design Approach

  • Unexported struct type + exported interface
  • Package-level var once sync.Once
  • Package-level var instance *Type
  • Exported GetInstance() calls once.Do()
  • Lazy initialization — created on first access, not at startup

Step-by-Step Pattern

01
Define unexported struct

Create the type that should have only one instance. Keep it unexported (type dbPool struct) so external packages can't instantiate it directly.

02
Declare package-level variables

Declare var once sync.Once and var instance *dbPool at package level — both zero values, no allocation yet.

03
Write GetInstance() accessor

Exported function calls once.Do(func(){ instance = newDBPool() }). The Do call is goroutine-safe — no mutex needed in your code.

04
Expose behavior via methods

Add methods to the struct. Callers use GetInstance().Query() — they get the singleton without knowing it's one.

05
Return interface, not pointer

Return an interface from GetInstance() so callers can mock it in tests — critical for testability.

Goroutine A Goroutine B sync.Once .Do() — runs once creates *instance single shared object returns interface Goroutine-safe by design One allocation ever
singleton/db_pool.go
package db import "sync" // Interface — keeps it testable / mockable type Pool interface { Query(sql string) ([]map[string]any, error) Close() } // Unexported — prevents external instantiation type dbPool struct { connStr string maxConn int } var ( instance *dbPool once sync.Once // zero value is ready to use ) // GetInstance — goroutine-safe, lazy initialization func GetInstance() Pool { once.Do(func() { instance = &dbPool{ connStr: "postgres://...", maxConn: 20, } }) return instance } func (p *dbPool) Query(sql string) ([]map[string]any, error) { // All goroutines share this single connection pool return nil, nil } // Usage across the entire app: // db.GetInstance().Query("SELECT * FROM orders")

✓ ADVANTAGES

  • Guarantees single instance — saves memory (DB pools, config)
  • sync.Once is zero-overhead after first call
  • Lazy init — no resource waste if never accessed
  • Global access without passing dependencies everywhere
  • Thread-safe without any extra locking in caller code

✗ DISADVANTAGES

  • Tight coupling — hard to swap implementations
  • Makes unit testing harder (global state)
  • Violates Single Responsibility Principle
  • Hidden dependency — caller doesn't declare it needs this
  • Shared mutable state = concurrency bugs if methods not safe
🍽️
Restaurant POS System — Database Connection Pool
A restaurant Point-of-Sale system has dozens of terminals (goroutines) simultaneously placing orders, updating inventory, and printing receipts. All of them use db.GetInstance() to access the same PostgreSQL connection pool — never creating 50 separate pools. The singleton manages pool sizing, keeps connections warm, and handles reconnection — transparently to every terminal handler.
DB Connection Pool Config Manager Logger Instance Cache Client Redis Client
Builder Pattern
🏗️
Builder
Creational Pattern · GoF Classic
Separates the construction of a complex object from its representation. Allows building objects step-by-step using method chaining, making construction readable and safe — especially when objects have many optional fields.
Category: Creational Method chaining: ✓ Replaces: Long constructors Industry use: Very High

What is Builder?

When an object needs many configuration options — some required, some optional — a single constructor becomes unwieldy. Builder solves this by using a separate Builder struct with setter methods that each return the builder itself, enabling fluent method chaining: NewQuery().Table("orders").Where("status=?","pending").Limit(10).Build().

How It Works

  • Create a Builder struct mirroring the target struct's fields
  • Each setter method modifies the builder and returns *Builder
  • Terminal Build() method validates and constructs the final object
  • Validation errors bubble up only at Build() — not during chaining
  • Final object is often immutable (value, not pointer)

Rules & Conditions

  • Required fields validated in Build() — never silently ignored
  • Setter methods must return *Builder for chaining
  • Builder should be a separate type, not the target itself
  • The target object should have no exported setters (immutability)
  • Consider functional options pattern as a Go-idiomatic alternative

Functional Options Variant

Go's idiomatic alternative: type Option func(*Server). Functions like WithTimeout(t time.Duration) return Option closures applied at construction. Used extensively in the Go standard library and popular packages (gRPC, zap).

Functional Options Fluent API Method Chaining
builder/query_builder.go — Restaurant Order Query
package query import ( "errors" "fmt" ) // Target object — immutable once built type Query struct { table string where string limit int offset int order string } func (q Query) SQL() string { return fmt.Sprintf("SELECT * FROM %s WHERE %s ORDER BY %s LIMIT %d OFFSET %d", q.table, q.where, q.order, q.limit, q.offset) } // Builder — accumulates config step by step type QueryBuilder struct { q Query; err error } func NewQuery() *QueryBuilder { return &QueryBuilder{} } func (b *QueryBuilder) Table(t string) *QueryBuilder { b.q.table = t; return b } func (b *QueryBuilder) Where(w string) *QueryBuilder { b.q.where = w; return b } func (b *QueryBuilder) Limit(n int) *QueryBuilder { b.q.limit = n; return b } func (b *QueryBuilder) Offset(n int) *QueryBuilder { b.q.offset = n; return b } func (b *QueryBuilder) OrderBy(o string) *QueryBuilder{ b.q.order = o; return b } func (b *QueryBuilder) Build() (Query, error) { if b.q.table == "" { return Query{}, errors.New("table is required") } if b.q.limit == 0 { b.q.limit = 20 } // sensible default if b.q.order == "" { b.q.order = "id" } return b.q, nil } // Fluent usage — reads like natural language: // q, _ := NewQuery().Table("orders").Where("status='pending'").Limit(10).OrderBy("created_at DESC").Build() // fmt.Println(q.SQL())

✓ ADVANTAGES

  • Readable, self-documenting object construction
  • Handles optional vs required fields cleanly
  • Validation in one place (Build())
  • Immutable final object — safer concurrency
  • Eliminates "telescoping constructor" anti-pattern
  • Easy to extend with new options

✗ DISADVANTAGES

  • More boilerplate than simple structs
  • Two types to maintain (Builder + Target)
  • Method chaining errors deferred to Build()
  • Overkill for simple objects (3-4 fields)
  • Go's functional options pattern often preferred
📧
Food Delivery — Notification Builder
Building complex push notifications with optional channels (SMS/Email/WebPush), localization, retry config, scheduling, and payload construction. NewNotification().To(userID).Via(SMS, Email).Message("Your order is ready!").Schedule(now.Add(5*time.Minute)).Build() — each field independent, validated at Build() time before sending to the notification microservice.
HTTP Client Config gRPC Server Options SQL Query Builder Email Builder Config Builder (zap.Logger)
Factory Method Pattern
🏭
Factory Method
Creational Pattern · GoF Classic
Defines an interface for creating objects but lets subclasses/implementations decide which type to instantiate. In Go — a factory function returns an interface, hiding the concrete type from the caller.
Category: Creational Returns: Interface Hides: Concrete type Industry use: Very High

What is Factory?

Instead of &ConcreteType{}, callers use a factory function like NewPaymentGateway("stripe") which returns a PaymentGateway interface. The caller doesn't know — or care — whether they got a Stripe, PayPal, or Razorpay implementation. Adding new implementations requires no changes to caller code.

How It Works

  • Define a product interface with common behavior
  • Create concrete types implementing the interface
  • Factory function takes a discriminator (string, enum, config)
  • Returns the interface type — caller is decoupled from concrete
  • New types added in factory only — Open/Closed Principle

Characteristics

  • Open/Closed: Open for extension, closed for modification
  • Dependency Inversion: Depend on abstractions (interfaces)
  • Single Responsibility: Object creation is centralized
  • Registry variant: map of factory functions per key
  • Plugin system variant: dynamic factory registration

Best Used For

  • Payment gateway switching (Stripe/PayPal/Crypto)
  • Database driver selection (PostgreSQL/MySQL/SQLite)
  • Notification channels (Email/SMS/Push/Slack)
  • Storage backends (S3/GCS/Azure/Local)
  • Logger implementations (zap/logrus/slog)
factory/payment_gateway.go — E-Commerce Payment Factory
package payment import "fmt" // Product interface — the contract all gateways must fulfill type Gateway interface { Charge(amount float64, currency, token string) (string, error) Refund(txID string, amount float64) error Name() string } // Concrete implementations type stripeGateway struct{ apiKey string } type paypalGateway struct{ clientID, secret string } type razorpayGateway struct{ key, secret string } func (s *stripeGateway) Name() string { return "Stripe" } func (p *paypalGateway) Name() string { return "PayPal" } func (r *razorpayGateway) Name() string { return "Razorpay" } // Registry pattern — map of factory functions type factoryFn func(cfg map[string]string) Gateway var registry = map[string]factoryFn{ "stripe": func(c map[string]string) Gateway { return &stripeGateway{apiKey: c["key"]} }, "paypal": func(c map[string]string) Gateway { return &paypalGateway{clientID: c["id"]} }, "razorpay": func(c map[string]string) Gateway { return &razorpayGateway{key: c["key"]} }, } // Factory function — decouples caller from concrete type func New(provider string, cfg map[string]string) (Gateway, error) { fn, ok := registry[provider] if !ok { return nil, fmt.Errorf("unknown gateway: %s", provider) } return fn(cfg), nil } // Caller never knows if it's Stripe or PayPal: // gw, _ := payment.New(cfg.PaymentProvider, cfg.Keys) // gw.Charge(99.99, "USD", token) ← same code, any gateway

✓ ADVANTAGES

  • Eliminates coupling to concrete implementations
  • Add new types without changing caller code (OCP)
  • Centralized object creation logic
  • Easy to mock in tests (inject fake implementation)
  • Runtime polymorphism — swap provider via config

✗ DISADVANTAGES

  • Type switch in factory can become complex
  • Indirect creation harder to trace in debugger
  • Runtime errors if discriminator is invalid
  • Possible over-engineering for small projects
Adapter Pattern
🔌
Adapter
Structural Pattern · GoF Classic
Makes incompatible interfaces work together by wrapping one type inside another that translates calls. In Go — you wrap a third-party type in a struct implementing your own interface, creating a bridge between your domain and external systems.
Category: Structural Also called: Wrapper Direction: One-way Industry use: Very High

What is Adapter?

You have a system expecting a Logger interface with Info(msg string), but the third-party library you want to use has Log(level, msg string). The Adapter wraps the third-party logger in a struct that implements your Logger interface — no changes to your system, no changes to theirs. Plug and play.

How It Works

  • Define your target interface (what your system expects)
  • You have an adaptee — the incompatible external type
  • Create an adapter struct that wraps the adaptee
  • Implement target interface methods by translating to adaptee calls
  • Your system uses the adapter, never the adaptee directly

When to Use

  • Integrating third-party libraries with your domain interfaces
  • Legacy code migration — new interface, old implementation
  • Testing — adapting production dependencies to mock interfaces
  • Payment/SMS/Email provider swapping without code changes
  • Converting data formats (JSON↔XML, REST↔gRPC)

Go's Implicit Interfaces

Go's structural typing makes Adapter very natural. If a type has the right methods, it automatically satisfies the interface — no implements keyword. This means many types are already adapters. The io.Reader/io.Writer ecosystem is built entirely on this principle.

Your System calls Logger.Info() Adapter implements Logger Info() → zap.Info() translates Adaptee go.uber.org/zap zap.Logger Output logs Your interface, unchanged. Their library, unchanged.
adapter/logger_adapter.go — Wrapping zap.Logger
package logger import "go.uber.org/zap" // Target interface — YOUR system's expectation type Logger interface { Info(msg string, fields ...any) Error(msg string, err error) With(key string, val any) Logger } // Adapter — wraps *zap.Logger to satisfy Logger interface type ZapAdapter struct{ z *zap.Logger } func NewZapAdapter(z *zap.Logger) Logger { return &ZapAdapter{z: z} } // Translation layer — adapt your interface to zap's API func (a *ZapAdapter) Info(msg string, fields ...any) { a.z.Sugar().Infow(msg, fields...) // translate } func (a *ZapAdapter) Error(msg string, err error) { a.z.Error(msg, zap.Error(err)) // translate } func (a *ZapAdapter) With(key string, val any) Logger { return &ZapAdapter{z: a.z.With(zap.Any(key, val))} } // Swap to logrus tomorrow — zero changes in business code: // type LogrusAdapter struct{ l *logrus.Logger } // func (a *LogrusAdapter) Info(msg string, fields ...any) { a.l.Info(msg) }

✓ ADVANTAGES

  • Swap third-party dependencies without touching domain code
  • Makes external libraries testable via mock adapters
  • Preserves your domain interface purity
  • Open/Closed — new providers without modifying existing code
  • Natural in Go — no boilerplate registration needed

✗ DISADVANTAGES

  • Adds indirection layer — slight performance overhead
  • Can create an explosion of adapter types
  • Leaking adaptee concepts through adapter can happen
  • Debugging crosses adapter boundary — harder to trace
Observer Pattern
👁️
Observer (Event Bus)
Behavioral Pattern · GoF Classic · Go: channels
Defines a one-to-many dependency — when one object (Subject/Publisher) changes state, all registered observers (Subscribers) are notified automatically. In Go this is elegantly expressed using channels, goroutines, and function callbacks.
Category: Behavioral Go idiom: channels Also: Pub/Sub, EventBus Industry use: Very High

How It Works (Go Style)

  • Publisher: holds a list of subscriber channels or callbacks
  • Subscribe(topic): returns a channel — caller receives events on it
  • Publish(event): sends to all subscriber channels (non-blocking)
  • Unsubscribe: remove channel from list, close it
  • Each subscriber runs in its own goroutine — concurrent handling

Conditions & Rules

  • Use buffered channels to avoid blocking publisher
  • Always handle subscriber removal — prevent memory leaks
  • sync.RWMutex for subscriber list — concurrent read/write
  • Non-blocking send: select { case ch <- e: default: }
  • Context cancellation for graceful subscriber shutdown

Real-World Triggers

  • Order placed → notify kitchen, inventory, analytics
  • Stock price change → notify traders, alerts, charting
  • User signup → send welcome email, init free trial, log
  • Sensor reading → trigger alarms, update dashboard, store
  • Payment success → fulfill order, notify user, update ledger

Go's Advantage

Go channels make Observer feel native. A channel IS a typed, buffered, goroutine-safe queue. Each subscriber is just a goroutine reading from its channel. This eliminates explicit locking in many patterns. The select statement enables non-blocking multi-topic subscription naturally.

observer/event_bus.go — Restaurant Order Event System
package events import "sync" type OrderEvent struct { Type string // "placed", "ready", "delivered" OrderID string TableNo int } // EventBus — the Subject/Publisher type EventBus struct { mu sync.RWMutex subscribers map[string][]chan OrderEvent } func NewEventBus() *EventBus { return &EventBus{subscribers: make(map[string][]chan OrderEvent)} } // Subscribe — returns a buffered channel the caller reads from func (b *EventBus) Subscribe(topic string) chan OrderEvent { ch := make(chan OrderEvent, 64) // buffered — publisher never blocks b.mu.Lock() b.subscribers[topic] = append(b.subscribers[topic], ch) b.mu.Unlock() return ch } // Publish — fanout to all subscribers of the topic func (b *EventBus) Publish(topic string, evt OrderEvent) { b.mu.RLock() defer b.mu.RUnlock() for _, ch := range b.subscribers[topic] { select { case ch <- evt: // non-blocking send default: // slow subscriber — drop or log } } } // Kitchen subscriber goroutine: // ch := bus.Subscribe("order.placed") // go func() { for evt := range ch { printTicket(evt) } }() // Analytics subscriber runs independently in parallel.

✓ ADVANTAGES

  • Decouples event producers from consumers
  • Add new subscribers without touching publisher
  • Concurrent handling — each subscriber is independent
  • Go channels are naturally goroutine-safe
  • Easy to test — inject event channel in unit tests

✗ DISADVANTAGES

  • Subscriber list management — memory leak risk
  • Event ordering not guaranteed across goroutines
  • Slow subscribers can drop events (non-blocking send)
  • Debugging: hard to trace which subscriber caused an issue
  • Circular event chains can cause infinite loops
🚚
Food Delivery — Real-Time Order Lifecycle
When a customer places an order: Kitchen subscriber prints the cooking ticket → Inventory subscriber decrements stock → Analytics subscriber logs the sale → Notification subscriber pushes "Order confirmed!" to customer's phone → Driver service subscriber queues a pickup assignment. All five run in separate goroutines, simultaneously, from one bus.Publish("order.placed", evt) call.
Strategy Pattern
♟️
Strategy
Behavioral Pattern · GoF Classic
Defines a family of algorithms, encapsulates each one, and makes them interchangeable at runtime. The client selects which strategy to use — the context delegates to it. In Go, strategies are simply interfaces with a single method.
Category: Behavioral Go idiom: func type / interface Runtime swappable: ✓ Industry use: Very High

What is Strategy?

Your Sorter service needs to sort data — sometimes QuickSort (best average), sometimes MergeSort (stable), sometimes RadixSort (integers only). Strategy wraps each algorithm behind a common interface. At runtime, inject the right strategy based on data type, size, or user preference — no if/else chains.

Go Function Types

In Go, a Strategy can be as simple as a function type: type PricingStrategy func(basePrice float64, quantity int) float64. Functions like BulkDiscount, MemberDiscount, FlashSale all satisfy this type. This is the most idiomatic Go approach — no interface boilerplate for simple strategies.

When to Use

  • Multiple pricing/discount algorithms for e-commerce
  • Different sorting/searching based on data characteristics
  • Authentication strategies (JWT, OAuth, API Key, Session)
  • Routing algorithms (round-robin, least-connections, weighted)
  • Tax calculation strategies per country/region
  • Compression: gzip vs zstd vs lz4 — pick per content type

Design Approach

  • Define Strategy interface with single method (ISP)
  • Implement multiple concrete strategies
  • Context struct holds current strategy as field
  • SetStrategy() method allows runtime swap
  • Execute() delegates to the current strategy
strategy/pricing.go — E-Commerce Dynamic Pricing
package pricing // Strategy interface — one method, one responsibility type PricingStrategy interface { Calculate(basePrice float64, qty int, user User) float64 } // Concrete strategies type StandardPricing struct{} func (s StandardPricing) Calculate(base float64, qty int, u User) float64 { return base * float64(qty) } type BulkDiscount struct{ Threshold int; Percent float64 } func (b BulkDiscount) Calculate(base float64, qty int, u User) float64 { price := base * float64(qty) if qty >= b.Threshold { price *= (1 - b.Percent/100) } return price } type MemberPricing struct{ DiscountPct float64 } func (m MemberPricing) Calculate(base float64, qty int, u User) float64 { if !u.IsMember { return base * float64(qty) } return base * float64(qty) * (1 - m.DiscountPct/100) } // Context — holds and delegates to strategy type PriceCalculator struct{ strategy PricingStrategy } func (c *PriceCalculator) SetStrategy(s PricingStrategy) { c.strategy = s } func (c *PriceCalculator) Price(base float64, qty int, u User) float64 { return c.strategy.Calculate(base, qty, u) } // Runtime switch based on cart contents: // if cart.Qty > 50 { calc.SetStrategy(BulkDiscount{50, 15}) } // if user.IsMember { calc.SetStrategy(MemberPricing{10}) }

✓ ADVANTAGES

  • Eliminates complex if/else or switch chains
  • Algorithms can change at runtime
  • Easy to add new strategies without modifying context
  • Each strategy is independently testable
  • Go function types make simple strategies zero-overhead

✗ DISADVANTAGES

  • Client must know which strategy to pick (coupling to strategy types)
  • Too many strategy types can overwhelm a package
  • Overhead if strategies are trivial (use simple if/else instead)
  • Can be confused with State pattern — they look similar
Decorator Pattern
🎁
Decorator
Structural Pattern · GoF Classic
Attaches additional responsibilities to an object dynamically without altering its structure. In Go — wrap one interface implementation inside another that adds behavior before/after delegating. This is how Go's io package works entirely.
Category: Structural Composable: ✓✓ Go stdlib: io.MultiWriter Industry use: High

What is Decorator?

You have a DataStore interface. You want to add caching to it — but only for certain deployments, and you don't want to modify the core store. Wrap it: CachedStore{inner: realStore}. The cached store checks cache first, falls through to inner on miss. Stack more: LoggedStore{inner: CachedStore{inner: realStore}}. Infinite composability.

How It Works

  • Decorator struct implements the same interface as the wrapped type
  • Holds a reference to the inner/wrapped implementation
  • Each method: does extra work, then delegates to inner
  • Stack decorators freely: A{B{C{core}}}
  • Order matters — outer runs first, then inner

Go stdlib Examples

  • bufio.NewReader(r io.Reader) — buffered decorator
  • gzip.NewReader(r io.Reader) — decompression decorator
  • io.LimitReader(r, n) — size-limiting decorator
  • http.ResponseWriter — HTTP middleware is decorator
  • tls.Client(conn) — TLS wrapping decorator

When to Use

  • Adding caching layer to data store
  • Adding logging/metrics to service calls
  • Adding retry logic to HTTP clients
  • Adding rate limiting to any resource
  • Adding encryption/compression to streams
decorator/cache_store.go — Layered Data Store
package store import "time" // Component interface type MenuStore interface { GetItem(id string) (*MenuItem, error) SaveItem(item *MenuItem) error } // ── Concrete base ────────────────────────────────────── type PostgresMenuStore struct{ db *DB } func (s *PostgresMenuStore) GetItem(id string) (*MenuItem, error) { return s.db.QueryItem(id) // hits database } // ── Caching Decorator ────────────────────────────────── type CachedMenuStore struct { inner MenuStore // wraps any MenuStore cache map[string]*MenuItem ttl time.Duration } func (c *CachedMenuStore) GetItem(id string) (*MenuItem, error) { if item, ok := c.cache[id]; ok { return item, nil } // cache hit item, err := c.inner.GetItem(id) // delegate to inner if err == nil { c.cache[id] = item } return item, err } // ── Logging Decorator ────────────────────────────────── type LoggedMenuStore struct { inner MenuStore; log Logger } func (l *LoggedMenuStore) GetItem(id string) (*MenuItem, error) { l.log.Info("GetItem", "id", id) item, err := l.inner.GetItem(id) l.log.Info("GetItem result", "err", err) return item, err } // Stack decorators — outer to inner: // var store MenuStore = &PostgresMenuStore{db} // store = &CachedMenuStore{inner: store, ttl: 5*time.Minute} // store = &LoggedMenuStore{inner: store, log: logger} // store.GetItem("burger-001") → logs → cache → postgres

✓ ADVANTAGES

  • Add behavior without modifying original code (OCP)
  • Composable — stack as many as needed
  • Each decorator is independently testable
  • Go's io package proves how powerful this is at scale
  • Runtime composition of behaviors

✗ DISADVANTAGES

  • Deep stacks make debugging harder
  • Many small objects — memory overhead
  • Order dependency is implicit and error-prone
  • Type identity lost — can't easily check underlying type
Worker Pool Pattern
⚙️
Worker Pool
Concurrency Pattern · Go-Idiomatic
Creates a bounded pool of goroutines (workers) that process jobs from a shared channel. Prevents goroutine explosion under high load. Workers pick up jobs when idle, process them, and return results — max concurrency is always N workers, regardless of job count.
Category: Concurrency Bounded goroutines: ✓ Tools: channels + goroutines Industry use: Very High

Why Worker Pool?

Without a pool: 10,000 incoming requests → 10,000 goroutines → OOM crash. With a pool: 10,000 requests → 100 workers process them sequentially via channel queue. Workers block waiting for jobs when idle — zero CPU waste. Max goroutines = pool size. Memory usage is bounded and predictable.

How It Works

  • Create a jobs channel (buffered) for incoming work
  • Create a results channel for output collection
  • Spin up N goroutines (workers) — each loops on jobs channel
  • Workers range jobs — blocks when idle, wakes on new job
  • Close jobs channel → workers finish and exit cleanly
  • sync.WaitGroup to know when all workers are done

Rules & Conditions

  • Pool size = number of CPU cores (for CPU-bound) or tuned for I/O
  • Never close results channel from worker — only from collector
  • Use sync.WaitGroup — signal when all workers done
  • Context propagation for cancellation support
  • Buffered jobs channel to handle burst without blocking sender

Real-World Use Cases

  • Image resizing: 1 goroutine per upload → pool of N resizers
  • Email blast: send 100K emails via 50-worker pool
  • Web scraping: bounded concurrent HTTP requests
  • Database batch inserts: bounded writer goroutines
  • Video transcoding pipelines
Dispatcher sends jobs Jobs Ch buffered chan Job Worker 1 goroutine Worker 2 goroutine Worker N goroutine · · · Results channel chan Result Collector aggregates Bounded concurrency — N workers max, always
workerpool/image_resizer.go — Restaurant Image Processing
package imgproc import ( "context" "runtime" "sync" ) type Job struct{ ImagePath string; Sizes []int } type Result struct{ Path string; Err error } func ProcessImages(ctx context.Context, images []Job) []Result { numWorkers := runtime.NumCPU() jobs := make(chan Job, len(images)) results := make(chan Result, len(images)) var wg sync.WaitGroup // Spin up N workers — bounded goroutine count for i := 0; i < numWorkers; i++ { wg. Add(1) go func() { defer wg.Done() for job := range jobs { // blocks until job arrives select { case <-ctx.Done(): return // context cancellation default: results <- resize(job) // CPU-bound work } } }() } // Dispatch all jobs for _, img := range images { jobs <- img } close(jobs) // signals workers to stop when channel empty // Collect results after all workers done go func() { wg.Wait(); close(results) }() var out []Result for r := range results { out = append(out, r) } return out }

✓ ADVANTAGES

  • Bounded goroutines — predictable memory usage
  • Prevents system overload under burst traffic
  • Worker reuse — no goroutine creation overhead per job
  • Context support for graceful cancellation
  • Results collected cleanly via output channel

✗ DISADVANTAGES

  • Worker count tuning required per workload type
  • Head-of-line blocking if one job is slow
  • Buffered channel sizing needs careful thought
  • More complex than simple goroutine spawning
Pipeline Pattern
🔗
Pipeline
Concurrency Pattern · Go-Idiomatic
Chains a series of processing stages connected by Go channels. Each stage consumes from an input channel, transforms data, and sends to an output channel — creating a concurrent data processing pipeline where stages run in parallel.
Category: Concurrency Stages: parallel Backpressure: channels Industry use: High

How It Works

  • Each stage is a function: func stage(in <-chan T) <-chan U
  • Stage receives, transforms, sends — runs in its own goroutine
  • Connect stages by passing output channel of one as input to next
  • Stages run concurrently — stage 2 starts processing while stage 1 continues
  • Cancel via context — upstream close propagates downstream

Backpressure

Channels provide natural backpressure. If stage 3 is slow, stage 2's output channel fills up, blocking stage 2, which signals stage 1 to slow down. The pipeline self-regulates — fast producers don't overwhelm slow consumers. This is a critical property for production data systems.

Real-World Use Cases

  • ETL pipelines: Extract → Transform → Load
  • Order processing: validate → price → reserve stock → charge
  • Log processing: parse → filter → enrich → index
  • Video: decode → transcode → thumbnail → upload
  • IoT: receive → calibrate → aggregate → alert

Rules

  • Each stage function owns its goroutine and closes its output channel
  • Never close an input channel from a stage — only the producer closes
  • Context cancellation: every stage checks ctx.Done()
  • Use defer close(out) at the start of each stage goroutine
pipeline/order_processing.go — E-Commerce Order Pipeline
package pipeline import "context" // Each stage: takes input channel, returns output channel func Validate(ctx context.Context, in <-chan Order) <-chan Order { out := make(chan Order, 100) go func() { defer close(out) for order := range in { select { case <-ctx.Done(): return default: if isValid(order) { out <- order } } } }() return out } func ApplyPricing(ctx context.Context, in <-chan Order) <-chan Order { out := make(chan Order, 100) go func() { defer close(out) for order := range in { order.Total = calculateTotal(order) out <- order } }() return out } func ChargePayment(ctx context.Context, in <-chan Order) <-chan Order { out := make(chan Order, 100) go func() { defer close(out) for order := range in { if err := chargeCard(order); err == nil { out <- order } } }() return out } // Wire the pipeline — stages run CONCURRENTLY: func RunOrderPipeline(ctx context.Context, orders <-chan Order) <-chan Order { validated := Validate(ctx, orders) priced := ApplyPricing(ctx, validated) charged := ChargePayment(ctx, priced) return charged // caller ranges over final channel }

✓ ADVANTAGES

  • Stages run concurrently — max throughput
  • Natural backpressure — channel blocking
  • Each stage independently testable
  • Clean data flow — easy to reason about
  • Context cancellation propagates cleanly

✗ DISADVANTAGES

  • Complex error handling across stages
  • Channel size tuning affects throughput
  • Goroutine leak risk if stages aren't properly closed
  • Debugging concurrent stages is non-trivial
Fan-Out / Fan-In Pattern
📡
Fan-Out / Fan-In
Concurrency Pattern · Go-Idiomatic
Fan-Out: distribute work to multiple goroutines processing in parallel. Fan-In: merge results from multiple goroutines back into a single channel. Together they implement parallel scatter-gather — the backbone of high-throughput search, aggregation, and multi-source data fetching.
Category: Concurrency Pattern: Scatter-Gather Key: reflect.Select / goroutines Industry use: High

Fan-Out: How It Works

  • One source of work (channel or slice)
  • Spawn N goroutines, each reading from the same input channel
  • Each goroutine processes independently and concurrently
  • Like having N workers at N parallel checkout lanes
  • Useful when processing is CPU-bound or I/O-bound and slow

Fan-In: How It Works

  • Multiple goroutines producing results on their own channels
  • Merge function collects all into one unified output channel
  • Uses sync.WaitGroup to close merged channel when all done
  • Caller reads from single channel — unaware of N sources
  • First result wins variant: use select to get fastest

Real-World: Search Aggregation

A hotel booking app queries Booking.com, Expedia, and Airbnb simultaneously (fan-out → 3 goroutines, 3 HTTP calls). Results fan-in to a merged channel. First response shows immediately, others fill in — total latency = slowest source, not sum of all sources. This can cut latency by 3x.

When to Use

  • Querying multiple APIs/databases simultaneously
  • Parallelizing slow I/O operations (HTTP, disk, DB)
  • Search across multiple indexes concurrently
  • Replicating writes to multiple stores
  • First-response-wins (cache + DB race)
fanout/price_aggregator.go — Travel Booking Price Comparison
package aggregator import ( "context" "sync" ) type PriceResult struct{ Source string; Price float64; Err error } // Fan-Out: call multiple price APIs concurrently func fetchFrom(ctx context.Context, source string, query string) <-chan PriceResult { ch := make(chan PriceResult, 1) go func() { defer close(ch) price, err := httpFetch(ctx, source, query) // slow I/O ch <- PriceResult{Source: source, Price: price, Err: err} }() return ch } // Fan-In: merge N result channels into one func merge(ctx context.Context, channels ...<-chan PriceResult) <-chan PriceResult { merged := make(chan PriceResult, len(channels)) var wg sync.WaitGroup forward := func(c <-chan PriceResult) { defer wg.Done() for r := range c { select { case merged <- r: case <-ctx.Done(): return } } } for _, c := range channels { wg.Add(1); go forward(c) } go func() { wg.Wait(); close(merged) }() return merged } // Search all providers SIMULTANEOUSLY — latency = max(sources), not sum! func GetBestPrice(ctx context.Context, query string) []PriceResult { // Fan-Out: 3 concurrent HTTP calls booking := fetchFrom(ctx, "booking.com", query) expedia := fetchFrom(ctx, "expedia.com", query) airbnb := fetchFrom(ctx, "airbnb.com", query) // Fan-In: collect all results from merged channel var results []PriceResult for r := range merge(ctx, booking, expedia, airbnb) { results = append(results, r) } return results // All 3 results collected concurrently }

✓ ADVANTAGES

  • Latency = max(sources) not sum — massive speedup
  • Efficient resource use — I/O waits in parallel
  • Elegant with Go channels — minimal boilerplate
  • First-result-wins pattern for cache-DB race
  • Context cancellation works across all goroutines

✗ DISADVANTAGES

  • Goroutine count grows with fan-out — limit it
  • Partial failure handling is complex
  • Merge logic needs careful channel closing
  • Race conditions in fan-in if not using WaitGroup correctly
Repository Pattern
🗄️
Repository
Architectural Pattern · Domain-Driven Design
Mediates between the domain/business layer and data mapping layer using a collection-like interface for accessing domain objects. The service layer never knows if it's talking to PostgreSQL, Redis, or a mock — just calls repo.FindByID().
Category: Architectural Layer: Data Access Pairs with: Clean Arch Industry use: Very High

What is Repository?

Instead of scattering SQL queries across your codebase, all data access for a domain entity is centralized in a Repository. The service layer depends on a UserRepository interface — not on PostgreSQL. Swap PostgreSQL for MongoDB? Write a new implementation, inject it. Service code unchanged.

Layers

  • Interface: UserRepository — domain methods (Find, Save, Delete)
  • PostgresRepo: implements interface using SQL
  • InMemoryRepo: implements interface using map (for tests)
  • CachedRepo: decorator adding Redis cache
  • Service: receives interface — knows nothing of SQL

Rules & Requirements

  • Repository interface defined in domain layer, not infrastructure
  • Methods use domain objects — never expose ORM types to service
  • One repository per aggregate root (DDD)
  • Transactions: accept *sql.Tx or use Unit of Work pattern
  • Interface in same package as domain types

Testability

The killer feature: your service tests use an InMemoryUserRepository — no database required. Tests run in milliseconds, not seconds. CI/CD pipelines need no database container. Integration tests run only for the repository implementation itself.

repository/user_repo.go — Multi-layer Repository
package domain // ── Domain Layer: interface lives here ───────────────── type User struct { ID, Name, Email string } type UserRepository interface { FindByID(ctx context.Context, id string) (*User, error) FindByEmail(ctx context.Context, email string) (*User, error) Save(ctx context.Context, user *User) error Delete(ctx context.Context, id string) error } // ── Infra Layer: PostgreSQL implementation ───────────── type PostgresUserRepo struct{ db *sql.DB } func (r *PostgresUserRepo) FindByID(ctx context.Context, id string) (*User, error) { var u User err := r.db.QueryRowContext(ctx, "SELECT id, name, email FROM users WHERE id=$1", id, ).Scan(&u.ID, &u.Name, &u.Email) return &u, err } // ── Test Layer: in-memory implementation ─────────────── type InMemoryUserRepo struct{ users map[string]*User } func (r *InMemoryUserRepo) FindByID(ctx context.Context, id string) (*User, error) { u, ok := r.users[id] if !ok { return nil, errors.New("not found") } return u, nil } // Service depends ONLY on interface — works with any impl: type UserService struct{ repo UserRepository } func (s *UserService) GetProfile(ctx context.Context, id string) (*User, error) { return s.repo.FindByID(ctx, id) // PostgreSQL? MongoDB? Doesn't care. }

✓ ADVANTAGES

  • Services are testable without a database
  • Swap data stores without touching business logic
  • Centralizes all data access — easy to audit
  • Pairs perfectly with Clean/Hexagonal Architecture
  • Add caching as decorator without changing service

✗ DISADVANTAGES

  • Boilerplate — interface + multiple implementations
  • Complex queries can be awkward to express as domain methods
  • ORM features (eager loading) harder to expose cleanly
  • For simple CRUD apps, may be over-engineering
Middleware / Chain of Responsibility
🔧
Middleware Chain
Behavioral Pattern · Chain of Responsibility · HTTP-native
Wraps an http.Handler with pre/post processing logic in a chainable, composable way. Each middleware is a function that takes the next handler and returns a new handler — creating an onion-layered request processing pipeline.
Category: Behavioral Go native: http.Handler Used in: Every Go web framework Industry use: Ubiquitous

What is Middleware?

In Go, a middleware is simply a function: func(http.Handler) http.Handler. It wraps any handler, adding behavior before and/or after the inner handler runs. Chain them together: Logger(Auth(RateLimit(actualHandler))). The request passes through each layer, response travels back through them in reverse order.

How It Works (Onion Model)

  • Outer middleware runs its pre-logic
  • Calls next.ServeHTTP(w, r) — passes to inner handler
  • Inner handler executes
  • Control returns to outer — runs post-logic
  • Response flows back through layers in reverse

Common Middleware Types

  • Auth: verify JWT/session, attach user to context
  • Logger: log method, path, status, duration
  • Recovery: catch panics, return 500
  • CORS: set cross-origin headers
  • RateLimiter: throttle requests per IP/token
  • Timeout: cancel slow requests via context

Design Rules

  • Middleware must call next.ServeHTTP() or terminate
  • Pass data between middleware using context.WithValue()
  • Don't write to ResponseWriter after calling next
  • Use http.ResponseWriter wrappers to capture status codes
  • Order matters — apply outer-most last in chain
middleware/chain.go — Custom Middleware Stack
package middleware import ( "context" "log" "net/http" "time" ) // Middleware type — the fundamental building block type Middleware func(http.Handler) http.Handler // ── Logger Middleware ────────────────────────────────── func Logger(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { start := time.Now() next.ServeHTTP(w, r) // call next in chain log.Printf("%s %s %v", r.Method, r.URL.Path, time.Since(start)) }) } // ── Auth Middleware — attaches user to context ───────── type ctxKey string const UserKey ctxKey = "user" func Auth(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { token := r.Header.Get("Authorization") user, err := verifyJWT(token) if err != nil { http.Error(w, "Unauthorized", 401) return // terminates chain — next not called } ctx := context.WithValue(r.Context(), UserKey, user) next.ServeHTTP(w, r.WithContext(ctx)) }) } // ── Chain helper — compose middleware left-to-right ─── func Chain(h http.Handler, mw ...Middleware) http.Handler { for i := len(mw) - 1; i >= 0; i-- { h = mw[i](h) } return h } // Wiring: requests flow Logger → Auth → Handler // http.Handle("/api/orders", Chain(ordersHandler, Logger, Auth, RateLimit)) // Onion: Logger wraps (Auth wraps (RateLimit wraps ordersHandler))

✓ ADVANTAGES

  • Cross-cutting concerns separated from business logic
  • Fully composable — mix and match per route
  • Standard http.Handler signature — works with any framework
  • Each middleware independently testable
  • Add/remove features without touching handlers

✗ DISADVANTAGES

  • Order dependency is implicit — easy to get wrong
  • Deeply chained middlewares can hide performance problems
  • Context value passing is untyped — runtime errors
  • Panic recovery middleware must be first — easy to forget
🏨
Hotel Booking API — Request Middleware Chain
Every API request flows through: RequestID (attach unique trace ID) → Logger (log start) → Recovery (catch panics) → Timeout (cancel after 30s) → RateLimit (100 req/min per IP) → CORS (allow frontend origin) → Auth (verify JWT, inject user) → Handler (business logic). Each middleware is one concern, one function, independently replaceable.
Which Patterns Are Used In Industry

🏦 Fintech / Banking

  • Factory Payment gateway switching
  • Strategy Fee/tax calculation
  • Observer Transaction event alerts
  • Repository Ledger data access
  • Singleton DB connection pool

🛒 E-Commerce

  • Builder Order/cart construction
  • Decorator Caching product store
  • Strategy Pricing/discount rules
  • Worker Pool Bulk email campaigns
  • Pipeline Order fulfillment flow

📈 Real-Time / Stock Market

  • Observer Price change subscriptions
  • Pipeline Tick data processing
  • Fan-Out Multi-exchange queries
  • Strategy Matching algorithms
  • Singleton Market data cache

🍽️ Restaurant / Delivery

  • Observer Order status events
  • Middleware Auth + logging chain
  • Worker Pool Image processing
  • Factory Notification channels
  • Repository Menu/order data layer

☁️ Cloud / Infrastructure

  • Adapter Multi-cloud storage
  • Decorator Retry + circuit breaker
  • Factory Provider selection
  • Pipeline Data processing streams
  • Fan-Out Multi-region replication

🏥 Healthcare / SaaS

  • Repository Patient data access
  • Middleware HIPAA audit logging
  • Adapter HL7/FHIR wrapping
  • Singleton Config/secrets manager
  • Builder Report generation
Complete Comparison Table
Criteria 🔒 Singleton 🏗️ Builder 🏭 Factory 🔌 Adapter 👁️ Observer ♟️ Strategy 🎁 Decorator ⚙️ Worker Pool 🔗 Pipeline 📡 Fan-Out/In 🗄️ Repository 🔧 Middleware
Category Creational Creational Creational Structural Behavioral Behavioral Structural Concurrency Concurrency Concurrency Architectural Behavioral
Go-Specific ~ (sync.Once) GoF classic GoF classic implicit iface channels func types GoF classic ✓✓ goroutines ✓✓ goroutines ✓✓ goroutines DDD classic ✓✓ http.Handler
Complexity Low Medium Low-Medium Low Medium Low Low Medium Medium-High Medium-High Medium Low
Boilerplate Very Low High Medium Low Medium Low Low Medium Medium Medium High Very Low
Testability Global state ✓✓ Immutable obj ✓✓ Mock impl ✓✓ Mock adapter Test channels ✓✓ Inject strategy ✓✓ Stack mock Channel test Channel test Channel test ✓✓ InMemory impl ✓✓ Wrap test handler
Performance Impact Near-zero (after once) None (object creation) None (interface call) One indirection Channel overhead None (interface call) One indirection/layer High gain (bounded) High gain (parallel) High gain (parallel) Negligible Negligible per middleware
Goroutine-safe sync.Once immutable result if impl is safe if inner safe RWMutex stateless ~ if inner safe ✓✓ channels ✓✓ channels ✓✓ channels ~ tx handling per-request
Open/Closed Principle ✓✓ ✓✓ ✓✓ ✓✓ ~ ~ ~ ✓✓ ✓✓
Decoupling Level Low (global) High (immutable obj) Very High Very High High Very High High High High High Very High High
When to Avoid If DI is possible Simple <4 fields Single impl only Same interface already Simple callbacks ok Only 1 algorithm No extra behavior Few goroutines ok Single-threaded ok Sequential is fine Simple CRUD apps Single handler apps
Industry Usage ★★★★★ ★★★★ ★★★★★ ★★★★★ ★★★★ ★★★★ ★★★★ ★★★★★ ★★★★ ★★★☆☆ ★★★★★ ★★★★★
Best Project Type Any — shared resources Complex configs Multi-provider systems 3rd party integration Event-driven systems Multi-algorithm contexts Cross-cutting behavior Bulk/async processing ETL, data streams Parallel API calls Any multi-DB system Any HTTP API
Go Stdlib Example sync.Once strings.Builder http.NewRequest io.Writer wrappers signal.Notify chan sort.Interface bufio, gzip readers sync.WaitGroup io.Pipe reflect.Select database/sql net/http Handler

🎯 Decision Guide — Which Pattern When?

🔒 SINGLETON → Shared resources DB pool, config, logger, Redis client — anything expensive to create, safe to share.
🏗️ BUILDER → Complex construction Object needs 5+ configuration fields, some optional. Query builders, config structs, HTTP requests.
🏭 FACTORY → Multiple implementations Payment gateways, storage backends, notification channels — caller shouldn't know which concrete type.
🔌 ADAPTER → Third-party integration Wrapping external libraries to match your domain interface. Essential for testability and vendor lock-in prevention.
👁️ OBSERVER → Reactive events One action triggers many independent reactions. Order placed → kitchen, inventory, analytics, notifications all react.
♟️ STRATEGY → Runtime algorithm swap Pricing, sorting, routing, auth methods that change per user/config/context. Eliminates if/else chains.
🎁 DECORATOR → Composable behavior Add caching, logging, retry, metrics to any service without modifying it. Stack like Go's io package.
⚙️ WORKER POOL → Bounded concurrency Process 100K jobs without 100K goroutines. Essential whenever goroutine count must be controlled.
🔗 PIPELINE → Sequential stream processing ETL, order processing, log enrichment — multi-stage data transformation where stages run concurrently.
📡 FAN-OUT/IN → Parallel API calls Query 3 APIs simultaneously, collect results. Latency = max(sources) not sum. Scatter-gather pattern.
🗄️ REPOSITORY → Data access abstraction Any project needing testable services, swappable databases, or clean architecture boundaries.
🔧 MIDDLEWARE → HTTP cross-cutting Auth, logging, CORS, rate limiting, recovery — every HTTP API. The single most-used Go pattern in web development.