Từ monolith sang microservices: cần những gì?

4 câu hỏi~20 phútTừ khóa: monolith, microservices, Spring Cloud, BOM, release train, spring-cloud-demo

Bắt đầu học Spring Cloud nhé

Ok. Cả chương mình học theo format quen thuộc: Bài toán → Vấn đề → Giải pháp → Code Spring Boot → Cách hoạt động → Phỏng vấn. Và mình làm một project chạy xuyên suốt: hệ thống E-commerce spring-cloud-demo. Mỗi bài thêm một mảnh vào project này.

1. Bài toán: app E-commerce ban đầu là một monolith

Lúc mới làm, mọi thứ nằm trong một Spring Boot app, một file JAR, một database:

shop-app.jar  (port 8080)
├── OrderController / OrderService
├── PaymentController / PaymentService
├── InventoryController / InventoryService
└── NotificationService
        |
        v
   1 database PostgreSQL

Tạo đơn hàng chỉ là gọi method trong cùng JVM:

@Service
public class OrderService {

    private final InventoryService inventoryService;
    private final PaymentService paymentService;
    private final NotificationService notificationService;

    // constructor injection ...

    @Transactional
    public Order createOrder(OrderRequest request) {
        inventoryService.reserve(request.productId(), request.quantity());
        paymentService.pay(request.orderId(), request.amount());
        notificationService.sendEmail(request.email());
        return orderRepository.save(new Order(request));
    }
}

Gọi method thì nhanh, một @Transactional là xong, deploy 1 file. Monolith không xấu. Với team nhỏ, đây thường là lựa chọn đúng.

2. Vấn đề: hệ thống lớn lên thì monolith bắt đầu đau

Nên người ta tách ra thành nhiều service nhỏ, mỗi service một process, một database riêng:

Order Service     : localhost:8081
Payment Service   : localhost:8082
Inventory Service : localhost:8083
Notification Svc  : localhost:8084

Nhưng tách ra thì gọi method biến thành gọi qua network. Và network sinh ra một loạt vấn đề mà monolith chưa từng có:

Trong monolithSau khi tách thành microservices
paymentService.pay() luôn tìm thấyPayment đang chạy ở IP nào, port nào? Có 3 instance thì gọi cái nào?
1 file application.yml20 service × 3 môi trường = 60 file config rải rác
Client gọi 1 địa chỉFrontend phải biết địa chỉ của từng service
Method gọi không bao giờ timeoutPayment chậm 30 giây kéo sập cả Order (cascading failure)
Gọi tuần tự trong 1 transactionCần gửi event bất đồng bộ cho nhiều service
1 file log, 1 stack trace1 request đi qua 4 service, lỗi ở đâu?

3. Giải pháp: Spring Cloud = bộ công cụ cho từng vấn đề

Spring Cloud không phải một thư viện, mà là một tập các project, mỗi project giải một bài toán của hệ phân tán. Bản đồ vấn đề → thành phần, cũng chính là lộ trình của chương này:

Vấn đềThành phần Spring CloudBài
Service tìm nhau thế nào?Eureka (Service Discovery)4.6.2
Config rải rác nhiều nơiSpring Cloud Config (+ Bus)4.6.3
Client cần một cửa vào duy nhấtSpring Cloud Gateway4.6.4
Gọi HTTP giữa service, chọn instanceOpenFeign + Spring Cloud LoadBalancertiếp theo
Một service lỗi kéo sập cả hệ thốngSpring Cloud Circuit Breaker + Resilience4jtiếp theo
Giao tiếp bất đồng bộ bằng eventSpring Cloud Stream (RabbitMQ / Kafka)tiếp theo
Trace một request qua nhiều serviceMicrometer Tracing (trước đây là Sleuth)tiếp theo
Chạy trên Kubernetes thì cần gì?Spring Cloud Kubernetes / bỏ bớt thành phần4.6.9

4. Code từng bước: dựng khung project spring-cloud-demo

Bước 1 — Cấu trúc project. Một thư mục cha, mỗi service là một Spring Boot app riêng:

spring-cloud-demo/
├── pom.xml                  (parent, quản lý version)
├── discovery-server/        :8761  Eureka          (bài 4.6.2)
├── config-server/           :8888  Config Server   (bài 4.6.3)
├── api-gateway/             :8080  Gateway         (bài 4.6.4)
├── order-service/           :8081
├── payment-service/         :8082
├── inventory-service/       :8083
└── notification-service/    :8084

Bước 2 — Spring Cloud BOM trong parent pom. Spring Cloud phát hành theo release train (2023.0.x, 2024.0.x, 2025.0.x...). Mỗi train chỉ tương thích với một dòng Spring Boot. Ta import BOM một lần, các module con không cần ghi version:

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>3.5.0</version>
</parent>

