Microservices Architecture Best Practices: Evolution from Monolith to Distributed Systems


Introduction: Why Do We Need Microservices?

Over the past decade, microservices architecture has evolved from an emerging concept to the mainstream choice for enterprise applications. However, many teams often fall into the trap of “microservices for the sake of microservices,” leading to skyrocketing system complexity with minimal benefits.

This article will share microservices architecture best practices based on real project experience, helping you avoid detours on your technology evolution journey.

Core Values of Microservices Architecture

1. Clear Business Boundaries

The greatest value of microservices architecture lies in reflecting business boundaries through service boundaries. Each microservice should:

  • Single Responsibility: Focus on a specific business capability
  • Independent Deployment: Can be developed, tested, and deployed independently
  • Data Isolation: Owns independent data storage

2. Technology Heterogeneity

Different services can choose the most suitable technology stack based on requirements:

# User Service - High concurrency read/write
Tech Stack: Spring Boot + Redis + MySQL

# Order Service - Strong consistency requirements
Tech Stack: Spring Cloud + PostgreSQL + Kafka

# Recommendation Service - Machine Learning
Tech Stack: Python + TensorFlow + MongoDB

Design Principles: How to Divide Microservices

Domain-Driven Design (DDD)

Use domain-driven design to identify service boundaries:

// ❌ Wrong approach - Layered by technology
@Service
public class UserService {
    @Autowired
    private UserRepository userRepository;
    @Autowired
    private NotificationService notificationService;
}

// ✅ Correct approach - By business capability
@Service
public class UserRegistrationService {
    @Autowired
    private UserRepository userRepository;
    @Autowired
    private DomainEventPublisher eventPublisher;
}

Service Granularity Control

Golden Rule: A microservice should be completely rewritable by a small team (2-pizza team) within 2-4 weeks.

GranularityCharacteristicsApplicable Scenarios
Too Fine>20 servicesSimple business systems
Moderate5-15 servicesMost enterprise applications
Too Coarse<5 servicesLegacy system migration initial phase

Technology Stack Selection

Service Registry and Discovery

Consul vs Eureka vs Kubernetes DNS

# Consul configuration example
consul:
  service:
    name: user-service
    tags:
      - v1
      - production
    health_check:
      path: /actuator/health
      interval: 30s

Configuration Management

Spring Cloud Config vs Kubernetes ConfigMap

# Kubernetes ConfigMap example
apiVersion: v1
kind: ConfigMap
metadata:
  name: user-service-config
data:
  application.yml: |
    server:
      port: 8080
    spring:
      datasource:
        url: jdbc:mysql://mysql:3306/userdb
        username: ${DB_USERNAME}
        password: ${DB_PASSWORD}

Circuit Breaker and Rate Limiting

Hystrix vs Sentinel

@RestController
public class UserController {
    
    @GetMapping("/users/{id}")
    @SentinelResource(value = "getUser", 
        blockHandler = "handleBlock",
        fallback = "handleFallback")
    public User getUser(@PathVariable Long id) {
        return userService.findById(id);
    }
    
    public User handleBlock(Long id, BlockException ex) {
        return new User("System busy, please try again later");
    }
    
    public User handleFallback(Long id, Throwable ex) {
        return new User("Service degraded, returning default user");
    }
}

Data Consistency Strategies

Eventual Consistency Patterns

1. Saga Pattern

@Component
public class OrderSaga {
    
    @SagaStart
    public void createOrder(CreateOrderCommand command) {
        // 1. Create order
        Order order = orderService.createOrder(command);
        
        // 2. Reserve inventory
        sagaManager.associateWith("inventoryId", command.getInventoryId());
        inventoryService.reserveInventory(command.getInventoryId(), 
            command.getQuantity());
            
        // 3. Deduct user balance
        sagaManager.associateWith("userId", command.getUserId());
        userService.deductBalance(command.getUserId(), 
            command.getAmount());
    }
    
    @CompensatingEvent
    public void compensateOrder(OrderFailedEvent event) {
        orderService.cancelOrder(event.getOrderId());
    }
}

2. Event Sourcing

@Entity
public class UserAggregate {
    
    @EventSourcingHandler
    public void on(UserCreatedEvent event) {
        this.userId = event.getUserId();
        this.username = event.getUsername();
        this.email = event.getEmail();
    }
    
