Nguyen Le Phong

TDD, BDD, DDD, and the Rest of the "-Driven Development" Family, Explained for Engineers

TDD, BDD, DDD, ATDD, EDD, CDD, FDD — the "-Driven Development" alphabet soup confuses even experienced engineers, partly because they're not rivals: they answer different questions and compose together. This is a practical, example-rich guide for software engineers: what each one really means, the core loop or concept, concrete code and Gherkin examples, when to reach for it, the pitfalls — and a clear map of how they fit together on a single real feature.

Spend a year in software and you'll collect a confusing little alphabet: TDD, BDD, DDD, ATDD, EDD, CDD, FDD. They sound like competing religions you must choose between — and engineers argue about them as if they were. They're not. Most of them answer different questions, operate at different layers, and happily compose on the same project.

The shared suffix is the key. "X-Driven Development" means: you let X drive the order and shape of your work — X is the thing you start from and the thing that forces your decisions. Test-Driven means tests come first and shape the code. Domain-Driven means the business domain shapes the design. Event-Driven means events shape the architecture. Once you see that, the soup turns into a clear menu.

This guide is written for engineers who want to actually use these — not recite them. For each one: what it really means, the core loop or concept, a concrete example, when to reach for it, and the pitfalls. We'll end with a map showing how they all fit together on a single feature.

A quick orientation

They live on different axes, which is exactly why they combine rather than compete:
Process / testing — TDD, BDD, ATDD (how and when you write tests)
Design — DDD (how you model the problem)
Architecture — EDD / event-driven (how parts communicate)
Build approach — CDD, FDD (what unit you build around)

TDD — Test-Driven Development

The most famous of the family. You write a failing test first, then the minimum code to pass it, then clean up — the Red → Green → Refactor loop. The test isn't an afterthought to verify code; it's a design tool that forces you to define behavior before implementation.

// 1. RED — write the test first; it fails (function doesn't exist yet)
test('applies a 10% discount over $100', () => {
  expect(priceAfterDiscount(120)).toBe(108)
})

// 2. GREEN — the minimum code to make it pass
function priceAfterDiscount(total) {
  return total > 100 ? total * 0.9 : total
}

// 3. REFACTOR — improve the code, tests stay green

Why it works: writing the test first forces you to think about the interface and the edge cases before you're attached to an implementation. It produces a safety net of regression tests as a side effect, and it tends to push you toward small, testable, decoupled units.

Pitfalls

TDD tests behavior at the unit level — it doesn't guarantee the right product. Over-mocking leads to tests that pass while the system is broken. And TDD'ing throwaway spikes or trivial code is wasted ceremony. Use it where logic is non-trivial and correctness matters.

BDD — Behavior-Driven Development

BDD is TDD's evolution toward shared understanding. Instead of unit tests written in code-speak, you describe behavior in plain language that business and engineers agree on, using a structured Given / When / Then format (often written in Gherkin). It pushes the conversation up from "does this function return 108?" to "what should the system do for the user?"

Feature: Checkout discount

  Scenario: Order over $100 gets 10% off
    Given a cart totaling $120
    When the customer checks out
    Then the final price should be $108

Those steps are then wired to test code, so the plain-language spec becomes an executable, living document. The big win is ubiquitous language and collaboration: the BA, PO, QA, and developer all read the same scenario and mean the same thing.

BDD vs ATDD vs TDD

TDD drives code from unit tests (developer-facing). ATDD (Acceptance Test-Driven Development) drives features from acceptance tests agreed up front. BDD is essentially ATDD with a disciplined, behavior-focused language and tooling. In practice: BDD/ATDD define the outer loop (the feature is right), TDD runs the inner loop (the units are right). They nest.

DDD — Domain-Driven Design

Note the D: Design, not Development. DDD is about modeling complex business domains so the code reflects the real problem. It has two halves:

Strategic DDD — the big-picture boundaries:

  • Ubiquitous language — one shared vocabulary used by domain experts and in the code. If the business says "policy," the class is Policy, not InsuranceRecord.
  • Bounded context — a boundary within which a model and its language are consistent. "Customer" in Billing and "Customer" in Support are different models; trying to force one shared "Customer" everywhere is a classic mistake.

Tactical DDD — the building blocks inside a context:

  • Entity — has identity over time (an Order with an id).
  • Value object — defined only by its values, immutable (a Money of {amount, currency}).
  • Aggregate — a cluster of objects treated as one unit with one entry point (the aggregate root) that guards the rules (invariants).
  • Repository — gives the illusion of an in-memory collection of aggregates.
  • Domain event — something meaningful that happened (OrderPlaced).
