Microservices Architecture: Nguyên Tắc, Pattern & Cạm Bẫy

Microservices là một trong những pattern kiến trúc được bàn tán nhiều nhất — và cũng bị hiểu sai nhiều nhất — trong phát triển phần mềm hiện đại. Netflix, Amazon, Uber, Spotify đều chạy trên microservices. Vậy đây chắc chắn là kiến trúc đúng cho mọi người, phải không?
Sai. Microservices giải quyết những problem cụ thể ở những quy mô cụ thể. Áp dụng quá sớm, chúng tạo ra nhiều complexity hơn những problem chúng giải quyết. Áp dụng đúng cách, chúng mở khóa khả năng deploy độc lập, team autonomy, và scaling chi tiết.
Trong bài viết này, chúng ta sẽ đề cập:
✅ Microservices thực sự là gì (và không phải là gì)
✅ Nguyên tắc cốt lõi: single responsibility, autonomous deployment, decentralized data
✅ Ranh giới service và bounded context
✅ Giao tiếp giữa các service: sync vs async
✅ API gateway pattern
✅ Quản lý dữ liệu: database per service, saga, eventual consistency
✅ Testing microservices: contract testing, integration testing
✅ Alignment tổ chức: Conway's Law, team topologies
✅ Khi nào KHÔNG nên dùng microservices
✅ Chiến lược migration từ monolith sang microservices
Microservices Là Gì?
Microservices architecture là một hệ thống được tạo thành từ các service nhỏ, có thể deploy độc lập, trong đó mỗi service chạy process riêng, sở hữu data riêng, và giao tiếp với các service khác thông qua API được định nghĩa rõ ràng.
So sánh với monolith nơi mọi chức năng nằm trong một đơn vị deploy duy nhất. Trong microservices, mỗi service:
- Deploy độc lập — deploy payment service mà không cần đụng đến user service
- Scale độc lập — scale order service trong giờ cao điểm mà không cần scale tất cả
- Phát triển độc lập — các team khác nhau làm việc trên các service khác nhau
- Không phụ thuộc công nghệ — user service có thể dùng Java, notification service có thể dùng Go
Microservices KHÔNG Phải Là Gì
Hãy làm rõ những hiểu lầm phổ biến:
- ❌ "Chỉ là service nhỏ" — Size không quan trọng. Quan trọng là ranh giới và ownership, không phải số dòng code.
- ❌ "SOA đổi tên" — SOA (Service-Oriented Architecture) dựa vào orchestration tập trung (ESB). Microservices ưu tiên choreography phi tập trung và smart endpoint với dumb pipe.
- ❌ "Luôn tốt hơn monolith" — Với team nhỏ và sản phẩm giai đoạn đầu, monolith gần như luôn là lựa chọn tốt hơn. (Xem Monolithic Architecture Guide)
- ❌ "Một service per REST endpoint" — Service đại diện cho business capability, không phải endpoint API riêng lẻ.
Nguyên Tắc Cốt Lõi
1. Single Responsibility (Business Capability)
Mỗi service nên đại diện cho một business capability duy nhất — thứ mà doanh nghiệp thực sự làm, không chỉ là một abstraction kỹ thuật.
Ranh giới service tốt (business capability):
- Order Service — quản lý vòng đời đơn hàng (tạo, cập nhật, hủy, hoàn thành)
- Payment Service — xử lý thanh toán, hoàn tiền, xuất hóa đơn
- Inventory Service — theo dõi tồn kho, đặt trước, nhập hàng
Ranh giới service tệ (chia theo kỹ thuật):
- ❌ Database Service — một service chỉ để truy cập DB
- ❌ Validation Service — một service chỉ để validate input
- ❌ Logging Service — một service chỉ để ghi log
Câu hỏi đúng không phải "service nên nhỏ cỡ nào?" — mà là "service này sở hữu business capability nào?"
2. Autonomous Deployment
Mỗi service có thể được build, test, và deploy độc lập mà không cần phối hợp với các service khác.
Điều này có nghĩa:
- Mỗi service có CI/CD pipeline riêng
- Deploy service A không yêu cầu redeploy service B
- Các service giao tiếp qua versioned API — không có shared library buộc phải deploy đồng bộ
Nếu deploy một service đòi hỏi phải deploy các service khác cùng lúc, bạn không có microservices — bạn có một distributed monolith.
3. Decentralized Data Management
Mỗi service sở hữu data riêng và là service duy nhất có thể đọc hoặc ghi vào database của nó. Các service khác phải request data thông qua API của service đó.
Tại sao không share database?
- Coupling — nếu hai service share một bảng, thay đổi schema đòi hỏi phối hợp cả hai service
- Ownership — ai quyết định cấu trúc bảng? Ai quản lý migration?
- Scalability — bạn không thể scale data storage độc lập cho từng service
- Technology lock-in — mọi service phải dùng cùng công nghệ database
Trade-off: query dữ liệu xuyên service trở nên khó hơn. Bạn không thể đơn giản JOIN giữa user data và order data nữa. Đây là thách thức cơ bản của quản lý data trong microservices, và chúng ta sẽ đề cập trong phần Data Management.
4. Design for Failure
Trong distributed system, lỗi mạng không phải ngoại lệ — mà là bình thường. Service sập, network bị chia cắt, response bị timeout. Microservices phải được thiết kế để xử lý partial failure một cách graceful.
Pattern chính:
- Circuit breaker — ngừng gọi service đang lỗi; dùng response mặc định thay thế
- Timeout — đừng chờ mãi mãi
- Retry with backoff — thử lại với delay tăng dần theo cấp số nhân
- Bulkhead — cô lập lỗi để một service sập không lan sang service khác
// Circuit breaker với Resilience4j (Spring Boot)
@CircuitBreaker(name = "paymentService", fallbackMethod = "paymentFallback")
@Retry(name = "paymentService", fallbackMethod = "paymentFallback")
@TimeLimiter(name = "paymentService")
public CompletableFuture<PaymentResult> processPayment(PaymentRequest request) {
return CompletableFuture.supplyAsync(() ->
paymentClient.charge(request)
);
}
// Fallback khi payment service không hoạt động
public CompletableFuture<PaymentResult> paymentFallback(
PaymentRequest request, Exception ex) {
log.warn("Payment service không khả dụng, đưa vào hàng đợi: {}", ex.getMessage());
paymentQueue.enqueue(request); // Đưa vào hàng đợi xử lý sau
return CompletableFuture.completedFuture(
PaymentResult.pending("Payment đã được đưa vào hàng đợi xử lý")
);
}Ranh Giới Service và Bounded Context
Phần khó nhất của microservices không phải công nghệ — mà là quyết định vẽ ranh giới service ở đâu. Vẽ sai, và bạn sẽ dành hết thời gian quản lý giao tiếp xuyên service cho những thứ lẽ ra chỉ là một function call đơn giản.
Domain-Driven Design (DDD) và Bounded Context
Kỹ thuật tốt nhất để tìm ranh giới service đến từ Domain-Driven Design (DDD). Một bounded context là ranh giới trong đó một domain model cụ thể nhất quán và có ý nghĩa.
Xét một hệ thống e-commerce. Từ "Product" có nghĩa khác nhau trong các context khác nhau:
Trong context Catalog, product có description và hình ảnh. Trong context Inventory, product có mức tồn kho và vị trí kho. Trong context Pricing, product có giá và quy tắc giảm giá.
Mỗi bounded context trở thành ứng cử viên cho một microservice. Chúng chia sẻ product ID, nhưng mỗi cái sở hữu góc nhìn riêng về product.
Heuristic Để Tìm Ranh Giới Service
Khi quyết định ranh giới service, hãy đặt các câu hỏi sau:
- Cái này có thể deploy độc lập không? — Nếu thay đổi service này luôn đòi hỏi thay đổi service khác, ranh giới sai rồi.
- Một team có sở hữu cái này không? — Ranh giới service tốt phải align với ownership của team.
- Data có cohesive không? — Data luôn được đọc/ghi cùng nhau nên ở cùng nhau.
- Giao tiếp có chatty quá không? — Nếu hai service liên tục gọi nhau, có lẽ chúng nên là một service.
Dấu hiệu ranh giới của bạn sai:
- ❌ Hai service luôn deploy cùng nhau
- ❌ Một service gọi service khác cho mọi request
- ❌ Data duplication mà không có chiến lược đồng bộ rõ ràng
- ❌ Circular dependency giữa các service
- ❌ Một "God service" điều phối mọi thứ
Giao Tiếp Giữa Các Service
Service cần nói chuyện với nhau. Có hai cách tiếp cận cơ bản: synchronous (request/response) và asynchronous (event-driven).
Synchronous Communication: REST & gRPC
REST (HTTP/JSON) là cách phổ biến nhất. Service A tạo HTTP request đến Service B và chờ response.
// Order Service gọi User Service (REST)
class OrderService {
async createOrder(userId: string, items: OrderItem[]): Promise<Order> {
// Gọi đồng bộ đến User Service
const user = await fetch(`http://user-service/users/${userId}`)
.then(res => {
if (!res.ok) throw new Error(`User not found: ${userId}`);
return res.json();
});
// Gọi đồng bộ đến Payment Service
const payment = await fetch('http://payment-service/payments', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
userId: user.id,
amount: calculateTotal(items),
}),
}).then(res => res.json());
return this.orderRepository.save({
userId,
items,
paymentId: payment.id,
status: 'CONFIRMED',
});
}
}gRPC dùng HTTP/2, Protocol Buffers (binary serialization), và nhanh hơn REST/JSON đáng kể. Rất lý tưởng cho giao tiếp nội bộ giữa các service.
// user_service.proto
syntax = "proto3";
service UserService {
rpc GetUser(GetUserRequest) returns (UserResponse);
rpc ValidateUser(ValidateUserRequest) returns (ValidateUserResponse);
}
message GetUserRequest {
string user_id = 1;
}
message UserResponse {
string id = 1;
string name = 2;
string email = 3;
bool is_active = 4;
}So sánh REST vs gRPC:
| Tính năng | REST (HTTP/JSON) | gRPC (HTTP/2 + Protobuf) |
|---|---|---|
| Format | Text (JSON) | Binary (Protobuf) |
| Tốc độ | Chậm hơn (serialization overhead) | Nhanh hơn (binary compact) |
| Schema | Tùy chọn (OpenAPI) | Bắt buộc (file .proto) |
| Streaming | Hạn chế | Bidirectional streaming |
| Hỗ trợ browser | Tự nhiên | Cần gRPC-Web proxy |
| Phù hợp cho | Public API, external client | Giao tiếp nội bộ giữa service |
Bẫy Synchronous: Temporal Coupling
Vấn đề lớn nhất của giao tiếp synchronous là temporal coupling — Service A chỉ hoạt động được khi Service B available ngay lúc đó.
Nếu Notification Service sập, Payment Service lỗi theo, nghĩa là Order Service cũng lỗi. Đây gọi là cascading failure — một điểm lỗi duy nhất kéo sập toàn bộ chuỗi.
Asynchronous Communication: Event & Message
Giao tiếp asynchronous giải quyết temporal coupling. Service giao tiếp bằng cách publish event lên message broker. Các service khác subscribe những event đó và phản ứng độc lập.
// Order Service — publish event (Spring Boot + Kafka)
@Service
public class OrderService {
private final KafkaTemplate<String, OrderEvent> kafkaTemplate;
public Order createOrder(CreateOrderCommand command) {
// 1. Lưu order locally
Order order = orderRepository.save(
Order.create(command.getUserId(), command.getItems())
);
// 2. Publish event — các service khác phản ứng async
kafkaTemplate.send("order-events", new OrderCreatedEvent(
order.getId(),
order.getUserId(),
order.getItems(),
order.getTotalAmount(),
Instant.now()
));
return order;
}
}
// Payment Service — subscribe event
@Component
public class PaymentEventHandler {
@KafkaListener(topics = "order-events", groupId = "payment-service")
public void handleOrderCreated(OrderCreatedEvent event) {
// Xử lý thanh toán bất đồng bộ
paymentService.processPayment(
event.getUserId(),
event.getTotalAmount(),
event.getOrderId()
);
}
}Lợi thế chính của async communication:
- Không temporal coupling — Order Service không quan tâm Payment Service có đang sập không; event sẽ được xử lý khi nó hoạt động lại
- Scalability tốt hơn — consumer có thể xử lý event theo tốc độ riêng
- Loose coupling — publisher không biết (và không quan tâm) ai đang lắng nghe
- Event replay — event có thể được replay để rebuild state hoặc thêm consumer mới
Trade-off: Giao tiếp asynchronous tạo ra eventual consistency. Order được tạo trước khi payment được xác nhận. Điều này đòi hỏi xử lý cẩn thận (xem Saga Pattern).
Khi Nào Dùng Sync vs Async
| Use Case | Sync (REST/gRPC) | Async (Event/Message) |
|---|---|---|
| User cần response ngay lập tức | ✅ | ❌ |
| Query data từ service khác | ✅ | ❌ |
| Notification fire-and-forget | ❌ | ✅ |
| Operation chạy lâu | ❌ | ✅ |
| Nhiều consumer cần cùng data | ❌ | ✅ |
| Tạo order → thanh toán → kho | Có thể | ✅ Ưu tiên |
Nguyên tắc thực tế: Dùng synchronous cho read (query), dùng asynchronous cho write (command gây side effect ở service khác).
API Gateway Pattern
Khi client (web app, mobile app) cần gọi nhiều microservice, bạn không muốn chúng tạo 5 HTTP call riêng biệt để render một trang duy nhất. API gateway nằm giữa client và service, cung cấp điểm truy cập duy nhất.
API Gateway Làm Gì
- Request routing — định tuyến request đến backend service phù hợp
- Composition/Aggregation — gom data từ nhiều service thành một response duy nhất
- Authentication & Authorization — validate token, kiểm soát quyền truy cập
- Rate limiting — bảo vệ backend service khỏi traffic spike
- Protocol translation — chuyển đổi giữa HTTP/REST (bên ngoài) và gRPC (bên trong)
- SSL termination — xử lý HTTPS ở edge
- Caching — cache data được request thường xuyên
// API Gateway — gom data từ nhiều service
// (ví dụ đơn giản với Express.js)
app.get('/api/dashboard/:userId', async (req, res) => {
const userId = req.params.userId;
// Gọi song song đến nhiều service
const [user, orders, notifications] = await Promise.all([
fetch(`http://user-service/users/${userId}`).then(r => r.json()),
fetch(`http://order-service/users/${userId}/orders?limit=5`).then(r => r.json()),
fetch(`http://notification-service/users/${userId}/unread`).then(r => r.json()),
]);
// Gom thành một response duy nhất
res.json({
user: { name: user.name, email: user.email },
recentOrders: orders,
unreadNotifications: notifications.count,
});
});Backend for Frontend (BFF)
Một biến thể là pattern Backend for Frontend — gateway riêng cho mỗi loại client, tối ưu cho nhu cầu của client đó:
Web BFF trả về response giàu dữ liệu theo hướng HTML. Mobile BFF trả về JSON compact được tối ưu cho thiết bị giới hạn băng thông.
Anti-Pattern API Gateway
- ❌ Business logic trong gateway — gateway nên route và aggregate, không phải tính toán
- ❌ Một gateway khổng lồ duy nhất — trở thành bottleneck và single point of failure; cân nhắc nhiều gateway per domain
- ❌ Gateway làm transformation layer — đừng map giữa domain model trong gateway
Service Discovery
Trong môi trường microservices, service được deploy trên nhiều host, container, hoặc pod. Vị trí mạng (IP address, port) thay đổi liên tục khi instance được tạo, hủy, hoặc scale. Service discovery giải quyết câu hỏi: "Service B đang ở đâu ngay bây giờ?"
Client-Side Discovery
Client query một service registry để tìm các instance available, rồi load-balance giữa chúng.
Công cụ: Netflix Eureka, HashiCorp Consul, Apache ZooKeeper
Server-Side Discovery
Client gửi request đến load balancer, load balancer query registry và route request.
Công cụ: Kubernetes Services (built-in), AWS Elastic Load Balancer, Nginx
Trong Kubernetes, service discovery gần như miễn phí — mỗi service được cấp DNS name (payment-service.default.svc.cluster.local) tự động resolve đến các pod healthy.
Data Management Pattern
Quản lý data là nơi microservices trở nên thực sự khó. Trong monolith, bạn có thể JOIN qua bất kỳ bảng nào. Trong microservices, mỗi service sở hữu database riêng. Làm sao xử lý transaction trải rộng nhiều service?
Database per Service
Mỗi service có database riêng, và không service nào khác có thể truy cập trực tiếp.
Lưu ý rằng các service khác nhau có thể dùng database khác nhau. Order Service dùng PostgreSQL cho transactional guarantee. Product Service dùng MongoDB cho flexible document schema. Đây gọi là polyglot persistence.
Vấn Đề Cross-Service Query
Làm sao hiển thị đơn hàng với tên khách hàng và chi tiết sản phẩm khi data đó nằm ở ba database khác nhau?
Cách 1: API Composition
Service cần data sẽ gọi API của các service khác:
// Order Service — API Composition
async getOrderDetails(orderId: string): Promise<OrderDetails> {
const order = await this.orderRepository.findById(orderId);
// Gọi User Service để lấy thông tin khách hàng
const user = await this.userClient.getUser(order.userId);
// Gọi Product Service để lấy chi tiết sản phẩm
const products = await Promise.all(
order.items.map(item =>
this.productClient.getProduct(item.productId)
)
);
return {
orderId: order.id,
customerName: user.name,
items: order.items.map((item, i) => ({
productName: products[i].name,
quantity: item.quantity,
price: item.price,
})),
total: order.totalAmount,
status: order.status,
};
}Cách 2: Data Duplication (CQRS-lite)
Mỗi service lưu bản sao read-only của data mà nó thường xuyên cần. Data được giữ đồng bộ qua event:
// Order Service — lưu data denormalized của user
@KafkaListener(topics = "user-events", groupId = "order-service")
public void handleUserUpdated(UserUpdatedEvent event) {
// Cập nhật cache tên user local
orderRepository.updateCustomerName(event.getUserId(), event.getName());
}Cách này nhanh hơn (không cần cross-service call để đọc) nhưng tạo ra eventual consistency — tên user trong cache có thể cũ vài giây sau khi cập nhật.
Saga Pattern
Trong monolith, tạo đơn hàng có thể trông như thế này:
BEGIN TRANSACTION;
INSERT INTO orders (...);
UPDATE inventory SET quantity = quantity - 1 WHERE product_id = ?;
INSERT INTO payments (...);
COMMIT;Trong microservices, bạn không thể làm distributed transaction xuyên database (thực ra là không nên — two-phase commit không scale). Thay vào đó, bạn dùng saga — chuỗi transaction local được phối hợp bằng event.
Choreography-Based Saga (mỗi service phản ứng với event):
Nếu thanh toán thất bại? Saga chạy compensating transaction ngược lại:
Orchestration-Based Saga (orchestrator trung tâm phối hợp các bước):
// Saga Orchestrator
@Component
public class CreateOrderSaga {
public void execute(CreateOrderCommand command) {
SagaBuilder.create()
.step("Create Order")
.invoke(() -> orderService.createOrder(command))
.compensate(() -> orderService.cancelOrder(command.getOrderId()))
.step("Reserve Inventory")
.invoke(() -> inventoryService.reserve(command.getItems()))
.compensate(() -> inventoryService.release(command.getItems()))
.step("Process Payment")
.invoke(() -> paymentService.charge(command.getUserId(), command.getTotal()))
.compensate(() -> paymentService.refund(command.getPaymentId()))
.step("Confirm Order")
.invoke(() -> orderService.confirmOrder(command.getOrderId()))
.build()
.execute();
}
}Choreography vs Orchestration:
| Khía cạnh | Choreography | Orchestration |
|---|---|---|
| Phối hợp | Phi tập trung (event) | Tập trung (orchestrator) |
| Coupling | Lỏng lẻo hơn | Chặt hơn (với orchestrator) |
| Visibility | Khó theo dõi flow | Dễ thấy toàn bộ flow |
| Complexity | Đơn giản cho 2-3 bước | Tốt hơn cho 4+ bước |
| Debug | Khó hơn (theo dõi event) | Dễ hơn (check orchestrator) |
Eventual Consistency
Trong hệ thống microservices, strong consistency xuyên service không thể đạt được (xem CAP theorem). Thay vào đó, bạn chấp nhận eventual consistency: hệ thống sẽ trở nên nhất quán theo thời gian, nhưng tại bất kỳ thời điểm nào, các service khác nhau có thể có góc nhìn hơi khác nhau về data.
Điều này đòi hỏi thay đổi tư duy:
- Thiết kế UI — hiển thị "Đang xử lý..." thay vì trạng thái xác nhận ngay lập tức
- Idempotency — mọi operation phải an toàn khi retry (vì message có thể được gửi nhiều lần)
- Conflict resolution — có chiến lược khi data bị lệch
// Idempotent event handler — an toàn khi xử lý cùng event hai lần
@KafkaListener(topics = "payment-events")
public void handlePaymentCompleted(PaymentCompletedEvent event) {
// Dùng event ID làm idempotency key
if (processedEvents.contains(event.getEventId())) {
log.info("Event đã xử lý, bỏ qua: {}", event.getEventId());
return;
}
orderService.confirmOrder(event.getOrderId());
processedEvents.add(event.getEventId());
}Deployment và Infrastructure
Microservices và container đi đôi với nhau. Mỗi service được đóng gói thành container image và được orchestrate bằng Kubernetes (hoặc nền tảng tương tự).
Container per Service
# Order Service — Dockerfile
FROM eclipse-temurin:21-jre-alpine
COPY target/order-service.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]Kubernetes Deployment
# order-service-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: order-service
spec:
replicas: 3 # Scale độc lập
selector:
matchLabels:
app: order-service
template:
metadata:
labels:
app: order-service
spec:
containers:
- name: order-service
image: registry.example.com/order-service:v2.1.0
ports:
- containerPort: 8080
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
readinessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 10
---
apiVersion: v1
kind: Service
metadata:
name: order-service
spec:
selector:
app: order-service
ports:
- port: 80
targetPort: 8080Với Kubernetes, bạn được:
- Service discovery —
order-service.default.svc.cluster.local - Load balancing — traffic phân phối đều giữa 3 replica
- Health check — tự động restart pod không healthy
- Rolling deployment — deploy zero-downtime
- Auto-scaling — scale dựa trên CPU/memory/custom metric
Observability: Logging, Monitoring, Tracing
Trong monolith, debug rất đơn giản — check log. Trong microservices, một user request có thể chạy qua 5 service khác nhau. Không có observability phù hợp, debug là ác mộng.
Ba Trụ Cột Observability
1. Centralized Logging
Gom log từ tất cả service vào một nền tảng duy nhất (ELK Stack, Grafana Loki, Datadog).
Mỗi log entry phải chứa correlation ID — identifier duy nhất theo dõi request xuyên tất cả service:
// Lan truyền Correlation ID
@Component
public class CorrelationFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
String correlationId = httpRequest.getHeader("X-Correlation-ID");
if (correlationId == null) {
correlationId = UUID.randomUUID().toString();
}
MDC.put("correlationId", correlationId); // Thêm vào tất cả log entry
chain.doFilter(request, response);
MDC.remove("correlationId");
}
}2. Distributed Tracing
Theo dõi hành trình đầy đủ của request xuyên các service bằng công cụ như Jaeger, Zipkin, hoặc OpenTelemetry.
Mỗi bước là một span. Tất cả span của một request chia sẻ trace ID. Bạn có thể visualize chính xác thời gian tiêu ở đâu và service nào là bottleneck.
3. Metric & Monitoring
Theo dõi RED metric cho mỗi service:
- Rate — request per second
- Error — tỉ lệ lỗi phần trăm
- Duration — phân bố response time (p50, p95, p99)
Công cụ: Prometheus + Grafana, Datadog, New Relic
Testing Microservices
Testing microservices khác biệt cơ bản so với testing monolith. Bạn không thể chạy toàn bộ hệ thống cho mỗi bài test. Thay vào đó, dùng testing pyramid được điều chỉnh cho distributed system.
Unit Test (Nhanh Nhất, Nhiều Nhất)
Test từng class và function riêng lẻ. Giống như trong monolith.
@Test
void shouldCalculateOrderTotal() {
Order order = new Order();
order.addItem(new OrderItem("product-1", 2, new BigDecimal("10.00")));
order.addItem(new OrderItem("product-2", 1, new BigDecimal("25.00")));
assertEquals(new BigDecimal("45.00"), order.getTotalAmount());
}Contract Test (Chìa Khóa Cho Microservices)
Contract test xác minh rằng API contract giữa hai service được duy trì. Nếu User Service thay đổi format response, contract test sẽ bắt được trước khi deploy.
// Consumer side — Order Service định nghĩa kỳ vọng từ User Service
@Pact(consumer = "order-service", provider = "user-service")
public V4Pact createPact(PactDslWithProvider builder) {
return builder
.given("user 123 exists")
.uponReceiving("a request for user 123")
.path("/users/123")
.method("GET")
.willRespondWith()
.status(200)
.body(newJsonBody(body -> {
body.stringType("id", "123");
body.stringType("name", "John Doe");
body.stringType("email", "[email protected]");
}).build())
.toPact(V4Pact.class);
}
@Test
@PactTestFor(pactMethod = "createPact")
void shouldGetUserFromUserService(MockServer mockServer) {
UserClient client = new UserClient(mockServer.getUrl());
User user = client.getUser("123");
assertEquals("John Doe", user.getName());
assertEquals("[email protected]", user.getEmail());
}Contract test chạy trong CI pipeline của mỗi service. Nếu team User Service thay đổi API theo cách phá vỡ contract, build của họ fail — không phải build của Order Service.
Integration Test
Test rằng service hoạt động đúng với database riêng và dependency bên ngoài (dùng test container):
@SpringBootTest
@Testcontainers
class OrderServiceIntegrationTest {
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16");
@Container
static KafkaContainer kafka = new KafkaContainer(
DockerImageName.parse("confluentinc/cp-kafka:7.5.0")
);
@Test
void shouldCreateOrderAndPublishEvent() {
// Given
CreateOrderCommand command = new CreateOrderCommand("user-123", items);
// When
Order order = orderService.createOrder(command);
// Then
assertNotNull(order.getId());
assertEquals("PENDING", order.getStatus());
// Xác minh event đã được publish lên Kafka
ConsumerRecord<String, String> record = KafkaTestUtils
.getSingleRecord(consumer, "order-events");
assertThat(record.value()).contains("OrderCreated");
}
}End-to-End Test (Ít Nhất, Tốn Kém Nhất)
Test toàn bộ hành trình người dùng xuyên tất cả service. Chạy ít thôi — chúng chậm, hay flaky, và tốn kém khi maintain.
Alignment Tổ Chức: Conway's Law
"Bất kỳ tổ chức nào thiết kế hệ thống sẽ tạo ra thiết kế mà cấu trúc là bản sao của cấu trúc giao tiếp trong tổ chức đó." — Melvin Conway
Đây không chỉ là quan sát — đây là quy luật tự nhiên trong software engineering. Kiến trúc của bạn sẽ phản ánh cấu trúc team, dù bạn có kế hoạch hay không.
Inverse Conway Maneuver
Tổ chức thông minh dùng Conway's Law có chủ đích: thiết kế cấu trúc team phù hợp với kiến trúc bạn muốn.
Mỗi team:
- Sở hữu một hoặc nhiều service end-to-end (phát triển, testing, deployment, vận hành)
- Cross-functional (frontend, backend, QA, DevOps trong cùng team)
- Giao tiếp qua API, không phải meeting
- Tự chủ hoàn toàn về lựa chọn công nghệ trong service của mình
Amazon gọi đây là "two-pizza team" — nhỏ đủ để hai chiếc pizza nuôi cả team (5-8 người).
Team Topologies cho Microservices
Dựa trên framework Team Topologies:
- Stream-aligned team — sở hữu một business capability end-to-end (team Order, team Payment)
- Platform team — cung cấp shared infrastructure (Kubernetes, CI/CD, observability)
- Enabling team — giúp stream-aligned team áp dụng practice mới (security, performance)
- Complicated subsystem team — sở hữu component phức tạp kỹ thuật (ML pipeline, video encoding)
Khi Nào KHÔNG Nên Dùng Microservices
Microservices không phải cải tiến phổ quát. Đây là những tình huống chúng là lựa chọn sai:
1. Team Nhỏ (< 10 Developer)
Nếu bạn có 5 developer quản lý 15 microservice, phần lớn thời gian của họ đi vào infrastructure, không phải business logic. Monolith với module sạch sẽ năng suất hơn nhiều.
2. Sản Phẩm Giai Đoạn Đầu
Bạn chưa hiểu đủ domain của mình. Ranh giới service vẽ quá sớm sẽ sai, và reshuffle microservices khó hơn nhiều so với reshuffle module trong monolith.
"Nếu bạn không thể xây một monolith có cấu trúc tốt, điều gì khiến bạn nghĩ bạn có thể xây một distributed system có cấu trúc tốt?" — Simon Brown
3. Không Có DevOps Capability
Microservices đòi hỏi:
- Containerization (Docker)
- Orchestration (Kubernetes hoặc tương đương)
- CI/CD pipeline per service
- Centralized logging và monitoring
- Distributed tracing
Nếu team bạn chưa có infrastructure này, bạn sẽ mất hàng tháng xây platform trước khi viết bất kỳ business logic nào.
4. Yêu Cầu Strong Consistency
Nếu domain của bạn đòi hỏi strict transaction xuyên các service (ngân hàng, giao dịch tài chính), model eventual consistency của microservices tạo ra complexity khổng lồ. Monolith với single database cho bạn ACID transaction miễn phí.
5. Anti-Pattern "Distributed Monolith"
Kết quả tệ nhất là hệ thống có tất cả complexity của microservices mà không có benefit nào:
- Service phải deploy cùng nhau
- Service share database
- Service không thể hoạt động mà không có synchronous call đến service khác
- Một team sở hữu tất cả service
Đây là distributed monolith — bạn đã lấy một monolith hoạt động hoàn hảo và thêm network latency, partial failure, và operational overhead.
Migration: Từ Monolith Sang Microservices
Nếu bạn đã quyết định microservices phù hợp với tình huống của mình, đừng viết lại mọi thứ cùng lúc. Dùng chiến lược gia tăng.
Strangler Fig Pattern
Đặt tên theo cây đa bóp nghẹt mọc quanh cây hiện tại và cuối cùng thay thế nó. Dần dần trích xuất chức năng từ monolith ra microservice:
Các Bước Migration
- Bắt đầu từ rìa — tách service có ranh giới rõ ràng và dependency tối thiểu (notification, email, file upload)
- Thêm API gateway — route traffic giữa monolith và service mới
- Duplicate, rồi migrate — chạy song song cả monolith và service mới trước khi chuyển hoàn toàn
- Tách data — chia shared database thành database riêng cho từng service (đây là phần khó nhất)
- Lặp lại — tiếp tục tách service từng cái một
Tách Gì Trước
Ứng cử viên tốt cho lần tách đầu tiên:
- ✅ Service có ranh giới rõ ràng, đơn giản (notification, email)
- ✅ Service có tần suất thay đổi cao (feature hay thử nghiệm)
- ✅ Service có nhu cầu scale khác biệt (xử lý ảnh, search)
- ✅ Service được sở hữu bởi team khác
Ứng cử viên tệ cho lần tách đầu tiên:
- ❌ Core domain logic (user, order) — quá nhiều dependency
- ❌ Shared data entity — coupling quá chặt
- ❌ Authentication — cross-cutting concern, tách cuối cùng
Kiến Trúc Thực Tế: E-Commerce Microservices
Hãy gom mọi thứ lại với một kiến trúc e-commerce hoàn chỉnh:
Quyết định chính:
- API Gateway — điểm truy cập duy nhất, authentication, rate limiting
- Synchronous — Gateway → Service cho query (duyệt catalog, profile user)
- Asynchronous — Event Order/Payment qua Kafka cho xử lý downstream
- Polyglot persistence — PostgreSQL cho transaction, MongoDB cho catalog, Redis cho inventory count, Elasticsearch cho search
- Event-driven update — Search index được cập nhật qua Kafka event, không phải truy cập DB trực tiếp
Quick Reference: Checklist Microservices
Trước khi áp dụng microservices, xác minh mức độ sẵn sàng:
Tiền đề:
- Team size > 10 developer
- CI/CD pipeline hoàn thiện
- Container orchestration (Kubernetes hoặc tương đương)
- Centralized logging và monitoring
- Distributed tracing capability
- Domain được hiểu rõ (không phải thử nghiệm greenfield)
Checklist per-service:
- Service sở hữu business capability rõ ràng
- Service có database riêng
- Service có thể deploy độc lập
- Service có CI/CD pipeline riêng
- API được version hóa và document
- Health check endpoint tồn tại
- Circuit breaker cho downstream call
- Correlation ID propagation trong log
- Contract test với consumer
Tóm Tắt
Microservices architecture phân rã hệ thống thành các service có thể deploy độc lập, mỗi service sở hữu một business capability và data riêng.
Nguyên tắc cốt lõi:
- Single responsibility per business capability
- Autonomous deployment — không lockstep release
- Database per service — không shared database
- Design for failure — circuit breaker, retry, timeout
Communication pattern:
- Synchronous (REST/gRPC) — cho query và response tức thì
- Asynchronous (event/message) — cho command và workflow xuyên service
- Saga pattern — cho distributed transaction với compensating action
Yêu cầu infrastructure:
- Container + orchestration (Kubernetes)
- API gateway cho traffic hướng client
- Service discovery (built-in trong Kubernetes)
- Distributed tracing và centralized logging
- Contract testing giữa các service
Khi nào dùng microservices:
- Team lớn (10+ developer) cần khả năng deploy độc lập
- Các phần khác nhau của hệ thống cần profile scaling khác nhau
- Nhiều team cần làm việc độc lập
- Domain được hiểu rõ và bounded context rõ ràng
Khi nào KHÔNG dùng microservices:
- Team nhỏ (< 10 developer)
- Sản phẩm giai đoạn đầu với yêu cầu không chắc chắn
- Không có DevOps capability hay container infrastructure
- Yêu cầu strong consistency xuyên toàn hệ thống
- Bạn sẽ kết thúc với distributed monolith
Bắt đầu với monolith có cấu trúc tốt. Chỉ tách microservices khi nỗi đau của monolith vượt quá complexity của distribution.
Tiếp Theo Trong Chuỗi Kiến Trúc Phần Mềm
Đây là bài viết thứ 5 trong 12 bài của chuỗi Software Architecture Patterns:
- ✅ ARCH-1: Software Architecture Patterns Roadmap
- ✅ ARCH-2: Monolithic Architecture
- ✅ ARCH-3: Layered (N-Tier) Architecture
- ✅ ARCH-4: MVC, MVP & MVVM Patterns
- ✅ ARCH-5: Microservices Architecture (bài viết này)
- 🔜 ARCH-6: Event-Driven Architecture
- 🔜 ARCH-7: CQRS & Event Sourcing
- 🔜 ARCH-8: Hexagonal Architecture (Ports & Adapters)
- 🔜 ARCH-9: Clean Architecture
- 🔜 ARCH-10: Domain-Driven Design (DDD)
- 🔜 ARCH-11: Serverless & Function-as-a-Service
- 🔜 ARCH-12: Choosing the Right Architecture
Bài viết liên quan:
📬 Đăng ký nhận bản tin
Nhận những bài viết mới nhất qua email mỗi tuần. Không spam, hủy đăng ký bất cứ lúc nào.
Chúng tôi tôn trọng quyền riêng tư của bạn. Hủy đăng ký bất cứ lúc nào.
💬 Comments
Đăng nhập để bình luận
Chúng tôi sẽ không đăng bất cứ điều gì mà không có sự cho phép của bạn.