Go Architecture Patterns Series: ← Previous: Modular Monolith | Series Overview | Next: Event-Driven Architecture →
What is Microservices Architecture?
Microservices Architecture is an approach where an application is composed of small, independent services that communicate over a network. Each service is self-contained, owns its data, and can be deployed, scaled, and updated independently.
Key Principles:
- Service Independence: Each service is deployed and scaled independently
- Single Responsibility: Each service handles a specific business capability
- Decentralized Data: Each service owns its database
- API-First Design: Services communicate through well-defined APIs
- Resilience: Services handle failures gracefully
- Technology Diversity: Services can use different technologies
Architecture Overview
Service Communication Patterns
Real-World Use Cases
- E-commerce Platforms: Amazon, eBay with separate services for products, orders, payments
- Streaming Services: Netflix with services for recommendations, playback, billing
- Ride-Sharing Apps: Uber with services for riders, drivers, payments, routing
- Financial Systems: Banking apps with separate services for accounts, transactions, loans
- Social Media: Twitter with services for posts, timelines, notifications, messages
- Cloud Platforms: AWS-like platforms with independent service offerings
Microservices Implementation
Project Structure (Multi-Repository)
microservices/
├── user-service/
│ ├── cmd/
│ │ └── server/
│ │ └── main.go
│ ├── internal/
│ │ ├── domain/
│ │ ├── handlers/
│ │ ├── repository/
│ │ └── service/
│ ├── proto/
│ │ └── user.proto
│ └── go.mod
├── product-service/
│ ├── cmd/
│ │ └── server/
│ │ └── main.go
│ ├── internal/
│ │ ├── domain/
│ │ ├── handlers/
│ │ ├── repository/
│ │ └── service/
│ └── go.mod
├── order-service/
│ ├── cmd/
│ │ └── server/
│ │ └── main.go
│ ├── internal/
│ │ ├── domain/
│ │ ├── handlers/
│ │ ├── repository/
│ │ ├── service/
│ │ └── clients/
│ └── go.mod
└── api-gateway/
├── cmd/
│ └── server/
│ └── main.go
└── go.mod
Service 1: User Service
// user-service/internal/domain/user.go
package domain
import (
"context"
"errors"
"time"
)
type User struct {
ID string `json:"id"`
Email string `json:"email"`
Name string `json:"name"`
Active bool `json:"active"`
CreatedAt time.Time `json:"created_at"`
}
var (
ErrUserNotFound = errors.New("user not found")
ErrUserExists = errors.New("user already exists")
)
type Repository interface {
Create(ctx context.Context, user *User) error
GetByID(ctx context.Context, id string) (*User, error)
GetByEmail(ctx context.Context, email string) (*User, error)
Update(ctx context.Context, user *User) error
}
// user-service/internal/service/user_service.go
package service
import (
"context"
"fmt"
"time"
"user-service/internal/domain"
)
type UserService struct {
repo domain.Repository
}
func NewUserService(repo domain.Repository) *UserService {
return &UserService{repo: repo}
}
func (s *UserService) CreateUser(ctx context.Context, email, name string) (*domain.User, error) {
existing, _ := s.repo.GetByEmail(ctx, email)
if existing != nil {
return nil, domain.ErrUserExists
}
user := &domain.User{
ID: generateID(),
Email: email,
Name: name,
Active: true,
CreatedAt: time.Now(),
}
if err := s.repo.Create(ctx, user); err != nil {
return nil, fmt.Errorf("failed to create user: %w", err)
}
return user, nil
}
func (s *UserService) GetUser(ctx context.Context, id string) (*domain.User, error) {
return s.repo.GetByID(ctx, id)
}
func (s *UserService) ValidateUser(ctx context.Context, id string) (bool, error) {
user, err := s.repo.GetByID(ctx, id)
if err != nil {
return false, err
}
return user.Active, nil
}
func generateID() string {
return fmt.Sprintf("user_%d", time.Now().UnixNano())
}
// user-service/internal/handlers/http_handler.go
package handlers
import (
"encoding/json"
"net/http"
"github.com/gorilla/mux"
"user-service/internal/service"
)
type HTTPHandler struct {
service *service.UserService
}
func NewHTTPHandler(service *service.UserService) *HTTPHandler {
return &HTTPHandler{service: service}
}
type CreateUserRequest struct {
Email string `json:"email"`
Name string `json:"name"`
}
func (h *HTTPHandler) CreateUser(w http.ResponseWriter, r *http.Request) {
var req CreateUserRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
respondError(w, http.StatusBadRequest, "invalid request")
return
}
user, err := h.service.CreateUser(r.Context(), req.Email, req.Name)
if err != nil {
respondError(w, http.StatusBadRequest, err.Error())
return
}
respondJSON(w, http.StatusCreated, user)
}
func (h *HTTPHandler) GetUser(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
user, err := h.service.GetUser(r.Context(), id)
if err != nil {
respondError(w, http.StatusNotFound, "user not found")
return
}
respondJSON(w, http.StatusOK, user)
}
func (h *HTTPHandler) ValidateUser(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
valid, err := h.service.ValidateUser(r.Context(), id)
if err != nil {
respondError(w, http.StatusNotFound, "user not found")
return
}
respondJSON(w, http.StatusOK, map[string]bool{"valid": valid})
}
func respondJSON(w http.ResponseWriter, status int, data interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(data)
}
func respondError(w http.ResponseWriter, status int, message string) {
respondJSON(w, status, map[string]string{"error": message})
}
// user-service/cmd/server/main.go
package main
import (
"database/sql"
"log"
"net/http"
"os"
"github.com/gorilla/mux"
_ "github.com/lib/pq"
"user-service/internal/handlers"
"user-service/internal/repository"
"user-service/internal/service"
)
func main() {
dbURL := os.Getenv("DATABASE_URL")
if dbURL == "" {
dbURL = "postgres://user:pass@localhost/users?sslmode=disable"
}
db, err := sql.Open("postgres", dbURL)
if err != nil {
log.Fatal(err)
}
defer db.Close()
repo := repository.NewPostgresRepository(db)
svc := service.NewUserService(repo)
handler := handlers.NewHTTPHandler(svc)
router := mux.NewRouter()
router.HandleFunc("/users", handler.CreateUser).Methods("POST")
router.HandleFunc("/users/{id}", handler.GetUser).Methods("GET")
router.HandleFunc("/users/{id}/validate", handler.ValidateUser).Methods("GET")
router.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("OK"))
})
port := os.Getenv("PORT")
if port == "" {
port = "8081"
}
log.Printf("User service starting on port %s", port)
if err := http.ListenAndServe(":"+port, router); err != nil {
log.Fatal(err)
}
}
Service 2: Product Service
// product-service/internal/domain/product.go
package domain
import (
"context"
"errors"
"time"
)
type Product struct {
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Price float64 `json:"price"`
Stock int `json:"stock"`
CreatedAt time.Time `json:"created_at"`
}
var (
ErrProductNotFound = errors.New("product not found")
ErrInsufficientStock = errors.New("insufficient stock")
)
type Repository interface {
Create(ctx context.Context, product *Product) error
GetByID(ctx context.Context, id string) (*Product, error)
Update(ctx context.Context, product *Product) error
ReserveStock(ctx context.Context, id string, quantity int) error
}
// product-service/internal/service/product_service.go
package service
import (
"context"
"fmt"
"time"
"product-service/internal/domain"
)
type ProductService struct {
repo domain.Repository
}
func NewProductService(repo domain.Repository) *ProductService {
return &ProductService{repo: repo}
}
func (s *ProductService) CreateProduct(ctx context.Context, name, desc string, price float64, stock int) (*domain.Product, error) {
product := &domain.Product{
ID: generateID(),
Name: name,
Description: desc,
Price: price,
Stock: stock,
CreatedAt: time.Now(),
}
if err := s.repo.Create(ctx, product); err != nil {
return nil, fmt.Errorf("failed to create product: %w", err)
}
return product, nil
}
func (s *ProductService) GetProduct(ctx context.Context, id string) (*domain.Product, error) {
return s.repo.GetByID(ctx, id)
}
func (s *ProductService) ReserveStock(ctx context.Context, id string, quantity int) error {
product, err := s.repo.GetByID(ctx, id)
if err != nil {
return err
}
if product.Stock < quantity {
return domain.ErrInsufficientStock
}
return s.repo.ReserveStock(ctx, id, quantity)
}
func generateID() string {
return fmt.Sprintf("product_%d", time.Now().UnixNano())
}
// product-service/cmd/server/main.go
package main
import (
"database/sql"
"encoding/json"
"log"
"net/http"
"os"
"github.com/gorilla/mux"
_ "github.com/lib/pq"
"product-service/internal/repository"
"product-service/internal/service"
)
func main() {
dbURL := os.Getenv("DATABASE_URL")
if dbURL == "" {
dbURL = "postgres://user:pass@localhost/products?sslmode=disable"
}
db, err := sql.Open("postgres", dbURL)
if err != nil {
log.Fatal(err)
}
defer db.Close()
repo := repository.NewPostgresRepository(db)
svc := service.NewProductService(repo)
router := mux.NewRouter()
router.HandleFunc("/products/{id}", func(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
product, err := svc.GetProduct(r.Context(), vars["id"])
if err != nil {
http.Error(w, err.Error(), http.StatusNotFound)
return
}
json.NewEncoder(w).Encode(product)
}).Methods("GET")
router.HandleFunc("/products/reserve", func(w http.ResponseWriter, r *http.Request) {
var req struct {
ProductID string `json:"product_id"`
Quantity int `json:"quantity"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if err := svc.ReserveStock(r.Context(), req.ProductID, req.Quantity); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]string{"status": "reserved"})
}).Methods("POST")
router.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
port := os.Getenv("PORT")
if port == "" {
port = "8082"
}
log.Printf("Product service starting on port %s", port)
if err := http.ListenAndServe(":"+port, router); err != nil {
log.Fatal(err)
}
}
Service 3: Order Service (Orchestrator)
// order-service/internal/clients/user_client.go
package clients
import (
"context"
"encoding/json"
"fmt"
"net/http"
"time"
)
type UserClient struct {
baseURL string
httpClient *http.Client
}
func NewUserClient(baseURL string) *UserClient {
return &UserClient{
baseURL: baseURL,
httpClient: &http.Client{
Timeout: 5 * time.Second,
},
}
}
func (c *UserClient) ValidateUser(ctx context.Context, userID string) (bool, error) {
url := fmt.Sprintf("%s/users/%s/validate", c.baseURL, userID)
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return false, err
}
resp, err := c.httpClient.Do(req)
if err != nil {
return false, fmt.Errorf("failed to call user service: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return false, fmt.Errorf("user service returned status %d", resp.StatusCode)
}
var result struct {
Valid bool `json:"valid"`
}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return false, err
}
return result.Valid, nil
}
// order-service/internal/clients/product_client.go
package clients
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"time"
)
type Product struct {
ID string `json:"id"`
Name string `json:"name"`
Price float64 `json:"price"`
Stock int `json:"stock"`
}
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, productID string) (*Product, error) {
url := fmt.Sprintf("%s/products/%s", c.baseURL, productID)
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return nil, err
}
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to call product service: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("product not found")
}
var product Product
if err := json.NewDecoder(resp.Body).Decode(&product); err != nil {
return nil, err
}
return &product, nil
}
func (c *ProductClient) ReserveStock(ctx context.Context, productID string, quantity int) error {
url := fmt.Sprintf("%s/products/reserve", c.baseURL)
reqBody := map[string]interface{}{
"product_id": productID,
"quantity": quantity,
}
body, err := json.Marshal(reqBody)
if err != nil {
return err
}
req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(body))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
resp, err := c.httpClient.Do(req)
if err != nil {
return fmt.Errorf("failed to reserve stock: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("failed to reserve stock: status %d", resp.StatusCode)
}
return nil
}
// order-service/internal/service/order_service.go
package service
import (
"context"
"fmt"
"time"
"order-service/internal/clients"
"order-service/internal/domain"
)
type OrderService struct {
repo domain.Repository
userClient *clients.UserClient
productClient *clients.ProductClient
}
func NewOrderService(
repo domain.Repository,
userClient *clients.UserClient,
productClient *clients.ProductClient,
) *OrderService {
return &OrderService{
repo: repo,
userClient: userClient,
productClient: productClient,
}
}
func (s *OrderService) CreateOrder(ctx context.Context, userID string, items []domain.OrderItem) (*domain.Order, error) {
// Validate user via User Service
valid, err := s.userClient.ValidateUser(ctx, userID)
if err != nil {
return nil, fmt.Errorf("failed to validate user: %w", err)
}
if !valid {
return nil, fmt.Errorf("user is not valid")
}
// Validate products and calculate total
var total float64
for i, item := range items {
product, err := s.productClient.GetProduct(ctx, item.ProductID)
if err != nil {
return nil, fmt.Errorf("failed to get product: %w", err)
}
items[i].Price = product.Price
total += product.Price * float64(item.Quantity)
}
// Reserve stock via Product Service
for _, item := range items {
if err := s.productClient.ReserveStock(ctx, item.ProductID, item.Quantity); err != nil {
return nil, fmt.Errorf("failed to reserve stock: %w", err)
}
}
order := &domain.Order{
ID: generateID(),
UserID: userID,
Items: items,
Total: total,
Status: "pending",
CreatedAt: time.Now(),
}
if err := s.repo.Create(ctx, order); err != nil {
return nil, fmt.Errorf("failed to create order: %w", err)
}
return order, nil
}
func generateID() string {
return fmt.Sprintf("order_%d", time.Now().UnixNano())
}
// order-service/cmd/server/main.go
package main
import (
"database/sql"
"encoding/json"
"log"
"net/http"
"os"
"github.com/gorilla/mux"
_ "github.com/lib/pq"
"order-service/internal/clients"
"order-service/internal/domain"
"order-service/internal/repository"
"order-service/internal/service"
)
func main() {
dbURL := os.Getenv("DATABASE_URL")
if dbURL == "" {
dbURL = "postgres://user:pass@localhost/orders?sslmode=disable"
}
userServiceURL := os.Getenv("USER_SERVICE_URL")
if userServiceURL == "" {
userServiceURL = "http://localhost:8081"
}
productServiceURL := os.Getenv("PRODUCT_SERVICE_URL")
if productServiceURL == "" {
productServiceURL = "http://localhost:8082"
}
db, err := sql.Open("postgres", dbURL)
if err != nil {
log.Fatal(err)
}
defer db.Close()
repo := repository.NewPostgresRepository(db)
userClient := clients.NewUserClient(userServiceURL)
productClient := clients.NewProductClient(productServiceURL)
svc := service.NewOrderService(repo, userClient, productClient)
router := mux.NewRouter()
router.HandleFunc("/orders", func(w http.ResponseWriter, r *http.Request) {
var req struct {
UserID string `json:"user_id"`
Items []domain.OrderItem `json:"items"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
order, err := svc.CreateOrder(r.Context(), req.UserID, req.Items)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(order)
}).Methods("POST")
router.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
port := os.Getenv("PORT")
if port == "" {
port = "8083"
}
log.Printf("Order service starting on port %s", port)
if err := http.ListenAndServe(":"+port, router); err != nil {
log.Fatal(err)
}
}
Docker Compose Setup
version: '3.8'
services:
user-service:
build: ./user-service
ports:
- "8081:8081"
environment:
- DATABASE_URL=postgres://user:pass@user-db:5432/users?sslmode=disable
- PORT=8081
depends_on:
- user-db
product-service:
build: ./product-service
ports:
- "8082:8082"
environment:
- DATABASE_URL=postgres://user:pass@product-db:5432/products?sslmode=disable
- PORT=8082
depends_on:
- product-db
order-service:
build: ./order-service
ports:
- "8083:8083"
environment:
- DATABASE_URL=postgres://user:pass@order-db:5432/orders?sslmode=disable
- USER_SERVICE_URL=http://user-service:8081
- PRODUCT_SERVICE_URL=http://product-service:8082
- PORT=8083
depends_on:
- order-db
- user-service
- product-service
user-db:
image: postgres:15
environment:
- POSTGRES_USER=user
- POSTGRES_PASSWORD=pass
- POSTGRES_DB=users
product-db:
image: postgres:15
environment:
- POSTGRES_USER=user
- POSTGRES_PASSWORD=pass
- POSTGRES_DB=products
order-db:
image: postgres:15
environment:
- POSTGRES_USER=user
- POSTGRES_PASSWORD=pass
- POSTGRES_DB=orders
Best Practices
- Service Boundaries: Define clear service boundaries based on business capabilities
- API Contracts: Use API versioning and maintain backward compatibility
- Service Discovery: Implement service registry for dynamic service location
- Circuit Breakers: Prevent cascading failures with circuit breaker pattern
- Distributed Tracing: Implement tracing to debug cross-service calls
- Health Checks: Provide health endpoints for monitoring
- Configuration Management: Externalize configuration
- Security: Implement service-to-service authentication
Common Pitfalls
- Distributed Monolith: Services too tightly coupled, defeating the purpose
- Chatty Services: Too many synchronous calls between services
- Shared Database: Multiple services accessing the same database
- Ignoring Network Failures: Not handling network errors gracefully
- No Service Versioning: Breaking changes without versioning
- Data Consistency Issues: Not handling eventual consistency
- Over-Engineering: Creating too many small services
When to Use Microservices Architecture
Use When:
- Application is large and complex with multiple domains
- Different services need independent scaling
- Teams are distributed and need autonomy
- Different services require different technologies
- Need to deploy services independently
- Organization can handle distributed systems complexity
Avoid When:
- Starting a new project with unclear requirements
- Team lacks distributed systems expertise
- Application is simple and doesn’t need scaling
- Organization can’t support the operational overhead
- Real-time consistency is critical across all operations
Advantages
- Independent Deployment: Deploy services without affecting others
- Technology Flexibility: Use different technologies per service
- Scalability: Scale individual services based on demand
- Team Autonomy: Teams can work independently on services
- Fault Isolation: Failures contained to individual services
- Easier Testing: Test services in isolation
- Better Resource Utilization: Optimize resources per service
Disadvantages
- Increased Complexity: Distributed systems are inherently complex
- Network Overhead: Inter-service communication adds latency
- Data Consistency: Eventual consistency is harder to reason about
- Operational Overhead: More services to deploy and monitor
- Debugging Challenges: Harder to trace issues across services
- Testing Complexity: Integration testing is more difficult
- Deployment Complexity: Need orchestration tools like Kubernetes
Microservices Architecture provides powerful benefits for large, complex applications but comes with significant operational complexity. Choose wisely based on your organization’s needs and capabilities.
Go Architecture Patterns Series: ← Previous: Modular Monolith | Series Overview | Next: Event-Driven Architecture →