Skip to main content
โšก Calmops

Go Microservices Patterns: Building Scalable Distributed Systems

Introduction

Building microservices in Go requires more than just writing API handlers. You need to handle service discovery, fault tolerance, distributed tracing, and inter-service communication. This guide covers essential patterns for building production-ready microservices in Go.


What Are Microservices Patterns?

The Basic Concept

Microservices patterns are battle-tested solutions to common distributed systems challenges. They help you build resilient, scalable, and maintainable microservices architectures.

Key Terms

  • Service Discovery: Automatically finding service instances
  • Circuit Breaker: Preventing cascade failures
  • Distributed Tracing: Tracking requests across services
  • Event-Driven Architecture: Services communicating via events
  • API Gateway: Single entry point for microservices
  • Sidecar Pattern: Co-located helper containers

Why These Patterns Matter

Challenge Without Pattern With Pattern
Service Failure Cascade failure Isolation
Finding Services Hardcoded IPs Auto-discovery
Debugging Impossible Full traceability
Scaling Manual Automatic

1. Service Discovery

Registry Pattern

package discovery

import (
    "fmt"
    "sync"
    "time"
)

type ServiceInstance struct {
    ID        string
    Name      string
    Host      string
    Port      int
    HealthURL string
    Metadata  map[string]string
    LastSeen  time.Time
}

type Registry struct {
    services map[string]map[string]*ServiceInstance
    mu       sync.RWMutex
}

func NewRegistry() *Registry {
    return &Registry{
        services: make(map[string]map[string]*ServiceInstance),
    }
}

func (r *Registry) Register(instance *ServiceInstance) error {
    r.mu.Lock()
    defer r.mu.Unlock()
    
    if _, ok := r.services[instance.Name]; !ok {
        r.services[instance.Name] = make(map[string]*ServiceInstance)
    }
    
    instance.LastSeen = time.Now()
    r.services[instance.Name][instance.ID] = instance
    
    fmt.Printf("Registered: %s at %s:%d\n", instance.Name, instance.Host, instance.Port)
    return nil
}

func (r *Registry) Deregister(serviceName, instanceID string) {
    r.mu.Lock()
    defer r.mu.Unlock()
    
    if _, ok := r.services[serviceName]; ok {
        delete(r.services[serviceName], instanceID)
    }
}

func (r *Registry) GetInstances(serviceName string) []*ServiceInstance {
    r.mu.RLock()
    defer r.mu.RUnlock()
    
    instances := make([]*ServiceInstance, 0)
    if _, ok := r.services[serviceName]; ok {
        for _, instance := range r.services[serviceName] {
            instances = append(instances, instance)
        }
    }
    
    return instances
}

func (r *Registry) StartHealthChecks(interval time.Duration) {
    ticker := time.NewTicker(interval)
    go func() {
        for range ticker.C {
            r.checkHealth()
        }
    }()
}

func (r *Registry) checkHealth() {
    r.mu.Lock()
    defer r.mu.Unlock()
    
    for name, instances := range r.services {
        for id, instance := range instances {
            if time.Since(instance.LastSeen) > 30*time.Second {
                delete(instances, id)
                fmt.Printf("Deregistered: %s/%s (unhealthy)\n", name, id)
            }
        }
    }
}

Client-Side Discovery

package discovery

import (
    "fmt"
    "math/rand"
    "net/http"
)

type ServiceClient struct {
    registry    *Registry
    serviceName string
}

func NewServiceClient(registry *Registry, serviceName string) *ServiceClient {
    return &ServiceClient{
        registry:    registry,
        serviceName: serviceName,
    }
}

func (c *ServiceClient) DoRequest(method, path string, body []byte) (*http.Response, error) {
    instances := c.registry.GetInstances(c.serviceName)
    if len(instances) == 0 {
        return nil, fmt.Errorf("no instances available for %s", c.serviceName)
    }
    
    instance := instances[rand.Intn(len(instances))]
    
    url := fmt.Sprintf("http://%s:%d%s", instance.Host, instance.Port, path)
    
    req, _ := http.NewRequest(method, url, nil)
    return http.DefaultClient.Do(req)
}

2. Circuit Breaker

Implementation

package circuit

import (
    "errors"
    "fmt"
    "sync"
    "time"
)

var ErrCircuitOpen = errors.New("circuit breaker is open")

type CircuitState int

