Skip to content

Microservice Decomposition

When business complexity increases, you can decompose the monolithic application into multiple independent microservices. This document explains how to perform microservice decomposition.

When to Decompose

Consider decomposing microservices in these situations:

  1. Team Growth: Teams exceeding 10 people find monolithic application collaboration difficult
  2. Strong Module Independence: Certain modules can be developed and deployed completely independently
  3. Performance Bottlenecks: Certain modules need independent scaling
  4. Technology Heterogeneity: Different modules need different technology stacks
  5. Different Release Frequency: Some modules need frequent releases while others are relatively stable

Decomposition Principles

1. Decompose by Business Domain (DDD)

bingo-user-service      # User service (users, authentication, permissions)
bingo-order-service     # Order service (orders, payment)
bingo-product-service   # Product service (products, inventory)
bingo-notification-service  # Notification service (email, SMS, push)

2. Decompose by Technical Responsibility

bingo-api-gateway       # API gateway
bingo-auth-service      # Authentication service
bingo-business-service  # Business service
bingo-data-service      # Data service

3. Single Responsibility Principle

Each service is responsible for one business domain:

Good Decomposition:

  • user-service: User management
  • auth-service: Authentication/authorization
  • order-service: Order management

Poor Decomposition:

  • user-auth-order-service: Mixed domains

Decomposition Steps

Step 1: Identify Boundaries

Analyze existing code and identify business boundaries:

internal/apiserver/
├── biz/
│   ├── user/       → user-service
│   ├── auth/       → auth-service
│   ├── order/      → order-service
│   └── product/    → product-service

Step 2: Database Decomposition

Each service uses independent database:

bingo-user-service    → bingo_user_db
bingo-order-service   → bingo_order_db
bingo-product-service → bingo_product_db

Note: Cross-database joins need to be done through service calls.

Step 3: Define Service Interfaces

Define service interfaces using gRPC:

protobuf
// pkg/proto/user/user.proto
syntax = "proto3";

package user;

service UserService {
  rpc GetUser(GetUserRequest) returns (GetUserResponse);
  rpc CreateUser(CreateUserRequest) returns (CreateUserResponse);
}

message GetUserRequest {
  uint64 user_id = 1;
}

message GetUserResponse {
  uint64 id = 1;
  string username = 2;
  string email = 3;
}

Step 4: Implement Services

Create independent service project:

bingo-user-service/
├── cmd/
│   └── server/
│       └── main.go
├── internal/
│   ├── biz/
│   ├── store/
│   └── grpc/          # gRPC service implementation
├── pkg/
│   └── proto/
└── go.mod

Step 5: Inter-Service Calls

Call user service from other services:

go
// order-service calls user-service
import userpb "github.com/bingo/user-service/pkg/proto/user"

type OrderBiz struct {
    userClient userpb.UserServiceClient
}

func (b *OrderBiz) CreateOrder(ctx context.Context, req *CreateOrderRequest) error {
    // Call user service to validate user
    userResp, err := b.userClient.GetUser(ctx, &userpb.GetUserRequest{
        UserId: req.UserID,
    })
    if err != nil {
        return err
    }

    // Create order logic
    // ...
}

Service Discovery

Using Consul

go
// internal/pkg/discovery/consul.go
import "github.com/hashicorp/consul/api"

type ServiceDiscovery struct {
    client *api.Client
}

func (s *ServiceDiscovery) Register(name, addr string) error {
    registration := &api.AgentServiceRegistration{
        ID:      name + "-" + addr,
        Name:    name,
        Address: addr,
        Port:    8080,
        Check: &api.AgentServiceCheck{
            HTTP:     "http://" + addr + "/health",
            Interval: "10s",
        },
    }

    return s.client.Agent().ServiceRegister(registration)
}

func (s *ServiceDiscovery) Discover(name string) (string, error) {
    services, _, err := s.client.Health().Service(name, "", true, nil)
    if err != nil {
        return "", err
    }

    if len(services) == 0 {
        return "", errors.New("service not found")
    }

    // Simple load balancing
    service := services[rand.Intn(len(services))]
    addr := fmt.Sprintf("%s:%d", service.Service.Address, service.Service.Port)

    return addr, nil
}

Distributed Transactions

Saga Pattern

For cross-service transactions, use Saga pattern:

