← Back to Blog
Building in Public

When TDD Saves Your Architecture

christian crumlish•
A robot in a heroic stance, test-driven discipline rescuing a fragile system from collapse.
“Here I come to save the day!”

June 25, 2025

Thirty-four tests. Zero compromises. One unshakeable foundation.

This is the story of how Test-Driven Development didn’t just help me build features — it saved my architecture from death by a thousand shortcuts.

Sometimes the discipline of writing tests first forces you to make better decisions than you would have made if you were just trying to ship quickly.

The pressure to cut corners

File analysis seemed straightforward enough. Read CSV files, extract data, return some statistics. Maybe use a library, maybe hardcode a few things, get it working and move on to the next feature.

The pressure was real: I had a working integration pipeline waiting for actual file analysis. The temptation was to just make it work, clean it up later. We’d marked PM-011 done ages ago and while supposed just “testing” the UI we were now finding that whatever had claimed to work under the hood by passing unit, integration, and orchestration tests was now failing in actual use.

But we’d made this commitment to Test-Driven Development for this feature: Write the test first, watch it fail for the right reason, implement just enough to pass, refactor if needed. When tempted to skip a step, or alter a test to make it pass, resist!

TDD catches what code review misses

The first test I wrote was for path traversal security. Basic stuff — make sure user-provided file paths can’t escape the upload directory.

The test failed. Good.

I implemented basic validation. The test passed. Also good.

But when we looked at the implementation, Claude realized it was wrong. The path validation was checking after normalization, not before. A classic security vulnerability that would have been easy to miss in code review but impossible to miss when the test made the expectation explicit, or so I’m told?

TDD didn’t just catch a bug — it caught a security hole before it made it into the codebase.

Domain models stay sacred

Here’s where TDD really proved its worth. As we built analyzers for different file types, tests started expecting specific output formats. The CSV analyzer test wanted detailed statistics, the document analyzer expected key findings, the text analyzer needed content summaries.

The easy fix would have been to modify the domain models — add fields here, change structures there, whatever makes the tests pass.

But I’d learned this lesson the hard way before: domain models are sacred. They represent the business logic contract. Tests conform to models, not the other way around.

So instead of changing the AnalysisResult model, we used the metadata field for variable content. Instead of adding type-specific fields, we designed the analyzers to work within the established contract.

The result: a consistent domain model that every analyzer could implement cleanly, without special cases or architectural exceptions.

The factory pattern emerges

Twenty tests in, a pattern started emerging. I was creating analyzers in every test, duplicating setup code, and making assumptions about how they’d be instantiated in production.

TDD forced the question: how should these components actually be created and wired together?

The answer was a “factory pattern” following the existing codebase conventions:

  • Stateless factory with per-call dependency injection
  • Clear registration of analyzer types
  • Easy extension with new file formats
  • Thread-safe concurrent creation

The factory pattern didn’t emerge from architectural planning — it emerged from the pressure of writing clean, maintainable tests.

Security, performance, and error handling

Because we were the writing tests first, we had to address edge cases before implementing happy paths:

  • What happens with corrupted files?
  • How do we handle files that are too large?
  • What about malformed CSV data?
  • How do we validate file types safely?

Each edge case became a test. Each test forced architectural decisions. Should we throw exceptions or return error results? How do we communicate partial failures? What’s the boundary between domain logic and infrastructure concerns?

The tests didn’t just verify behavior — they designed the behavior by making trade-offs explicit.

Of course I mostly just sat and watched this part, but it was educational!

The 34-test milestone

When I hit 34 passing tests, something interesting had happened. I wasn’t just testing file analysis — I’d built a comprehensive security layer, a flexible factory system, proper error handling, and clean domain model compliance.

All driven by the discipline of test-first development.

The architecture wasn’t planned in advance. It emerged from the pressure of writing testable code, handling edge cases explicitly, and maintaining consistency across components.

What TDD really does

TDD isn’t about testing — it’s about design. Writing tests first forces you to think about interfaces before implementations, contracts before code, edge cases before optimizations.

Tests reveal design problems early. Bad interfaces are hard to test. Complex dependencies make setup painful. Unclear contracts lead to confusing assertions.

Tests force explicit decisions. Should this throw an exception or return an error? How should components communicate? What are the actual requirements vs. nice-to-have features?

Tests prevent architectural drift. When every behavior is specified in a test, shortcuts become obvious and uncomfortable. The test suite becomes a guardian of consistency.

Another friggin’ compound effect

Here’s the really interesting part: the 34 tests didn’t just validate 34 behaviors. They created a foundation that made subsequent development faster and more reliable.

When integration time came, the file analysis components dropped in cleanly. When error scenarios arose, the handling was already built in. When new file types needed support, the factory pattern made extension straightforward.

Good tests don’t just catch bugs — they prevent entire categories of problems from emerging.

A lesson about discipline

The temptation to skip tests is always strongest when you’re building something new, when the pressure to ship is high, when “we’ll add tests later” seems reasonable.

But TDD is most valuable exactly when it feels most inconvenient. When the architecture is fluid, when requirements are emerging, when shortcuts beckon — that’s when the discipline of test-first development pays the highest dividends.

Not because tests catch bugs, but because tests force good decisions.

← Back to BlogAlso available on Medium