TDD Does Not Mean Writing Tests for Everything. Here Is What It Actually Means.

by Eric Hanson, Backend Developer at Clean Systems Consulting

The Straw Man Version

The version of TDD that most developers reject is this: before you write any code, you must write a test. Every class, every method, every configuration file must be test-driven. It is a strict process with no exceptions, and deviation from it means you are not doing TDD.

This version is a straw man. It is not what the practitioners who developed TDD actually advocate, and it is not what experienced TDD practitioners actually do. But it is what gets described in dismissive discussions, and it is the thing developers try once and reject.

The actual practice is more purposeful: apply TDD where it helps, skip it where it does not.

Where TDD Provides High Leverage

TDD is most valuable for code where the design of the interface is non-obvious, where the logic has multiple branches and edge cases, or where the behavior needs to be precisely specified before implementation begins.

Complex business logic. Discount calculations, eligibility rules, pricing tiers, state machines, workflow transitions — anywhere that has conditional logic and domain-specific rules. Writing the tests first forces you to enumerate the cases explicitly, which surfaces edge cases before they become production bugs.

Public APIs of core domain objects. The interface that other parts of the system depend on is worth designing through tests. The test is a first caller — the most direct way to experience whether the API is natural to use.

Code you are going to maintain for a long time. TDD produces well-specified, well-isolated code with good coverage. The cost is worth paying for code that will be read, modified, and extended repeatedly.

Where TDD Adds Less Value

Exploratory/spike code. When you genuinely do not know what the right approach is yet, writing tests before code is premature specification. Write the spike without tests to understand the problem space, then throw it away and rebuild test-first. The spike is research; the rebuild is engineering.

Trivial data transfer objects and configuration. A class that is three getters and a constructor does not benefit from TDD. The design is obvious, there is no logic to specify, and there is no edge case to discover.

Glue code and framework wiring. Code that connects your application to a framework — Spring configuration, dependency injection setup, route registration — is largely shaped by the framework's conventions. TDD does not add design leverage here because the design space is constrained by the framework.

Infrastructure and I/O code. Database repositories, HTTP clients, file readers — code whose primary behavior is interacting with external systems — is better covered by integration tests. TDD can still be applied, but the feedback loop is slower (you need a database running), and the design leverage is lower (the interface is often prescribed by the external system).

The Practical Application

A realistic TDD session on a feature looks like this:

  1. Identify the core logic that drives the feature — the part with the business rules and conditional behavior.
  2. Start TDD on that logic. Write the first test that specifies the simplest behavior. Make it pass. Write the next test for the next case. Refactor as needed.
  3. For the infrastructure code that the logic depends on (database access, external APIs), define the interface through the tests (by mocking it) and implement it separately — potentially without strict TDD if the interface is prescribed by the external system.
  4. Skip tests entirely for configuration, boilerplate, and one-liners that have no interesting logic.
# Apply TDD here: complex rules with multiple cases
class SubscriptionEligibilityChecker:
    def is_eligible_for_upgrade(self, user: User, plan: Plan) -> EligibilityResult:
        # Business logic with 6 conditions — worth TDD'ing
        ...

# Skip TDD here: trivial data class, no logic
@dataclass
class EligibilityResult:
    eligible: bool
    reason: str | None = None

The discipline is not "test before all code." It is "test before code where the test adds design or specification value." Applying that judgment consistently — rather than either ignoring TDD entirely or applying it dogmatically — is what experienced practitioners actually do.

TDD is a tool. Like any tool, its value depends on whether you are using it in the right situation. The question is not "am I doing TDD?" but "is writing this test before this code going to improve the design or specification of this behavior?" When the answer is yes, write the test first. When the answer is no, do not.

Scale Your Backend - Need an Experienced Backend Developer?

We provide backend engineers who join your team as contractors to help build, improve, and scale your backend systems.

We focus on clean backend design, clear documentation, and systems that remain reliable as products grow. Our goal is to strengthen your team and deliver backend systems that are easy to operate and maintain.

We work from our own development environments and support teams across US, EU, and APAC timezones. Our workflow emphasizes documentation and asynchronous collaboration to keep development efficient and focused.

  • Production Backend Experience. Experience building and maintaining backend systems, APIs, and databases used in production.
  • Scalable Architecture. Design backend systems that stay reliable as your product and traffic grow.
  • Contractor Friendly. Flexible engagement for short projects, long-term support, or extra help during releases.
  • Focus on Backend Reliability. Improve API performance, database stability, and overall backend reliability.
  • Documentation-Driven Development. Development guided by clear documentation so teams stay aligned and work efficiently.
  • Domain-Driven Design. Design backend systems around real business processes and product needs.

Tell us about your project

Our offices

  • Copenhagen
    1 Carlsberg Gate
    1260, København, Denmark
  • Magelang
    12 Jalan Bligo
    56485, Magelang, Indonesia

More articles

You're Probably Overcomplicating Your Spring Boot Tests

Most Spring Boot test suites are slow, brittle, and hard to maintain — not because testing is hard, but because teams default to @SpringBootTest when they should be using slices or plain unit tests. Here's how to fix it.

Read more

Database Indexing in Rails — What I Check Before Every Deploy

Missing indexes are the most common cause of avoidable database performance problems in Rails applications. Here is the pre-deploy checklist I run and the index decisions that actually matter.

Read more

How to Onboard a Remote Backend Contractor So They Deliver From Week One

Contractor onboarding isn't employee onboarding with fewer steps. It's a different process with a different goal — and most startups set it up wrong.

Read more

What Fault Tolerance Actually Means in a Real Backend System

Fault tolerance is not a binary property — it is a spectrum of degraded behaviors that the system can sustain while continuing to function. Defining what that spectrum looks like is the design work.

Read more