// The Order aggregate root guards its own invariants
class Order {
  addItem(product, qty) {
    if (this.status !== 'DRAFT')
      throw new Error('Cannot modify a placed order')
    this.items.push({ product, qty })
  }
  place() {
    if (this.items.length === 0)
      throw new Error('Cannot place an empty order')
    this.status = 'PLACED'
    this.raise(new OrderPlaced(this.id))
  }
}

The point: business rules live in the domain model, not scattered across controllers and services. DDD shines on complex domains with rich rules; it's overkill for a simple CRUD app. It also pairs naturally with the architectures in Clean / Onion / Hexagonal, which exist precisely to keep that domain model pure.

Pitfalls

DDD is frequently cargo-culted — teams adopt the tactical patterns (entities, repositories) while ignoring the strategic heart (ubiquitous language, bounded contexts), and end up with ceremony without benefit. If your domain is genuinely simple, plain code beats a DDD costume.

EDD — Event-Driven Development

Event-Driven Development (and its bigger sibling, event-driven architecture) makes events the backbone: instead of components calling each other directly, they emit facts about what happened, and other components react. "Something happened" replaces "do this now."

// The order service just announces the fact — it doesn't know who cares
eventBus.publish(new OrderPlaced({ orderId, total }))

// Independent consumers react on their own
onEvent('OrderPlaced', e => inventory.reserve(e.orderId))
onEvent('OrderPlaced', e => email.sendConfirmation(e.orderId))

