Test Driven Development Is Not About Tests. It Is About Design.

by Arif Ikhsanudin, Backend Developer

The Misunderstanding That Makes TDD Look Like Overhead

Most developers who reject TDD do so after experiencing it as a slower way to produce the same code with tests attached. Write the test first, then write the code to pass it — compared to write the code, then write the test — the output seems identical except for the order of operations and a few extra minutes.

This framing misses the point entirely. TDD is not a different order for producing the same code. It is a design discipline that uses tests as a medium for working out the design before committing to an implementation.

The tests are evidence that the design process happened. They are not the goal.

What TDD Actually Produces

When you write a test before the implementation, you are forced to use the interface before you have built it. This is the entire point. Using an interface before building it reveals problems that are invisible when you design and implement in the same motion.

Consider designing a notification service. Implementation-first, you might start with the data model, work outward to a service class, and end up with something like:

public class NotificationService {
    public void send(NotificationRequest request) {
        NotificationTemplate template = templateRepository.findById(request.getTemplateId());
        String rendered = templateEngine.render(template, request.getVariables());
        Channel channel = channelFactory.getChannel(request.getChannelType());
        channel.dispatch(rendered, request.getRecipient());
        auditLog.record(request.getUserId(), request.getTemplateId(), LocalDateTime.now());
    }
}

This looks reasonable. But the design decisions were made while building, not before. The caller has to construct a NotificationRequest with a templateId, variables, channelType, recipient, and userId — five pieces of information bundled into a request object — before knowing whether that is a natural grouping.

TDD-first, you write the test first:

@Test
void sendsEmailNotificationWithRenderedTemplate() {
    // What does the caller naturally provide?
    String userId = "user_123";
    String email = "user@example.com";
    Map<String, String> vars = Map.of("name", "Alice", "resetUrl", "https://...");

    notificationService.sendPasswordReset(userId, email, vars);

    verify(emailChannel).send(eq(email), contains("Alice"));
    verify(auditLog).record(eq(userId), any(), any());
}

Writing the test first reveals that sendPasswordReset is a more natural interface than send(NotificationRequest) for this use case. The caller knows they are sending a password reset — they should not have to know about template IDs and channel types. The TDD process surfaced a better abstraction before a line of production code was written.

The Design Benefits That Accumulate

Lower coupling. When you write tests first, you naturally inject dependencies rather than constructing them inside the class. You cannot test a class that creates its own database connection without hitting a real database, so you inject the connection. This discipline, applied consistently, produces codebases where dependencies are explicit and exchangeable.

Smaller, more focused classes. Hard-to-write tests are the first signal that a class is doing too much. When a test requires five mocks and forty lines of setup to test one behavior, TDD practitioners respond by decomposing the class — not by writing the complicated test. The test difficulty constrains the design toward simplicity.

Better naming. Writing the test first requires naming the behavior before implementing it. This forces the kind of precise naming that is easy to skip when you are focused on making something work. The test reads like a specification; the production code names follow from that specification.

Emergent design. In implementation-first development, the design is usually decided before coding starts. In TDD, the design emerges from the tests. Each test shapes the interface slightly. The final interface is the one that was most natural to call across all the tests — which is typically the interface that is most natural to call in production code too.

The Refactor Phase Is Where the Design Lives

The red-green-refactor cycle is often described as: write a failing test, make it pass by any means, then clean up the code. The refactor phase is undersold. It is not cleanup — it is design.

When the test is green, you have verified behavior. The refactor phase is where you ask: is this the right abstraction? Is this method in the right class? Are these concerns properly separated? You can refactor confidently because the test will catch any behavioral regression.

Without the test, this refactoring is risky. With the test, it is safe to restructure significantly. The combination — behavioral verification plus design freedom — is what produces the design quality that TDD advocates point to.

The tests produced along the way are genuinely valuable. A fully TDD'd codebase typically has excellent coverage of the behaviors that matter, because every behavior was specified in a test before it was implemented. But that coverage is the output of a design process, not the goal of one. Treat TDD as a design tool that produces tested code, not as a testing tool that requires writing tests first.

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

How to Keep a “Lessons Learned” Notebook

Ever finish a project and realize you forgot all the small mistakes and smart hacks you discovered? A “lessons learned” notebook can turn fleeting experiences into a goldmine of knowledge for the next project.

Read more

How to Evaluate a Backend Project Before Accepting the Work

“Seems doable… but something feels off.” That hesitation is worth listening to — especially in backend work.

Read more

Virtual Threads in Java — What Changes, What Doesn't, and How to Migrate

Virtual threads are production-ready in Java 21 and change the scalability profile of I/O-bound Java services without requiring reactive programming. Here is the precise model, the traps, and a migration checklist.

Read more

Why Software Projects Often Go Over Budget

Software projects rarely fail because of one big mistake. They go over budget because of many small, predictable ones.

Read more