← Back to list

Designing for Change: How SOLID, TDD, and Characterization Tests Helped Us Evolve an Excel + LLM…

I used to think software quality was mostly a style problem. Readable code, decent naming, maybe a few patterns, and we’re done.

Belva Ghani Abhinaya · 2026-05-25 03:54 · 0 claps · 6.3 min read
#solid-principles #tdd #characterization #backend-development
Open on Medium ↗
Wiki topics: LLM · Large Language Models 🌐 · Web Development 👗 · Fashion

Designing for Change: How SOLID, TDD, and Characterization Tests Helped Us Evolve an Excel + LLM System

Software quality is measured by how gracefully a system absorbs change.

Software quality is measured by how gracefully a system absorbs change.

I used to think software quality was mostly a style problem. Readable code, decent naming, maybe a few patterns, and we’re done.

This project forced me to unlearn that quickly.

We were building an Excel + LLM platform that looked simple from the outside: upload document, parse data, generate structured output, handle auth, provide monitoring, and support benchmark analysis. But underneath, this was a high-change environment. Requirements moved between sprints, data shapes were inconsistent, and “just one more edge case” kept appearing in places we thought were stable.

At some point, we had to make a decision: keep stacking fixes and hope stability emerges naturally, or deliberately redesign the codebase to absorb change. We chose the second path.

This is not a story about a heroic rewrite. It’s a story about engineering discipline under changing constraints.

When the failing test reproduces the exact production edge case.

When the failing test reproduces the exact production edge case.

The Real Problem Wasn’t Bugs, It Was Change Volatility

Early in development, we could still move fast with straightforward implementations. Feature velocity looked good. But as the system grew, we started seeing a pattern: each new requirement was harder than the previous one, not because the logic was inherently complex, but because dependencies were becoming tangled.

A change in parsing behavior affected output formatting. An auth adjustment unexpectedly touched endpoint behavior and test fixtures. A monitoring improvement forced edits in multiple layers that should have been independent.

That’s when we reframed the problem technically:

  • We didn’t just have defect risk.
  • We had change amplification.
  • A small business requirement triggered disproportionately large code edits.

In software architecture terms, this usually means coupling is too high and boundaries are too weak. And when that happens, productivity degrades silently. You still deliver, but with increasing anxiety, longer QA cycles, and lower confidence in refactors.

So the target changed. Instead of “make code cleaner,” we aimed for “reduce blast radius per change.”

That subtle shift ended up driving almost every strong decision afterward.

TDD Became a Risk-Control Strategy, Not a Ritual

Behavior first, implementation second: tests turned uncertainty into constraints.

Behavior first, implementation second: tests turned uncertainty into constraints.

A concrete example came from Excel parsing behavior. We had to handle different file realities: .xlsx, .xls, and file-like stream inputs. If we had solved this by patching conditionals directly in parsing logic, we probably would have shipped quickly, and carried hidden regressions into future updates.

Instead, we used a strict RED-GREEN-REFACTOR progression.

First, we wrote failing tests for cases that were likely to break in production-like usage: file-like object handling, row normalization edge behavior, and format-specific path differences. This gave us executable behavior constraints before touching internals.

Then we implemented the minimum logic to satisfy those tests.

Only after behavior was protected did we refactor and unify row-processing paths, removing duplicated transformation logic and reducing accidental complexity between .xls and .xlsx flows.

From a technical standpoint, this created leverage in three ways:

  1. Behavioral contracts became explicit Test cases documented expected handling of malformed and non-standard inputs better than comments ever could.
  2. Refactor confidence increased We could aggressively simplify internals because behavior was locked by tests.
  3. Future change cost dropped Shared pathways meant subsequent requirement changes were centralized, not format-fragmented.

This was the moment TDD stopped feeling like ceremony and started feeling like architecture support infrastructure.

SOLID Worked Best at the Boundary Level

Dependency flow after refactor: policies depend on abstractions, not frameworks.

Dependency flow after refactor: policies depend on abstractions, not frameworks.

A lot of teams discuss SOLID at class-level granularity, but our biggest gains came at module boundaries.

As auth and monitoring logic matured, the dangerous pattern was clear: transport concerns (HTTP/controller) and business policy were interwoven. That made it hard to reason about correctness and even harder to test failure semantics without over-mocking framework behavior.

So we introduced stronger separations:

  • Use-case orchestration to host business flow,
  • contracts/ports for dependencies,
  • adapters for framework/infrastructure integration,
  • interfaces for transport-facing logic.

This was especially visible in login flow evolution. Instead of a monolithic endpoint-centric implementation, we moved toward a modular use-case model with explicit dependency ports like user lookup, failure tracking, and token generation.

Why this mattered technically:

  • DIP (Dependency Inversion Principle): use cases depend on abstractions, enabling infra swaps without rewriting business logic.
  • ISP (Interface Segregation Principle): narrow role-specific interfaces reduced leakage and brittle mocks.
  • SRP (Single Responsibility Principle): each layer had a clearer reason to change, which improved maintainability and test precision.

