Skip to main content

Microservices Patterns — Production Architecture with Go

Microservices are a collection of small, independently deployable services. Go is optimized for microservices with fast startup time, low memory usage, and single binary deployment.


Service Structure Design

Standard Project Layout

user-service/
├── cmd/
│ └── server/
│ └── main.go ← Entry point
├── internal/
│ ├── domain/
│ │ └── user.go ← Domain model & business rules
│ ├── repository/
│ │ ├── interface.go ← Repository interface
│ │ └── postgres.go ← PostgreSQL implementation
│ ├── service/
│ │ └── user_service.go ← Business logic
│ ├── handler/
│ │ ├── http.go ← HTTP handlers
│ │ └── grpc.go ← gRPC handlers
│ └── config/
│ └── config.go ← Configuration loading
├── api/
│ └── proto/ ← Proto files
├── migrations/ ← Database migrations
├── Dockerfile
└── go.mod

Domain Model

// internal/domain/user.go
package domain

import (
"errors"
"time"
)

// Domain error definitions
var (
ErrUserNotFound = errors.New("user not found")
ErrEmailAlreadyInUse = errors.New("email already in use")
ErrInvalidEmail = errors.New("invalid email format")
)

type User struct {
ID int64
Name string
Email string
CreatedAt time.Time
UpdatedAt time.Time
}

// Domain business rules
func (u *User) Validate() error {
if u.Name == "" {
return errors.New("name is required")
}
if !isValidEmail(u.Email) {
return ErrInvalidEmail
}
return nil
}

Inter-Service Communication Patterns

HTTP Client — Retry & Circuit Breaker

// internal/client/product_client.go
package client

import (
"context"
"encoding/json"
"fmt"
"net/http"
"time"
)

type ProductClient struct {
baseURL string
httpClient *http.Client
}

func NewProductClient(baseURL string) *ProductClient {
return &ProductClient{
baseURL: baseURL,
httpClient: &http.Client{
Timeout: 5 * time.Second,
},
}
}

func (c *ProductClient) GetProduct(ctx context.Context, id int64) (*Product, error) {
url := fmt.Sprintf("%s/products/%d", c.baseURL, id)

req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, fmt.Errorf("request creation failed: %w", err)
}

resp, err := c.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("request failed: %w", err)
}
defer resp.Body.Close()

if resp.StatusCode == http.StatusNotFound {
return nil, ErrProductNotFound
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}

var product Product
if err := json.NewDecoder(resp.Body).Decode(&product); err != nil {
return nil, fmt.Errorf("response decode failed: %w", err)
}
return &product, nil
}

Circuit Breaker Pattern (gobreaker)

import "github.com/sony/gobreaker"

func NewProductClientWithBreaker(baseURL string) *ProductClient {
cb := gobreaker.NewCircuitBreaker(gobreaker.Settings{
Name: "product-service",
MaxRequests: 3, // Max requests in half-open state
Interval: 10 * time.Second, // Counter reset interval in closed state
Timeout: 30 * time.Second, // Duration of open state
ReadyToTrip: func(counts gobreaker.Counts) bool {
failureRatio := float64(counts.TotalFailures) / float64(counts.Requests)
return counts.Requests >= 3 && failureRatio >= 0.6
},
OnStateChange: func(name string, from, to gobreaker.State) {
log.Printf("Circuit breaker state change: %s → %s", from, to)
},
})

return &ProductClient{baseURL: baseURL, breaker: cb}
}

func (c *ProductClient) GetProductSafe(ctx context.Context, id int64) (*Product, error) {
result, err := c.breaker.Execute(func() (interface{}, error) {
return c.GetProduct(ctx, id)
})
if err == gobreaker.ErrOpenState {
// Fallback: return from cache or default value
return c.getFallbackProduct(id)
}
if err != nil {
return nil, err
}
return result.(*Product), nil
}

Event-Driven Communication — Kafka

go get github.com/segmentio/kafka-go

Event Publisher

// internal/events/publisher.go
package events

import (
"context"
"encoding/json"

"github.com/segmentio/kafka-go"
)

type UserCreatedEvent struct {
UserID int64 `json:"user_id"`
Email string `json:"email"`
Name string `json:"name"`
Timestamp int64 `json:"timestamp"`
}

type EventPublisher struct {
writer *kafka.Writer
}

func NewEventPublisher(brokers []string) *EventPublisher {
return &EventPublisher{
writer: &kafka.Writer{
Addr: kafka.TCP(brokers...),
Balancer: &kafka.LeastBytes{},
RequiredAcks: kafka.RequireAll, // Strong durability
},
}
}

