Mở đầu — vì sao bài này quan trọng
Nếu bạn từng nghe một đội kỹ thuật cãi nhau nảy lửa trong buổi retro, khả năng cao chủ đề là… branching. "Nên tạo nhánh release không?", "Sao lại có tận 40 nhánh feature chưa merge?", "Ai để cái nhánh develop chết cứng suốt 3 tuần vậy?". Đằng sau những câu hỏi này là một quyết định kiến trúc quan trọng bậc nhất của quy trình CI/CD: chiến lược nhánh (branching strategy).
Branching strategy không chỉ là chuyện đặt tên nhánh cho đẹp. Nó quyết định code được tích hợp (integrate) nhanh hay chậm, quyết định pipeline CI của bạn phải build cái gì, quyết định đội có thể release mỗi ngày hay mỗi quý, và quyết định mức độ "đau đầu" khi giải quyết conflict. Chọn sai chiến lược, bạn sẽ thấy đội mình rơi vào cảnh "merge hell" — mỗi lần gộp code là một cuộc chiến kéo dài cả ngày trời.
Trong bài này chúng ta sẽ mổ xẻ hai triết lý đối lập nhau: GitFlow — bài bản, nhiều tầng nhánh, hợp với release theo phiên bản; và Trunk-based Development — tối giản, tích hợp liên tục, là nền tảng của các đội deploy nhiều lần mỗi ngày. Mục tiêu không phải để bạn tôn thờ một cái và ghét cái kia, mà để bạn biết khi nào dùng cái nào và cấu hình pipeline CI/CD tương ứng cho đúng.
Khái niệm cốt lõi
GitFlow — mô hình nhiều nhánh có cấu trúc
GitFlow do Vincent Driessen đề xuất năm 2010, xây quanh một hệ thống nhánh có vai trò rõ ràng:
main ──●────────────●───────────● (production releases: v1.2, v1.3)
\ / \ /
release/1.2 \ ●──● \ / (ổn định hoá trước khi phát hành)
\ / \ /
develop ──●──●──●──────●───●──●──●─── (tích hợp các feature)
\ / \ /
feature/login ●─● \ / (một tính năng)
feature/cart ●─●
hotfix/1.2.1 ──────────────────●──● (sửa lỗi khẩn trên production)
Các nhánh chính:
main(hoặcmaster): luôn phản ánh code đang chạy trên production. Mỗi commit trênmainlà một bản phát hành, thường gắn tag phiên bản (v1.2.0).develop: nhánh tích hợp. Mọi tính năng hoàn thành được gộp về đây.developlà "phiên bản tương lai" đang được xây dựng.feature/*: mỗi tính năng có một nhánh riêng, tách ra từdevelopvà gộp lại vàodevelop. Nhánh này có thể sống vài ngày đến vài tuần.release/*: khidevelopđủ tính năng cho một bản phát hành, tạo nhánh release để "đóng băng" phạm vi, chỉ sửa bug và chuẩn bị tài liệu. Sau đó merge vào cảmainlẫndevelop.hotfix/*: sửa lỗi khẩn cấp trên production, tách từmain, merge ngược lạimainvàdevelop.
Trunk-based Development — tất cả đổ về một dòng
Trunk-based Development (TBD) đi ngược lại: chỉ có một nhánh dài hạn duy nhất là main (còn gọi là "trunk"). Mọi lập trình viên commit trực tiếp vào main hoặc thông qua các nhánh feature cực ngắn (short-lived branch) sống chưa tới 1–2 ngày rồi merge ngay.
main ──●──●──●──●──●──●──●──●──●──●── (luôn deploy được)
\ / \ / \ /
●● ●● ●● (nhánh nhỏ, sống < 1 ngày)
Triết lý cốt lõi: tích hợp càng thường xuyên càng tốt — đây chính là chữ CI (Continuous Integration) đúng nghĩa gốc. Vì code luôn được gộp về main hàng ngày, conflict được phát hiện và xử lý khi còn nhỏ. main luôn ở trạng thái "xanh" (build pass, test pass) và có thể deploy bất cứ lúc nào.
Câu hỏi tự nhiên: "Vậy tính năng chưa xong thì sao, chẳng phải sẽ lộ ra production à?". Câu trả lời là feature flags (cờ tính năng) — code được merge nhưng bị ẩn sau một công tắc, chỉ bật khi sẵn sàng. Đây là kỹ thuật đồng hành bắt buộc của TBD (bạn học sâu về nó ở Bài 20).
So sánh nhanh hai triết lý
| Tiêu chí | GitFlow | Trunk-based |
|---|---|---|
| Số nhánh dài hạn | Nhiều (main, develop, release) | Một (main) |
| Tuổi thọ nhánh feature | Vài ngày → vài tuần | Vài giờ → 1–2 ngày |
| Tần suất tích hợp | Thấp | Rất cao (nhiều lần/ngày) |
| Tần suất release | Theo phiên bản, định kỳ | Liên tục, on-demand |
| Rủi ro merge conflict | Cao (tích tụ) | Thấp (xử lý sớm) |
| Phụ thuộc feature flags | Ít | Bắt buộc |
| Phù hợp với | Sản phẩm có nhiều phiên bản, release định kỳ | SaaS, web, deploy liên tục |
Tình huống thực tế
Tình huống 1 — Startup fintech ở TP.HCM chuyển từ GitFlow sang Trunk-based
Một startup ví điện tử tại Quận 1 (gọi là "PayViet") có 12 kỹ sư, ban đầu dùng GitFlow theo đúng sách. Sau 8 tháng, họ đo được: mỗi feature trung bình mất 9 ngày từ lúc bắt đầu tới lúc lên production, và cứ mỗi lần merge nhánh release vào main là mất trung bình 4 giờ giải quyết conflict. Nhánh develop thường xuyên ở trạng thái "đỏ" vì tích tụ quá nhiều thay đổi chưa được test cùng nhau.
Nút thắt lớn nhất: đội marketing muốn A/B test tính năng "quét QR thanh toán" nhưng phải chờ cả chu kỳ release 2 tuần. Không thể chấp nhận được với một startup đang chạy đua thị phần.
Họ chuyển sang Trunk-based: bỏ nhánh develop, mọi PR nhỏ merge thẳng vào main trong ngày, dùng feature flags cho tính năng chưa hoàn thiện. Kết quả sau 3 tháng: thời gian đưa feature ra production giảm từ 9 ngày xuống 1,5 ngày; số lần deploy mỗi tuần tăng từ 2 lên 15; thời gian giải quyết conflict gần như biến mất vì mỗi PR chỉ động vào vài chục dòng.
Bài học: GitFlow tạo ra độ trễ có hệ thống. Với sản phẩm SaaS/web cần ra mắt nhanh, Trunk-based giải phóng tốc độ — nhưng chỉ khi có feature flags và test tự động tốt đi kèm.
Tình huống 2 — Công ty phần mềm gia công giữ nguyên GitFlow là đúng
Một công ty outsourcing ở Đà Nẵng (gọi là "SoftBridge") phát triển phần mềm quản lý kho cài đặt on-premise cho khách hàng Nhật. Sản phẩm được đóng gói thành các phiên bản đánh số (v3.4, v3.5) và khách hàng cài đặt trên máy chủ riêng của họ. Có khách đang chạy v3.2, khách khác chạy v3.5.
Khi khách v3.2 báo lỗi nghiêm trọng, đội không thể bắt họ nâng cấp lên v3.5. Họ cần tạo hotfix/3.2.1 từ đúng điểm phát hành v3.2 trên main, vá lỗi, đóng gói riêng cho khách đó. Đây chính xác là kịch bản GitFlow được sinh ra để phục vụ: nhiều phiên bản cùng tồn tại, cần bảo trì song song.
Có một senior đề xuất chuyển sang Trunk-based vì "công ty bên kia dùng thấy hay". May mắn là tech lead phản biện: mô hình phát hành của họ không phải deploy-liên-tục mà là release theo phiên bản có hợp đồng. Trunk-based sẽ khiến việc bảo trì phiên bản cũ trở nên hỗn loạn.
Bài học: Đừng chạy theo trend. Trunk-based không phải "tốt hơn" một cách tuyệt đối. Với sản phẩm đóng gói theo phiên bản, cần bảo trì nhiều nhánh phát hành song song, GitFlow (hoặc biến thể của nó) vẫn là lựa chọn hợp lý.
Tình huống 3 — Đội lớn ở Grab/Shopee-style và mô hình lai
Một đội platform ở một công ty thương mại điện tử lớn tại Singapore (bối cảnh kiểu Shopee) có gần 200 kỹ sư trên cùng một hệ thống. Họ dùng biến thể Trunk-based với short-lived branch + pull request bắt buộc: không ai push thẳng lên main, mọi thay đổi qua PR nhỏ, được review và chạy CI đầy đủ trong dưới 15 phút, rồi merge trong ngày.
Điểm khác biệt so với TBD "thuần" là họ vẫn giữ nhánh release/* cực ngắn hạn (sống vài giờ) chỉ để cắt phiên bản deploy theo lịch cho các dịch vụ nhạy cảm, chứ không phải để tích hợp. Đây là mô hình lai (hybrid) rất phổ biến ở các công ty lớn: lấy tốc độ tích hợp của Trunk-based, nhưng giữ một điểm cắt release có kiểm soát.
Bài học: Ở quy mô lớn, ranh giới giữa hai mô hình mờ đi. Cái cốt lõi cần giữ là tích hợp thường xuyên vào trunk và main luôn deploy được, còn hình thức nhánh release có thể linh hoạt.
Hướng dẫn từng bước
Giả sử bạn đang dẫn dắt một đội và muốn chọn + triển khai chiến lược nhánh. Đây là quy trình thực tế:
Bước 1 — Xác định mô hình phát hành của sản phẩm. Trả lời câu hỏi: bạn deploy liên tục (SaaS, web) hay phát hành theo phiên bản (app đóng gói, on-premise, mobile app qua store)? Đây là yếu tố quyết định lớn nhất.
Bước 2 — Đánh giá năng lực test tự động. Trunk-based chỉ an toàn khi CI đủ mạnh: test tự động phủ tốt, chạy nhanh (dưới 10–15 phút). Nếu test còn yếu, đội chưa sẵn sàng cho commit-thẳng-vào-main.
Bước 3 — Chọn mô hình:
- Deploy liên tục + CI mạnh → Trunk-based.
- Phát hành theo phiên bản, bảo trì song song → GitFlow.
- Chưa chắc → bắt đầu với Trunk-based có PR (an toàn và hiện đại nhất cho đa số web/SaaS).
main: bắt buộc PR, bắt buộc CI pass, bắt buộc ít nhất 1 review. Ví dụ với GitHub:# .github/workflows/ci.yml — chạy trên mọi PR vào main
on:
pull_request:
branches: [ main ]
jobs:
ci:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm run lint
- run: npm test
Rồi bật branch protection rule yêu cầu job ci phải xanh mới được merge.
Bước 5 — Áp đặt quy tắc nhánh ngắn. Đặt quy ước: nhánh feature không sống quá 1–2 ngày. Nếu tính năng lớn, chia nhỏ thành nhiều PR và giấu sau feature flag. Đây là kỷ luật văn hoá, không phải cấu hình kỹ thuật.
Bước 6 — Chuẩn hoá quy ước đặt tên (nếu dùng GitFlow): feature/JIRA-123-mo-ta-ngan, release/1.4.0, hotfix/1.3.2. Nhất quán giúp tự động hoá pipeline theo tên nhánh dễ dàng.
Bước 7 — Đo lường và điều chỉnh. Theo dõi các chỉ số DORA: lead time (thời gian từ commit đến production) và deployment frequency. Nếu lead time cao và nhánh feature ngày càng dài, đó là dấu hiệu nên siết kỷ luật hoặc chuyển hướng chiến lược.
Lỗi thường gặp & mẹo
Lỗi 1 — Gọi là "Trunk-based" nhưng nhánh feature sống cả tuần. Đây là lỗi phổ biến nhất. Nhánh short-lived mà không "short" thì bạn chỉ đang làm GitFlow thiếu nhánh develop, mất hết lợi ích tích hợp sớm. Mẹo: đặt giới hạn cứng — PR mở quá 2 ngày sẽ bị nhắc trong daily standup.
Lỗi 2 — Dùng Trunk-based mà không có feature flags. Kết quả là hoặc code nửa vời lộ ra production, hoặc mọi người lại quay về nhánh dài để "giấu" tính năng. Mẹo: đầu tư một cơ chế feature flag (dù chỉ là một biến môi trường đơn giản) trước khi chuyển sang TBD.
Lỗi 3 — Áp GitFlow đầy đủ cho một startup 5 người deploy web mỗi ngày. Bạn sẽ ngập trong thủ tục nhánh develop, release không cần thiết, làm chậm mọi thứ. Mẹo: GitFlow chỉ đáng dùng khi bạn thực sự cần bảo trì nhiều phiên bản.
Lỗi 4 — Để nhánh develop và main trôi xa nhau. Trong GitFlow, quên merge main (sau hotfix) ngược về develop là nguyên nhân kinh điển của việc "lỗi đã sửa rồi lại tái xuất hiện ở phiên bản sau". Mẹo: tự động hoá — mỗi hotfix merge vào main phải tạo PR đồng bộ về develop.
Lỗi 5 — Nhầm lẫn merge với rebase gây rối lịch sử. Mẹo: thống nhất một chiến lược merge cho toàn đội (ví dụ squash-merge cho PR vào main) để lịch sử main sạch, mỗi commit ứng với một tính năng có thể revert gọn gàng.
Mẹo vàng: Chiến lược nhánh nào cũng chỉ tốt bằng kỷ luật của đội. Công cụ (branch protection, CI, flags) hỗ trợ, nhưng thói quen "merge sớm, merge nhỏ" mới là thứ tạo ra khác biệt.
Bài tập thực hành
- Phân loại tình huống: Với mỗi sản phẩm sau, chọn GitFlow hay Trunk-based và giải thích trong 2–3 câu: (a) một web app đặt vé xem phim deploy mỗi ngày; (b) một firmware cho thiết bị IoT bán ra thị trường nhiều đợt; (c) một thư viện mã nguồn mở phát hành theo semver cho hàng nghìn người dùng.
- Thiết lập thực tế: Tạo một repo demo trên GitHub, cấu hình branch protection cho
mainyêu cầu PR + CI pass. Mở một PR nhỏ, quan sát pipeline chặn merge khi test fail.
- Mô phỏng merge hell: Tự tạo 3 nhánh feature cùng sửa một file, để chúng "sống" tách biệt rồi merge lần lượt. Đo xem bạn tốn bao lâu giải quyết conflict. Sau đó làm lại theo kiểu Trunk-based (merge từng thay đổi nhỏ ngay) và so sánh trải nghiệm.
- Viết ADR: Soạn một Architecture Decision Record ngắn (nửa trang) cho một dự án giả định, trình bày lý do chọn chiến lược nhánh, có dẫn chứng bằng mô hình phát hành và năng lực test của đội.
Tóm tắt
- GitFlow là mô hình nhiều nhánh có cấu trúc (
main,develop,feature,release,hotfix), phù hợp với sản phẩm phát hành theo phiên bản và cần bảo trì nhiều phiên bản song song. Đổi lại, nó gây độ trễ tích hợp và rủi ro conflict cao. - Trunk-based Development tập trung mọi thay đổi về một nhánh
mainvới các nhánh feature cực ngắn, tích hợp nhiều lần mỗi ngày. Đây là nền tảng của các đội deploy liên tục, nhưng đòi hỏi CI mạnh và feature flags. - Yếu tố quyết định lớn nhất là mô hình phát hành: deploy liên tục thì chọn Trunk-based; phát hành theo phiên bản có bảo trì song song thì GitFlow.
- Ở quy mô lớn, mô hình lai (Trunk-based với PR + release cắt ngắn hạn) rất phổ biến, kết hợp tốc độ tích hợp với điểm cắt release có kiểm soát.
- Đừng chạy theo trend. Chiến lược nhánh chỉ tốt khi đi kèm kỷ luật đội: merge sớm, merge nhỏ, giữ
mainluôn xanh và luôn deploy được.