The immediate result wasn’t “beautiful code screenshots.” The result was predictable modification behavior. A policy change in rate limiting no longer forced collateral edits across unrelated endpoint logic.

That’s the kind of maintainability you feel in sprint planning, not just in code review aesthetics.

Utility Functions Were Treated as Contract Surfaces

Small utility code, high product impact: deterministic naming prevents silent UX degradation.

Small utility code, high product impact: deterministic naming prevents silent UX degradation.

One underappreciated lesson: tiny utility code can carry major product risk.

Download filename generation is a good example. It looks minor until you encounter mixed payload schemas, unsafe path-like names, control characters, extension mismatch, and fallback consistency requirements. If this layer is sloppy, user-facing behavior degrades quickly.

We chose to treat filename logic as a contract surface:

  • strict object-shape guards,
  • normalization of path-like inputs into safe basenames,
  • explicit extension handling aligned with artifact type,
  • tests for invalid/blank/malformed scenarios.

This is classic defensive programming, but with a product reliability mindset. The goal was not “handle happy path elegantly.” The goal was deterministic behavior under unpredictable inputs.

And this matters because utility instability tends to create high-friction bugs: hard to prioritize, embarrassing in UX, and repetitive across sessions. A few precise tests in the right utility often save more support time than large architectural debates.

In the Benchmark Repo, Characterization Tests Prevented Refactor Drift

Characterization tests preserved behavior while enabling aggressive structural cleanup.

Characterization tests preserved behavior while enabling aggressive structural cleanup.

In the llm benchmarking repository, we faced a different challenge: orchestration-heavy code that worked, but was difficult to evolve. This is exactly where teams are tempted into risky rewrites.

Instead, we took a characterization-first approach.

Before extracting modules (ingest pipeline, path guard, report pipeline), we locked observable behavior with tests. Only then did we modularize. This sequence matters because refactoring without characterization tends to produce “semantic drift” — code looks cleaner, output subtly changes, confidence drops.

With characterization tests in place, extraction became a constrained transformation:

  • behavior parity remained non-negotiable,
  • module boundaries tightened gradually,
  • refactor quality was measured by preserved outputs, not by subjective readability alone.

Technically, this is one of the highest-ROI patterns for legacy-adjacent refactors. You turn unknown behavior into known contracts, then improve internals without product surprises.

If I had to defend one “extra” engineering practice academically, this would be it. It shows maturity: we optimized for correctness continuity, not refactor vanity.

Why We Chose Certain Best Practices Over Others

Big rewrites without characterization tests usually start here.

Big rewrites without characterization tests usually start here.

Best practice choices are context decisions, not moral decisions. We compared multiple implementation styles repeatedly.

We could have done a big-bang rewrite. We didn’t, because requirement volatility was still high and delivery continuity mattered. Incremental extraction with test-backed parity gave better risk-adjusted velocity.

We could have kept logic framework-centric for speed. We didn’t, because auth and monitoring were already high-change domains, and framework-coupled policy logic would accumulate maintenance cost faster than feature value.

We could have patched utility bugs ad hoc. We didn’t, because user-facing output reliability requires deterministic fallback behavior, and deterministic behavior requires explicit contracts plus edge-case tests.

So the architecture wasn’t “over-engineered.” It was targeted engineering, applied where volatility and blast radius justified the cost.

That’s a key distinction worth making in front of expert reviewers.

What This Project Changed in My Engineering Philosophy

Before this project, I associated technical quality with local code cleanliness. After this project, I associate quality with system adaptability.

The important question is no longer: “Is this implementation elegant today?”

It is: “When requirements change next week, does this commit reduce or increase adaptation cost?”

That mindset changed how we wrote tests, scoped refactors, designed module seams, and reviewed pull requests. “Done” became more rigorous: not only passing now, but also lowering future change friction.

In practical terms, this is what made the project sustainable under pressure. Not perfect architecture. Not perfect specs. Just consistent engineering choices that made change cheaper over time.

And for real software teams, that is usually the difference between surviving growth and drowning in maintenances.

And for real software teams, that is usually the difference between surviving growth and drowning in maintenance.

Parity validated, boundaries clean, CI green. Now we ship.

Parity validated, boundaries clean, CI green. Now we ship.


메타데이터
post_id
331f1a85d631
slug
designing-for-change-how-solid-tdd-and-characterization-tests-helped-us-evolve-an-excel-llm-331f1a85d631
url
https://medium.com/@belvaghaniabhinaya2020/designing-for-change-how-solid-tdd-and-characterization-tests-helped-us-evolve-an-excel-llm-331f1a85d631
canonical_url
https://medium.com/@belvaghaniabhinaya2020/designing-for-change-how-solid-tdd-and-characterization-tests-helped-us-evolve-an-excel-llm-331f1a85d631
author_url
https://medium.com/@belvaghaniabhinaya2020
status
ok
fetched_at
2026-06-09 15:37:30