func (p *EventPublisher) PublishUserCreated(ctx context.Context, userID int64, email, name string) error {
event := UserCreatedEvent{
UserID: userID,
Email: email,
Name: name,
Timestamp: time.Now().Unix(),
}

data, err := json.Marshal(event)
if err != nil {
return fmt.Errorf("event serialization failed: %w", err)
}

return p.writer.WriteMessages(ctx, kafka.Message{
Topic: "user.created",
Key: []byte(fmt.Sprintf("%d", userID)),
Value: data,
})
}

func (p *EventPublisher) Close() error {
return p.writer.Close()
}

Event Consumer

// internal/events/consumer.go
package events

type EventConsumer struct {
reader *kafka.Reader
handlers map[string]EventHandler
}

type EventHandler func(ctx context.Context, data []byte) error

func NewEventConsumer(brokers []string, groupID string) *EventConsumer {
return &EventConsumer{
reader: kafka.NewReader(kafka.ReaderConfig{
Brokers: brokers,
GroupID: groupID,
Topic: "user.created",
MinBytes: 10e3,
MaxBytes: 10e6,
CommitInterval: time.Second,
}),
handlers: make(map[string]EventHandler),
}
}

func (c *EventConsumer) Start(ctx context.Context) error {
for {
msg, err := c.reader.FetchMessage(ctx)
if err != nil {
if ctx.Err() != nil {
return nil // Normal shutdown
}
return fmt.Errorf("message receive failed: %w", err)
}

if err := c.processMessage(ctx, msg); err != nil {
log.Printf("message processing failed (offset=%d): %v", msg.Offset, err)
continue // Skip on error (use DLQ recommended)
}

// Manual commit (at-least-once processing)
c.reader.CommitMessages(ctx, msg)
}
}

Distributed Tracing — OpenTelemetry

go get go.opentelemetry.io/otel
go get go.opentelemetry.io/otel/sdk
go get go.opentelemetry.io/otel/exporters/otlp/otlptrace
// internal/telemetry/tracer.go
package telemetry

import (
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
"go.opentelemetry.io/otel/sdk/trace"
)

func InitTracer(serviceName, endpoint string) (func(), error) {
ctx := context.Background()

exporter, err := otlptracegrpc.New(ctx,
otlptracegrpc.WithEndpoint(endpoint),
otlptracegrpc.WithInsecure(),
)
if err != nil {
return nil, err
}

tp := trace.NewTracerProvider(
trace.WithBatcher(exporter),
trace.WithResource(resource.NewWithAttributes(
semconv.SchemaURL,
semconv.ServiceNameKey.String(serviceName),
)),
)
otel.SetTracerProvider(tp)

shutdown := func() {
tp.Shutdown(context.Background())
}
return shutdown, nil
}

// Use in service code
func (s *UserService) GetUser(ctx context.Context, id int64) (*User, error) {
tracer := otel.Tracer("user-service")
ctx, span := tracer.Start(ctx, "UserService.GetUser")
defer span.End()

span.SetAttributes(attribute.Int64("user.id", id))

user, err := s.repo.FindByID(ctx, id)
if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return nil, err
}

return user, nil
}

Health Checks & Graceful Shutdown

// cmd/server/main.go
func main() {
srv := &http.Server{Addr: ":8080", Handler: newRouter()}

// Health check endpoint
http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
status := map[string]string{
"status": "ok",
"version": version,
}
json.NewEncoder(w).Encode(status)
})

http.HandleFunc("/ready", func(w http.ResponseWriter, r *http.Request) {
if err := db.Ping(); err != nil {
http.Error(w, "DB not ready", http.StatusServiceUnavailable)
return
}
w.WriteHeader(http.StatusOK)
})

// Graceful shutdown
go func() {
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGTERM, syscall.SIGINT)
<-sigCh

log.Println("Shutdown starting...")
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()

if err := srv.Shutdown(ctx); err != nil {
log.Printf("Shutdown error: %v", err)
}
}()

log.Fatal(srv.ListenAndServe())
}

Key Takeaways

PatternToolUse Case
Synchronous communicationgRPC, RESTImmediate response needed
Asynchronous communicationKafka, NATSEvent-driven
Circuit breakergobreakerPrevent cascading failures
Distributed tracingOpenTelemetryRequest tracking
Health checks/health, /readyDeployment automation
  • Liveness (/health): Is the service alive — restart on failure
  • Readiness (/ready): Is the service ready for traffic — remove from load balancer on failure
  • Graceful shutdown ensures in-flight requests complete before termination