TDD vs SDD vs DDD: Comparing Three Development Methodologies

A comparison of TDD (Test-Driven Development), SDD (Spec-Driven Development), and DDD (Domain-Driven Design) — each one's characteristics, applicable scenarios, and how to combine them — laid out from a practical perspective with a comparison table.
代表 / エンジニア
"TDD, SDD, DDD — the acronyms are so similar I honestly can't tell them apart." Have you felt this? I have. Every time I encountered these methodologies on a project, the boundaries between them blurred. This article organizes the differences among the three with a comparison table and explains how to choose between them in practice.
The Basic Concepts of TDD, SDD, and DDD
First, succinct definitions.
TDD (Test-Driven Development) is a methodology where you write the test code before the implementation code. The core cycle proposed by Kent Beck is "Red → Green → Refactor": write a failing test, make it pass with the minimum implementation, then refactor — repeated continuously.
SDD (Spec-Driven Development) is a methodology where you define the specification first and then drive development with that spec as the "Single Source of Truth." "Spec" here means a machine-readable interface definition like OpenAPI (Swagger), a GraphQL schema, Protocol Buffers, or JSON Schema. Its hallmark is using the spec as the starting point for automating code generation, validation, and documentation, enabling parallel frontend/backend development.
DDD (Domain-Driven Design) is a design methodology proposed by Eric Evans that puts business domain knowledge at the center of software design. Using concepts like ubiquitous language, bounded contexts, and aggregates, it tackles complexity in business logic.
I used to lump them together as "all design stuff," but realized their focuses are actually quite different.
TDD — The "Red → Green → Refactor" Cycle in Action
Even understanding the concept, many say it's hard to picture how TDD actually proceeds. Let's look at the cycle concretely with an example: calculating an ecommerce cart total.
Step 1: Red (Write a Failing Test)
First, write a test for a function that doesn't exist yet:
// cart.test.js
test("With one item, total is price × quantity", () => {
const items = [{ name: "T-Shirt", price: 2000, quantity: 3 }];
expect(calculateTotal(items)).toBe(6000);
});At this point the calculateTotal function doesn't exist, so the test naturally fails. That's the "Red" state.
Step 2: Green (Make It Pass with Minimal Implementation)
Next, write the minimum implementation to make the test pass:
// cart.js
function calculateTotal(items) {
return items.reduce((sum, item) => sum + item.price * item.quantity, 0);
}The test passes — that's "Green." Crucial at this stage: don't aim for "clean code." Focus only on getting the test to pass.
Step 3: Refactor (Improve While Keeping Tests Green)
Keep the Green state and improve the code's structure. If discount logic is coming later, you might restructure the calculation for easier extension. As long as the tests stay green, you can refactor with confidence.
Running the Cycle
Continue adding new test cases:
test("For multiple items, returns the sum of each item's subtotal", () => {
const items = [
{ name: "T-Shirt", price: 2000, quantity: 3 },
{ name: "Mug", price: 800, quantity: 1 },
];
expect(calculateTotal(items)).toBe(6800);
});
test("Returns 0 for an empty cart", () => {
expect(calculateTotal([])).toBe(0);
});Stacking small tests like this results in robust implementations that comprehensively cover the spec.
What I valued most when practicing TDD is that "tests guide the design." Writing tests first makes you think about the function's interface from the user's perspective — and naturally, easy-to-use APIs emerge. On the other hand, for UI display logic or database boundary code — where inputs and outputs are fuzzy — I sometimes found myself stuck trying to force-fit TDD. Not a universal tool; recognizing where it shines matters.
SDD — A Concrete Example of Spec-First Development
The heart of SDD is "write the spec first and generate everything from it." Let's walk through a concrete development flow using an API spec defined in OpenAPI.
Step 1: Define the Spec in OpenAPI
For example, the spec for a user-information API:
# openapi.yaml
openapi: 3.0.3
info:
title: User API
version: 1.0.0
paths:
/users/{userId}:
get:
summary: Get user information
parameters:
- name: userId
in: path
required: true
schema:
type: string
format: uuid
responses:
"200":
description: Success
content:
application/json:
schema:
$ref: "#/components/schemas/User"
"404":
description: User not found
components:
schemas:
User:
type: object
required: [id, name, email]
properties:
id:
type: string
format: uuid
name:
type: string
example: "Taro Tanaka"
email:
type: string
format: emailThis YAML file becomes the whole team's "contract."
Step 2: Generate from the Spec
Once the spec is fixed, automatically generate various artifacts.
- TypeScript type definitions: Tools like
openapi-typescriptgenerate types for requests and responses, usable on both frontend and backend - Mock server: Use
Prismor similar to instantly spin up a mock API conforming to the spec. The frontend team can start development without waiting for the backend implementation to finish - API documentation: Tools like
Swagger UIorRedocpublish documentation always synced to the spec - Validation: Middleware automatically verifies that requests and responses conform to the spec
Step 3: Parallel Development Around the Spec
This is where SDD truly shines. With a shared spec as common language, frontend and backend teams can start moving simultaneously.
- Frontend: Develop screens against the mock server. Types are already generated, so no need to guess API structure
- Backend: Implement the endpoints defined in the spec. Validation middleware catches divergence from the spec
- QA: Generate test case skeletons from the spec and progress edge-case verification
What Happens When Spec Changes?
SDD manages changes as "change the spec file → regenerate → review the diff." For example, to add a phoneNumber field to the User schema: update the spec and regenerate, and type definitions, documentation, and validation all follow. The risk of code diverging from the spec is structurally eliminated.
I really felt SDD's value after one project where frontend and backend teams tangled over "is this field included in the API response?" Verbal consensus on Slack or Confluence documentation inevitably produces misalignments. Placing a single machine-readable spec dissolved most of that friction.
DDD — Writing Code in the Language of the Business
DDD is said to have the highest learning cost of the three, but for projects with complex business logic, it pays enormous dividends. Using an "online reservation system" as an example, let's look at how DDD thinking reflects in code.
Establishing Ubiquitous Language
DDD emphasizes developers and domain experts (business specialists) using the same vocabulary. For a hotel reservation system, for example, you might define:
- Reservation: A request made by a guest specifying check-in dates and a room
- Availability: The state of a room being bookable on specific dates
- CancellationPolicy: Rules governing charges when canceling a reservation
Crucially, use those same terms in variable and class names in the code. Avoid generic names like "data" or "info" — write code in the language of the business. The goal is to reach a state where you can read the code and understand business rules.
Protecting Business Rules with Aggregates
DDD's "Aggregate" is the unit that ensures data consistency. In a reservation system, you might design Reservation as the aggregate root.
class Reservation {
private status: ReservationStatus;
private checkIn: Date;
private cancellationPolicy: CancellationPolicy;
cancel(currentDate: Date): void {
if (this.status !== "confirmed") {
throw new Error("Only confirmed reservations can be cancelled");
}
// Calculate fee based on cancellation policy
const fee = this.cancellationPolicy.calculateFee(
this.checkIn, currentDate
);
this.status = "cancelled";
this.addDomainEvent(new ReservationCancelled(this.id, fee));
}
}Business rules like "only confirmed reservations can be cancelled" and "calculate cancellation fee per policy" are cohesively held within the aggregate. When these rules are scattered across service or controller layers, it becomes hard to trace the impact of a change. DDD makes the design change-resilient by containing business rules inside domain objects.
Bounded Contexts
As systems grow, the same concept of "user" can mean different things in different contexts. In the reservation context it's "Guest," in the payment context "Payer," in the marketing context "Lead." DDD, rather than forcing unification, separates these as "bounded contexts" and explicitly defines translation rules between them.
When I first approached DDD, I was overwhelmed by the number of concepts and stuck on "we need to do it all perfectly from the start." In reality, even just establishing ubiquitous language noticeably improved team communication. You don't need to adopt everything at once — introducing the high-impact parts first, step by step, is the pragmatic approach.
Comparing the Three — Organization by Aspect
For those who've felt "I can't quite tell them apart," here's a comparison table across key aspects:
| Aspect | TDD | SDD | DDD |
|---|---|---|---|
| Main concern | Code correctness and quality | Interface contracts | Structure of business logic |
| Starting artifact | Test code | Specification file (OpenAPI, etc.) | Domain model |
| Applicable phase | Implementation | API/interface design | Requirements/design |
| Primary effect | Regression bug prevention, design improvement | Enables parallel team development | Alignment between business and implementation |
| Representative tools | Jest, pytest, RSpec | OpenAPI, GraphQL, Protobuf | EventStorming, Context Map |
| Learning cost | Medium | Low-medium | High |
| Team-size fit | Any scale | Good for parallel multi-team development | Medium-to-large |
What I re-realized while making this table: the three aren't in competition — they solve problems at different layers. TDD answers "is the implementation correct?"; SDD answers "is the team's understanding aligned?"; DDD answers "does this capture the essence of the business?"
Why the "Different Layers" Claim Holds
Digging a bit deeper: the three methods differ in when and what they decide.
- DDD sits at the most upstream end of the project, deciding "what to build" and "how to model the business structure"
- SDD sits in the design phase, deciding "how to define interactions between components"
- TDD sits in the implementation phase, verifying "do individual functions and classes work correctly"
Think of the applicable range narrowing as you go from upstream to downstream. That's why they aren't mutually exclusive; using them in combination has real meaning.
Choosing in Practice — Criteria by Project Characteristics
Answering "so which should I use?" based on what I've learned from practice. No blanket answer, but these criteria may help:
When TDD is especially effective:
- Lots of logic with clear inputs and outputs, like calculations and validation
- Safely refactoring existing code
- Preventing bug regressions in long-term operations
When SDD is especially effective:
- Frontend and backend developed by separate teams
- Heavy inter-service coordination in microservices
- API-first services with external public APIs
When DDD is especially effective:
- Complex business rules that simple CRUD can't express
- Collaboration with domain experts (business specialists) is essential
- The code is becoming harder to read as the system grows
Looking back, I went through a period of thinking "just adopting TDD will improve quality." But writing tests while the business logic structure itself was unorganized meant the tests couldn't keep up with business changes, and maintenance cost actually increased. Changing the order — organize the domain model with DDD first, then write tests with TDD — dramatically improved the situation.
Common Pitfalls During Adoption
Beyond methodology selection, the adoption process itself needs care. Here are pitfalls I've seen on projects:
TDD pitfall: Coverage absolutism Chasing coverage numbers often mass-produces meaningless tests. Tests for getters/setters or verifications of framework internals contribute almost nothing to bug detection relative to their maintenance cost. Constantly ask yourself: "If this test fails, does that really indicate a bug?"
SDD pitfall: Letting spec and implementation drift apart Even when the spec is defined first, as development progresses, "change the code without updating the spec" situations creep in. Once that happens, the spec loses credibility and SDD loses its value. Build spec-code consistency checks into CI and fail the build on divergence.
DDD pitfall: Over-abstraction Staying faithful to DDD's concepts to the point of bringing aggregates, repositories, and domain services into small CRUD applications. Applying the full DDD kit to an app with almost no business logic complicates only the structure, slowing development. DDD is "a method for handling complexity"; where there's no complexity, it's unnecessary.
Approaches to Combining the Three
In real projects, you'll probably use these in combination rather than alone. Here are practical patterns:
Pattern 1: DDD + TDD (Domain-centric, robust development)
Design bounded contexts and aggregates with DDD, then implement each aggregate's business rules with TDD. The resulting thorough test coverage at the domain layer produces a structure resilient to business logic changes.
- Surface domain events via EventStorming
- Design aggregates, entities, and value objects
- Implement domain objects' behavior with TDD
- Repeat Red → Green → Refactor
For the reservation system above, you'd implement Reservation.cancel()'s business rules (cancellation deadline check, fee calculation) one test at a time via TDD. Since the business rules are explicitly expressed in the test code, six months later when requirements change, it's immediately clear "which tests are affected."
Pattern 2: SDD + TDD (Ensuring API quality)
Define the spec first via OpenAPI or similar, then generate test cases from the spec and fold them into the TDD cycle. High affinity with contract testing.
- Define API spec in OpenAPI
- Auto-generate mocks, types, and test scaffolds from the spec
- Use the generated tests as a base for TDD
- Frontend develops in parallel using mocks
The strength of this combo is continuously verifying via automated tests that "the response structure in the spec matches the implementation." On spec changes, the natural SDD/TDD fusion is: update the spec file first, watch the test fail (Red), then fix the implementation (Green).
Pattern 3: DDD + SDD + TDD (Full-stack application)
For large systems, adopting all three in stages is effective. DDD designs the overall structure, SDD defines inter-service contracts, and TDD ensures each service's implementation quality.
Concretely: the bounded contexts identified in DDD's EventStorming become microservice boundaries; interactions between contexts are formalized in OpenAPI or Protocol Buffers; domain logic inside each service is implemented with TDD. Trying to apply everything perfectly from the start is heavy, so phasing adoption is realistic.
Summary — Work Backward from Your Goal
TDD, SDD, and DDD are approaches to distinct problems: "code quality," "team coordination," and "business structure." What matters is not clinging to the methodology itself but identifying the problem your project actually has and picking the most effective method.
In my experience, the highest-success-rate approach is: identify where your project is currently hurting the most, and introduce the method that addresses it. Lots of inter-team misalignment → SDD; recurring bugs → TDD; being crushed by business logic complexity → DDD.
Then, once you've felt the effect of one method, layer in other methods to cover what's missing. This "start small, expand gradually" approach is, I believe, the secret to making methodologies take root in a team without strain.
If you're agonizing over methodology choices or technical strategy, feel free to reach out here. We'll think through the best approach for your project's situation together.