Monolith hay microservices: chọn thế nào cho đúng

1. Tổng quan

Câu hỏi “nên chọn monolith hay microservices” gần như xuất hiện ở mọi buổi thiết kế hệ thống. Bài này không đưa ra câu trả lời one-size-fits-all, mà đi qua các tiêu chí cụ thể: team size, tốc độ release, độ phức tạp vận hành, và chi phí giao tiếp giữa các service. Đọc xong, bạn sẽ có một checklist để tự đánh giá hệ thống của mình nên nằm ở đâu trên “trục” monolith – modular monolith – microservices.

2. Monolith: đơn giản nhưng dễ bị đánh giá thấp

Monolith nghĩa là toàn bộ nghiệp vụ (Order, Customer, Payment, Inventory…) chạy trong một process, một codebase, một lần deploy.

@RestController
@RequestMapping("/orders")
public class OrderController {

    private final OrderService orderService;

    public OrderController(OrderService orderService) {
        this.orderService = orderService;
    }

    @PostMapping
    public ResponseEntity<OrderResponse> createOrder(@RequestBody OrderRequest request) {
        Order order = orderService.create(request);
        return ResponseEntity.ok(OrderResponse.from(order));
    }
}

@Service
public class OrderService {

    private final InventoryService inventoryService;
    private final PaymentService paymentService;

    public OrderService(InventoryService inventoryService, PaymentService paymentService) {
        this.inventoryService = inventoryService;
        this.paymentService = paymentService;
    }

    public Order create(OrderRequest request) {
        inventoryService.reserve(request.productId(), request.quantity());
        paymentService.charge(request.customerId(), request.amount());
        return new Order(request); // giả định constructor lưu DB
    }
}

OrderService gọi trực tiếp InventoryService và PaymentService bằng method call trong cùng JVM. Không có network, không có serialization, transaction có thể gói trong một @Transactional duy nhất. Đây là lợi thế lớn nhất của monolith: consistency dễ đảm bảo, debug dễ, deploy một lần là xong.

Nhược điểm xuất hiện khi team lớn lên: mọi người cùng sửa một codebase, build time tăng, risk khi deploy một module nhỏ cũng kéo theo rebuild toàn bộ app.

Lưu ý: Monolith không đồng nghĩa với “code rối”. Một monolith viết tốt với module rõ ràng (package theo domain, không phụ thuộc chéo) vẫn chạy ổn cho hệ thống hàng triệu user.

3. Microservices: tách để scale độc lập, đổi lại độ phức tạp vận hành

Microservices tách Order, Inventory, Payment thành các service riêng, giao tiếp qua network (REST, gRPC, message queue).

@Service
public class OrderService {

    private final RestClient restClient;

    public OrderService(RestClient restClient) {
        this.restClient = restClient;
    }

    public Order create(OrderRequest request) {
        // Gọi Inventory Service qua HTTP
        restClient.post()
                .uri("http://inventory-service/reservations")
                .body(new ReservationRequest(request.productId(), request.quantity()))
                .retrieve()
                .toBodilessEntity();

        // Gọi Payment Service qua HTTP
        restClient.post()
                .uri("http://payment-service/charges")
                .body(new ChargeRequest(request.customerId(), request.amount()))
                .retrieve()
                .toBodilessEntity();

        return new Order(request);
    }
}

So với bản monolith ở mục 2, điểm khác biệt nằm ở restClient.post(...): method call trong-process đã biến thành HTTP call qua network. Hệ quả:

  • Mỗi call có thể fail do network timeout, service down, DNS lỗi — những lỗi không tồn tại trong monolith.
  • Không còn transaction ACID xuyên service. Bạn phải xử lý bằng saga pattern hoặc eventual consistency.
  • Cần thêm service discovery, load balancer, distributed tracing để debug khi có lỗi.

3.1. Chi phí ẩn: distributed transaction

@Service
public class OrderSagaOrchestrator {

    public void createOrder(OrderRequest request) {
        try {
            inventoryClient.reserve(request.productId(), request.quantity());
            paymentClient.charge(request.customerId(), request.amount());
        } catch (PaymentFailedException e) {
            // Phải tự viết bước compensate để rollback
            inventoryClient.release(request.productId(), request.quantity());
            throw e;
        }
    }
}

Trong monolith, rollback là @Transactional tự động. Trong microservices, bạn phải tự viết logic compensate (release sau khi reserve thất bại). Đây là phần code không tồn tại ở bản monolith nhưng bắt buộc phải có ở microservices để tránh dữ liệu inconsistency.

4. Tiêu chí quyết định: không chỉ dựa vào “công ty lớn đang làm gì”

4.1. Team size và Conway’s Law

Nguyên lý Conway: cấu trúc hệ thống phản chiếu cấu trúc tổ chức. Nếu bạn có 5 developer, chia thành 5 service nghĩa là mỗi người ôm một service — không ai review code cho ai, và bạn mất lợi ích “nhiều người cùng nhìn vào một bug”.

Ngưỡng tham khảo thực tế:

1–15 dev, 1 team         -> Monolith (modular)
15–50 dev, 2–5 team      -> Modular monolith hoặc tách 2-3 service lớn
50+ dev, nhiều team      -> Microservices theo bounded context

4.2. Tốc độ release có thực sự bị nghẽn?

Nếu team của bạn release monolith mỗi 2 tuần và không có conflict merge nghiêm trọng, tách service để “release độc lập” không mang lại giá trị — nó chỉ thêm chi phí vận hành (CI/CD riêng, monitoring riêng, versioning API).

