Từ monolith sang microservices: cần những gì?
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
- Team 30 người cùng sửa một codebase, merge conflict liên tục.
- Sửa 1 dòng ở Notification cũng phải build và deploy lại cả app.
- Black Friday chỉ Order và Inventory bị tải cao, nhưng phải scale cả khối (cả Payment, Notification đi theo).
- Một bug memory leak ở Notification làm sập luôn cả Order.
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 monolith | Sau khi tách thành microservices |
|---|---|
paymentService.pay() luôn tìm thấy | Payment đang chạy ở IP nào, port nào? Có 3 instance thì gọi cái nào? |
1 file application.yml | 20 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ờ timeout | Payment chậm 30 giây kéo sập cả Order (cascading failure) |
| Gọi tuần tự trong 1 transaction | Cần gửi event bất đồng bộ cho nhiều service |
| 1 file log, 1 stack trace | 1 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 Cloud | Bài |
|---|---|---|
| Service tìm nhau thế nào? | Eureka (Service Discovery) | 4.6.2 |
| Config rải rác nhiều nơi | Spring Cloud Config (+ Bus) | 4.6.3 |
| Client cần một cửa vào duy nhất | Spring Cloud Gateway | 4.6.4 |
| Gọi HTTP giữa service, chọn instance | OpenFeign + Spring Cloud LoadBalancer | tiếp theo |
| Một service lỗi kéo sập cả hệ thống | Spring Cloud Circuit Breaker + Resilience4j | tiếp theo |
| Giao tiếp bất đồng bộ bằng event | Spring Cloud Stream (RabbitMQ / Kafka) | tiếp theo |
| Trace một request qua nhiều service | Micrometer 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ần | 4.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í | Monolith | Microservices |
|---|---|---|
| Deploy | 1 artifact, deploy cả khối | Từng service deploy độc lập |
| Scale | Scale cả app | Scale riêng service đang nóng |
| Giao tiếp | Method call trong JVM | HTTP / message qua network |
| Transaction | Một @Transactional | Distributed, cần Saga / eventual consistency |
| Lỗi | Một bug có thể sập cả app | Cô lập tốt hơn, nhưng lỗi network nhiều hơn |
| Vận hành | Đơn giản | Cần discovery, config, gateway, tracing, CI/CD |
| Phù hợp | Team nhỏ, sản phẩm mới | Nhiề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ễ.
http://localhost:8082. Nếu Payment chạy 3 instance, có instance chết, có instance mới thêm vào, Order làm sao biết? Đó là bài toán Service Discovery.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ệm | Bản chất | Vai trò |
|---|---|---|
| Spring Boot | Framework dựng một ứng dụng | Auto-config, embedded Tomcat, starter |
| Spring Cloud | Tập project trên Spring Boot | Giả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-dependenciestrongdependencyManagementđể 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-dependenciesBOM independencyManagementso 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
- Monolith = method call. Microservices = network call, kéo theo mọi vấn đề của network.
- Mỗi vấn đề có một mảnh Spring Cloud: tìm nhau = Eureka, config = Config Server, cửa vào = Gateway, lỗi lan = Circuit Breaker, event = Stream.
- Spring Cloud chạy trên Spring Boot, không thay Spring Boot.
- BOM = một version cho cả bộ, tránh lệch phiên bả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.