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:
- Team Growth: Teams exceeding 10 people find monolithic application collaboration difficult
- Strong Module Independence: Certain modules can be developed and deployed completely independently
- Performance Bottlenecks: Certain modules need independent scaling
- Technology Heterogeneity: Different modules need different technology stacks
- 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 service3. Single Responsibility Principle
Each service is responsible for one business domain:
✅ Good Decomposition:
user-service: User managementauth-service: Authentication/authorizationorder-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-serviceStep 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_dbNote: Cross-database joins need to be done through service calls.
Step 3: Define Service Interfaces
Define service interfaces using gRPC:
// 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.modStep 5: Inter-Service Calls
Call user service from other services:
// 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
// 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:
// 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:
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:
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.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:
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
- Clear service boundaries: Avoid circular dependencies
- Independent databases: Each service uses independent database
- Asynchronous communication: Use message queues to decouple services
- Circuit breaker and degradation: Use Hystrix etc. for circuit breakers
- Complete monitoring: Each service needs monitoring and logging
- 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.