Đi làm phần mềm một năm là bạn sẽ gom được một mớ chữ cái khó hiểu: TDD, BDD, DDD, ATDD, EDD, CDD, FDD. Chúng nghe như những tôn giáo đối địch buộc bạn phải chọn một — và engineer cãi nhau về chúng đúng kiểu như vậy. Thực ra không phải. Phần lớn chúng trả lời những câu hỏi khác nhau, hoạt động ở những tầng khác nhau, và vui vẻ kết hợp với nhau trên cùng một dự án.
Hậu tố chung chính là chìa khóa. "X-Driven Development" nghĩa là: bạn để X dẫn dắt thứ tự và hình hài công việc — X là thứ bạn bắt đầu từ đó và là thứ buộc bạn ra quyết định. Test-Driven nghĩa là test đi trước và định hình code. Domain-Driven nghĩa là miền nghiệp vụ định hình thiết kế. Event-Driven nghĩa là sự kiện định hình kiến trúc. Thấy được điều đó, mớ hỗn độn biến thành một thực đơn rõ ràng.
Bài viết này dành cho engineer muốn thực sự dùng chúng — không phải đọc thuộc. Với mỗi cái: nó thực sự nghĩa là gì, vòng lặp/khái niệm cốt lõi, một ví dụ cụ thể, khi nào nên dùng, và các cạm bẫy. Cuối bài là một bản đồ cho thấy tất cả ghép với nhau ra sao trên cùng một tính năng.
Chúng nằm trên những trục khác nhau, và đó đúng là lý do chúng kết hợp chứ không cạnh tranh:
• Quy trình / testing — TDD, BDD, ATDD (viết test thế nào và khi nào)
• Thiết kế — DDD (mô hình hóa bài toán thế nào)
• Kiến trúc — EDD / event-driven (các phần giao tiếp thế nào)
• Cách xây — CDD, FDD (xây quanh đơn vị nào)
TDD — Test-Driven Development
Nổi tiếng nhất trong họ. Bạn viết một test fail trước, rồi viết lượng code tối thiểu để pass, rồi dọn dẹp — vòng lặp Red → Green → Refactor. Test không phải thứ làm sau để kiểm code; nó là công cụ thiết kế buộc bạn định nghĩa hành vi trước khi hiện thực.
// 1. RED — viết test trước; nó fail (hàm chưa tồn tại)
test('giảm 10% cho đơn trên $100', () => {
expect(priceAfterDiscount(120)).toBe(108)
})
// 2. GREEN — lượng code tối thiểu để pass
function priceAfterDiscount(total) {
return total > 100 ? total * 0.9 : total
}
// 3. REFACTOR — cải thiện code, test vẫn xanh
Vì sao hiệu quả: viết test trước buộc bạn nghĩ về interface và các edge case trước khi bị "dính" vào một cách hiện thực. Nó tạo ra một tấm lưới regression như sản phẩm phụ, và đẩy bạn về phía những đơn vị nhỏ, dễ test, ít phụ thuộc.
TDD kiểm hành vi ở cấp đơn vị — nó không bảo đảm sản phẩm đúng. Mock quá đà dẫn tới test pass trong khi hệ thống hỏng. Và TDD cho spike vứt đi hay code tầm thường là nghi thức lãng phí. Dùng nó nơi logic không tầm thường và tính đúng đắn quan trọng.
BDD — Behavior-Driven Development
BDD là bước tiến hóa của TDD hướng về hiểu biết chung. Thay vì unit test viết bằng ngôn ngữ code, bạn mô tả hành vi bằng ngôn ngữ đời thường mà business và engineer cùng đồng ý, theo cấu trúc Given / When / Then (thường viết bằng Gherkin). Nó đẩy câu chuyện từ "hàm này có trả về 108 không?" lên "hệ thống nên làm gì cho người dùng?".
Feature: Giảm giá khi thanh toán
Scenario: Đơn trên $100 được giảm 10%
Given giỏ hàng tổng $120
When khách thanh toán
Then giá cuối cùng phải là $108
Các bước đó sau đó được nối với code test, nên bản đặc tả ngôn ngữ đời thường trở thành một tài liệu sống, chạy được. Lợi ích lớn là ngôn ngữ chung (ubiquitous language) và sự cộng tác: BA, PO, QC và dev cùng đọc một scenario và hiểu giống nhau.
TDD dẫn dắt code từ unit test (hướng dev). ATDD (Acceptance Test-Driven Development) dẫn dắt tính năng từ acceptance test thống nhất trước. BDD về cơ bản là ATDD với một ngôn ngữ kỷ luật, tập trung vào hành vi cùng công cụ đi kèm. Thực tế: BDD/ATDD định nghĩa vòng ngoài (tính năng đúng), TDD chạy vòng trong (các đơn vị đúng). Chúng lồng vào nhau.
DDD — Domain-Driven Design
Để ý chữ D: Design, không phải Development. DDD nói về việc mô hình hóa các miền nghiệp vụ phức tạp sao cho code phản ánh đúng bài toán thật. Nó có hai nửa:
DDD chiến lược — ranh giới bức tranh lớn:
- Ubiquitous language — một bộ từ vựng chung được dùng bởi chuyên gia nghiệp vụ và trong code. Nếu nghiệp vụ nói "policy", thì class là
Policy, không phảiInsuranceRecord. - Bounded context — ranh giới mà trong đó một mô hình và ngôn ngữ của nó nhất quán. "Customer" trong Billing và "Customer" trong Support là hai mô hình khác nhau; cố ép một "Customer" chung khắp nơi là sai lầm kinh điển.
DDD chiến thuật — các viên gạch bên trong một context:
- Entity — có danh tính theo thời gian (một
Ordercó id). - Value object — chỉ định nghĩa bằng giá trị, bất biến (một
Moneygồm {amount, currency}). - Aggregate — một cụm đối tượng được xem như một đơn vị, có một cửa vào duy nhất (aggregate root) canh giữ các quy tắc (invariant).
- Repository — tạo ảo giác về một tập aggregate trong bộ nhớ.
- Domain event — một điều có ý nghĩa đã xảy ra (
OrderPlaced).
// Aggregate root Order tự canh giữ invariant của chính nó
class Order {
addItem(product, qty) {
if (this.status !== 'DRAFT')
throw new Error('Không thể sửa đơn đã đặt')
this.items.push({ product, qty })
}
place() {
if (this.items.length === 0)
throw new Error('Không thể đặt đơn rỗng')
this.status = 'PLACED'
this.raise(new OrderPlaced(this.id))
}
}
Điểm mấu chốt: quy tắc nghiệp vụ sống trong mô hình miền, không rải rác khắp controller và service. DDD tỏa sáng ở các miền phức tạp với nhiều quy tắc; nó là quá mức cho một app CRUD đơn giản. Nó cũng kết hợp tự nhiên với các kiến trúc trong Clean / Onion / Hexagonal — vốn tồn tại chính là để giữ cho mô hình miền đó tinh khiết.
DDD hay bị bắt chước kiểu phong trào — đội ngũ áp dụng các pattern chiến thuật (entity, repository) nhưng bỏ qua trái tim chiến lược (ubiquitous language, bounded context), và kết cục là nghi thức mà không có lợi ích. Nếu miền của bạn thật sự đơn giản, code mộc mạc thắng một bộ "trang phục DDD".
EDD — Event-Driven Development
Event-Driven Development (và người anh lớn của nó, event-driven architecture) lấy sự kiện làm xương sống: thay vì các thành phần gọi trực tiếp nhau, chúng phát ra các sự thật về điều đã xảy ra, và các thành phần khác phản ứng. "Có chuyện vừa xảy ra" thay cho "hãy làm ngay điều này".
// Order service chỉ thông báo sự thật — không cần biết ai quan tâm
eventBus.publish(new OrderPlaced({ orderId, total }))
// Các consumer độc lập tự phản ứng
onEvent('OrderPlaced', e => inventory.reserve(e.orderId))
onEvent('OrderPlaced', e => email.sendConfirmation(e.orderId))
Vì sao nên dùng: ghép lỏng (producer không biết consumer), scale độc lập, và rất hợp với hệ thống bất đồng bộ, phân tán. Nó là nền cho các pattern như event sourcing (lưu sự kiện, suy ra trạng thái) và CQRS. Domain event từ DDD thường chính là thứ chảy qua một hệ event-driven — hai thứ ghép với nhau rất đẹp.
Sự kiện đánh đổi lời gọi trực tiếp lấy sự gián tiếp: luồng khó lần theo hơn, thứ tự và giao trùng (duplicate) trở thành vấn đề thật, và "nhất quán cuối cùng" (eventual consistency) làm bất ngờ những ai mong đọc ngay. Tuyệt cho việc ghép lỏng ở quy mô lớn; đau đầu vô ích cho một app đồng bộ nhỏ.
CDD — Component-Driven Development
Phổ biến nhất ở frontend: bạn xây UI từ dưới lên, mỗi lần một component độc lập, trước khi lắp chúng thành trang. Mỗi component được phát triển và review trong cô lập (thường bằng công cụ như Storybook), với tất cả trạng thái của nó hiển thị cùng lúc.
// Xây & review Button độc lập, mọi trạng thái, trước khi trang nào dùng
export const Primary = { args: { variant: 'primary', label: 'Mua ngay' } }
export const Loading = { args: { loading: true } }
export const Disabled = { args: { disabled: true } }
Vì sao hiệu quả: UI tái sử dụng, nhất quán; một thư viện component sống; làm song song; và component được test trước khi tích hợp. Đây là người anh em phía "xây" của một design system.
Ở backend/microservices, CDD thường nghĩa là Contract-Driven Development: các service thống nhất một contract API tường minh trước (ví dụ consumer-driven contract với công cụ như Pact), và hai phía test độc lập với contract đó. Cùng tinh thần — contract dẫn dắt công việc — khác tầng.
FDD — Feature-Driven Development
Một phương pháp Agile cũ hơn, nhẹ nhàng, tổ chức toàn bộ quy trình quanh một danh sách các tính năng nhỏ, có giá trị với khách hàng ("tính tổng của một đơn hàng"). Bạn xây mô hình miền, lập danh sách tính năng, rồi plan/design/build theo từng tính năng trong các vòng lặp ngắn. Hãy xem nó như cách chạy delivery lấy tính năng làm trung tâm — gần với planning của Scrum hơn là vòng lặp code của TDD.
Những thành viên mới và ít gặp
- Type-Driven Development — trong các ngôn ngữ định kiểu mạnh, bạn thiết kế bằng hệ thống kiểu trước ("khiến các trạng thái bất hợp lệ không thể biểu diễn được"), để compiler dẫn dắt tính đúng đắn ngay cả trước khi test chạy.
- Model-Driven Development (MDD) — sinh code từ các mô hình cấp cao; phổ biến trong môi trường có quy định chặt hoặc low-code.
- Eval-Driven Development — tân binh thời AI. Với tính năng dùng LLM, test đúng/sai kinh điển vỡ trận (output bất định), nên bạn dẫn dắt phát triển bằng eval: tập dữ liệu được chấm điểm để đo chất lượng output. Đó là tinh thần TDD áp dụng cho hệ thống xác suất — xem AI đang định hình lại vai trò phần mềm.
Chúng ghép với nhau ra sao — một tính năng, nhiều "DD"
Đây là điểm chốt: đây không phải thực đơn để chọn một. Một tính năng được xây tốt có thể dùng nhiều cái cùng lúc, mỗi cái ở tầng của nó:
| Tầng | Phương pháp | Việc của nó trên tính năng |
|---|---|---|
| Hiểu biết chung | BDD / ATDD | Định nghĩa hành vi mọi người đồng ý (Given/When/Then) |
| Thiết kế | DDD | Mô hình hóa miền — aggregate Order và các quy tắc |
| Code | TDD | Dẫn dắt từng đơn vị bằng test fail trước |
| Kiến trúc | EDD | Phát OrderPlaced; để inventory và email phản ứng |
| Xây UI | CDD | Xây các component checkout trong cô lập |
Một kết hợp lành mạnh hay gặp: BDD định nghĩa vòng ngoài ("giảm giá checkout chạy đúng cho người dùng"), TDD chạy vòng trong (mỗi hàm đúng), DDD định hình các đơn vị đó là gì, và EDD nối các mảnh lại. Bạn không tuyên bố "chúng tôi làm năm DD" — bạn chỉ dùng đúng người dẫn dắt ở đúng tầng.
Chọn khôn ngoan — và tránh giáo điều
Mạch xuyên suốt qua tất cả: một phương pháp là công cụ, không phải danh tính. Vài nguyên tắc:
- Khớp phương pháp với bài toán. Miền phức tạp → DDD. Code nặng logic → TDD. Tính năng liên đội → BDD. Hệ ghép lỏng/bất đồng bộ → EDD. Thư viện component → CDD. CRUD đơn giản → đừng over-engineer cái nào cả.
- Chúng kết hợp, không cạnh tranh. Câu hỏi hiếm khi là "TDD hay DDD?" — mà là "tính năng này cần những người dẫn dắt nào, ở những tầng nào?".
- Cảnh giác bắt chước kiểu phong trào. Áp dụng nghi thức (thư mục tên
aggregates/, file Gherkin rỗng) mà thiếu mục đích bên dưới thì chỉ thêm nghi thức và bớt tốc độ. - Giáo điều mới là anti-pattern thật sự. "Luôn 100% TDD" hay "DDD ở mọi nơi" gây hại nhiều hơn là bỏ qua chúng. Hãy có chủ đích, đừng cuồng tín.
Những điều đọng lại
- "X-Driven" nghĩa là X dẫn dắt thứ tự và hình hài công việc — thứ bạn bắt đầu từ đó và buộc bạn ra quyết định.
- Chúng nằm trên những trục khác nhau: quy trình/testing (TDD, BDD, ATDD), thiết kế (DDD), kiến trúc (EDD), cách xây (CDD, FDD) — nên chúng kết hợp chứ không cạnh tranh.
- TDD: Red → Green → Refactor; test như công cụ thiết kế cho vòng trong.
- BDD/ATDD: hành vi bằng ngôn ngữ chung Given/When/Then; vòng ngoài và hiểu biết chung.
- DDD: mô hình hóa miền phức tạp — ubiquitous language và bounded context (chiến lược) + entity, value object, aggregate, event (chiến thuật). Là Design, không phải Development.
- EDD: các thành phần phát sự kiện và phản ứng, để ghép lỏng trong hệ bất đồng bộ/phân tán.
- CDD: xây UI từ component độc lập lên (hoặc, ở backend, Contract-Driven). FDD: tổ chức delivery quanh các tính năng nhỏ có giá trị khách hàng.
- Tránh giáo điều và bắt chước phong trào. Khớp người dẫn dắt với bài toán và tầng; một phương pháp là công cụ, không phải danh tính.
Họ "-Driven Development" chỉ đáng sợ cho tới khi bạn thấy mánh giấu trong hậu tố chung: mỗi cái đơn giản gọi tên thứ bạn để dẫn dắt. Test, hành vi, miền nghiệp vụ, sự kiện, component, tính năng — những người dẫn dắt khác nhau cho những phần khác nhau của công việc. Bước đi của một người senior không phải là thuộc lòng tất cả hay thề trung thành với một cái; mà là biết, cho bài toán trước mắt, người dẫn dắt nào thuộc về tầng nào — và đủ phán đoán để cất phần còn lại vào hộp đồ nghề. Làm chủ điều đó, mớ chữ cái sẽ trở về đúng bản chất nó luôn nên là: không phải một bộ luật phải tuân theo, mà một bộ thấu kính để xây phần mềm tốt hơn.