Skip to content

BDD VS DDD VS TDD

Full Stack edited this page Aug 13, 2025 · 2 revisions

BDD vs DDD vs TDD: A Comprehensive Guide

Last Updated: 2025-08-13 07:23:34 UTC

Table of Contents

Introduction

In modern software development, three key methodologies help teams build better software: BDD, DDD, and TDD. While they sound similar, each serves a distinct purpose in the development lifecycle. Understanding when and how to use them can dramatically improve your software quality, team collaboration, and business alignment.


BDD (Behavior-Driven Development)

"What should the system DO from a user's perspective?"

Definition

BDD focuses on capturing and validating business requirements through concrete examples that everyone can understand. It bridges the communication gap between business stakeholders and technical teams.

Key Concepts

  • Given-When-Then scenarios written in natural language
  • Gherkin syntax for structured requirements
  • Living documentation that stays updated with code
  • Collaboration between developers, testers, and business stakeholders

Real-World Example: E-commerce Shopping Cart

Feature: Shopping Cart Management
  As a customer
  I want to add items to my cart
  So that I can purchase multiple products together

Scenario: Adding items to cart
  Given I am logged in as a customer
  And I have an empty shopping cart
  When I add a "MacBook Pro" priced at $2000 to my cart
  Then my cart should contain 1 item
  And the total should be $2000

Scenario: Stock validation
  Given a product "iPhone" has only 1 item in stock
  And another customer has it in their cart
  When I try to add "iPhone" to my cart
  Then I should see "Product out of stock" error

Scenario: Premium customer discount
  Given I am a premium customer
  And I have items worth $1000 in my cart
  When I proceed to checkout
  Then I should get a 10% discount
  And my total should be $900

Benefits of BDD

  • Clear Communication: Business stakeholders can read and validate scenarios
  • Living Documentation: Requirements stay updated and testable
  • Early Validation: Catch misunderstandings before development
  • Acceptance Criteria: Clear definition of "done"
  • Automated Testing: Scenarios become executable tests

When to Use BDD

  • Requirements are unclear or changing frequently
  • Complex user workflows need validation
  • Multiple stakeholders need alignment
  • You want living documentation
  • Building customer-facing features

DDD (Domain-Driven Design)

"How should we MODEL the business domain in code?"

Definition

DDD focuses on creating a software model that reflects real business concepts and rules. It tackles complexity by aligning software design closely with business processes.

Key Concepts

  • Ubiquitous Language: Shared vocabulary between business and tech
  • Bounded Contexts: Clear boundaries for different parts of the domain
  • Entities: Objects with identity that persist over time
  • Value Objects: Immutable objects defined by their attributes
  • Aggregates: Clusters of related objects treated as a unit
  • Domain Services: Business logic that doesn't belong to entities

Real-World Example: E-commerce Domain Model

// Value Object - Money
class Money {
  constructor(private amount: number, private currency: string) {
    if (amount < 0) throw new Error("Amount cannot be negative");
  }
  
  add(other: Money): Money {
    if (this.currency !== other.currency) {
      throw new CurrencyMismatchError();
    }
    return new Money(this.amount + other.amount, this.currency);
  }
  
  multiply(factor: number): Money {
    return new Money(this.amount * factor, this.currency);
  }
}

// Entity - Product
class Product {
  constructor(
    public readonly id: ProductId,
    public readonly name: string,
    private stock: number,
    private price: Money
  ) {}
  
  isInStock(requestedQuantity: number): boolean {
    return this.stock >= requestedQuantity;
  }
  
  reserveStock(quantity: number): void {
    if (!this.isInStock(quantity)) {
      throw new OutOfStockError(`${this.name} is out of stock`);
    }
    this.stock -= quantity;
  }
}

// Aggregate Root - Shopping Cart
class ShoppingCart {
  private items: CartItem[] = [];
  private customerId: CustomerId;
  
  constructor(customerId: CustomerId) {
    this.customerId = customerId;
  }
  
  addItem(product: Product, quantity: number): void {
    // Business Rule: Can't add out-of-stock items
    if (!product.isInStock(quantity)) {
      throw new OutOfStockError(`${product.name} is out of stock`);
    }
    
    // Business Rule: Maximum 10 items per product
    const existingItem = this.findItem(product.id);
    if (existingItem && existingItem.quantity + quantity > 10) {
      throw new MaxQuantityExceededError();
    }
    
    if (existingItem) {
      existingItem.increaseQuantity(quantity);
    } else {
      this.items.push(new CartItem(product, quantity));
    }
  }
  
  calculateTotal(): Money {
    return this.items.reduce(
      (total, item) => total.add(item.getSubtotal()), 
      Money.zero()
    );
  }
  
  // Business Rule: Cart expires after 30 days of inactivity
  isExpired(): boolean {
    return this.lastModified.isOlderThan(Duration.days(30));
  }
}

// Domain Service - Pricing
class PricingService {
  calculateDiscount(customer: Customer, cartTotal: Money): Money {
    if (customer.membershipType === MembershipType.PREMIUM) {
      return cartTotal.multiply(0.10); // 10% discount
    }
    return Money.zero();
  }
}

