-
Notifications
You must be signed in to change notification settings - Fork 0
BDD VS DDD VS TDD
Last Updated: 2025-08-13 07:23:34 UTC
- Introduction
- BDD (Behavior-Driven Development)
- DDD (Domain-Driven Design)
- TDD (Test-Driven Development)
- Comparison Matrix
- How They Work Together
- Practical Workflow Example
- When to Use Each
- Conclusion
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.
"What should the system DO from a user's perspective?"
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.
- 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
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
- ✅ 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
- Requirements are unclear or changing frequently
- Complex user workflows need validation
- Multiple stakeholders need alignment
- You want living documentation
- Building customer-facing features
"How should we MODEL the business domain in code?"
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.
- 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
// 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();
}
}
- ✅ 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
- 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
"How do we ensure our code works correctly?"
TDD emphasizes writing tests before writing the corresponding code. It follows a strict cycle: write a failing test, make it pass, then refactor.
- Red-Green-Refactor cycle
- Unit tests drive design decisions
- Fast feedback loop
- Confidence in code changes
- Documentation through tests
1. 🔴 RED: Write a failing test
2. 🟢 GREEN: Write minimal code to pass
3. 🔵 REFACTOR: Improve code quality
4. Repeat
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);
});
});
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);
}
}
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}`
);
}
}
}
- ✅ 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
- Building critical business logic
- Working with legacy code
- Aiming for high code quality
- Frequent refactoring needed
- Complex algorithms or calculations
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 |
BDD (What?) → DDD (How?) → TDD (Implementation)
↓ ↓ ↓
Requirements → Domain Model → Tested Code
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
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();
}
}
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);
});
});
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
// 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;
}
}
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);
});
});
- ❓ Requirements are unclear or complex
- 👥 Multiple stakeholders need alignment
- 📋 You need living documentation
- 🔄 Requirements change frequently
- 🎯 Building user-facing features
- 🏢 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
- ⚡ Building critical business logic
- 🔧 Working with legacy code
- 🎯 High code quality required
- 🔄 Frequent refactoring needed
- 🧮 Complex algorithms or calculations
- 🏗️ 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
- Write scenarios from user's perspective
- Use concrete examples, avoid abstractions
- Keep scenarios independent
- Involve business stakeholders in writing
- Automate scenario execution
- Start with understanding the domain
- Use ubiquitous language consistently
- Define clear bounded contexts
- Keep aggregates small
- Separate domain logic from infrastructure
- Write the simplest test first
- Follow the Red-Green-Refactor cycle
- Keep tests fast and independent
- Test behavior, not implementation
- Refactor regularly
- ❌ Writing scenarios that are too technical
- ❌ Not involving business stakeholders
- ❌ Creating dependent scenarios
- ❌ Focusing on UI details instead of behavior
- ❌ Creating god objects or large aggregates
- ❌ Mixing domain logic with infrastructure
- ❌ Not defining clear bounded contexts
- ❌ Over-engineering simple domains
- ❌ Writing tests after implementation
- ❌ Testing implementation details
- ❌ Creating slow or brittle tests
- ❌ Not refactoring regularly
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
When used together, these methodologies create a powerful development approach:
- BDD captures what the system should do
- DDD models how the business domain works
- 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
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.