Microservices đáng giá khi: team A cần deploy 10 lần/ngày cho tính năng Payment, còn team B chỉ deploy Inventory 1 lần/tuần. Tách ra giúp A không phải chờ B.

4.3. Khả năng chịu lỗi của hạ tầng

# docker-compose.yml - minh họa độ phức tạp hạ tầng tối thiểu cho microservices
services:
  order-service:
    image: order-service:1.0
    depends_on:
      - inventory-service
      - payment-service
      - service-registry
  inventory-service:
    image: inventory-service:1.0
  payment-service:
    image: payment-service:1.0
  service-registry:
    image: eureka-server:1.0
  api-gateway:
    image: spring-cloud-gateway:1.0

Chỉ với 3 service nghiệp vụ, bạn đã cần thêm service-registry và api-gateway. Nhân với 20 service thực tế, số lượng moving part tăng theo cấp số — đòi hỏi team DevOps đủ mạnh để vận hành Kubernetes, distributed logging (ELK), tracing (Jaeger/Zipkin).

Lưu ý: Nếu công ty chưa có ai rành container orchestration và observability stack, microservices sẽ biến mỗi incident thành buổi “mò kim đáy biển” qua nhiều service log khác nhau.

5. Modular monolith: lựa chọn trung gian hay bị bỏ qua

Modular monolith là monolith nhưng code được tổ chức thành module độc lập, giao tiếp qua interface rõ ràng, không phụ thuộc chéo table database.

// Package: com.nonladev.shop.order
public interface InventoryPort {
    void reserve(String productId, int quantity);
}

// Package: com.nonladev.shop.inventory
@Service
public class InventoryService implements InventoryPort {
    @Override
    public void reserve(String productId, int quantity) {
        // logic nghiệp vụ, truy cập DB riêng của module inventory
    }
}

OrderService chỉ phụ thuộc vào interface InventoryPort, không biết chi tiết implementation của module inventory. Nhờ vậy, nếu sau này cần tách inventory thành service riêng, bạn chỉ cần thay implementation của InventoryPort bằng một HTTP client — code gọi ở OrderService không đổi.

Đây chính là chiến lược “bắt đầu monolith, tách dần khi cần” mà nhiều hệ thống lớn (Shopify, GitHub ở giai đoạn đầu) đã áp dụng thành công.

6. Test: đo chi phí latency khi chuyển từ in-process sang network call

Ví dụ test dưới đây minh họa sự khác biệt về độ trễ — một yếu tố cần đo thực tế trước khi quyết định tách service.

<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.assertj</groupId>
    <artifactId>assertj-core</artifactId>
    <scope>test</scope>
</dependency>
class OrderServiceLatencyTest {

    @Test
    void inProcessCallShouldBeFasterThanRemoteCall() {
        InventoryPort inProcess = (productId, qty) -> { /* no-op */ };
        long startInProcess = System.nanoTime();
        inProcess.reserve("P-1", 1);
        long inProcessDuration = System.nanoTime() - startInProcess;

        InventoryPort remoteSimulated = (productId, qty) -> {
            try {
                Thread.sleep(20); // giả lập network round-trip ~20ms
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        };
        long startRemote = System.nanoTime();
        remoteSimulated.reserve("P-1", 1);
        long remoteDuration = System.nanoTime() - startRemote;

        assertThat(remoteDuration).isGreaterThan(inProcessDuration);
    }
}

Test này không đo hiệu năng thật của hạ tầng bạn, nhưng minh họa đúng bản chất: mỗi lần Order gọi sang Inventory qua network, bạn cộng thêm vài–vài chục milliseconds. Nếu một request cần gọi chuỗi 5 service, latency cộng dồn có thể vượt SLA — đây là chi phí thường bị bỏ qua khi vẽ sơ đồ kiến trúc trên giấy.

7. Khi nào KHÔNG nên chọn microservices

  • Team nhỏ hơn 10 người: chi phí vận hành (CI/CD, monitoring, on-call) cho nhiều service vượt quá lợi ích tách biệt.
  • Domain chưa ổn định: nếu nghiệp vụ còn thay đổi liên tục (startup giai đoạn tìm product-market fit), ranh giới service sẽ sai liên tục, tách rồi lại phải merge — tốn công gấp đôi.
  • Chưa có observability: không có distributed tracing, centralized logging thì debug lỗi xuyên service gần như bất khả thi.
  • Giao dịch cần strong consistency cao (ví dụ core banking): giữ trong một transaction boundary (monolith) an toàn hơn implement saga phức tạp.

Ngược lại, microservices đáng đầu tư khi: nhiều team độc lập, cần scale riêng từng phần (ví dụ search service cần nhiều CPU hơn order service), hoặc cần công nghệ khác nhau cho mỗi domain (Node.js cho real-time chat, Java cho billing).

8. Kết luận

Không có kiến trúc “đúng tuyệt đối” — chỉ có kiến trúc phù hợp với team size, tốc độ release, và năng lực vận hành hiện tại của bạn. Nếu chưa chắc, hãy bắt đầu với modular monolith: giữ interface rõ ràng giữa các domain, tránh phụ thuộc chéo database, để khi thực sự cần tách (do scale hoặc do team lớn lên), bạn chỉ phải thay implementation, không phải viết lại toàn bộ hệ thống. Microservices là công cụ giải quyết bài toán tổ chức và scale, không phải mục tiêu kiến trúc tự thân.

microservicesmonolithsystem-designspring-bootkiến-trúc