
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.
Granularity | Characteristics | Applicable Scenarios |
---|---|---|
Too Fine | >20 services | Simple business systems |
Moderate | 5-15 services | Most enterprise applications |
Too Coarse | <5 services | Legacy 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
- Metrics: System health status
- Tracing: Request call chains
- 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:
Metric | Before | After |
---|---|---|
Development Efficiency | Low | Improved by 300% |
System Availability | 99.5% | 99.95% |
Scalability | Vertical scaling | Horizontal scaling |
Technical Debt | High | Low |
Summary and Recommendations
Microservices architecture is not a silver bullet. Before implementation, you need to fully consider:
- Business Complexity: Simple businesses don’t need microservices
- Team Size: Small teams struggle to maintain too many services
- Technical Capability: Requires mature DevOps and monitoring capabilities
- Infrastructure: Needs comprehensive CI/CD and containerization platforms
Progressive Evolution Strategy
- Phase 1: Modularize monolithic application
- Phase 2: Extract core services
- Phase 3: Full microservices transformation
- Phase 4: Service mesh implementation
Remember: The purpose of architecture evolution is to better support business development, not for technology’s sake.
References
- Microservices.io - Microservices design patterns
- Spring Cloud Official Documentation
- Kubernetes Microservices Best Practices
- Microservices Design Patterns
This article is based on real project experience. Feel free to leave comments if you have questions. Follow GeekCattle for more technical insights!