Back to blog
EngineeringDate unavailable· min read

The Service Layer: When the Audit Finds 25 Things

Three TDD audit rounds found 25 issues in our ListService before it hit production. Here's what the audit caught, why it matters, and what it means for infrastructure-first development.

We just shipped the third and final piece of the Unified Lists feature: a 352-line service layer that provides full CRUD operations for lists. But before it merged, three rounds of TDD audits found 25 distinct issues—security gaps, type-safety regressions, logic errors, and missing test coverage.

This is PR #288, the final layer in a three-part build: database migration (PR #286) → Pydantic models (PR #287) → service layer (PR #288). Each layer had its own TDD audit. Each audit caught things that would have broken in production.

What Shipped

ListService is a Python class that wraps Supabase CRUD operations for the lists table. It provides five methods:

  • create_list: Insert a new list with validation
  • get_list: Fetch a single list by ID
  • get_lists: Fetch all lists for a user
  • update_list: Partial update with validation
  • archive_list: Soft-delete via is_archived flag

Every query enforces user_id isolation. If you try to fetch or update a list you don't own, you get a 404—not a 403—to avoid leaking list existence. Custom exceptions (ListNotFoundError, ListValidationError, ListItemNotFoundError, ListPromotionError) inherit from the SABINEError hierarchy for consistent API error responses.

The service ships with 877 lines of tests: 313 lines of functional tests (test_list_service.py) covering 14 test cases, and 564 lines of TDD audit tests (test_list_service_audit.py) documenting every finding from three audit rounds.

Three Audit Rounds, 25 Findings

The TDD audit process ran three times on this PR. Each round found issues that would have caused production failures:

Round 1 (9 findings): The migration lost RLS and FK constraints during initial scaffold. ListUpdate allowed empty updates that would send a no-op to Supabase and return a false 404. Ambiguous responses weren't enforcing the ≥2 matches invariant. ValidationInfo type annotations degraded to object.

Round 2 (9 findings): Test coverage gaps (archive_list wrong-user scenario, DatabaseError propagation, create_list empty response). Unused helper functions (_chain) cluttering test files. Lazy imports in create_list that weren't actually lazy.

Round 3 (7 findings): Cross-file consistency issues—models from PR #287 had added a model_validator to ListUpdate that made the original audit tests redundant. Tests needed updating to match the new validation layer.

Every one of these would have broken in production. The missing RLS would have allowed cross-user data leaks. The empty update bug would have returned misleading 404s. The type-safety regressions would have bypassed mypy checks.

Why It Matters

This is the third consecutive PR where TDD audits caught critical issues before merge. The pattern is consistent:

  1. PR #286 (migration): 4 findings (missing RLS, type mismatches, FK constraints, trigger scope collision)
  2. PR #287 (models): 5 findings (validation bypass, type-safety gaps, missing constraints)
  3. PR #288 (service): 25 findings across three rounds

That's 34 production-breaking bugs caught before any of this code shipped. The audit isn't finding style issues or edge cases—it's finding security gaps, type-safety regressions, and logic errors that would have caused data leaks, false errors, or runtime crashes.

The audit tests themselves become institutional memory. When someone touches ListService in six months, the 564-line test_list_service_audit.py file documents every mistake we made and every invariant we learned. That's not dead weight—it's load-bearing context.

What's Next

The Unified Lists feature now has three shipped layers: migration, models, service. What's missing:

  • API routes: FastAPI endpoints that call ListService
  • List items service: CRUD for items within lists
  • Reminder promotion: Convert list items → reminders
  • Frontend UI: React components for Sabine's dashboard

Each layer will go through the same TDD audit process. We'll scaffold, audit, fix, audit again, then merge. If the pattern holds, we'll catch another 20-30 issues before any of this reaches production.

The infrastructure-first approach means we're building in sequence: data layer, validation layer, service layer, API layer, UI layer. It's slower than shipping an MVP in a weekend. But when autonomous agents are writing the code, you optimize for correctness, not speed.

ListService is done. The audit caught 25 things. None of them made it to production. That's the process working.