Benefits of DDD

  • Business Alignment: Software reflects real business processes
  • Ubiquitous Language: Common vocabulary reduces misunderstandings
  • Maintainability: Complex logic is properly organized
  • Flexibility: Model can evolve with changing business needs
  • Team Scaling: Clear boundaries help multiple teams work together

When to Use DDD

  • Complex business domains with many rules
  • Long-term, maintainable systems
  • Multiple teams working on the same domain
  • Business logic is scattered and hard to maintain
  • Domain experts are available for collaboration

TDD (Test-Driven Development)

"How do we ensure our code works correctly?"

Definition

TDD emphasizes writing tests before writing the corresponding code. It follows a strict cycle: write a failing test, make it pass, then refactor.

Key Concepts

  • Red-Green-Refactor cycle
  • Unit tests drive design decisions
  • Fast feedback loop
  • Confidence in code changes
  • Documentation through tests

The TDD Cycle

1. 🔴 RED: Write a failing test
2. 🟢 GREEN: Write minimal code to pass
3. 🔵 REFACTOR: Improve code quality
4. Repeat

Real-World Example: Money Calculator

Step 1: Write Failing Test (RED)

describe('Money', () => {
  it('should add two money amounts with same currency', () => {
    const money1 = new Money(10, 'USD');
    const money2 = new Money(5, 'USD');
    
    const result = money1.add(money2);
    
    expect(result.amount).toBe(15);
    expect(result.currency).toBe('USD');
  });
  
  it('should throw error when adding different currencies', () => {
    const usd = new Money(10, 'USD');
    const eur = new Money(5, 'EUR');
    
    expect(() => usd.add(eur)).toThrow(CurrencyMismatchError);
  });
});

Step 2: Write Minimal Code (GREEN)

class Money {
  constructor(public amount: number, public currency: string) {}
  
  add(other: Money): Money {
    if (this.currency !== other.currency) {
      throw new CurrencyMismatchError();
    }
    return new Money(this.amount + other.amount, this.currency);
  }
}

Step 3: Refactor (REFACTOR)

class Money {
  constructor(public readonly amount: number, public readonly currency: string) {
    if (amount < 0) throw new Error("Amount cannot be negative");
  }
  
  add(other: Money): Money {
    this.validateSameCurrency(other);
    return new Money(this.amount + other.amount, this.currency);
  }
  
  private validateSameCurrency(other: Money): void {
    if (this.currency !== other.currency) {
      throw new CurrencyMismatchError(
        `Cannot add ${other.currency} to ${this.currency}`
      );
    }
  }
}

Benefits of TDD

  • Code Quality: Forces good design decisions
  • Confidence: Safe refactoring with test coverage
  • Documentation: Tests describe expected behavior
  • Fast Feedback: Immediate validation of changes
  • Regression Prevention: Catches bugs early

When to Use TDD

  • Building critical business logic
  • Working with legacy code
  • Aiming for high code quality
  • Frequent refactoring needed
  • Complex algorithms or calculations

Comparison Matrix

Aspect BDD DDD TDD
Primary Focus Behavior & Requirements Business Domain Modeling Code Quality & Design
Audience Business + Tech Domain Experts + Developers Developers
Artifacts Feature files, Scenarios Domain Models, Bounded Contexts Unit Tests
Language Natural (Gherkin) Business Terms Code
Validation Acceptance Tests Domain Rules Unit Tests
Timing Requirements Phase Design Phase Implementation Phase
Collaboration High (Cross-functional) Medium (Domain experts) Low (Developer-focused)
Complexity User Workflows Business Logic Code Implementation

How They Work Together

The Triple Alliance: BDD + DDD + TDD

BDD (What?) → DDD (How?) → TDD (Implementation)
     ↓             ↓              ↓
Requirements → Domain Model → Tested Code

Practical Integration Example

1. BDD Defines Behavior

Scenario: Premium customer gets discount
  Given I am a premium customer
  And my cart total is $1000
  When I proceed to checkout
  Then I should receive a 10% discount
  And my final total should be $900

2. DDD Models the Domain

class Customer {
  constructor(private membershipType: MembershipType) {}
  
  isPremium(): boolean {
    return this.membershipType === MembershipType.PREMIUM;
  }
}

class DiscountService {
  calculateDiscount(customer: Customer, amount: Money): Money {
    if (customer.isPremium()) {
      return amount.multiply(0.10);
    }
    return Money.zero();
  }
}

3. TDD Ensures Quality

describe('DiscountService', () => {
  it('should give 10% discount to premium customers', () => {
    const premiumCustomer = new Customer(MembershipType.PREMIUM);
    const amount = new Money(1000, 'USD');
    const service = new DiscountService();
    
    const discount = service.calculateDiscount(premiumCustomer, amount);
    
    expect(discount.amount).toBe(100);
  });
});

Practical Workflow Example

Building a User Registration Feature

Phase 1: Requirements Discovery (BDD)

Feature: User Registration
  
