DEV Community

Cover image for TDD is Backwards: Why Prototype-First Development Ships Better Software
Hunter Wiginton
Hunter Wiginton

Posted on

TDD is Backwards: Why Prototype-First Development Ships Better Software

Stop writing tests before you know what you're building

You're about to build a new feature. The TDD playbook says write the tests first. But what tests? You don't even know what the API should look like yet. You don't know if this approach will work. You spend 2 hours writing tests for an interface that you'll rewrite in 30 minutes once you actually understand the problem.

This isn't learning. It's cargo-cult development.

I've spent the last year building multiple production tools. I've built a CLI for repository intelligence, a suite of workflow automation scripts, production agents for an enterprise system, and not one started with tests. All shipped successfully. Recently, my team at work switched from Behavior Driven Development (BDD) to Specification Driven Development (SDD), and the lightbulb finally clicked.

There's a better path: build the prototype first, formalize it with specifications, then let those specs drive your tests. This isn't cowboy coding, it's pragmatic engineering that respects how software actually evolves.

The TDD Ritual We Keep Performing

Test Driven Development has become religious dogma. The ritual goes like this:

  1. Write a failing test (red)
  2. Write minimal code to pass (green)
  3. Refactor
  4. Repeat

The benefits sound compelling: testable code, thoughtful interfaces, regression safety, no over-engineering. It's been called an "industry best practice" for so long that questioning it feels like heresy.

But here's the hidden assumption that breaks everything: TDD assumes you already know what you're building.

When you're implementing a known algorithm like sorting, searching, or standard data structures, then TDD works beautifully. The interface is predetermined. The behavior is well-defined. You're translating a spec that exists in your head (or a textbook) into code.

But when you're exploring a new problem space, and you don't know if your approach will even work, TDD falls apart.

The evidence is everywhere if you look:

  • Surveys consistently show less than 30% of developers practice strict TDD
  • Successful open-source projects rarely start with comprehensive test suites
  • Early-stage startups ship working prototypes first, tests later
  • Even TDD advocates describe it as "difficult" and requiring "discipline", which is usually just code for "this doesn't feel natural"

What Actually Happens When Building Something New

Let me show you what really happens when you're solving a novel problem.

RepoG: Repository Intelligence CLI

I built RepoG, a CLI tool that provides semantic search and AI-powered analysis over your git repositories. It's now published to Homebrew with real users.

I didn't write a single test during initial exploration.

Here's what the development actually looked like:

Week 1: Built repog init, repog sync, and repog embed commands by trying different approaches. I experimented with three different chunking strategies before finding one that actually worked for code.

Week 2: Evaluated vector databases. I Tried Pinecone. I Tried Weaviate. I Tried Qdrant. Then I settled on SQLite with the sqlite-vec extension. Each attempt involved real code, real API calls, real performance testing.

Week 3: Discovered the API surface that made sense. I Added tests during v0.1.0 finalization only after I understood what the tool actually needed to do.

Result: Shipped to production. Published to Homebrew.

If I'd started with TDD:

  • All tests for chunking strategy #1 would be deleted
  • All tests for Pinecone integration would be deleted
  • All tests for the original API design would be rewritten
  • I would have wasted hours testing interfaces that never shipped

The tests I eventually wrote? Rock solid. Why? Because they validated a stable API that I understood deeply after building it.

Staksmith: My Personal Workflow Automation

I built five workflow automation skills: Inbox Gradient Accelerator (auto-classifies notes using AI), Weekly Momentum Report (aggregates git commits and tasks), Code-to-Docs Sync (detects documentation drift), and two others.

Zero tests across all five skills.

Why? They're exploratory bash scripts combined with AI prompts. The "test" is simple: does this actually save time in my workflow?

I iterated on prompt engineering, confidence thresholds, output formats based on real usage. Each script was rewritten 3-5 times as I discovered what actually mattered.

Tests would have been rewritten alongside every iteration. Or worseβ€”I would have felt pressure to keep a bad design just because I'd invested time writing tests for it.

Enterprise Agent Development

At work, I build production agents that process thousands of requests daily. Recently, I built an agent that identifies failed dispatch tasks requiring manual intervention.

The development process:

  • Tried one AI model, discovered it hallucinated tool parameters
  • Switched models, refined tool schemas based on actual API behavior
  • Discovered edge cases: null timestamp fields, missing triggered dates
  • Refined error handling based on production data

Tests written upfront would have validated hallucinated interfaces that never existed in production.

The pattern is clear: when you don't know what you're building, tests are documentation of ignorance.

Specification-Driven Development: The Missing Link

In April 2026, my team made a subtle but profound shift from Behavior Driven Development (BDD) to Specification Driven Development (SDD).

BDD said: Write behavior specs in Gherkin format, let those drive tests.

SDD says: Write comprehensive product specifications, let those drive everything.

