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
| Pattern | Tool | Use Case |
|---|---|---|
| Synchronous communication | gRPC, REST | Immediate response needed |
| Asynchronous communication | Kafka, NATS | Event-driven |
| Circuit breaker | gobreaker | Prevent cascading failures |
| Distributed tracing | OpenTelemetry | Request tracking |
| Health checks | /health, /ready | Deployment 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