Scenario: Successful registration with valid data
  Given I am on the registration page
  When I enter valid user details
  And I submit the form
  Then I should receive a confirmation email
  And my account should be created

Scenario: Registration with existing email
  Given a user with email "john@example.com" already exists
  When I try to register with "john@example.com"
  Then I should see "Email already registered" error

Phase 2: Domain Modeling (DDD)

// Value Objects
class Email {
  constructor(private value: string) {
    if (!this.isValid(value)) {
      throw new InvalidEmailError();
    }
  }
  
  private isValid(email: string): boolean {
    return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
  }
}

// Entity
class User {
  constructor(
    public readonly id: UserId,
    private email: Email,
    private hashedPassword: string
  ) {}
  
  static create(email: Email, password: string): User {
    const hashedPassword = PasswordService.hash(password);
    return new User(UserId.generate(), email, hashedPassword);
  }
}

// Domain Service
class RegistrationService {
  constructor(private userRepository: UserRepository) {}
  
  async registerUser(email: Email, password: string): Promise<User> {
    const existingUser = await this.userRepository.findByEmail(email);
    if (existingUser) {
      throw new EmailAlreadyExistsError();
    }
    
    const user = User.create(email, password);
    await this.userRepository.save(user);
    return user;
  }
}

Phase 3: Implementation with Tests (TDD)

describe('User', () => {
  it('should create user with valid email and password', () => {
    const email = new Email('john@example.com');
    const user = User.create(email, 'password123');
    
    expect(user.id).toBeDefined();
    expect(user.email).toBe(email);
  });
});

describe('RegistrationService', () => {
  it('should throw error when email already exists', async () => {
    const existingUser = new User(/* ... */);
    mockRepository.findByEmail.mockResolvedValue(existingUser);
    
    await expect(
      service.registerUser(new Email('john@example.com'), 'password')
    ).rejects.toThrow(EmailAlreadyExistsError);
  });
});

When to Use Each

Use BDD When:

  • ❓ Requirements are unclear or complex
  • 👥 Multiple stakeholders need alignment
  • 📋 You need living documentation
  • 🔄 Requirements change frequently
  • 🎯 Building user-facing features

Use DDD When:

  • 🏢 Complex business domain with many rules
  • 📈 Long-term, maintainable systems needed
  • 👥 Multiple teams working on same domain
  • 🔀 Business logic is scattered
  • 💼 Domain experts are available

Use TDD When:

  • ⚡ Building critical business logic
  • 🔧 Working with legacy code
  • 🎯 High code quality required
  • 🔄 Frequent refactoring needed
  • 🧮 Complex algorithms or calculations

Use All Three When:

  • 🏗️ Building enterprise applications
  • 🎯 Need both clear requirements AND clean architecture
  • 🤝 Want to align business goals with technical implementation
  • 👥 Working in large, distributed teams
  • 📊 Domain complexity is high

Best Practices

BDD Best Practices

  • Write scenarios from user's perspective
  • Use concrete examples, avoid abstractions
  • Keep scenarios independent
  • Involve business stakeholders in writing
  • Automate scenario execution

DDD Best Practices

  • Start with understanding the domain
  • Use ubiquitous language consistently
  • Define clear bounded contexts
  • Keep aggregates small
  • Separate domain logic from infrastructure

TDD Best Practices

  • Write the simplest test first
  • Follow the Red-Green-Refactor cycle
  • Keep tests fast and independent
  • Test behavior, not implementation
  • Refactor regularly

Common Pitfalls to Avoid

BDD Pitfalls

  • ❌ Writing scenarios that are too technical
  • ❌ Not involving business stakeholders
  • ❌ Creating dependent scenarios
  • ❌ Focusing on UI details instead of behavior

DDD Pitfalls

  • ❌ Creating god objects or large aggregates
  • ❌ Mixing domain logic with infrastructure
  • ❌ Not defining clear bounded contexts
  • ❌ Over-engineering simple domains

TDD Pitfalls

  • ❌ Writing tests after implementation
  • ❌ Testing implementation details
  • ❌ Creating slow or brittle tests
  • ❌ Not refactoring regularly

Conclusion

BDD, DDD, and TDD are complementary approaches that address different aspects of software development:

  • BDD ensures you build the right thing by focusing on user behavior and requirements
  • DDD helps you build it right by creating maintainable domain models
  • TDD ensures your implementation works correctly through comprehensive testing

The Power of Integration

When used together, these methodologies create a powerful development approach:

  1. BDD captures what the system should do
  2. DDD models how the business domain works
  3. TDD ensures the implementation is correct

This combination leads to software that is:

  • Aligned with business needs
  • Well-architected and maintainable
  • Thoroughly tested and reliable
  • Documented and understandable

Final Recommendation

Start with one methodology that addresses your biggest pain point:

  • Choose BDD if communication and requirements are your challenge
  • Choose DDD if complexity and maintainability are your issues
  • Choose TDD if code quality and bugs are your concern

As your team matures, gradually integrate the other approaches to create a comprehensive development strategy.


This guide was last updated on 2025-08-13 07:23:34 UTC. For questions or contributions, please open an issue in the repository.

Clone this wiki locally