The critical difference? BDD still wants you to specify behavior before understanding the problem deeply. SDD acknowledges you need a working prototype to write meaningful specifications.

The SDD Workflow

Phase 1: Prototype (Exploration)

Build a working proof-of-concept. Try different approaches. Understand the actual problem space.

No tests yet. You're learning.

Phase 2: Specify (Formalization)

Once you have a working prototype, document what it should do, not just what it currently does.

Define clear boundaries and constraints, specify edge cases and error handling, outline expected behaviors and outcomes, and create a formal specification document.

Here's what a real specification looks like (simplified from used by real engineers at a real software company):

# Failed Task Identifier Specification

## Purpose
Identify failed tasks requiring manual intervention

## Input Constraints
- Must handle null timestamp fields
- Must validate before making API calls
- Must return structured error responses (not raw errors)

## Expected Behaviors
- Fetch tasks from last 7 days by default
- Filter by status: FAILED
- Return count + task details
- Handle API errors gracefully (return empty list, not error)

## Success Criteria
- Zero hallucinated parameters
- Consistent counts across multiple invocations
- Proper null checking prevents runtime errors
Enter fullscreen mode Exit fullscreen mode

The specification becomes your source of truth.

Phase 3: Test (Validation)

Now you write tests based on the specification.

Tests validate the spec, not your exploration. Tests document intended behavior, not implementation accidents. Tests remain stable as implementation details change.

Phase 4: Iterate (Refinement)

When requirements change:

  1. Update the specification
  2. Update tests to match new spec
  3. Refactor implementation knowing spec + tests protect you

Why This Works

Specifications require domain understanding, and you get that from prototyping.

Tests validate specifications (which are stable), not implementations (which change frequently during exploration).

The spec becomes living documentation that guides future development.

I converted the failed task agent to SDD after building it. The specification revealed gaps I'd missed: inadequate error handling, missing validation, inconsistent behavior under edge cases. Now the tests validate against the spec, and when I refactor the implementation, the tests don't break because they're testing behavior, not structure.

When TDD Actually Makes Sense

I'm not anti-testing. I'm anti-premature-testing.

TDD is excellent for:

1. Implementing Known Algorithms

Sorting, searching, data structure operations. The interface is predetermined, and the behavior is well-defined. You're just translating a known specification into code.

2. Bug Fixes with Regression Tests

Write a test that reproduces the bug. Fix the bug. Test prevents regression. This is actually where TDD came from.

3. API Contract Enforcement

Public APIs with versioning commitments. Breaking changes are expensive. Tests document and enforce the contract.

4. Refactoring Existing Code

You know what it should do because it already does it. Tests ensure behavior preservation during refactoring.

The key distinction:

  • TDD works when the problem is known
  • Prototype-first works when the problem is unknown
  • SDD bridges the gap between exploration and formalization

The Modern Development Workflow πŸ› οΈ

Here's the pragmatic approach that respects how software actually evolves:

Unknown Problem β†’ Prototype β†’ Specify β†’ Test β†’ Production
   (Explore)      (Discover)  (Formalize) (Validate) (Maintain)
Enter fullscreen mode Exit fullscreen mode

Stage 1: Prototype

Goal: Does this approach even work?

Tools: REPL, throwaway scripts, experimental code

Output: Working proof-of-concept

Tests: None yet

Stage 2: Specify

Goal: What should this do?

Tools: Specification documents, Architecture Decision Records (ADRs)

Output: Formal requirements and constraints

Tests: Not yet, spec comes first

Stage 3: Test

Goal: Does it meet the specification?

Tools: Unit tests, integration tests, end-to-end tests

Output: Test suite validating spec compliance

Tests: Now write tests driven by the specification

Stage 4: Iterate

Goal: Maintain and evolve

Process: Spec change β†’ Test update β†’ Implementation update

Tests remain stable because they validate the spec, not implementation details.

Real Examples

