Phân tích ba lựa chọn kiến trúc — Monolith, Modular Monolith và Domain Services — dưới góc độ chi phí, đội ngũ và khả năng mở rộng, lấy Odoo làm điểm tham chiếu.
Khi một công ty công nghệ quyết định tự xây ERP nội bộ, câu hỏi kiến trúc đầu tiên thường không phải “dùng framework nào” mà là: nên gom thành một hệ thống lớn, xé thành nhiều service nhỏ, hay chọn một mô hình ở giữa.
Đây là một quyết định khó đảo ngược, ảnh hưởng tới chi phí vận hành và tốc độ phát triển trong nhiều năm. Bài viết phân tích ba lựa chọn cho một tình huống cụ thể — doanh nghiệp 2.000 nhân sự tự xây ERP — và lấy Odoo làm điểm tham chiếu, vì đó là một trong số ít ERP quy mô lớn có kiến trúc đủ rõ ràng để học hỏi.
Mục lục
1. Bối cảnh
2. Odoo thực chất được thiết kế thế nào
3. Phương án 1 — Monolith
4. Phương án 2 — Microservices
5. Phương án 3 — Domain-Oriented Architecture
6. So sánh chi tiết
7. Góc nhìn tài chính
8. Khi nào nên tách service
9. Kiến trúc đề xuất
10. Kết luận
1. Bối cảnh
Lấy một công ty giả định để cả bài bám vào, tạm gọi là TechCorp:
- 2.000 nhân sự, 4 chi nhánh.
- Nhiều phòng ban, dữ liệu đang nằm rải rác trên phần mềm HR cũ, CRM thuê ngoài, timesheet trên Excel, kế toán trên một hệ thống riêng.
- Có sẵn đội kỹ sư mạnh, muốn tự xây ERP nội bộ để kiểm soát quy trình và gom dữ liệu về một mối.
ERP dự kiến gồm các phân hệ:

Câu hỏi trung tâm: nên xây một service lớn, nhiều service nhỏ, hay một mô hình ở giữa?
Trước khi trả lời, cần làm rõ một con số mà nhiều người bỏ qua: tải thực tế của hệ thống. ERP nội bộ cho 2.000 người nghĩa là gì về mặt kỹ thuật? Giả sử 70% nhân viên dùng mỗi ngày, đỉnh điểm vào giờ chấm công sáng và lúc duyệt timesheet cuối tuần:
- Người dùng đồng thời lúc cao điểm: khoảng 300–500.
- Request mỗi giây lúc cao điểm: vài trăm, hiếm khi chạm 1.000.
- Tổng request mỗi ngày: khoảng 2–5 triệu, phần lớn là CRUD và đọc dashboard.
| Insight — ERP nội bộ cho 2.000 người không phải hệ tải cao. Nó là hệ độ phức tạp nghiệp vụ cao nhưng lưu lượng thấp. Cái khó nằm ở quy tắc duyệt nghỉ phép chồng chéo, ở chuyện payroll phụ thuộc attendance phụ thuộc leave, ở vài chục loại báo cáo tài chính — không nằm ở việc chịu 50.000 request mỗi giây. Nhầm hai loại độ khó này là nguồn gốc của phần lớn quyết định kiến trúc sai. |
Điểm này quan trọng vì microservices sinh ra để giải hai bài toán: scale tải rất lớn và scale tổ chức hàng nghìn kỹ sư. TechCorp không có bài toán thứ nhất, và dự án này sẽ chỉ có chừng 18–30 kỹ sư, chưa chạm bài toán thứ hai. Vậy mà nhiều đội vẫn chọn microservices đầu tiên — thường vì đội giỏi và muốn dùng công nghệ mới, chứ không vì bài toán đòi hỏi.
2. Odoo thực chất được thiết kế thế nào
Odoo phục vụ hàng chục nghìn doanh nghiệp, không thiếu công ty vài chục nghìn nhân sự. Và đây là điều làm nhiều người bất ngờ: Odoo không phải microservices. Nó là một modular monolith, và đó chính là lý do nó chạy ổn định suốt 20 năm.
Cấu trúc thật của Odoo:

Vài đặc điểm cốt lõi:
- Mọi module (addon) chạy trong cùng một tiến trình Python, dùng chung một database PostgreSQL.
- Các module được đóng gói tách bạch: mỗi addon là một thư mục có manifest khai báo phụ thuộc, có model, view và quyền riêng. Ranh giới giữa các module rất rõ.
- Khi module HR cần ghi một bút toán, nó không gọi HTTP sang một “Accounting Service”. Nó gọi thẳng trong bộ nhớ, kiểu self.env[‘account.move’].create(…), và lời gọi đó nằm trong cùng một transaction database.
Vì sao mô hình này hiệu quả:
- Tính nhất quán dữ liệu gần như miễn phí. Duyệt một bảng lương đụng vào employee, attendance, account, payslip — tất cả trong một transaction. Hoặc thành công hết, hoặc rollback hết. Không cần saga, không cần outbox, không phải giải thích “eventual consistency” cho kế toán.
- Tích hợp module qua ORM, không qua mạng. Lời gọi liên module là in-process: không độ trễ mạng, không serialize JSON, không phải version hóa API nội bộ.
- Một lần deploy, một pipeline. Một người vận hành được cả hệ thống.
Bài học rút ra: tách bạch về module không bắt buộc phải tách bạch về triển khai. Bạn có thể có ranh giới domain rất rõ mà vẫn deploy chung. Ranh giới logic mạnh cộng triển khai đơn giản là điểm cân bằng tốt cho phần lớn ERP doanh nghiệp.
Nếu ERP mã nguồn mở thành công nhất chọn modular monolith, đội của TechCorp cần một lý do đủ mạnh để làm khác. Phần dưới xem từng phương án.
3. Phương án 1 — Monolith
Đơn giản nhất: một codebase, một database, một lần deploy.

Ưu điểm:
- Ra mắt nhanh nhất. Tháng đầu đã demo được, không tốn thời gian dựng hạ tầng phân tán.
- Dễ debug, dễ test. Một stack trace, một log. Reproduce bug trên máy local đơn giản.
- Giao dịch ACID toàn cục, không có vấn đề nhất quán dữ liệu phân tán.
- Chi phí hạ tầng và vận hành thấp nhất: một app server (vẫn scale ngang được), một database, một pipeline.
Nhược điểm — và đây mới là phần đáng nói. Vấn đề của monolith không phải hiệu năng. Với tải của TechCorp, một monolith viết tử tế chạy vô tư trên 3–4 instance sau load balancer. Vấn đề thật xuất hiện khi codebase và đội ngũ phình to. Hình dung TechCorp sau hai năm:
- Khoảng 300 API endpoint trong một dự án.
- Khoảng 500 bảng trong một schema.
- 30 developer cùng push vào một repo.
Lúc này hay gặp các triệu chứng sau:
- Coupling âm thầm. Không có gì ngăn developer Finance import thẳng model của HR. Sau hai năm, mọi thứ dính vào nhau, một thay đổi nhỏ ở Employee làm vỡ vài chỗ không ai ngờ.
- Bán kính ảnh hưởng bằng cả hệ thống. Một memory leak ở module báo cáo có thể kéo chậm cả phần chấm công sáng thứ Hai.
- Release khóa lẫn nhau. Team Finance muốn lên production, nhưng nhánh main đang dở một tính năng HR chưa test xong, thế là cả nhóm chờ nhau.
- Build và test chậm dần, vòng phản hồi của developer dài ra, năng suất giảm.
| Warning — Đừng đánh đồng “monolith” với “code dính bùn”. Một monolith vẫn có thể có ranh giới module tốt — đó chính là modular monolith. Nhưng nếu để code tự do dính vào nhau mà không có rào chắn (module boundary, kiểm tra phụ thuộc tự động trong CI), sau 18 tháng bạn sẽ có một khối 500 bảng mà không ai dám động. |
Khi nào monolith thuần phù hợp: giai đoạn đầu, đội dưới 10 người, cần ra mắt nhanh để kiểm chứng nghiệp vụ. Với TechCorp ở quy mô mục tiêu, đây là điểm khởi đầu tốt nhưng không nên là đích đến.
4. Phương án 2 — Microservices
Phương án mà đội kỹ sư giỏi hay bị hút vào: tách mỗi phân hệ thành một service, scale độc lập, deploy độc lập, công nghệ tự do.

