The Problem with Requirement Silos
In most software projects, requirements live in Jira. Code lives in a repository. Tests live somewhere else again. When a developer asks "which code implements Feature-456?", the answer requires jumping between systems, reading comments, grepping for magic strings, and hoping someone documented things correctly.
In a wide solution -- or worse, a multi-solution mega mono-repo -- this problem is exponentially worse. A single Feature like "Order Processing" spans OrderService.dll (API server), PaymentGateway.dll (separate service on another machine), NotificationWorker.dll (background worker), and OrderTests.dll (verification). A developer touching one of these has no way to know which Feature it belongs to, which acceptance criteria are validated, which tests cover them, or what the Product Owner actually specified. The requirement silo isn't just an inconvenience -- it's a structural gap that makes large-scale development fragile.
Common "solutions" make it worse:
// String-based approach: looks safe, is not
[Feature(Id = "FEATURE-456", AcceptanceCriteria = new[] { "Admin can assign roles" })]
public class UserRoleFeature { } // Empty marker class — dead code
[Implements("FEATURE-456")] // String. Typo? Compiles fine.
public void AssignRole() { }
[FeatureTest("FEATURE-456", 0)] // Index 0? What if someone reorders the array?
public void AdminCanAssign() { }// String-based approach: looks safe, is not
[Feature(Id = "FEATURE-456", AcceptanceCriteria = new[] { "Admin can assign roles" })]
public class UserRoleFeature { } // Empty marker class — dead code
[Implements("FEATURE-456")] // String. Typo? Compiles fine.
public void AssignRole() { }
[FeatureTest("FEATURE-456", 0)] // Index 0? What if someone reorders the array?
public void AdminCanAssign() { }The problems are structural:
- Requirements are metadata on marker classes -- not real types with behavior
- Links are string-based --
"FEATURE-456"is not checked by the compiler. Typos, renames, and deletions are silent - Acceptance criteria are string arrays --
"Admin can assign roles"says nothing the compiler can verify - Tests reference requirements by index --
AC[0]breaks when someone reorders the acceptance criteria list - No compile-time guarantee that an implementation actually satisfies a specification
What if requirements were types? What if acceptance criteria were abstract methods? What if the compiler refused to build until every AC was implemented and tested?
This approach applies the same principle as DDD -- model the problem domain first, let the compiler enforce the model -- and the same meta-metamodeling architecture: requirements become M2 concepts, and specific features become M1 instances that the source generator validates at build time.
The Chain: Six Projects, One Compiler
The design is six C# projects linked by <ProjectReference>. Each boundary is enforced by the C# type system. Each project compiles to a DLL artifact with a clear role:
MyApp.sln
├── src/
│ ├── MyApp.Requirements/ ← Features as types, ACs as abstract methods
│ ├── MyApp.SharedKernel/ ← Domain types (User, Role, Permission)
│ ├── MyApp.Specifications/ ← Interfaces the domain must implement
│ ├── MyApp.Domain/ ← Domain implementation (: ISpec)
│ └── MyApp.Api/ ← Production host (DI, controllers, middleware)
├── test/
│ └── MyApp.Tests/ ← Type-linked verification
└── tools/
└── MyApp.Requirements.Analyzers/ ← Source generator (registry, matrix, diagnostics)MyApp.sln
├── src/
│ ├── MyApp.Requirements/ ← Features as types, ACs as abstract methods
│ ├── MyApp.SharedKernel/ ← Domain types (User, Role, Permission)
│ ├── MyApp.Specifications/ ← Interfaces the domain must implement
│ ├── MyApp.Domain/ ← Domain implementation (: ISpec)
│ └── MyApp.Api/ ← Production host (DI, controllers, middleware)
├── test/
│ └── MyApp.Tests/ ← Type-linked verification
└── tools/
└── MyApp.Requirements.Analyzers/ ← Source generator (registry, matrix, diagnostics)Project reference graph:
Requirements ──┐
├──> Specifications ──> Domain ← Api
SharedKernel ──┘ │ │
└────────────────┴──> TestsRequirements ──┐
├──> Specifications ──> Domain ← Api
SharedKernel ──┘ │ │
└────────────────┴──> Tests| Project | DLL Artifact | Role | Depends On |
|---|---|---|---|
| Requirements | MyApp.Requirements.dll |
Features as types, AC methods, generated attributes | Nothing |
| SharedKernel | MyApp.SharedKernel.dll |
Domain value types (User, Role, Permission, Result) | Nothing |
| Specifications | MyApp.Specifications.dll |
Interfaces per feature, validator bridges | Requirements + SharedKernel |
| Domain | MyApp.Domain.dll |
Implements spec interfaces -- compiler enforces | Specifications |
| Api | MyApp.Api.dll |
Production host: DI, controllers, middleware | Domain |
| Tests | MyApp.Tests.dll |
Verifies implementation via type refs | Domain + Specifications |
Every link between layers uses typeof() or nameof() -- Ctrl+Click navigable in the IDE, rename-safe, compiler-checked.