Chương 6. Monolith Không Xấu
Trong nhiều cuộc trò chuyện về kiến trúc, "monolith" thường bị nói như một thứ lỗi thời, cồng kềnh, khó scale. Ngược lại, "microservices" nghe hiện đại, chuyên nghiệp, giống kiến trúc của các công ty lớn.
Nhưng thực tế không đơn giản như vậy.
Monolith không xấu. Monolith rối mới xấu.
Một monolith được tổ chức tốt có thể giúp team đi rất nhanh, giữ dữ liệu nhất quán, deploy đơn giản, debug dễ hơn và giảm chi phí vận hành. Rất nhiều sản phẩm nên bắt đầu bằng monolith hoặc modular monolith trước khi nghĩ đến microservices.
Chương này không cổ vũ "monolith mãi mãi". Mục tiêu là hiểu đúng: monolith giải quyết vấn đề gì, đau ở đâu, và khi nào nó vẫn là lựa chọn tốt nhất.
---
6.1. Monolith là gì?
Monolith là một ứng dụng được xây và deploy như một khối chính.
Ví dụ đơn giản:
Web App
|
Database
Bên trong web app có thể có nhiều phần:
- User
- Product
- Order
- Payment
- Notification
- Admin
- Report
Nhưng tất cả cùng nằm trong một codebase, chạy trong cùng một ứng dụng, deploy cùng nhau.
Ví dụ:
Monolith App
|-- users
|-- products
|-- orders
|-- payments
|-- notifications
|-- reports
Điểm quan trọng: monolith không có nghĩa là code phải để lộn xộn trong một file hoặc một folder. Một monolith vẫn có thể chia module rõ ràng, có service layer, có domain boundary, có test, có queue, có cache, có background worker.
Monolith nói về cách deploy và runtime, không nhất thiết nói về chất lượng tổ chức code.
---
6.2. Ví dụ quán bánh một cửa hàng
Hãy quay lại ví dụ quán bánh.
Ban đầu, quán chỉ có một cửa hàng:
- Một quầy nhận đơn.
- Một bếp.
- Một kho nguyên liệu.
- Một sổ đơn hàng.
- Một quản lý.
Mọi thứ ở cùng một nơi. Khi có vấn đề, quản lý nhìn một vòng là biết:
- Khách đang xếp hàng ở quầy.
- Bếp đang chậm.
- Kho hết bột.
- Shipper chưa đến.
Đây giống monolith.
Khi quán nhỏ, một cửa hàng là hợp lý. Nếu ngay ngày đầu đã chia thành:
- Một công ty riêng nhận đơn.
- Một công ty riêng làm bánh.
- Một công ty riêng quản lý kho.
- Một công ty riêng giao hàng.
- Một công ty riêng kế toán.
Nghe có vẻ "phân tán", nhưng vận hành sẽ rất mệt. Mỗi việc nhỏ phải gọi qua lại, hợp đồng, đồng bộ trạng thái, xử lý lỗi giữa các bên.
Khi quán còn nhỏ, chia quá sớm làm chậm hơn chứ không nhanh hơn.
Hệ thống phần mềm cũng vậy.
---
6.3. Vì sao monolith thường là lựa chọn tốt lúc đầu?
Dễ phát triển
Một codebase, một ứng dụng, một database. Developer dễ chạy local, dễ tìm logic, dễ sửa luồng nghiệp vụ.
Nếu cần sửa flow đặt hàng, ta có thể sửa trong cùng một project thay vì phải sửa 4 service.
Dễ transaction
Trong monolith, nhiều nghiệp vụ có thể nằm trong cùng một database transaction.
Ví dụ:
- Tạo đơn hàng.
- Trừ tồn kho.
- Ghi lịch sử.
Nếu cùng database, có thể dùng transaction để đảm bảo cùng thành công hoặc cùng rollback.
Trong microservices, nếu order, inventory và payment ở ba service khác nhau, transaction sẽ phức tạp hơn nhiều.
Dễ debug
Request thường đi qua ít thành phần hơn.
Browser -> Monolith -> Database
So với:
Browser -> API Gateway -> Order Service -> Payment Service -> Inventory Service -> Notification Service
Ít thành phần hơn nghĩa là ít nơi cần kiểm tra hơn.
Dễ deploy
Một ứng dụng deploy một lần. Không cần điều phối version giữa nhiều service.
Ít chi phí vận hành
Ít service nghĩa là:
- Ít server/container.
- Ít pipeline.
- Ít log stream.
- Ít dashboard.
- Ít network call.
- Ít secret.
- Ít monitoring phức tạp.
Với team nhỏ, đây là lợi thế rất lớn.
---
6.4. Monolith tốt trông như thế nào?
Một monolith tốt không phải một đống code dính vào nhau. Nó có tổ chức.
Ví dụ:
app/
users/
products/
orders/
payments/
notifications/
reports/
Mỗi module có trách nhiệm rõ.
Logic nghiệp vụ không nằm hết trong controller
Controller/view chỉ nhận request, validate cơ bản, gọi use case/service, trả response.
Logic chính nằm trong application service hoặc domain service.
Module có ranh giới
Module orders không nên tự tiện chọc sâu vào mọi model/hàm nội bộ của payments.
Nếu cần tương tác, nên đi qua interface/service rõ ràng.
Database được thiết kế cẩn thận
Monolith dùng chung database không có nghĩa là muốn join gì cũng được. Join trong cùng aggregate/ngữ cảnh thì ổn. Nhưng coupling lung tung giữa mọi phần sẽ làm khó tách sau này.
Có background job
Monolith không có nghĩa là mọi thứ chạy trong request. Một monolith tốt vẫn dùng queue/worker cho việc dài:
- Report
- File processing
- External API lâu
Có observability
Monolith cũng cần logs, metrics, traces ở mức phù hợp. Đừng chờ đến microservices mới quan sát hệ thống.
Có test ở phần quan trọng
Đặc biệt là nghiệp vụ tiền, quyền, dữ liệu quan trọng, retry, idempotency.
Tóm lại:
> Monolith tốt là một hệ thống deploy chung nhưng tổ chức bên trong rõ ràng.
---
6.5. Monolith xấu trông như thế nào?
Monolith xấu thường không xấu vì nó "mono". Nó xấu vì code không có ranh giới.
Dấu hiệu:
- Một file/module chứa quá nhiều logic.
- Controller/view rất dài.
- Model có quá nhiều trách nhiệm.
- Module nào cũng import module nào.
- Sửa payment làm hỏng order.
- Sửa notification làm hỏng user.
- Không ai biết logic thật nằm ở đâu.
- Không có test.
- Không có job queue, mọi việc dài nằm trong request.
- Database bị join chéo lung tung.
- Deploy rất sợ vì không biết ảnh hưởng gì.
Đây thường được gọi là "big ball of mud" - một cục bùn lớn.
Điểm quan trọng:
> Big ball of mud có thể xảy ra trong monolith, nhưng microservices cũng có thể thành big ball of mud phân tán.
Nếu code không có ranh giới, tách thành service chỉ biến vấn đề từ "hàm gọi hàm lung tung" thành "service gọi service lung tung".
---
6.6. Monolith khác modular monolith ở đâu?
Monolith thường chỉ nói rằng ứng dụng deploy như một khối.
Modular monolith là monolith có chia module rõ ràng.
Modular Monolith
|-- users
|-- catalog
|-- ordering
|-- payment
|-- notification
Các module vẫn chạy trong cùng ứng dụng, nhưng có ranh giới:
- Mỗi module có trách nhiệm riêng.
- Module phơi bày interface rõ.
- Module không tùy tiện import nội bộ của nhau.
- Business logic nằm ở nơi có chủ đích.
- Có thể tách module thành service sau này nếu cần.
So với microservices:
Microservices
users service
catalog service
ordering service
payment service
notification service
Modular monolith chưa tách runtime/deploy/database hoàn toàn. Nó là bước trung gian rất thực dụng.
Tư duy:
> Tách trong code trước. Tách thành service sau.
---
6.7. Khi nào monolith bắt đầu đau?
Monolith bắt đầu đau khi áp lực thật vượt quá khả năng tổ chức hiện tại.
Codebase quá lớn và không có ranh giới
Developer sợ sửa vì không biết ảnh hưởng đâu.
Đây là dấu hiệu cần modular hóa, chưa chắc cần microservices ngay.
Một phần cần scale riêng
Ví dụ:
- Phần xử lý video cần nhiều CPU.
- Phần AI cần concurrency cao.
- Phần realtime cần giữ nhiều connection.
- Phần search có tải khác hẳn API thường.
Nếu scale cả monolith chỉ vì một phần nhỏ, có thể lãng phí.
Một phần lỗi làm ảnh hưởng toàn hệ thống
Ví dụ:
- Job xử lý file làm web request chậm.
- Tác vụ AI làm cạn database connection.
- Report nặng làm app chính chết.
Đây là dấu hiệu cần tách workload, có thể bằng queue/worker trước, service riêng sau.
Team dẫm chân nhau
Nhiều team cùng sửa một codebase, deploy chung, merge conflict nhiều, release chậm.
Lúc này tách service có thể giúp nếu domain boundary rõ.
Deploy quá rủi ro
Một thay đổi nhỏ ở notification cũng phải deploy cả app lớn. Nếu test/observability kém, team sợ deploy.
Trước khi tách service, hãy xem có thể cải thiện test, feature flag, modularity và pipeline không.
---
6.8. Những vấn đề không cần microservices để giải quyết
Nhiều vấn đề của monolith có thể giải quyết bằng cách đơn giản hơn microservices.
Request chậm vì việc dài
Giải pháp đầu tiên thường là queue/worker, không phải microservices.
Database đọc nhiều
Có thể cần index, query optimization, cache, read replica. Không nhất thiết tách service.
Code rối
Cần refactor module/service layer/domain boundary. Tách service khi code còn rối thường làm rối hơn.
Deploy sợ
Cần test, CI/CD, rollback, feature flag, monitoring. Microservices không tự làm deploy an toàn.
Một số job nặng
Có thể tách worker riêng trước khi tách service hoàn chỉnh.
Static file tải chậm
Dùng CDN/object storage, không liên quan microservices.
Tư duy:
> Đừng dùng microservices để chữa mọi bệnh của monolith. Hãy chữa đúng bệnh.
---
6.9. Vì sao nhiều công ty lớn vẫn bắt đầu hoặc giữ monolith lâu?
Các công ty lớn không chọn microservices vì nó "xịn". Họ chọn khi có áp lực thật:
- Nhiều team cần deploy độc lập.
- Một số phần cần scale riêng.
- Một số domain có ownership riêng.
- Failure isolation quan trọng.
- Hạ tầng và observability đủ mạnh.
Trước khi đạt đến áp lực đó, monolith thường giúp đi nhanh hơn.
Nhiều hệ thống nổi tiếng từng bắt đầu bằng monolith. Điều này hợp lý: lúc đầu điều quan trọng là tìm sản phẩm đúng, người dùng thật, dòng tiền thật. Kiến trúc quá phức tạp sớm có thể giết tốc độ.
Một monolith tốt có thể phục vụ lượng người dùng rất lớn nếu:
- Code tổ chức tốt.
- Database tối ưu tốt.
- Cache đúng chỗ.
- Job nền tách hợp lý.
- Static/media qua CDN.
- Observability tốt.
- Deploy an toàn.
Scale không bắt buộc đồng nghĩa microservices ngay lập tức.
---
6.10. Monolith và database chung
Một lợi thế lớn của monolith là dùng chung database dễ hơn.
Điều này giúp:
- Transaction đơn giản.
- Query dễ hơn.
- Constraint rõ hơn.
- Ít đồng bộ dữ liệu giữa services.
Nhưng database chung cũng có rủi ro:
- Module nào cũng chọc vào bảng của module khác.
- Join chéo lung tung.
- Dữ liệu không có chủ sở hữu rõ.
- Sau này khó tách service.
Cách làm thực dụng:
- Trong cùng một module/aggregate, dùng foreign key và transaction bình thường.
- Giữa các module lớn, hạn chế phụ thuộc trực tiếp nếu muốn tách sau này.
- Dùng service layer/interface khi tương tác nghiệp vụ.
- Không để mọi nơi tự query mọi bảng chỉ vì "cùng database".
Monolith không yêu cầu vô kỷ luật. Nó chỉ cho phép ta dùng sự đơn giản của database chung một cách có kiểm soát.
---
6.11. Monolith có thể có queue, cache, worker, CDN
Một hiểu lầm phổ biến: monolith nghĩa là một server làm tất cả.
Không đúng.
Một monolith production có thể trông như sau:
Browser
|
CDN / Reverse Proxy
|
Monolith App
|------ Database
|------ Cache
|------ Queue
|
Worker
|
Object Storage
App vẫn là monolith, nhưng các workload đã được tách hợp lý:
- Request chính vào monolith.
- File tĩnh qua CDN.
- File lớn vào object storage.
- Việc dài qua queue/worker.
- Dữ liệu đọc nhiều qua cache.
Đây là kiến trúc rất mạnh cho nhiều sản phẩm.
Trước khi tách microservices, hãy chắc rằng monolith đã dùng đúng các công cụ nền tảng này.
---
6.12. Khi nào nên giữ monolith?
Nên giữ monolith khi:
- Team còn nhỏ.
- Domain còn thay đổi nhiều.
- Sản phẩm chưa ổn định.
- Chưa có bottleneck rõ cần tách.
- Deploy một khối vẫn ổn.
- Database transaction còn quan trọng.
- Observability chưa đủ cho microservices.
- Chưa có người vận hành hệ phân tán.
Giữ monolith không có nghĩa là bỏ qua kiến trúc. Ngược lại, đây là lúc nên tổ chức code tốt để không thành cục bùn.
Nên đầu tư vào:
- Module boundary.
- Service layer.
- Test phần quan trọng.
- Queue cho việc dài.
- Logging/metrics.
- Database index/transaction.
- Deploy/rollback đơn giản.
---
6.13. Khi nào nên bắt đầu nghĩ đến tách?
Bắt đầu nghĩ đến tách khi có áp lực rõ:
Scale riêng
Một phần có tải khác hẳn phần còn lại.
Ví dụ:
- AI grading cần concurrency cao.
- Video processing cần CPU/GPU.
- Realtime cần nhiều connection.
- Search cần engine riêng.
Failure isolation
Một phần lỗi không được làm chết toàn hệ thống.
Ví dụ:
- Notification provider lỗi không được làm checkout chết.
- Report nặng không được làm API chính chậm.
Deploy độc lập
Một phần thay đổi thường xuyên và cần deploy riêng.
Ownership rõ
Một team chịu trách nhiệm trọn vẹn một domain.
Security boundary
Dữ liệu hoặc quyền truy cập cần cách ly mạnh hơn.
Nhưng "nghĩ đến tách" không có nghĩa là tách ngay. Có thể bắt đầu bằng:
- Tách module rõ hơn.
- Tách queue riêng.
- Tách worker riêng.
- Tách database table ownership.
- Tách API boundary.
- Sau đó mới tách service vật lý.
---
6.14. Con đường tiến hóa thực dụng
Một con đường thực tế:
Bước 1: Monolith đơn giản
App -> Database
Tốt cho giai đoạn đầu.
Bước 2: Monolith có nền tảng production
CDN/Proxy -> App -> Database
-> Cache
-> Queue -> Worker
-> Object Storage
Tốt cho phần lớn MVP/sản phẩm nhỏ-vừa.
Bước 3: Modular monolith
App
users
catalog
ordering
payment
notification
Tốt khi codebase bắt đầu lớn và cần ranh giới.
Bước 4: Tách workload riêng
App -> Queue -> AI Worker / Video Worker / Report Worker
Tách theo tải và failure mode trước.
Bước 5: Tách service thật sự
App / API Gateway
-> User Service
-> Order Service
-> Payment Service
-> Search Service
Chỉ khi có lý do rõ và khả năng vận hành tương ứng.
---
6.15. Những lỗi tư duy phổ biến về monolith
Lỗi 1: Nghĩ monolith là code xấu
Monolith là cách deploy. Code xấu là vấn đề tổ chức code.
Lỗi 2: Nghĩ microservices tự làm hệ thống scale
Microservices có thể scale từng phần, nhưng cũng thêm network, dữ liệu phân tán và vận hành phức tạp.
Lỗi 3: Tách service khi domain chưa rõ
Nếu nghiệp vụ còn thay đổi mạnh, service boundary sẽ thay đổi liên tục. Tách sớm có thể làm chậm team.
Lỗi 4: Dùng microservices để né refactor
Nếu code đang rối, tách service có thể chỉ biến code rối thành hệ phân tán rối.
Lỗi 5: Không dùng queue/cache/CDN vì nghĩ chỉ microservices mới scale
Rất nhiều vấn đề scale ban đầu được giải quyết bằng queue, cache, CDN, index, worker, không cần service riêng.
Lỗi 6: Giữ monolith nhưng không modular hóa
Giữ monolith không có nghĩa là để mọi thứ dính vào nhau. Monolith muốn sống lâu phải có ranh giới bên trong.
---
6.16. Checklist: monolith của bạn đang khỏe hay đang bệnh?
Dấu hiệu khỏe
- Code chia module rõ.
- Request quan trọng có latency ổn.
- Database query được theo dõi.
- Việc dài đã đưa vào queue.
- Deploy đơn giản và rollback được.
- Logs đủ để debug.
- Team sửa tính năng không quá sợ ảnh hưởng lan rộng.
- Có test cho nghiệp vụ quan trọng.
Dấu hiệu bệnh
- Sửa một tính năng không biết ảnh hưởng đâu.
- Không có module boundary.
- Controller/model quá béo.
- Mọi thứ chạy trong request.
- Query chậm nhưng không ai biết.
- Không có queue cho việc dài.
- Deploy là một lần cầu may.
- Không có observability.
Nếu monolith bệnh, bước đầu tiên thường là chữa tổ chức bên trong, không phải lập tức cắt thành microservices.
---
6.17. Kết luận của chương
Monolith không xấu. Monolith là một lựa chọn kiến trúc rất thực dụng, đặc biệt khi:
- Team nhỏ.
- Sản phẩm còn thay đổi.
- Domain chưa ổn định.
- Tải chưa buộc phải tách.
- Cần đi nhanh và giữ hệ thống dễ hiểu.
Điều cần tránh không phải monolith, mà là monolith không có ranh giới.
Một monolith tốt có thể có:
- Module rõ ràng.
- Service layer.
- Database transaction mạnh.
- Queue/worker cho việc dài.
- Cache đúng chỗ.
- CDN/object storage cho file.
- Observability.
- Test phần quan trọng.
Khi áp lực thật xuất hiện, ta có thể tiến hóa từ monolith sang modular monolith, rồi tách một vài service cần thiết. Đó là con đường tự nhiên và ít rủi ro hơn nhiều so với việc bắt đầu bằng microservices chỉ vì nó nghe hiện đại.