go
// Order creation Saga
type CreateOrderSaga struct {
    orderService   *OrderService
    productService *ProductService
    paymentService *PaymentService
}

func (s *CreateOrderSaga) Execute(ctx context.Context, req *CreateOrderRequest) error {
    // Step 1: Create order
    orderID, err := s.orderService.Create(ctx, req)
    if err != nil {
        return err
    }

    // Step 2: Decrease stock
    if err := s.productService.DecreaseStock(ctx, req.ProductID, req.Quantity); err != nil {
        // Compensate: Cancel order
        s.orderService.Cancel(ctx, orderID)
        return err
    }

    // Step 3: Charge payment
    if err := s.paymentService.Charge(ctx, req.Amount); err != nil {
        // Compensate: Restore stock
        s.productService.IncreaseStock(ctx, req.ProductID, req.Quantity)
        // Compensate: Cancel order
        s.orderService.Cancel(ctx, orderID)
        return err
    }

    return nil
}

Configuration Center

Use Consul as configuration center:

go
import "github.com/hashicorp/consul/api"

type ConfigCenter struct {
    client *api.Client
}

func (c *ConfigCenter) Get(key string) (string, error) {
    pair, _, err := c.client.KV().Get(key, nil)
    if err != nil {
        return "", err
    }

    return string(pair.Value), nil
}

func (c *ConfigCenter) Watch(key string, callback func(string)) {
    // Watch configuration changes
    go func() {
        var index uint64
        for {
            pair, meta, err := c.client.KV().Get(key, &api.QueryOptions{
                WaitIndex: index,
            })
            if err != nil {
                continue
            }

            if meta.LastIndex > index {
                index = meta.LastIndex
                callback(string(pair.Value))
            }
        }
    }()
}

Distributed Tracing

Integrate Jaeger for distributed request tracing:

go
import (
    "github.com/opentracing/opentracing-go"
    "github.com/uber/jaeger-client-go"
)

func InitTracer(serviceName string) (opentracing.Tracer, io.Closer) {
    cfg := &config.Configuration{
        ServiceName: serviceName,
        Sampler: &config.SamplerConfig{
            Type:  jaeger.SamplerTypeConst,
            Param: 1,
        },
        Reporter: &config.ReporterConfig{
            LocalAgentHostPort: "127.0.0.1:6831",
        },
    }

    tracer, closer, _ := cfg.NewTracer()
    opentracing.SetGlobalTracer(tracer)

    return tracer, closer
}

// Use in HTTP middleware
func TracingMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        span := opentracing.GlobalTracer().StartSpan(c.Request.URL.Path)
        defer span.Finish()

        c.Set("tracing-span", span)
        c.Next()
    }
}

API Gateway

Use Nginx or Kong as API gateway:

nginx
# nginx.conf
upstream user-service {
    server user-service-1:8080;
    server user-service-2:8080;
}

upstream order-service {
    server order-service-1:8080;
    server order-service-2:8080;
}

server {
    listen 80;

    location /api/users {
        proxy_pass http://user-service;
    }

    location /api/orders {
        proxy_pass http://order-service;
    }
}

Monitoring and Alerting

Each service exposes Prometheus metrics:

go
import "github.com/prometheus/client_golang/prometheus"

var (
    requestCounter = prometheus.NewCounterVec(
        prometheus.CounterOpts{
            Name: "api_requests_total",
            Help: "Total number of API requests",
        },
        []string{"service", "method", "status"},
    )
)

func init() {
    prometheus.MustRegister(requestCounter)
}

// Record in middleware
func MetricsMiddleware(serviceName string) gin.HandlerFunc {
    return func(c *gin.Context) {
        c.Next()

        requestCounter.WithLabelValues(
            serviceName,
            c.Request.Method,
            strconv.Itoa(c.Writer.Status()),
        ).Inc()
    }
}

Best Practices

  1. Clear service boundaries: Avoid circular dependencies
  2. Independent databases: Each service uses independent database
  3. Asynchronous communication: Use message queues to decouple services
  4. Circuit breaker and degradation: Use Hystrix etc. for circuit breakers
  5. Complete monitoring: Each service needs monitoring and logging
  6. Version management: Versioned APIs with backward compatibility

Next Step

Congratulations! You've completed the Bingo documentation. Return to the Introduction to revisit any topics, or start building your project with confidence.

Released under the Apache 2.0 License.