<properties>
    <java.version>17</java.version>
    <spring-cloud.version>2025.0.0</spring-cloud.version>
</properties>

<modules>
    <module>discovery-server</module>
    <module>config-server</module>
    <module>api-gateway</module>
    <module>order-service</module>
    <module>payment-service</module>
    <module>inventory-service</module>
    <module>notification-service</module>
</modules>

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>${spring-cloud.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

Version cụ thể ở trên chỉ là ví dụ. Luôn tra bảng tương thích Boot ↔ Cloud trên trang Spring Cloud (hoặc dùng Spring Initializr, nó tự chọn cặp đúng).

Bước 3 — Một service tối thiểu. Ví dụ payment-service, lúc này chưa có gì "cloud", chỉ là Spring Boot bình thường:

// payment-service/src/main/java/com/example/payment/PaymentController.java
@RestController
@RequestMapping("/payments")
public class PaymentController {

    @Value("${server.port}")
    private String port;

    @GetMapping("/status")
    public String getStatus() {
        return "Payment Service running on port " + port;
    }
}
# payment-service/src/main/resources/application.yml
server:
  port: 8082
spring:
  application:
    name: payment-service

spring.application.name rất quan trọng: các bài sau Eureka, Config Server, Gateway đều nhận diện service bằng cái tên này.

Bước 4 — Test bằng curl. Order gọi Payment bằng địa chỉ hardcode (cách "ngây thơ"):

return restTemplate.getForObject(
    "http://localhost:8082/payments/status", String.class);
curl http://localhost:8082/payments/status
# Payment Service running on port 8082

Chạy được. Nhưng đây chính là điểm xuất phát của bài sau.

5. Cách hoạt động: bức tranh tổng sau khi học xong chương

                 Client (web / mobile)
                        |
                        v
               +-----------------+
               |   API Gateway   |  :8080  routing, auth, rate limit
               +-----------------+
                        |  lb://order-service
          +-------------+-------------+
          v             v             v
   Order Service  Payment Service  Inventory Service
      :8081          :8082           :8083
          |   OpenFeign + LoadBalancer
          |   + Circuit Breaker
          |
          +--- ORDER_CREATED ---> RabbitMQ/Kafka ---> Notification
                                   (Spring Cloud Stream)

   Mọi service:
     - đăng ký + tìm nhau qua  Discovery Server :8761 (Eureka)
     - lấy config từ           Config Server    :8888 (Git)

6. So sánh: Monolith vs Microservices

Tiêu chíMonolithMicroservices
Deploy1 artifact, deploy cả khốiTừng service deploy độc lập
ScaleScale cả appScale riêng service đang nóng
Giao tiếpMethod call trong JVMHTTP / message qua network
TransactionMột @TransactionalDistributed, cần Saga / eventual consistency
LỗiMột bug có thể sập cả appCô lập tốt hơn, nhưng lỗi network nhiều hơn
Vận hànhĐơn giảnCần discovery, config, gateway, tracing, CI/CD
Phù hợpTeam nhỏ, sản phẩm mớiNhiều team, domain rõ, cần scale riêng

7. Khi nào nên / không nên tách microservices

Nên khi: nhiều team cần deploy độc lập, các phần có tải rất khác nhau, domain đã rõ ranh giới (Order, Payment, Inventory tách bạch).

Không nên khi: team 3–5 người, sản phẩm còn đang tìm hướng, chưa có CI/CD và monitoring. Lúc đó nên làm modular monolith trước: một app nhưng chia module rõ ràng, sau này tách dễ.

Spring Cloud với Spring Boot khác nhau chỗ nào? Mình tưởng Spring Cloud là một framework riêng.

Bạn hiểu gần đúng, chỉ cần chỉnh một điểm: Spring Cloud không thay Spring Boot, mà chạy trên Spring Boot.

Khái niệmBản chấtVai trò
Spring BootFramework dựng một ứng dụngAuto-config, embedded Tomcat, starter
Spring CloudTập project trên Spring BootGiải các bài toán khi có nhiều ứng dụng nói chuyện với nhau
Spring Cloud   (Eureka, Config, Gateway, OpenFeign, Stream...)
      |  dùng
      v
Spring Boot    (auto-config, starters, actuator)
      |  dùng
      v
Spring Framework (IoC, AOP, MVC/WebFlux)

Mỗi service vẫn là một Spring Boot app bình thường. Thêm một starter của Spring Cloud vào là nó có thêm năng lực, ví dụ spring-cloud-starter-netflix-eureka-client để tự đăng ký với Eureka.

Sao không ghi version cho từng dependency Spring Cloud luôn, phải import BOM làm gì?

Đúng là ghi tay được, nhưng rất dễ hỏng. Spring Cloud có hàng chục module (eureka, config, gateway, openfeign, stream...), mỗi module có version riêng. Chúng phải khớp nhau và khớp Spring Boot.

Không dùng BOM:
  eureka-client 4.1.x  + gateway 4.3.x + Boot 3.5
       -> ClassNotFoundException / NoSuchMethodError lúc khởi động

Dùng BOM (release train 2025.0.x):
  spring-cloud-dependencies 2025.0.0
       -> tự chọn eureka, gateway, config... đã test chung với nhau

Lỗi thường gặp nhất khi mới học: lấy Spring Boot mới nhưng Spring Cloud cũ, app khởi động báo lỗi tương thích. Cách tránh: dùng Spring Initializr hoặc tra bảng tương thích trước khi nâng version.

Phỏng vấn hỏi "Vì sao chuyển sang microservices, và Spring Cloud giúp gì?" thì trả lời sao?

Q1. Monolith khác microservices thế nào? Khi nào nên chuyển?

🇻🇳 Tiếng Việt:

Monolith là một ứng dụng deploy thành một khối, các module gọi nhau bằng method call và thường dùng chung một database. Microservices tách thành nhiều service nhỏ, mỗi service deploy và scale độc lập, giao tiếp qua network. Em chỉ chuyển khi có lý do rõ: nhiều team cần deploy độc lập, các phần có tải khác nhau, domain đã rõ ranh giới. Nếu team nhỏ thì em ưu tiên modular monolith vì microservices kéo theo nhiều chi phí vận hành.

🇬🇧 English:

A monolith is deployed as a single unit; modules call each other in-process and usually share one database. Microservices split the system into small services that are deployed and scaled independently and communicate over the network. I would migrate only with a clear reason, such as multiple teams needing independent deployments, uneven load, or well-defined domain boundaries. For a small team, I prefer a modular monolith because microservices add significant operational overhead.

Q2. Spring Cloud là gì?

🇻🇳 Tiếng Việt:

Spring Cloud là một tập các project chạy trên Spring Boot, giải quyết những vấn đề chung của hệ phân tán: service discovery với Eureka, cấu hình tập trung với Config Server, API Gateway, gọi service bằng OpenFeign và LoadBalancer, fault tolerance với Circuit Breaker, messaging với Spring Cloud Stream.

🇬🇧 English:

Spring Cloud is a set of projects built on top of Spring Boot that address common distributed-system concerns: service discovery with Eureka, centralized configuration with Config Server, API Gateway, inter-service calls with OpenFeign and LoadBalancer, fault tolerance with Circuit Breaker, and messaging with Spring Cloud Stream.

Q3. Release train và BOM của Spring Cloud là gì?

🇻🇳 Tiếng Việt:

Spring Cloud phát hành theo release train, mỗi train gom các module đã được test chung và tương thích với một dòng Spring Boot. Em import BOM spring-cloud-dependencies trong dependencyManagement để các module con không phải ghi version và tránh lỗi lệch phiên bản.

🇬🇧 English:

Spring Cloud is released as release trains; each train bundles modules that are tested together and compatible with a specific Spring Boot line. I import the spring-cloud-dependencies BOM in dependencyManagement so modules don't declare versions and we avoid version mismatches.

Q4. Nhược điểm của microservices là gì?

🇻🇳 Tiếng Việt:

Gọi qua network nên có latency và lỗi mạng, cần timeout, retry, circuit breaker. Không còn một transaction chung nên phải dùng Saga và chấp nhận eventual consistency. Debug khó hơn, cần distributed tracing và log tập trung. Vận hành cũng nặng hơn: discovery, config, gateway, CI/CD cho từng service.

🇬🇧 English:

Network calls introduce latency and partial failures, so we need timeouts, retries, and circuit breakers. There is no single transaction anymore, so we use Sagas and accept eventual consistency. Debugging is harder and requires distributed tracing and centralized logging. Operations are heavier too: discovery, configuration, gateway, and CI/CD per service.

Cách nhớ ngắn gọn


Tự kiểm tra

Ba lý do chính để tách monolith thành microservices?

Deploy độc lập cho nhiều team, scale riêng phần bị tải cao, cô lập lỗi giữa các phần.

Sau khi tách, vấn đề nào xuất hiện mà monolith không có?

Service tìm nhau, config rải rác, nhiều địa chỉ cho client, lỗi lan qua network, transaction phân tán, khó trace request.

Vấn đề "service tìm nhau" và "config tập trung" ứng với thành phần nào?

Eureka (Service Discovery) và Spring Cloud Config Server.

Vì sao nên import Spring Cloud BOM?

Để mọi module Spring Cloud dùng version đã được test chung và khớp với Spring Boot, tránh lỗi lệch phiên bản.

Thuộc tính nào giúp các thành phần Spring Cloud nhận diện service?

spring.application.name, ví dụ payment-service.