const (
    StateClosed CircuitState = iota
    StateOpen
    StateHalfOpen
)

type CircuitBreaker struct {
    name        string
    maxRequests int
    interval    time.Duration
    timeout     time.Duration
    
    mu          sync.Mutex
    state       CircuitState
    failures    int
    successes   int
    lastFailure time.Time
    nextRequest time.Time
}

func NewCircuitBreaker(name string, maxRequests int, interval, timeout time.Duration) *CircuitBreaker {
    return &CircuitBreaker{
        name:        name,
        maxRequests: maxRequests,
        interval:    interval,
        timeout:     timeout,
        state:       StateClosed,
    }
}

func (cb *CircuitBreaker) Execute(fn func() error) error {
    cb.mu.Lock()
    
    if cb.state == StateOpen {
        if time.Since(cb.lastFailure) > cb.timeout {
            cb.state = StateHalfOpen
            cb.successes = 0
        } else {
            cb.mu.Unlock()
            return ErrCircuitOpen
        }
    }
    
    cb.mu.Unlock()
    
    err := fn()
    
    cb.mu.Lock()
    defer cb.mu.Unlock()
    
    if err != nil {
        cb.failures++
        cb.lastFailure = time.Now()
        
        if cb.failures >= cb.maxRequests {
            cb.state = StateOpen
            fmt.Printf("Circuit %s opened\n", cb.name)
        }
        return err
    }
    
    cb.successes++
    cb.failures = 0
    
    if cb.state == StateHalfOpen && cb.successes >= 2 {
        cb.state = StateClosed
        fmt.Printf("Circuit %s closed\n", cb.name)
    }
    
    return nil
}

func (cb *CircuitBreaker) GetState() CircuitState {
    cb.mu.Lock()
    defer cb.mu.Unlock()
    return cb.state
}

3. Distributed Tracing

Trace Middleware

package tracing

import (
    "fmt"
    "math/rand"
    "net/http"
    "time"
)

type TraceID string
type SpanID string

type TraceContext struct {
    TraceID TraceID
    SpanID  SpanID
    Sampled bool
}

func generateTraceID() TraceID {
    return TraceID(fmt.Sprintf("%016x", rand.Int63()))
}

func generateSpanID() SpanID {
    return SpanID(fmt.Sprintf("%016x", rand.Int63()))
}

func TracingMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        ctx := r.Context()
        
        traceID := r.Header.Get("X-Trace-ID")
        if traceID == "" {
            traceID = string(generateTraceID())
        }
        
        spanID := generateSpanID()
        
        ctx = WithTraceContext(ctx, &TraceContext{
            TraceID: TraceID(traceID),
            SpanID:  spanID,
            Sampled: true,
        })
        
        start := time.Now()
        
        next.ServeHTTP(w, r.WithContext(ctx))
        
        duration := time.Since(start)
        
        fmt.Printf("[TRACE] %s %s - %v\n", r.Method, r.URL.Path, duration)
    })
}

type contextKey string

const traceContextKey contextKey = "trace_context"

func WithTraceContext(ctx interface{ Context }, tc *TraceContext) interface {
    Context
} {
    return context.WithValue(ctx.Context(), traceContextKey, tc)
}

func GetTraceContext(ctx interface{ Context }) *TraceContext {
    if tc, ok := ctx.Value(traceContextKey).(*TraceContext); ok {
        return tc
    }
    return nil
}

4. Event-Driven Architecture

Event Bus

package events

import (
    "encoding/json"
    "fmt"
    "sync"
    "time"
)

type Event interface {
    GetType() string
    GetPayload() interface{}
}

type EventHandler func(Event) error

type EventBus struct {
    handlers map[string][]EventHandler
    mu       sync.RWMutex
}

func NewEventBus() *EventBus {
    return &EventBus{
        handlers: make(map[string][]EventHandler),
    }
}

func (eb *EventBus) Subscribe(eventType string, handler EventHandler) {
    eb.mu.Lock()
    defer eb.mu.Unlock()
    
    eb.handlers[eventType] = append(eb.handlers[eventType], handler)
}

func (eb *EventBus) Publish(event Event) error {
    eb.mu.RLock()
    handlers := eb.handlers[event.GetType()]
    eb.mu.RUnlock()
    
    for _, handler := range handlers {
        if err := handler(event); err != nil {
            return err
        }
    }
    
    return nil
}

