Skip to main content
Welcome. This site supports keyboard navigation and screen readers. Press ? at any time for keyboard shortcuts. Press [ to focus the sidebar, ] to focus the content. High-contrast themes are available via the toolbar.
serard@dev00:~/cv

Where Requirements Meet DDD: Bridging Feature Tracking, Clean Architecture, and Use Cases

A Feature is a Bounded Context. An Acceptance Criterion is a Use Case. The @Implements decorator is a Port. The test is the Adapter. The compiler is the invariant enforcer.

The Gap Between Requirements and Architecture

Most projects have a requirements system (Jira, user stories, acceptance criteria) and an architecture (DDD, hexagonal, clean architecture). They exist in separate worlds. Requirements are written in natural language by product people. Architecture is written in code by developers. The mapping between them is informal — a mental model, a wiki page, a conversation.

The TypeScript requirements tracking system closes this gap. But the mapping to established architecture patterns is not obvious at first glance. This post makes it explicit.

The Mapping

Requirements Concept DDD Equivalent Clean/Hexagonal Equivalent In the TS System
Feature Bounded Context Module / Component abstract class Feature
Acceptance Criterion Domain Invariant / Use Case Application Service / Port abstract method(): ACResult
Implementation Domain Service / Aggregate Use Case Interactor The production code
Test Specification Adapter (test adapter) @Implements<Feature>('ac')
Compliance Scanner Anti-Corruption Layer audit Dependency Rule verifier compliance-report.ts
@FeatureTest Context Map link Port declaration Class decorator
@Implements Satisfies invariant Implements port Method decorator
@Exclude Infrastructure concern Framework adapter detail Helper decorator

Features as Bounded Contexts

In DDD, a Bounded Context is a boundary within which a particular model is defined and applicable. The NavigationFeature is exactly that — it defines the boundary of what "navigation" means in this system, and its acceptance criteria define the invariants that must hold within that boundary.

// This IS a bounded context definition
export abstract class NavigationFeature extends Feature {
  readonly id = 'NAV';
  readonly title = 'SPA Navigation + Deep Links';

  abstract tocClickLoadsPage(): ACResult;       // invariant: clicking loads
  abstract backButtonRestores(): ACResult;       // invariant: back works
  abstract activeItemHighlights(): ACResult;     // invariant: state is visible
  abstract deepLinkLoads(): ACResult;            // invariant: URLs are stable
}

Each abstract method is a domain invariant — a rule that must always be true within this context. In traditional DDD, these would be assertions inside an Aggregate Root. Here, they're acceptance criteria that the test suite verifies.

The parallel is exact:

DDD Aggregate:
  class OrderAggregate {
    invariant: order.lines.length > 0       // must always be true
    invariant: order.total >= 0              // must always be true
  }

Requirements Feature:
  abstract class NavigationFeature {
    abstract tocClickLoadsPage(): ACResult;  // must always be true
    abstract backButtonRestores(): ACResult; // must always be true
  }

The difference: DDD invariants are checked at runtime (domain logic). Requirements ACs are checked at test time (test suite). The shape is the same — a class that declares what must be true.

Acceptance Criteria as Use Cases

In Clean Architecture, a Use Case (or Interactor) encapsulates a single business operation. It takes input, applies business rules, and produces output. Robert C. Martin's structure:

Use Case: Place Order
  Input: customer ID, items, payment method
  Business Rules: validate stock, calculate total, check credit
  Output: order confirmation or failure reason

An acceptance criterion follows the same structure:

AC: tocClickLoadsPage
  Input: user clicks a TOC item (implicit: current page state)
  Business Rules: fetch content, swap DOM, update URL, highlight item
  Output: ACResult { satisfied: true } or { satisfied: false, reason: "..." }

The ACResult type enforces this: every criterion produces a boolean outcome with an optional failure reason. This is the Use Case output boundary — the same Result<T> pattern used in the Result Pattern for domain operations.

The Hexagonal Architecture View

Hexagonal Architecture (Ports & Adapters) separates the application into:

  • Core (domain logic, use cases) — no framework dependencies
  • Ports (interfaces the core exposes or requires) — contracts
  • Adapters (implementations of ports) — framework-specific

The requirements system maps cleanly:

┌──────────────────────────────────────────────────┐
│                    CORE                          │
│                                                  │
│  requirements/features/navigation.ts             │
│    abstract tocClickLoadsPage(): ACResult  ←── PORT (what must be true)
│    abstract backButtonRestores(): ACResult ←── PORT (what must be true)
│                                                  │
│  requirements/base.ts                            │
│    abstract class Feature               ←── DOMAIN ENTITY
│    interface ACResult                   ←── VALUE OBJECT
│                                                  │
└───────────────────────┬──────────────────────────┘
                        │
                        │ @Implements<Feature>('ac')
                        │ (the adapter implements the port)
                        │