    @EventSourcingHandler
    public void on(UserEmailUpdatedEvent event) {
        this.email = event.getNewEmail();
    }
}

Monitoring and Observability

Three Pillars

  1. Metrics: System health status
  2. Tracing: Request call chains
  3. Logging: Detailed event records

Implementation Solution

# Prometheus + Grafana monitoring configuration
apiVersion: v1
kind: ServiceMonitor
metadata:
  name: user-service-monitor
spec:
  selector:
    matchLabels:
      app: user-service
  endpoints:
  - port: metrics
    interval: 30s
    path: /actuator/prometheus

Distributed Tracing

@RestController
public class UserController {
    
    @Autowired
    private Tracer tracer;
    
    @GetMapping("/users/{id}")
    public User getUser(@PathVariable Long id) {
        Span span = tracer.nextSpan()
            .name("getUser")
            .tag("user.id", id.toString())
            .start();
            
        try (Tracer.SpanInScope ws = tracer.withSpanInScope(span)) {
            return userService.findById(id);
        } finally {
            span.end();
        }
    }
}

Deployment Strategies

Containerization Best Practices

Dockerfile Optimization

# Multi-stage build
FROM openjdk:11-jre-slim as runtime
WORKDIR /app

# Create non-root user
RUN addgroup --system appgroup && adduser --system appuser --ingroup appgroup

COPY --from=build /app/target/user-service.jar app.jar
USER appuser

EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]

Kubernetes Deployment

apiVersion: apps/v1
kind: Deployment
metadata:
  name: user-service
spec:
  replicas: 3
  selector:
    matchLabels:
      app: user-service
  template:
    metadata:
      labels:
        app: user-service
    spec:
      containers:
      - name: user-service
        image: user-service:latest
        ports:
        - containerPort: 8080
        env:
        - name: SPRING_PROFILES_ACTIVE
          value: "kubernetes"
        resources:
          requests:
            memory: "256Mi"
            cpu: "250m"
          limits:
            memory: "512Mi"
            cpu: "500m"
        livenessProbe:
          httpGet:
            path: /actuator/health
            port: 8080
          initialDelaySeconds: 30
          periodSeconds: 10
        readinessProbe:
          httpGet:
            path: /actuator/health/readiness
            port: 8080
          initialDelaySeconds: 10
          periodSeconds: 5

Common Pitfalls and Solutions

1. Distributed Transaction Trap

Problem: Overuse of distributed transactions leads to excessive system complexity.

Solution: Adopt eventual consistency and handle cross-service transactions through event-driven architecture.

2. Service Call Chain Too Long

Problem: A single request needs to call more than 5 services.

Solutions:

  • Use API gateway to aggregate services
  • Implement CQRS pattern to separate read/write operations
  • Consider using GraphQL

3. Data Consistency Difficult to Guarantee

Problem: Cross-service data updates are difficult to maintain consistently.

Solutions:

  • Use event sourcing pattern
  • Implement Saga transaction management
  • Establish data validation and repair mechanisms

Real-world Case Study

E-commerce Platform Microservices Transformation

Background: An e-commerce platform migrated from monolithic architecture to microservices

Before Transformation:

  • Monolithic application, 500K lines of code
  • Deployment frequency: once per month
  • Recovery time from failure: 2 hours

After Transformation:

  • 12 microservices, average 40K lines per service
  • Deployment frequency: 10 times per day
  • Recovery time from failure: 5 minutes

Key Metrics Comparison:

MetricBeforeAfter
Development EfficiencyLowImproved by 300%
System Availability99.5%99.95%
ScalabilityVertical scalingHorizontal scaling
Technical DebtHighLow

Summary and Recommendations

Microservices architecture is not a silver bullet. Before implementation, you need to fully consider:

  1. Business Complexity: Simple businesses don’t need microservices
  2. Team Size: Small teams struggle to maintain too many services
  3. Technical Capability: Requires mature DevOps and monitoring capabilities
  4. Infrastructure: Needs comprehensive CI/CD and containerization platforms

Progressive Evolution Strategy

  1. Phase 1: Modularize monolithic application
  2. Phase 2: Extract core services
  3. Phase 3: Full microservices transformation
  4. Phase 4: Service mesh implementation

Remember: The purpose of architecture evolution is to better support business development, not for technology’s sake.

References


This article is based on real project experience. Feel free to leave comments if you have questions. Follow GeekCattle for more technical insights!