Có một kiểu PR mà ai cũng ngại review: title “Fix issue #245”, diff 1,200 dòng, description trống, commit message toàn “wip” và “update”. Mở lên scroll 5 phút không hiểu thay đổi gì, vì sao thay đổi, test chưa. Kết quả thường là “LGTM” cho qua vì reviewer không còn năng lượng đọc kỹ — và bug lọt qua.
Ngược lại, có kiểu PR 40 dòng, title rõ “Fix NPE when user has no profile photo”, description giải thích context 3 câu, có regression test, screenshot before/after. Review 10 phút, approve, merge. Khoảng cách giữa hai kiểu PR không phải do tài năng code — mà do kỹ năng giao tiếp kỹ thuật.
Những nguyên tắc dưới đây đúc kết từ cả hai phía: người hay viết PR to rồi bị chê, và người phải review PR to rồi mệt — giúp team merge nhanh hơn, ít conflict hơn, và quan trọng nhất là ít khó chịu với nhau hơn.
Reviewer là người đọc, không phải máy diff
Khi mở PR, bạn đang yêu cầu một người khác dừng việc họ đang làm, chuyển context sang code của bạn, đọc hiểu, và đưa ra nhận xét có giá trị. Người đó đang bận task riêng, chưa có context bạn đã nghĩ hai ngày, và muốn trả lời nhanh 5 câu hỏi: vấn đề là gì, giải pháp là gì, tại sao cách này, test thế nào, có rủi ro gì.
Nếu 5 câu hỏi đó không trả lời được trong 60 giây đầu mở PR, bạn đang đẩy khó cho reviewer. Họ phải đọc code để đoán context, phải tự suy luận tại sao bạn chọn approach này, phải tự tìm xem test ở đâu. Năng lượng reviewer có hạn — nếu dùng hết cho việc “hiểu PR đang làm gì” thì không còn năng lượng cho “phát hiện bug và cải thiện design”.
Mỗi quyết định khi viết PR nên xuất phát từ nguyên lý này: làm cho reviewer dễ dàng nhất có thể.
Kích thước: chia nhỏ là kỷ luật hàng đầu
Vì sao PR nhỏ thắng
Google đã công bố nghiên cứu nội bộ về code review: review quality rơi mạnh khi PR vượt 400 dòng thay đổi. Reviewer bỏ sót bug nhiều hơn, thời gian phản hồi chậm hơn, và merge kéo dài hơn. Điều này trực giác cũng hiểu — đọc 50 dòng thay đổi thì bạn phân tích từng dòng, đọc 1000 dòng thì bạn scan qua.
PR nhỏ còn có lợi ích khác ít ai nghĩ tới. Revert dễ — nếu phát hiện bug sau merge, revert một PR 50 dòng gần như không rủi ro, revert PR 1000 dòng thì lo ngay “có kéo theo gì không”. git bisect chính xác hơn — commit càng nhỏ thì bisect càng nhanh tìm được chỗ gây regression. Và nếu một phần PR bị block (reviewer muốn thảo luận thêm), phần còn lại vẫn merge được nếu chúng là PR riêng.
Con số thực tế
Target hợp lý: dưới 200 dòng thay đổi net (đã trừ test, config, generated file). 200-500 dòng chấp nhận được khi có lý do (migration file lớn, refactor rename), nhưng description phải chi tiết hơn bù lại. Trên 500 dòng thì gần như luôn nên chia, trừ khi toàn bộ là rename/move file không đổi logic.
Cách chia một thay đổi lớn
Khi implement một feature lớn, nên xếp thành chuỗi PR. Đầu tiên là PR refactor chuẩn bị — restructure code hiện tại để có chỗ cho code mới, không đổi behavior. Tiếp theo PR thêm API hoặc schema mới, đặt sau feature flag tắt. Rồi PR implement logic chính với unit test. Sau đó PR wire vào UI hoặc endpoint, bật flag cho internal test. Cuối cùng là PR rollout (flag từ 1% lên 100%) và PR dọn code cũ sau khi stable.
Mỗi bước là một PR nhỏ, mỗi PR có thể review và merge độc lập. Reviewer đọc từng bước thấy logic rõ ràng hơn nhiều so với đọc cả feature 2000 dòng một lúc. Công cụ stacked diffs (Graphite, git stack, Stacked PRs plugin) hỗ trợ workflow này rất tốt.
Title và description: bộ mặt của PR
Title
Title là thứ đầu tiên reviewer đọc — nó quyết định họ mở PR ngay hay để sau. Title tốt ngắn, rõ, ở thể mệnh lệnh: “Add rate limit for login endpoint”, “Fix NPE when user has no profile”, “Refactor checkout service into strategy pattern”.
Title tệ: “fixed stuff”, “WIP”, “update code”, “various fixes”. Những title này không nói gì về nội dung, không giúp reviewer quyết định priority, và không giúp ai tìm lại PR sau 6 tháng.
Nếu team dùng prefix conventional commits (feat:, fix:, refactor:, chore:), tuân theo. Link ticket thì đặt prefix [JIRA-123] hoặc để trong description — không quan trọng chỗ nào, miễn là có.
Description — phần quan trọng nhất
Template sau phù hợp cho hầu hết PR. Không phải PR nào cũng cần đủ mọi section, nhưng hai phần What và Test plan gần như không bao giờ thiếu:
## Context
(1-3 câu) Vấn đề là gì, vì sao cần thay đổi. Link design doc/ticket.
## What
Tóm tắt những gì PR này làm — cụ thể, không mơ hồ.
## Why this approach
(khi có nhiều lựa chọn) Đã cân nhắc A (từ chối vì...),
chọn B (vì...).
## Test plan
- Unit test cho ... (đã thêm)
- Manual test: cURL command, kết quả mong đợi
- Chạy trên staging: kết quả ...
## Risks & rollback
- Rủi ro: performance impact (benchmark: +2ms P99)
- Rollback: revert commit là đủ, không migration irreversible
## Related
- Issue: #123, Previous PR: #120
Section “Why this approach” thường có giá trị nhất — khi reviewer hiểu bạn đã cân nhắc những gì, họ trust decision hơn và review nhanh hơn. Nếu không giải thích, reviewer phải tự hỏi “sao không dùng X?” rồi comment, rồi bạn trả lời, mất thêm một ngày.
Screenshot before/after
Với UI change, ảnh before/after đáng giá hơn 1000 chữ mô tả. Với performance change, chart benchmark. Với CLI output, log mẫu. Reviewer nhìn thấy kết quả cụ thể sẽ confident approve hơn.
Tự review trước khi mở PR
Trước khi nhấn “Create Pull Request”, nên mở diff và đọc lại như một reviewer. Gần như lần nào cũng tìm thấy ít nhất một trong mấy thứ: console.log debug quên xóa, file generated lẫn vào diff, TODO comment chưa xử lý, edge case chưa có test. 10 phút tự review tiết kiệm reviewer 30 phút và tránh comment kiểu “bạn quên xóa debug log” — vừa mất thời gian vừa hơi ngại.
Commit history
Commit có ý nghĩa
Commit không phải nút Save. Mỗi commit nên compile được, pass test được, và message giải thích “why” chứ không chỉ “what”. Cách phổ biến: code xong rồi dùng git rebase -i trước khi push để gom commit lặt vặt, reword message cho rõ.
Message theo quy ước phổ biến: tiêu đề dưới 72 ký tự, dòng trống, body giải thích lý do và tóm tắt thay đổi. Body là nơi lưu quyết định mà không ai đọc code sau 6 tháng sẽ nhớ.
Refactor checkout service into strategy pattern
Prepares for the new checkout algorithm (see #234) by splitting
the current monolithic service into a strategy interface + default
implementation. No behavior change intended.
- Extract CheckoutStrategy interface
- Keep existing logic in DefaultCheckoutStrategy
- Update DI module to inject default strategy
Squash hay preserve
Squash khi merge: PR nhiều commit không ý nghĩa (“wip”, “fix typo”, “try again”) gộp thành một commit sạch trên main. Phổ biến với GitHub-style flow. Preserve history: giữ commit chi tiết khi chúng đã được tỉa gọn, có ý nghĩa. Quy ước phụ thuộc team — quan trọng là thống nhất và main branch có history đọc được.
Code trong PR: không trộn concern
Tách format khỏi logic
Formatter toàn repo + feature mới trong cùng PR → diff 3000 dòng, reviewer không phân biệt được đâu là format đâu là logic thật. Luôn tách: PR format riêng (0 thay đổi logic, approve nhanh), rồi PR feature riêng. Tương tự với rename — rename file/biến trước, thay đổi logic sau.
Test đi kèm thay đổi
Bug fix phải có regression test — test fail trước fix, pass sau fix. Đây là bằng chứng rằng fix thực sự fix đúng bug, và bug không quay lại trong tương lai.
Feature mới cần unit test happy path và vài edge case. Không cần cover 100%, nhưng reviewer cần thấy bạn đã nghĩ đến boundary conditions.
Refactor thuần không cần test mới, nhưng mọi test cũ phải pass — đó là bằng chứng “không đổi behavior”.
Migration và data change
Migration trong PR cần đặc biệt chú ý backward compatibility. Code mới phải đọc được data cũ, và code cũ (vẫn đang chạy trong rolling update) phải chịu được data mới. Nếu migration phức tạp, dùng expand-migrate-contract pattern: thêm column mới (expand), migrate data dần (migrate), xóa column cũ (contract) — mỗi bước là PR riêng.
Ghi rõ trong description: migration cần chạy trước hay sau deploy, có irreversible không, rollback thế nào.
Phản hồi comment: đây là nơi văn hóa team thể hiện
Comment là tín hiệu, không phải công kích
Reviewer bắt bẻ tên biến không phải vì ghét bạn — họ muốn code bền vững cho cả team. Nhiều developer ban đầu defensive khi nhận comment nhiều, cảm giác như bị chê. Nhưng thực tế: PR có nhiều comment thường ra code tốt hơn PR “LGTM” ngay — vì có ai đó thực sự đọc kỹ.
Phản ứng khó chịu, defensive kéo dài quá trình merge và làm reviewer ngại comment lần sau — team mất feedback loop.
Phản hồi cụ thể
Đồng ý: fix và reply “Done in abc123” — reviewer biết đã fix ở commit nào, có thể verify nhanh.
Không đồng ý: giải thích lý do với bằng chứng. “Tôi chọn cách này vì benchmark cho thấy approach A nhanh hơn 30%” hoặc “Case Y không xảy ra vì input đã validate ở layer X”. Không phải “tôi thích cách này hơn”.
Chưa hiểu comment: hỏi lại, đừng đoán rồi fix sai.
Phân loại comment
Không phải mọi comment đều cùng trọng lượng. Nhiều team dùng prefix: blocker: cho bug hoặc vi phạm kiến trúc phải fix. nit: cho style preference, không bắt buộc. q: cho câu hỏi reviewer muốn hiểu thêm. Phân loại giúp author biết phải fix gì trước, cái nào optional.
Thảo luận dài thì chuyển ra ngoài PR
Comment thread quá 5-6 lượt qua lại mà chưa có kết luận → chuyển ra meeting 15 phút hoặc document riêng. PR không phải nơi thiết kế lại kiến trúc — nó là nơi review implementation cụ thể.
Đừng force-push sau khi reviewer đã đọc
Force-push rewrite history khiến reviewer mất vị trí — diff họ đã đọc biến mất. Thay vào đó, thêm commit mới “Address review feedback” rồi squash khi merge. GitHub hỗ trợ “Changes since last review” dựa trên commit history — giữ history giúp reviewer chỉ đọc phần mới.
Khi bạn là reviewer
Mọi nguyên tắc trên đối xứng — khi review người khác, áp dụng ngược lại.
Trả lời nhanh. PR chờ review quá 1 ngày tạo frustration và context switch cost cho author. Nếu không thể review kỹ ngay, ít nhất comment “Sẽ review chiều nay” để author biết PR không bị quên.
Hỏi thay vì phán. “Có lý do gì dùng approach này không?” thay vì “Approach này sai”. Có thể author có context bạn không biết.
Chỉ ra điều tốt. Không chỉ comment lỗi — “Cách xử lý case này clean!” cũng quan trọng. Positive feedback khuyến khích pattern tốt lan rộng.
Phân loại comment. Đánh dấu rõ blocker vs nit để author không phải đoán “comment này có phải fix mới approve không”.
Handle conflict và CI
Resolve conflict
Rebase lên main gần nhất trước khi request review — giảm xung đột. Khi có conflict: rebase, resolve, test lại local, push. Không resolve qua GitHub UI nếu conflict phức tạp — dễ miss edge case khi merge.
CI đỏ
Đừng merge khi CI đỏ. Nếu CI flaky: mở issue track, không “retry until green” vô tội vạ. Retry che giấu flaky test, flaky test tích lũy làm mọi người mất tin CI. Lint warning: fix hoặc giải thích tại sao suppress — // eslint-disable phải kèm comment lý do.
Hotfix
Hotfix vẫn qua PR — nhưng PR nhỏ, tập trung fix, không gộp refactor. Một reviewer approve là đủ, merge nhanh, theo dõi kỹ sau deploy. Không dùng “khẩn cấp” làm lý do bỏ qua review hoàn toàn — trừ khi production đang down và mỗi phút mất tiền.
Thói quen cá nhân
Những thói quen nhỏ tích lũy qua thời gian, mỗi cái tiết kiệm vài phút nhưng tổng thể đáng kể.
Trước khi mở PR, chạy git diff main...HEAD đọc lại toàn bộ diff. Chạy test, lint, format một lần cuối. Đặt tên nhánh mô tả: fix/login-500-error, feat/ratelimit-gateway — không phải branch-1 hay test-fix. Dọn nhánh local sau khi merge: git branch -d, git fetch --prune. Giữ checklist cá nhân trong note app — “đã tự review diff chưa, test plan viết chưa, screenshot chưa” — để không quên bước nào trước khi nhấn Create.
Tóm tắt
PR nhỏ (dưới 400 dòng) là đòn bẩy lớn nhất — review nhanh hơn, ít bug lọt qua, revert dễ dàng. Chia feature lớn thành chuỗi PR nhỏ, mỗi PR độc lập có thể review và merge riêng.
Description rõ ràng với context, what, why, test plan là thứ biến một PR từ “khó hiểu” thành “dễ approve”. Tự review trước khi gửi — 10 phút tìm debug log quên xóa tiết kiệm cả ngày comment qua lại.
Commit có ý nghĩa, không trộn concern (format riêng, logic riêng, rename riêng). Test đi kèm thay đổi — regression test cho bug fix, unit test cho feature mới. Migration backward compatible.
Phản hồi comment cụ thể và không defensive. Phân loại blocker vs nit. Chuyển thảo luận dài ra ngoài PR.
Làm reviewer tốt để nhận review tốt: nhanh, hỏi thay vì phán, phân loại comment, chỉ ra điều tốt.
Không có trick thần kỳ — chỉ có kỷ luật nhỏ làm đều mỗi ngày. Team có văn hóa PR tốt release ít bug hơn, onboarding nhanh hơn, và đơn giản là dễ chịu hơn để làm cùng.