┌───────────────────────▼───────────────────────────┐
│                  ADAPTERS                         │
│                                                   │
│  test/e2e/navigation.spec.ts                      │
│    @Implements<NavigationFeature>('tocClickLoads')│
│    → Playwright test (framework-specific)         │
│                                                   │
│  test/unit/slugify.test.ts                        │
│    @Implements<SlugifyFeature>('neverThrows')     │
│    → Vitest test (framework-specific)             │
│                                                   │
│  js/app-static.js                                 │
│    → Production code (the actual implementation)  │
│                                                   │
└───────────────────────────────────────────────────┘

The feature definition is the core — pure, no framework dependency, just abstract declarations of what must be true. The tests are adapters — they use Playwright, Vitest, axe-core (framework-specific tools) to verify the abstract criteria. The @Implements decorator is the port binding — it declares which adapter satisfies which port.

This is why the feature files have zero imports from Playwright, Vitest, or any test framework. They only import Feature, Priority, and ACResult from the base module. The core depends on nothing.

The Dependency Rule

Clean Architecture's Dependency Rule: source code dependencies must point inward — toward higher-level policies. Concretely:

requirements/base.ts         ← depends on nothing
requirements/features/*.ts   ← depends on base.ts only
requirements/decorators.ts   ← depends on base.ts only
test/**/*.spec.ts            ← depends on features + decorators + test framework
scripts/compliance-report.ts ← depends on features (reads files)

The arrows point inward:

  • Tests depend on features (not the reverse)
  • Features depend on base types (not on tests)
  • The scanner depends on features (not on tests or production code)

No feature file imports from test/. No feature file imports from js/ or src/. The domain (requirements) is isolated from the infrastructure (test runners, build tools, browser APIs).

Use Case Driven Development

In Use Case Driven Development (UCDD), you start with use cases, derive the domain model from them, then implement. The requirements system inverts the typical flow:

Traditional:
  Write code → Write tests → Hope tests cover the requirements

UCDD with Requirements as Code:
  Define Feature (bounded context) → Define ACs (use cases)
  → Write @Implements tests → Compiler verifies the chain
  → Scanner reports coverage → Quality gate blocks gaps

The Feature class IS the use case specification. The @Implements decorator IS the "this code satisfies this use case" declaration. The compliance scanner IS the traceability audit.

SOLID Applied to Requirements

Single Responsibility

Each feature class has one reason to change: when the requirements for that feature change. NavigationFeature doesn't change because search changes. SlugifyFeature doesn't change because the hire modal changes.

Open/Closed

New features are added by creating new classes — never by modifying existing ones. Adding AccentPaletteFeature doesn't touch ThemeSwitchingFeature. The system is open for extension (new features), closed for modification (existing features don't change when new ones are added).

Liskov Substitution

Every feature extends the same Feature base class. The compliance scanner processes them uniformly — it doesn't know or care whether a feature is hand-written or generated from Jira. Any Feature subclass works in any context that expects a Feature.

Interface Segregation

Each feature exposes only its own ACs. A test file that @Implements<NavigationFeature>('tocClickLoadsPage') doesn't see SlugifyFeature's methods. The keyof T constraint ensures you can only reference ACs that belong to the feature you declared.

Dependency Inversion

High-level policy (feature definitions) doesn't depend on low-level detail (test frameworks, browser APIs). The @Implements decorator inverts the dependency: the test declares that it satisfies a feature requirement, not the other way around. The feature doesn't know which tests exist.

The Ubiquitous Language

DDD insists that code speaks the same language as the business. The requirements system enforces this at the method name level:

// The AC method name IS the ubiquitous language term
abstract tocClickLoadsPage(): ACResult;
abstract backButtonRestores(): ACResult;
abstract scrollDownUpdatesHeading(): ACResult;

When a developer writes @Implements<NavigationFeature>('tocClickLoadsPage'), they're using the exact same term as the feature definition. No translation layer. No mapping table. The same word, checked by the compiler.

If someone renames the AC in the feature definition — because the ubiquitous language evolved — every @Implements reference breaks. The IDE shows the error. The developer updates the test. The language stays consistent.

The Anti-Corruption Layer

In DDD, an Anti-Corruption Layer (ACL) prevents one bounded context's model from leaking into another. The requirements system has its own ACL: the compliance scanner.

The scanner sits between the requirements world (feature definitions) and the implementation world (test files). It translates between them:

  • Features speak in "abstract methods" — tocClickLoadsPage
  • Tests speak in "Playwright assertions" — expect(page.locator(...)).toContainText(...)
  • The scanner translates: "does the test world satisfy the requirements world?"

If a feature has 8 ACs and only 6 tests link to it, the scanner reports the gap. It doesn't try to fix it — it surfaces the mismatch between the two worlds, like an ACL surfacing translation failures between bounded contexts.

What This Means in Practice

When you look at a feature file, you're looking at a bounded context specification — what must be true, expressed as abstract methods. When you look at a test file with @Implements decorators, you're looking at adapter implementations — proof that the specifications are satisfied, using framework-specific tools.

The compiler enforces the port binding (test references a real AC). The scanner enforces completeness (every AC has a test). The self-tracking feature (REQ-TRACK) enforces the infrastructure itself.

This is DDD, Clean Architecture, and Use Case Driven Development — applied not to the application domain, but to the requirements domain. The requirements ARE the domain model. The tests ARE the implementation. The compiler IS the invariant enforcer.