Why reach for it: loose coupling (the producer doesn't know its consumers), independent scaling, and a natural fit for asynchronous, distributed systems. It underpins patterns like event sourcing (store the events, derive state) and CQRS. Domain events from DDD are often what flow through an event-driven system — the two compose beautifully.

Pitfalls

Events trade direct calls for indirection: flows become harder to trace, ordering and duplicate delivery become real problems, and "eventual consistency" surprises people who expected immediate reads. Great for decoupling at scale; needless pain for a small synchronous app.

CDD — Component-Driven Development

Most common in frontend: you build the UI from the bottom up, one isolated component at a time, before assembling them into pages. Each component is developed and reviewed in isolation (often in a tool like Storybook), with all its states visible at once.

// Build & review Button in isolation, every state, before any page uses it
export const Primary = { args: { variant: 'primary', label: 'Buy now' } }
export const Loading = { args: { loading: true } }
export const Disabled = { args: { disabled: true } }

Why it works: reusable, consistent UI; a living component library; parallel work; and components tested before integration. It's the build-side cousin of a design system.

CDD has a second meaning: Contract-Driven

In backend/microservices, CDD often means Contract-Driven Development: services agree on an explicit API contract first (e.g. consumer-driven contracts with a tool like Pact), and both sides test against it independently. Same spirit — the contract drives the work — different layer.

FDD — Feature-Driven Development

An older, lightweight Agile methodology that organizes the whole process around a list of small, client-valued features ("calculate the total of a sale"). You build a domain model, build a feature list, then plan/design/build by feature in short iterations. Think of it as a feature-centric way to run delivery — closer in spirit to Scrum's planning than to TDD's code loop.

The newer and niche members

  • Type-Driven Development — in strongly-typed languages, you design with the type system first ("make illegal states unrepresentable"), letting the compiler drive correctness before tests even run.
  • Model-Driven Development (MDD) — generate code from higher-level models; common in regulated or low-code contexts.
  • Eval-Driven Development — the AI-era newcomer. For LLM features, classic pass/fail tests break (outputs are non-deterministic), so you drive development with evals: graded datasets that measure output quality. It's TDD's spirit applied to probabilistic systems — see how AI is reshaping software roles.

How they fit together — one feature, many "DD"s

Here's the punchline: these aren't a menu to pick one from. A single well-built feature can use several at once, each on its own layer:

LayerMethodIts job on the feature
Shared understandingBDD / ATDDDefine the behavior everyone agrees on (Given/When/Then)
DesignDDDModel the domain — the Order aggregate and its rules
CodeTDDDrive each unit with failing tests first
ArchitectureEDDEmit OrderPlaced; let inventory and email react
UI buildCDDBuild the checkout components in isolation
A nested loop, in practice

A common, healthy combination: BDD defines the outer loop ("the checkout discount works for the user"), TDD runs the inner loop (each function is correct), DDD shapes what those units are, and EDD connects the pieces. You don't announce "we do five DDs" — you just use the right driver at the right layer.

Choosing wisely — and avoiding dogma

The throughline across all of these: a methodology is a tool, not an identity. A few principles:

  • Match the method to the problem. Rich domain → DDD. Logic-heavy code → TDD. Cross-team features → BDD. Decoupled/async systems → EDD. Component libraries → CDD. Simple CRUD → don't over-engineer any of them.
  • They compose; they don't compete. The question is rarely "TDD or DDD?" — it's "which drivers does this feature need, at which layers?"
  • Beware cargo-culting. Adopting the rituals (folders named aggregates/, empty Gherkin files) without the underlying purpose adds ceremony and removes velocity.
  • Dogma is the real anti-pattern. "100% TDD always" or "DDD everywhere" causes more harm than skipping them. Be deliberate, not religious.

Key takeaways

  • "X-Driven" means X drives the order and shape of your work — the thing you start from and that forces your decisions.
  • They live on different axes: process/testing (TDD, BDD, ATDD), design (DDD), architecture (EDD), build approach (CDD, FDD) — so they compose rather than compete.
  • TDD: Red → Green → Refactor; tests as a design tool for the inner loop.
  • BDD/ATDD: behavior in shared Given/When/Then language; the outer loop and ubiquitous understanding.
  • DDD: model complex domains — ubiquitous language and bounded contexts (strategic) + entities, value objects, aggregates, events (tactical). It's Design, not Development.
  • EDD: components emit events and react, for loose coupling in async/distributed systems.
  • CDD: build UI from isolated components up (or, in backend, Contract-Driven). FDD: organize delivery around small client-valued features.
  • Avoid dogma and cargo-culting. Match the driver to the problem and the layer; a methodology is a tool, not an identity.

The "-Driven Development" family looks intimidating only until you see the trick hiding in the shared suffix: each one simply names what you let lead. Tests, behavior, the domain, events, components, features — different leaders for different parts of the work. The senior move isn't memorizing all of them or swearing allegiance to one; it's knowing, for the problem in front of you, which driver belongs at which layer — and having the judgment to leave the rest in the toolbox. Master that, and the alphabet soup becomes what it was always meant to be: not a set of rules to obey, but a set of lenses for building better software.

Qu'en avez-vous pensé ?

Questions fréquentes

What does the "-Driven Development" suffix actually mean?
"X-Driven Development" means you let X drive the order and shape of your work — X is the thing you start from and the thing that forces your design decisions. In Test-Driven Development, tests come first and shape the code; in Domain-Driven Design, the business domain shapes the model; in Event-Driven Development, events shape the architecture. Seeing this shared idea is the key to realizing these approaches aren't rivals — they answer different questions at different layers and combine on the same project.
What's the difference between TDD, BDD, and ATDD?
TDD (Test-Driven Development) drives code from unit tests written first, in the Red → Green → Refactor loop — it's developer-facing and runs the inner loop. ATDD (Acceptance Test-Driven Development) drives features from acceptance tests agreed before building. BDD (Behavior-Driven Development) is essentially ATDD with a disciplined, behavior-focused language (Given/When/Then, often Gherkin) and a focus on shared understanding between business and engineers. In practice they nest: BDD/ATDD define the outer loop (the feature is right), and TDD runs the inner loop (the units are right).
Is DDD only for big, complex systems?
Largely, yes. Domain-Driven Design pays off when the business domain is genuinely complex, with rich rules and subtle language — that's where a shared ubiquitous language, bounded contexts, and aggregates that guard invariants prevent chaos. For a simple CRUD application, full DDD is usually over-engineering: you get the ceremony (entities, repositories, aggregates) without the benefit. A common failure is cargo-culting the tactical patterns while ignoring the strategic heart (ubiquitous language and bounded contexts), which adds cost without the payoff.
Do I have to choose one of these methodologies?
No — and that's the most important point. They live on different axes (process/testing, design, architecture, build approach), so a single feature commonly uses several at once: BDD to agree on behavior, DDD to model the domain, TDD to drive each unit, Event-Driven to connect the parts, and Component-Driven to build the UI. The real question is never "TDD or DDD?" but "which drivers does this feature need, and at which layer?" Match the method to the problem rather than picking one identity.
What is Eval-Driven Development?
It's the AI-era newcomer to the family. For features built on large language models, classic pass/fail tests don't work because outputs are non-deterministic — a correct answer can take countless valid forms. So instead of unit tests, you drive development with evals: curated, graded datasets that measure the quality of the model's outputs (accuracy, safety, tone, hallucination rate). It applies the spirit of TDD — define the target first, measure against it, iterate — to probabilistic systems, and it's becoming a core skill as more products embed AI features.