ProtoFlow (my subscription-based prototyping service):

  • Started with comprehensive implementation plan (specification-first)
  • Building features based on spec
  • Tests will validate: subscription tier limits, request workflows, file handling
  • Spec written before code because the problem is well understood (I've seen similar apps)

RepoG (my repository intelligence CLI):

  • Problem was novel (semantic search over repos with multi-model support)
  • Prototyped first, discovered constraints, then formalized
  • Tests written after understanding the actual requirements

Different problems require different approaches. That's the point.

Why This Matters in the AI Era πŸ’‘

AI coding assistants like Claude Code, GitHub Copilot, and Cursor have fundamentally changed the economics of software development.

With these tools:

  • Generating tests is trivial
  • Generating implementation code is trivial
  • Understanding what to build is not trivial

The new bottleneck is specification, not implementation.

What AI Can't Do

  • Decide what problem to solve
  • Determine the right abstraction level
  • Make architectural trade-offs
  • Write meaningful specifications (requires deep domain understanding)

What AI Excels At

  • Generating tests from specifications
  • Implementing code to match specs
  • Refactoring while preserving behavior
  • Finding edge cases in specifications

The Economic Shift

Old world: Tests were expensive to write, so write them first to ensure good design.

AI world: Tests are cheap to generate, but specifications are expensive to write well. Prototype first to inform specifications.

Your time is better spent:

  1. Building prototypes to understand the problem space (AI assists)
  2. Writing clear specifications based on what you learned (human insight)
  3. Letting AI generate tests that validate the spec (AI excels)
  4. Iterating on real usage (human judgment)

I use Claude Code daily. It can generate a comprehensive test suite from my specification in minutes. It cannot tell me if I'm solving the right problem.

Handling the Pushback

"But TDD forces better design!"

No. Specifications force better design. TDD just forces testable code, which isn't the same thing.

Testable code can still have terrible abstractions, leaky boundaries, and solve the wrong problem. Prototype-first lets you discover the right design through exploration, then formalize it with specifications.

"Without tests first, you'll write untestable code!"

Only if you never write tests. The SDD workflow includes tests, they're just written after you understand what you're testing.

Modern refactoring tools (especially AI-assisted) make it straightforward to retrofit testability. I've refactored entire modules to be more testable after the fact using Claude Code. It took hours, not weeks.

"This is just cowboy coding with extra steps!"

Let's be clear about the differences:

Cowboy coding: No tests, no specs, ship and pray

This approach: Prototype β†’ Specify β†’ Test β†’ Ship with confidence

The specification step is the discipline. It's actually more rigorous than TDD because it forces you to think about the problem holistically. You're not just thinking about testable interfaces, but the entire behavior, edge cases, error handling, and success criteria.

"What about code coverage?"

Code coverage is a metric, not a goal.

100% coverage of the wrong abstraction is worthless. Better: 80% coverage of well-specified behavior after understanding the problem deeply.

I've seen codebases with 95% test coverage that were impossible to change because tests were coupled to implementation details. I've seen codebases with 60% test coverage that were easy to maintain because tests validated behavior through specifications.

Test the right things.

Practical Guidelines

Use the right tool for the job.

When to prototype first:

  • Building something new or novel
  • Unclear problem space
  • Evaluating multiple approaches
  • Early-stage product development
  • Exploratory automation and tooling

When to specify first (SDD):

  • Well-understood problem
  • Clear requirements upfront
  • Regulated industries
  • Public APIs
  • Team collaboration on defined features

When to use TDD:

  • Implementing known algorithms
  • Bug fixes
  • Refactoring existing code
  • API contract preservation

Red Flags You're Doing TDD Wrong

  • Rewriting tests multiple times during initial development
  • Tests that just mirror implementation
  • "Testing" private methods
  • Extensive mocking to make tests pass
  • Tests that break on every refactor

Green Flags You're Doing SDD Right

  • Specification is readable by non-programmers
  • Tests validate specification, not implementation details
  • Specification includes edge cases discovered during prototyping
  • Specification guides future development decisions
  • Tests remain stable as implementation evolves

A Template for Specifications

If you're wondering what a full SDD specification looks like, I've created a generalized template based on what my team uses at work (adapted to be product-agnostic rather than agent-specific).

The template includes sections for:

  • Purpose & Success Metrics: What this does and how you'll measure success
  • Context: When to use this (and when not to use it)
  • Dependencies: What you need from other teams/systems
  • User Workflow: End-to-end flow with error handling
  • Technical Specification: API contracts, data models, external dependencies
  • Acceptance Criteria: Happy path, edge cases, error handling
  • Examples: Real inputs/outputs with business value explanations
  • Testing Strategy: What to test and how

Get the complete SDD Template β€” includes both Markdown and PDF versions.

The key insight: you fill this out after prototyping, when you actually understand the problem. Then the specification drives your tests.

Test the Right Thing at the Right Time

The TDD dogma assumes we know what we're building before we start. But most interesting software problems require exploration first, formalization second.

The modern workflow respects this reality:

  1. Prototype when the problem is unclear
  2. Specify once you understand what you're building
  3. Test to validate the specification
  4. Iterate with confidence

This isn't abandoning testing. It's testing smarter.

Specifications informed by working prototypes lead to better tests than tests written in a vacuum. Tests that validate specifications remain stable as implementations evolve. Tests that validate implementation details break constantly.

Your job as an engineer is to solve problems, not to follow rituals.

Sometimes that means writing tests firstβ€”when you're implementing a known algorithm, fixing a bug, or enforcing an API contract.

Often it means building a working prototype, understanding what you learned, formalizing it with specifications, and then writing tests that validate those specifications.

Test the right thing at the right time. Everything else is dogma.

Which approach matches how you actually work β€” TDD, prototype-first, or somewhere in between? Drop your take in the comments. πŸ’‘

Top comments (0)