Nguyen Le Phong

TDD, BDD, DDD và cả họ "-Driven Development", Giải thích Cho Engineer

TDD, BDD, DDD, ATDD, EDD, CDD, FDD — mớ chữ cái "-Driven Development" làm bối rối cả engineer nhiều kinh nghiệm, một phần vì chúng không phải đối thủ của nhau: chúng trả lời những câu hỏi khác nhau và kết hợp được với nhau. Đây là hướng dẫn thực tế, nhiều ví dụ cho lập trình viên: mỗi cái thực sự nghĩa là gì, vòng lặp/khái niệm cốt lõi, ví dụ code và Gherkin cụ thể, khi nào nên dùng, các cạm bẫy — và một bản đồ rõ ràng cho thấy chúng ghép với nhau ra sao trên cùng một tính năng thật.

Đ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.

Định hướng nhanh

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.

Cạm bẫy

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.

BDD vs ATDD vs TDD

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ụ trong code. Nếu nghiệp vụ nói "policy", thì class là Policy, không phải InsuranceRecord.
  • 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 Order có id).
  • Value object — chỉ định nghĩa bằng giá trị, bất biến (một Money gồ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.

Cạm bẫy

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.

Cạm bẫy

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.

CDD còn một nghĩa thứ hai: Contract-Driven

Ở 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ầngPhương phápViệc của nó trên tính năng
Hiểu biết chungBDD / ATDDĐịnh nghĩa hành vi mọi người đồng ý (Given/When/Then)
Thiết kếDDDMô hình hóa miền — aggregate Order và các quy tắc
CodeTDDDẫn dắt từng đơn vị bằng test fail trước
Kiến trúcEDDPhát OrderPlaced; để inventory và email phản ứng
Xây UICDDXây các component checkout trong cô lập
Một vòng lặp lồng nhau, trong thực tế

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.

Bạn thấy bài viết thế nào?

Câu hỏi thường gặp

Hậu tố "-Driven Development" thực sự nghĩa là gì?
"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 thiết kế. Trong Test-Driven Development, test đi trước và định hình code; trong Domain-Driven Design, miền nghiệp vụ định hình mô hình; trong Event-Driven Development, sự kiện định hình kiến trúc. Thấy được ý tưởng chung này là chìa khóa để nhận ra các cách tiếp cận này không phải đối thủ — chúng trả lời những câu hỏi khác nhau ở những tầng khác nhau và kết hợp được trên cùng một dự án.
TDD, BDD và ATDD khác nhau thế nào?
TDD (Test-Driven Development) dẫn dắt code từ unit test viết trước, theo vòng Red → Green → Refactor — hướng dev và chạy vòng trong. ATDD (Acceptance Test-Driven Development) dẫn dắt tính năng từ acceptance test thống nhất trước khi xây. BDD (Behavior-Driven Development) về cơ bản là ATDD với một ngôn ngữ kỷ luật, tập trung vào hành vi (Given/When/Then, thường là Gherkin) và chú trọng hiểu biết chung giữa business và engineer. Thực tế chúng lồng nhau: BDD/ATDD định nghĩa vòng ngoài (tính năng đúng), còn TDD chạy vòng trong (các đơn vị đúng).
DDD có phải chỉ dành cho hệ thống lớn, phức tạp?
Phần lớn là vậy. Domain-Driven Design đáng giá khi miền nghiệp vụ thực sự phức tạp, với nhiều quy tắc và ngôn ngữ tinh tế — đó là nơi ubiquitous language chung, bounded context, và aggregate canh giữ invariant ngăn được sự hỗn loạn. Với một ứng dụng CRUD đơn giản, DDD đầy đủ thường là over-engineer: bạn nhận nghi thức (entity, repository, aggregate) mà không có lợi ích. Một thất bại phổ biến là bắt chước các pattern chiến thuật trong khi bỏ qua trái tim chiến lược (ubiquitous language và bounded context), khiến chi phí tăng mà chẳng được gì.
Tôi có buộc phải chọn một trong các phương pháp này?
Không — và đó là điểm quan trọng nhất. Chúng nằm trên những trục khác nhau (quy trình/testing, thiết kế, kiến trúc, cách xây), nên một tính năng thường dùng nhiều cái cùng lúc: BDD để thống nhất hành vi, DDD để mô hình hóa miền, TDD để dẫn dắt từng đơn vị, Event-Driven để nối các phần, và Component-Driven để xây UI. Câu hỏi thật chưa bao giờ là "TDD hay DDD?" mà là "tính năng này cần những người dẫn dắt nào, và ở tầng nào?". Hãy khớp phương pháp với bài toán thay vì chọn một danh tính.
Eval-Driven Development là gì?
Đó là tân binh thời AI của họ này. Với các tính năng xây trên mô hình ngôn ngữ lớn, test đúng/sai kinh điển không chạy được vì output bất định — một câu trả lời đúng có thể mang vô số hình thức hợp lệ. Nên thay vì unit test, bạn dẫn dắt phát triển bằng eval: các tập dữ liệu được tuyển chọn và chấm điểm để đo chất lượng output của mô hình (độ chính xác, an toàn, giọng điệu, tỉ lệ hallucination). Nó áp dụng tinh thần TDD — định nghĩa mục tiêu trước, đo so với nó, lặp lại — cho hệ thống xác suất, và đang trở thành kỹ năng cốt lõi khi ngày càng nhiều sản phẩm nhúng tính năng AI.