Nghe hấp dẫn, nhưng cái giá thật ít khi được nói trước.
Cái giá kỹ thuật
Database-per-service phá vỡ giao dịch. Trong monolith, duyệt timesheet rồi cập nhật payroll là một transaction. Khi tách Timesheet Service và Payroll Service với hai database riêng, bạn mất ACID xuyên service. Để bù lại phải dùng Saga hoặc 2-Phase Commit: viết các bước bù trừ, xử lý trạng thái treo, và chấp nhận có lúc dữ liệu tạm thời không khớp.

Kèm theo đó là một loạt thành phần hạ tầng bắt buộc: API Gateway, service discovery, config tập trung. Để giảm coupling đồng bộ thì đưa thêm message queue (Kafka/RabbitMQ) vào, và giờ phải lo thứ tự message, idempotency, dead-letter queue, replay khi service chết.
Cái giá vận hành
Đây là phần các đội nhỏ hay ước lượng thiếu:
- Monitoring và tracing: một lỗi “lương sai” giờ phải truy qua 4 service. Không có distributed tracing thì gần như không debug nổi.
- Logging: log nằm rải rác nhiều nơi, phải gom về một chỗ và nối lại bằng trace-id.
- CI/CD: mỗi service một pipeline, mỗi service một lần lo version và backward compatibility của API nội bộ.
- Chi phí vận hành: một modular monolith cần chừng một người lo hạ tầng. Một hệ chục microservices thực tế cần 2–3 người chuyên trách, cộng chi phí cụm Kubernetes chạy 24/7.
Anti-pattern hay gặp: chia service theo entity
Sai lầm phổ biến nhất khi mới chuyển sang microservices là lấy module HR rồi “tách nhỏ cho gọn”:

Vì sao đây là anti-pattern:
- Các service này chia sẻ dữ liệu cực nhiều. Gần như mọi nghiệp vụ HR đều cần thông tin Employee. Mỗi lần tính lương, Payroll phải gọi HTTP sang Employee, Attendance, Leave — vài lời gọi mạng cho việc trước đây chỉ là một câu JOIN.
- Chúng cần nhất quán giao dịch. Nghỉ phép ảnh hưởng công, công ảnh hưởng lương. Đây là một bounded context chặt chẽ, không nên xé. Xé ra là tự biến một transaction SQL đơn giản thành một saga phân tán.
- Chúng cùng phát triển, cùng release. Luật lao động đổi thì cả cụm đổi cùng lúc, lợi ích “deploy độc lập” biến mất, chỉ còn lại cái giá.
- Mỗi service quá nhỏ để tự đứng vững, dẫn tới cái gọi là distributed monolith — tệ hơn cả monolith lẫn microservices, vì gánh độ phức tạp phân tán mà không có sự độc lập thật.
| Warning — Ranh giới service nên trùng ranh giới nghiệp vụ (bounded context), không phải ranh giới bảng dữ liệu. Nếu hai “service” luôn đổi cùng nhau và luôn cần dữ liệu của nhau trong cùng transaction, chúng phải là một service. Employee, Attendance, Leave, Payroll thuộc cùng một mảng nghiệp vụ. |
Microservices có lúc đúng — khi đội lên cả trăm kỹ sư, tải tăng nhiều lần, và các domain tiến hóa với nhịp rất khác nhau. Nhưng đó là bài toán của tương lai, không phải của ngày khởi công.
5. Phương án 3 — Domain-Oriented Architecture
Đây là điểm cân bằng, và là phương án khuyến nghị cho TechCorp. Thay vì tách theo entity (sai) hay gộp tất cả (rủi ro dài hạn), tách theo domain nghiệp vụ.

Mỗi domain là một service thô — đủ lớn để tự đứng vững, đủ rõ để một team sở hữu trọn vẹn. Bên trong mỗi domain, code tổ chức như một modular monolith. Giữa các domain, giao tiếp qua API rõ ràng và event. Chi tiết từng domain, cũng chính là các bounded context:

Bốn nguyên tắc làm nên sự khác biệt:
- Domain boundary theo nghiệp vụ. Ranh giới được vẽ ở chỗ nghiệp vụ cho phép eventual consistency. Ví dụ HRM “chốt” dữ liệu chấm công và nghỉ phép trong transaction của nó, rồi phát event sang Finance để tính lương. Lương tính sau khi công đã chốt là điều hoàn toàn tự nhiên, nên ranh giới này hợp lý. Ngược lại, những thứ cần nhất quán tức thời thì giữ chung một domain.
- Bounded context, mỗi domain có “ngôn ngữ” riêng. “Khách hàng” trong CRM (lead, cơ hội) khác với “đối tác” trong Finance (công nợ, hóa đơn). Mỗi context giữ mô hình dữ liệu riêng, không ép cả công ty dùng chung một bảng customer khổng lồ.
- Team ownership. Tổ chức của bạn rồi sẽ in dấu lên kiến trúc dù muốn hay không, nên chủ động: mỗi domain một team, sở hữu trọn vẹn từ database tới API tới release. Hết chuyện đổ lỗi qua lại.
- Deploy độc lập theo domain. Team Finance lên production tính năng mới mà không phải chờ team HRM. Đây là lợi ích lớn nhất so với monolith, đạt được trong khi chỉ phải vận hành 5–6 service thay vì 15–20.
| Insight — Modular monolith và domain services là hai điểm trên cùng một quang phổ, không phải hai phe đối lập. Một modular monolith có ranh giới domain sạch có thể “tách” thành domain services bằng cách kéo từng module ra thành tiến trình riêng, gần như không phải viết lại nghiệp vụ. Vì thế cách an toàn nhất là bắt đầu bằng modular monolith ranh giới rõ, rồi tách dần domain nào thực sự cần. Bạn giữ được quyền chọn mà không trả trước chi phí phân tán. |
6. So sánh chi tiết
| Tiêu chí | Monolith | Modular Monolith | Domain Services | Microservices |
|---|---|---|---|---|
| Độ phức tạp | Thấp lúc đầu, tăng nhanh khi lớn | Thấp đến trung bình | Trung bình | Cao |
| Chi phí hạ tầng | Thấp | Thấp | Trung bình | Cao |
| Chi phí DevOps | 1 người | 1 người | 1–2 người | 2–3 người trở lên |
| Tốc độ phát triển ban đầu | Nhanh | Nhanh | Trung bình | Chậm |
| Tốc độ sau 2–3 năm | Chậm dần | Tốt | Tốt | Tốt nếu đội đủ lớn |
| Khả năng scale tải | Scale cả khối | Scale cả khối | Scale theo domain | Scale từng service |
| Khả năng scale đội | Dưới 15 người | Dưới 30–40 | 30–100 | 100+ |
| Khả năng bảo trì | Giảm dần | Tốt nếu giữ kỷ luật | Tốt | Tốt nhưng cần công cụ mạnh |
| Nhất quán dữ liệu | ACID toàn cục | ACID toàn cục | ACID trong domain, event giữa domain | Eventual consistency |
| Tuyển dụng | Dễ | Dễ | Trung bình | Khó, cần senior phân tán |
| Phù hợp cho TechCorp | Rủi ro nếu không tiến hóa | Tốt | Tốt nhất | Quá sớm |
Đọc theo cột, một điều rõ dần: độ phức tạp bạn phải gánh nên tương xứng với độ phức tạp bài toán bạn có. Microservices không “tốt hơn”, nó chỉ phù hợp với một loại bài toán mà TechCorp chưa có.
7. Góc nhìn tài chính
Kiến trúc không chỉ là quyết định kỹ thuật, nó là một cam kết ngân sách kéo dài nhiều năm. Lấy đội hình thực tế của TechCorp:
- 10 backend developer
- 5 frontend developer
- 2 QA
- 1 DevOps
Tổng 18 người. Tác động của từng kiến trúc lên cùng đội hình này:
Với modular monolith hoặc domain services (5–6 service):
- Một người lo hạ tầng vẫn vận hành được: ít pipeline, ít cụm, observability gọn.
- 10 backend chia thành 3–4 nhóm domain, mỗi nhóm 2–3 người, đủ để không domain nào phụ thuộc vào một cá nhân.
- Chi phí cloud: vài app instance, 1–3 PostgreSQL, một Redis. Phần lớn ngân sách hạ tầng nằm ở database và môi trường staging.
Với microservices đầy đủ (15–20 service):
- Một người lo hạ tầng là bất khả thi. Phải tuyển thêm 1–2 người chuyên hạ tầng, đội phình lên ngay và đắt hơn.
- 10 backend chia cho 15 service nghĩa là nhiều service chỉ có “nửa người” trông coi, rủi ro một người nghỉ là một service mồ côi.
- Chi phí cloud: cụm Kubernetes 24/7, message broker có HA, observability đầy đủ. Hóa đơn cloud có thể gấp 2–4 lần, chưa kể thời gian kỹ sư bỏ ra để vận hành.
| Insight — Phần lớn ngân sách dự án là lương kỹ sư, không phải hóa đơn cloud. Mỗi service tạo ra đều ngốn một phần năng lực vận hành hữu hạn. Microservices không làm đội nhanh hơn, nó chia nhỏ một đội vốn không lớn, biến kỹ sư từ người xây tính năng thành người vá hạ tầng phân tán. Một kiến trúc khiến 30% năng lực kỹ sư đi lo hạ tầng thay vì nghiệp vụ là một kiến trúc đốt tiền, dù hóa đơn AWS trông vẫn ổn. |
8. Khi nào nên tách service
Tách service là quyết định khó đảo ngược, nên cần tiêu chí rõ. Chỉ tách một domain ra thành service riêng khi nó thỏa ít nhất một tín hiệu mạnh dưới đây, lý tưởng là vài tín hiệu cùng lúc:

Soi checklist này vào TechCorp:
- Đội trên 50 developer chia nhiều team thường xuyên đụng độ trên một codebase: TechCorp đang 18 người, chưa đạt.
- Một domain riêng có tải vượt 1 triệu request mỗi ngày với nhu cầu scale lệch hẳn: tổng hệ thống có thể chạm mức này nhưng phân bổ không lệch tới mức buộc tách, chưa cần.
- Cần deploy độc lập liên tục với nhịp release khác hẳn nhau: đúng cho 2–3 domain, ví dụ Business release theo tuần còn Finance theo tháng. Đây là lý do hợp lệ để dùng domain services, không phải để xé thành microservices đầy đủ.
- Domain tiến hóa rất khác nhau hoặc có yêu cầu kỹ thuật đặc thù: ví dụ Reporting cần data warehouse riêng, Notification cần throughput cao và độc lập vòng đời. Đây là các ứng viên tách đầu tiên hợp lý.
| Best Practice — Tách khi đau, không tách phòng xa. Bắt đầu với modular monolith ranh giới rõ, đo lường, rồi tách đúng domain đang gây đau (thường là Notification và Reporting trước, vì chúng độc lập về vòng đời và scale). Tách trước những đau đớn chưa xảy ra là trả lãi suất phức tạp cho một khoản vay chưa cần. |
Nếu TechCorp chưa chạm các điều kiện trên thì câu trả lời là chưa nên tách thành nhiều service.
9. Kiến trúc đề xuất
Tổng hợp lại, đây là kiến trúc đặt lên bàn cho TechCorp: một Domain-Oriented Architecture thực dụng, có thể bắt đầu như modular monolith và tách dần.

Giải thích từng thành phần:
- API Gateway: một cửa vào duy nhất cho Web và Mobile. Lo xác thực token, định tuyến tới domain, rate-limit, và che cấu trúc nội bộ khỏi client.
- Core / Identity: nơi quản lý user, role, permission. Tách riêng vì mọi domain đều phụ thuộc nó và nó cần ổn định cao. Nên tách sớm và giữ nhỏ, sạch.
- HRM: giữ Employee, Attendance, Leave, Recruitment trong cùng một domain và cùng database, vì chúng cần nhất quán giao dịch chặt chẽ.
- Business (CRM, Project, Timesheet): một domain vì chúng quấn vào nhau, timesheet thuộc project, project gắn với cơ hội CRM.
- Finance (Accounting, Payroll): domain nhạy cảm nhất về tính đúng đắn. Nhận dữ liệu công và nghỉ đã chốt từ HRM qua event, rồi tính lương trong transaction nội bộ của mình.
- Message Bus: đường truyền event giữa các domain, là chỗ eventual consistency được phép tồn tại và nghiệp vụ chấp nhận.
- Notification: ứng viên tách đầu tiên vì độc lập vòng đời và cần throughput cao, dùng Redis làm hàng đợi và cache.
- Reporting và Data Warehouse: không cho báo cáo nặng truy vấn thẳng vào database vận hành, vì sẽ làm chậm cả hệ thống. Đẩy dữ liệu qua ETL sang một kho riêng, tối ưu cho đọc và phân tích.
| Warning — Một sai lầm hay gặp về database: tách service nhưng vẫn cho mọi service dùng chung một database. Đó là shared database anti-pattern, có vẻ ngoài microservices nhưng coupling chặt ở tầng dữ liệu, mất hết lợi ích mà giữ nguyên cái giá. Nếu đã tách domain thì tách cả database của nó. Còn nếu chưa tách (modular monolith) thì dùng chung database hoàn toàn ổn, miễn schema được phân vùng theo module để sau này tách ra dễ. |
Về lộ trình triển khai, đừng dựng toàn bộ sơ đồ trên ở ngày đầu. Một lộ trình thực tế:

Bắt đầu ở giai đoạn 0 cho bạn tốc độ ra mắt của monolith cộng với ranh giới sạch để tiến hóa. Mỗi giai đoạn sau chỉ làm khi có tín hiệu đau cụ thể từ phần 8.
10. Kết luận
Quay lại câu hỏi ban đầu: nếu là ERP cho doanh nghiệp 2.000 nhân sự thì lựa chọn nào hợp lý nhất?
Câu trả lời: Domain-Oriented Architecture, khởi đầu dưới dạng modular monolith có ranh giới domain rõ, rồi tách dần thành domain services theo nhu cầu thực tế.
Ba điều cần nhớ:
- Không microservices cực đoan. Xé ERP nội bộ thành 15–20 service nhỏ là quyết định đốt tiền và đốt năng lực kỹ sư. Với 18 người và tải vài trăm request mỗi giây, bạn gánh toàn bộ chi phí của hệ phân tán mà không thu được lợi ích tương xứng. Tệ hơn, chia theo entity tạo ra distributed monolith, phương án xấu nhất.
- Không monolith khổng lồ buông lỏng. Một khối 500 bảng, 300 API, 30 người cùng đạp lên nhau trên một nhánh main sẽ bóp nghẹt tốc độ sau hai năm. Monolith được phép, nhưng phải là modular monolith có kỷ luật, ranh giới được bảo vệ bằng kiến trúc và công cụ chứ không bằng lời hứa.
- Điểm cân bằng nằm ở giữa. Domain-Oriented Architecture (5–6 domain) hoặc modular monolith mở rộng cho bạn nhất quán giao dịch trong domain, deploy độc lập giữa domain, team ownership rõ ràng, và chi phí vận hành mà một đội 18 người gánh được. Đây cũng là triết lý đã giúp Odoo phục vụ doanh nghiệp lớn suốt 20 năm.
| Insight — Kiến trúc tốt không phải kiến trúc hiện đại nhất, mà là kiến trúc tương xứng với độ phức tạp thật của bài toán và năng lực thật của đội ngũ. Con số 2.000 nhân sự nghe to nhưng nói về độ phức tạp nghiệp vụ, không phải tải kỹ thuật. Giải đúng bài toán mình có, và chọn một kiến trúc cho phép mình đổi ý sau này — modular monolith với ranh giới domain sạch chính là tấm vé giữ quyền chọn đó. |
Tóm tắt khuyến nghị hành động:
- Khởi công bằng modular monolith, phân vùng code và schema theo 6 domain ngay từ đầu.
- Bảo vệ ranh giới domain bằng công cụ (module boundary check, kiểm tra phụ thuộc tự động trong CI). Đây là khoản đầu tư rẻ nhất, lãi cao nhất.
- Tách Notification và Reporting ra trước khi cần, vì đây là hai domain dễ tách và lợi rõ.
- Chỉ tách HRM, Business, Finance khi chạm tín hiệu đau cụ thể ở phần 8, không tách phòng xa.
- Coi quyết định kiến trúc là quyết định ngân sách nhiều năm, tối ưu cho giá trị nghiệp vụ trên mỗi kỹ sư, không phải cho độ phức tạp của sơ đồ.
Vui lòng đăng nhập để bình luận.