Back to blog

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

software-architecturemicroservicesbackendsystem-designdesign-patterns
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:

  1. 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.
  2. 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.
  3. Data có cohesive không? — Data luôn được đọc/ghi cùng nhau nên ở cùng nhau.
  4. 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ăngREST (HTTP/JSON)gRPC (HTTP/2 + Protobuf)
FormatText (JSON)Binary (Protobuf)
Tốc độChậm hơn (serialization overhead)Nhanh hơn (binary compact)
SchemaTùy chọn (OpenAPI)Bắt buộc (file .proto)
StreamingHạn chếBidirectional streaming
Hỗ trợ browserTự nhiênCần gRPC-Web proxy
Phù hợp choPublic API, external clientGiao 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 CaseSync (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 → khoCó 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ì

  1. Request routing — định tuyến request đến backend service phù hợp
  2. Composition/Aggregation — gom data từ nhiều service thành một response duy nhất
  3. Authentication & Authorization — validate token, kiểm soát quyền truy cập
  4. Rate limiting — bảo vệ backend service khỏi traffic spike
  5. Protocol translation — chuyển đổi giữa HTTP/REST (bên ngoài) và gRPC (bên trong)
  6. SSL termination — xử lý HTTPS ở edge
  7. 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ạnhChoreographyOrchestration
Phối hợpPhi tập trung (event)Tập trung (orchestrator)
CouplingLỏng lẻo hơnChặt hơn (với orchestrator)
VisibilityKhó theo dõi flowDễ thấy toàn bộ flow
ComplexityĐơn giản cho 2-3 bướcTốt hơn cho 4+ bước
DebugKhó 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: 8080

Vớ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

  1. 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)
  2. Thêm API gateway — route traffic giữa monolith và service mới
  3. Duplicate, rồi migrate — chạy song song cả monolith và service mới trước khi chuyển hoàn toàn
  4. Tách data — chia shared database thành database riêng cho từng service (đây là phần khó nhất)
  5. 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:

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.