type OrderCreatedEvent struct {
    Type      string          `json:"type"`
    Timestamp time.Time       `json:"timestamp"`
    Payload   OrderPayload    `json:"payload"`
}

type OrderPayload struct {
    OrderID    string  `json:"order_id"`
    CustomerID string  `json:"customer_id"`
    Total      float64 `json:"total"`
}

func (e OrderCreatedEvent) GetType() string   { return e.Type }
func (e OrderCreatedEvent) GetPayload() interface{} { return e.Payload }

func main() {
    bus := NewEventBus()
    
    bus.Subscribe("order.created", func(e Event) error {
        payload := e.GetPayload().(OrderPayload)
        fmt.Printf("Processing order: %s - $%.2f\n", payload.OrderID, payload.Total)
        return nil
    })
    
    event := OrderCreatedEvent{
        Type:      "order.created",
        Timestamp: time.Now(),
        Payload: OrderPayload{
            OrderID:    "ORD-001",
            CustomerID: "CUST-123",
            Total:      99.99,
        },
    }
    
    bus.Publish(event)
}

5. API Gateway

package gateway

import (
    "encoding/json"
    "fmt"
    "math/rand"
    "net/http"
    "strings"
)

type Route struct {
    Path    string
    Methods []string
    Backend string
}

type APIGateway struct {
    routes []Route
}

func NewAPIGateway() *APIGateway {
    return &APIGateway{
        routes: []Route{
            {Path: "/api/users", Backend: "user-service:8081"},
            {Path: "/api/orders", Backend: "order-service:8082"},
            {Path: "/api/products", Backend: "product-service:8083"},
        },
    }
}

func (gw *APIGateway) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    route := gw.findRoute(r.URL.Path)
    if route == nil {
        http.Error(w, "Not Found", http.StatusNotFound)
        return
    }
    
    gw.proxyRequest(w, r, route.Backend)
}

func (gw *APIGateway) findRoute(path string) *Route {
    for _, route := range gw.routes {
        if strings.HasPrefix(path, route.Path) {
            return &route
        }
    }
    return nil
}

func (gw *APIGateway) proxyRequest(w http.ResponseWriter, r *http.Request, backend string) {
    url := fmt.Sprintf("http://%s%s", backend, r.URL.Path)
    
    req, _ := http.NewRequest(r.Method, url, r.Body)
    req.Header = r.Header.Clone()
    
    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        http.Error(w, "Bad Gateway", http.StatusBadGateway)
        return
    }
    defer resp.Body.Close()
    
    w.WriteHeader(resp.StatusCode)
    json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
}

type LoadBalancer struct {
    backends map[string][]string
}

func NewLoadBalancer() *LoadBalancer {
    return &LoadBalancer{
        backends: make(map[string][]string),
    }
}

func (lb *LoadBalancer) GetBackend(service string) string {
    backends := lb.backends[service]
    if len(backends) == 0 {
        return ""
    }
    return backends[rand.Intn(len(backends))]
}

Best Practices

1. Graceful Shutdown

func main() {
    server := &http.Server{Addr: ":8080", Handler: router}
    
    go func() {
        if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
            log.Fatal(err)
        }
    }()
    
    quit := make(chan os.Signal, 1)
    signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
    <-quit
    
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()
    
    server.Shutdown(ctx)
}

2. Health Checks

func healthCheck(w http.ResponseWriter, r *http.Request) {
    ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second)
    defer cancel()
    
    if err := checkDependencies(ctx); err != nil {
        w.WriteHeader(http.StatusServiceUnavailable)
        json.NewEncoder(w).Encode(map[string]string{"status": "unhealthy"})
        return
    }
    
    w.WriteHeader(http.StatusOK)
    json.NewEncoder(w).Encode(map[string]string{"status": "healthy"})
}

3. Context Propagation

func callDownstreamService(ctx context.Context, url string) (*http.Response, error) {
    req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
    return http.DefaultClient.Do(req)
}

External Resources

Tools

  • Go Kit - Microservices toolkit
  • gRPC - High-performance RPC
  • Consul - Service discovery
  • Jaeger - Distributed tracing

Learning


Key Takeaways

  • Service Discovery enables dynamic service registration
  • Circuit Breaker prevents cascade failures
  • Distributed Tracing provides visibility across services
  • Event-Driven architecture decouples services
  • API Gateway provides unified entry point
  • Best Practices: graceful shutdown, health checks, context propagation

Comments