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

Requirements and Specifications ARE Projects

Industrial Monorepo Series — Part 3 of 7 1. The Problem · 2. Physical vs Logical · 3. Requirements as Projects · 4. At Scale · 5. Migration · 6. ROI · 7. Inverted Deps

A .Requirements project is not documentation. It's a compilation unit. Every feature is a type. Every acceptance criterion is an abstract method. The compiler refuses to build until every AC is specified, implemented, and tested.


Part 1 showed the 50-project monorepo with ServiceProvider god-objects and no logical boundaries. Part 2 dissected why DLLs are packaging, not architecture — and why physical boundaries fail to answer "what business feature does this code implement?"

This post is the answer. It introduces two new projects to the monorepo — .Requirements and .Specifications — that create logical boundaries the compiler enforces. These aren't documentation projects. They aren't metadata projects. They are compilation units whose types propagate through the entire dependency graph and force every team, every service, and every test to declare which business capability they implement.

This is not a refactoring. It's the introduction of a new architectural layer that the existing codebase lacked entirely.


The Two Missing Projects

Every industrial monorepo has dozens of projects: Web, Core, Data, Services, Workers, Tests. None of them answer the question: "What does the business require?"

We add two:

MegaCorp.sln
├── src/
│   ├── MegaCorp.Requirements/NEW: Features as types, ACs as abstract methods
│   ├── MegaCorp.Specifications/NEW: Interfaces per feature, compiler-enforced contracts
│   ├── MegaCorp.SharedKernel/          ← Domain value types shared across all layers
│   ├── MegaCorp.OrderService/          ← : IOrderProcessingSpec (compiler-enforced)
│   ├── MegaCorp.PaymentGateway/        ← : IPaymentIntegrationSpec (compiler-enforced)
│   ├── MegaCorp.InventoryService/      ← : IInventoryIntegrationSpec (compiler-enforced)
│   ├── MegaCorp.NotificationService/   ← : INotificationIntegrationSpec (compiler-enforced)
│   ├── MegaCorp.BillingService/        ← : IBillingSpec (compiler-enforced)
│   ├── MegaCorp.Web/                   ← API host, DI wiring
│   ├── MegaCorp.Worker/                ← Background job host
│   └── ... (remaining projects)
├── test/
│   ├── MegaCorp.OrderService.Tests/    ← [TestsFor(typeof(OrderProcessingFeature))]
│   ├── MegaCorp.PaymentGateway.Tests/  ← [TestsFor(typeof(PaymentProcessingFeature))]
│   └── ...
└── tools/
    └── MegaCorp.Requirements.Analyzers/ ← Roslyn source generator + analyzers

The key insight: every implementation project now references MegaCorp.Specifications (which transitively references MegaCorp.Requirements). This creates a compile-time link from every service to the business features it implements.

Diagram

The dependency flow is unidirectional:

Requirements → Specifications → Implementation → Host
                                              → Tests

No cycles. No reverse references. The Requirements project depends on nothing. The Specifications project depends on Requirements + SharedKernel. Every implementation project depends on Specifications. The host wires everything together. The tests reference both the implementation and the specifications.


Project 1: MegaCorp.Requirements — Features Are Types

This project has zero dependencies. It references no other project, no NuGet package except netstandard2.0 or net8.0. It is the root of the dependency tree — the most independent, most stable, most important project in the solution.

The Base Types

// MegaCorp.Requirements/RequirementMetadata.cs
namespace MegaCorp.Requirements;

/// <summary>
/// Base type for all requirements. Every Feature, Story, Task, and Bug
/// inherits from this. The source generator scans for subclasses.
/// </summary>
public abstract record RequirementMetadata
{
    public abstract string Title { get; }
    public abstract RequirementPriority Priority { get; }
    public abstract string Owner { get; }
}

public enum RequirementPriority { Critical, High, Medium, Low, Backlog }
public enum BugSeverity { Critical, Major, Minor, Cosmetic }
// MegaCorp.Requirements/Hierarchy.cs
namespace MegaCorp.Requirements;

/// <summary>
/// Epic: a strategic goal container. Features belong to Epics.
/// </summary>
public abstract record Epic : RequirementMetadata;

/// <summary>
/// Feature: a user-facing capability. Parented under an Epic.
/// Each abstract method IS an acceptance criterion.
/// </summary>
public abstract record Feature<TParent> : RequirementMetadata
    where TParent : Epic;

/// <summary>
/// Feature: root-level, no parent Epic.
/// </summary>
public abstract record Feature : RequirementMetadata;

/// <summary>
/// Story: an implementable unit of work.
/// </summary>
public abstract record Story<TParent> : RequirementMetadata
    where TParent : RequirementMetadata;

/// <summary>
/// Task: concrete implementation work with an estimate.
/// </summary>
public abstract record Task<TParent> : RequirementMetadata
    where TParent : RequirementMetadata
{
    public abstract int EstimatedHours { get; }
}

/// <summary>
/// Bug: a defect with severity.
/// </summary>
public abstract record Bug : RequirementMetadata
{
    public abstract BugSeverity Severity { get; }
}
// MegaCorp.Requirements/AcceptanceCriterionResult.cs
namespace MegaCorp.Requirements;

/// <summary>
/// The return type for every acceptance criterion method.
/// Every AC must produce a verifiable result — not void, not bool.
/// </summary>
public readonly record struct AcceptanceCriterionResult
{
    public bool IsSatisfied { get; }
    public string? FailureReason { get; }

    private AcceptanceCriterionResult(bool satisfied, string? reason)
    {
        IsSatisfied = satisfied;
        FailureReason = reason;
    }

    public static AcceptanceCriterionResult Satisfied() => new(true, null);
    public static AcceptanceCriterionResult Failed(string reason) => new(false, reason);
    public static implicit operator bool(AcceptanceCriterionResult r) => r.IsSatisfied;
}

Lightweight Domain Concepts

AC method signatures use lightweight value types that represent domain concepts without importing domain implementation details. These live in the Requirements project itself — they are part of the ubiquitous language:

// MegaCorp.Requirements/DomainConcepts.cs
namespace MegaCorp.Requirements;

// These are the words the PM and developers agreed on.
// They appear in AC method signatures.
// They carry no implementation — just shape.

public readonly record struct OrderId(Guid Value);
public readonly record struct CustomerId(Guid Value);
public readonly record struct ProductSku(string Value);
public readonly record struct Money(decimal Amount, string Currency);
public readonly record struct Quantity(int Value);
public readonly record struct ReservationId(Guid Value);
public readonly record struct PaymentId(Guid Value);
public readonly record struct InvoiceId(Guid Value);
public readonly record struct NotificationId(Guid Value);

/// <summary>
/// A lightweight order representation for AC signatures.
/// Not the full domain entity — just enough to express the acceptance criterion.
/// </summary>
public record OrderSummary(
    OrderId Id,
    CustomerId Customer,
    IReadOnlyList<OrderLineSummary> Lines,
    Money Total);

public record OrderLineSummary(
    ProductSku Sku,
    Quantity Quantity,
    Money UnitPrice);

public record StockReservation(
    ReservationId Id,
    ProductSku Sku,
    Quantity Reserved);

public record PaymentResult(
    PaymentId Id,
    bool Success,
    Money Amount,
    string? FailureReason);

public record ShippingAddress(
    string Street,
    string City,
    string PostalCode,
    string Country);

These types are intentionally simple. They're record types with no behavior — just data shapes that express the language of the business domain. They exist so that AC method signatures are precise:

// Instead of: AcceptanceCriterionResult OrderTotalMustBePositive(object order)
// We write:   AcceptanceCriterionResult OrderTotalMustBePositive(OrderSummary order)
//
// The signature documents WHAT the AC needs. The type system verifies it.

The Epics

// MegaCorp.Requirements/Epics/ECommerceEpic.cs
namespace MegaCorp.Requirements.Epics;

public abstract record ECommerceEpic : Epic
{
    public override string Title => "E-Commerce Platform";
    public override RequirementPriority Priority => RequirementPriority.Critical;
    public override string Owner => "platform-team";
}

// MegaCorp.Requirements/Epics/ComplianceEpic.cs
public abstract record ComplianceEpic : Epic
{
    public override string Title => "Regulatory Compliance";
    public override RequirementPriority Priority => RequirementPriority.High;
    public override string Owner => "compliance-team";
}

// MegaCorp.Requirements/Epics/CustomerExperienceEpic.cs
public abstract record CustomerExperienceEpic : Epic
{
    public override string Title => "Customer Experience";
    public override RequirementPriority Priority => RequirementPriority.High;
    public override string Owner => "cx-team";
}

The Features — The Heart of the System

Here is the complete OrderProcessingFeature — the same feature that was untraceable in the ServiceLocator-based monorepo. Now it's a type:

// MegaCorp.Requirements/Features/OrderProcessingFeature.cs
namespace MegaCorp.Requirements.Features;

/// <summary>
/// Feature: Order Processing.
/// Owner: order-team.
/// Parent Epic: E-Commerce Platform.
///
/// This feature spans: OrderService, PaymentGateway, InventoryService,
/// NotificationService, BillingService.
///
/// Each abstract method below IS an acceptance criterion.
/// The method signature defines the inputs. The return type enforces verifiability.
/// Adding a new AC here breaks the build in every project that implements
/// a specification derived from this feature — until the AC is satisfied.
/// </summary>
public abstract record OrderProcessingFeature : Feature<ECommerceEpic>
{
    public override string Title => "Order Processing";
    public override RequirementPriority Priority => RequirementPriority.Critical;
    public override string Owner => "order-team";

    // ─── Acceptance Criteria ───────────────────────────────────────────

    /// <summary>
    /// AC-1: Orders with negative or zero total are rejected before any
    /// payment processing or inventory reservation occurs.
    /// </summary>
    public abstract AcceptanceCriterionResult OrderTotalMustBePositive(
        OrderSummary order);

    /// <summary>
    /// AC-2: Inventory is checked and reserved before payment is attempted.
    /// If any line item is out of stock, the order is rejected without
    /// touching the payment system.
    /// </summary>
    public abstract AcceptanceCriterionResult InventoryReservedBeforePayment(
        OrderSummary order,
        IReadOnlyList<StockReservation> reservations);

    /// <summary>
    /// AC-3: Payment is captured only after successful inventory reservation.
    /// The payment amount must exactly match the order total.
    /// </summary>
    public abstract AcceptanceCriterionResult PaymentCapturedAfterReservation(
        OrderSummary order,
        IReadOnlyList<StockReservation> reservations,
        PaymentResult payment);

    /// <summary>
    /// AC-4: An order confirmation notification (email) is sent to the customer
    /// after successful payment capture. The notification includes the order ID,
    /// total amount, and estimated delivery date.
    /// </summary>
    public abstract AcceptanceCriterionResult ConfirmationSentAfterPayment(
        OrderSummary order,
        PaymentResult payment,
        NotificationId notification);

    /// <summary>
    /// AC-5: All order operations (creation, payment, reservation, notification)
    /// are recorded in the audit log with timestamps and actor information.
    /// </summary>
    public abstract AcceptanceCriterionResult AllOperationsAudited(
        OrderSummary order,
        IReadOnlyList<AuditEntry> auditEntries);

    /// <summary>
    /// AC-6: If payment capture fails after inventory reservation, the reservation
    /// is released within 30 seconds. No orphaned reservations.
    /// </summary>
    public abstract AcceptanceCriterionResult FailedPaymentReleasesInventory(
        OrderSummary order,
        PaymentResult failedPayment,
        IReadOnlyList<StockReservation> releasedReservations);

    /// <summary>
    /// AC-7: Bulk orders (10+ line items) receive a volume discount.
    /// The discount percentage is configurable per product category.
    /// </summary>
    public abstract AcceptanceCriterionResult BulkOrderDiscountApplied(
        OrderSummary order,
        Money originalTotal,
        Money discountedTotal);

    /// <summary>
    /// AC-8: Orders are persisted with full line-item detail, payment reference,
    /// and reservation references. The order can be reconstructed from the
    /// persisted state alone (no external lookups required).
    /// </summary>
    public abstract AcceptanceCriterionResult OrderPersistedWithFullDetail(
        OrderSummary order,
        PaymentResult payment,
        IReadOnlyList<StockReservation> reservations);
}

/// <summary>
/// Lightweight audit entry for AC-5 signature.
/// </summary>
public record AuditEntry(
    string Operation,
    DateTimeOffset Timestamp,
    string ActorId,
    string Details);

Eight acceptance criteria. Each one has:

  1. A name (OrderTotalMustBePositive) — compiler-checked via nameof()
  2. A typed signature (OrderSummary order) — documents exactly what inputs the AC needs
  3. A return type (AcceptanceCriterionResult) — enforces that every AC produces a verifiable result
  4. A doc comment — human-readable description of the criterion

The class is abstract. You cannot instantiate it directly. Its sole purpose is to declare the shape of the requirement — what must be true for "Order Processing" to be considered complete.

More Features

The monorepo has more than one feature. Here are the others that interact with Order Processing:

// MegaCorp.Requirements/Features/PaymentProcessingFeature.cs
namespace MegaCorp.Requirements.Features;

public abstract record PaymentProcessingFeature : Feature<ECommerceEpic>
{
    public override string Title => "Payment Processing";
    public override RequirementPriority Priority => RequirementPriority.Critical;
    public override string Owner => "payment-team";

    /// AC-1: Payment amount must exactly match the order total.
    public abstract AcceptanceCriterionResult PaymentAmountMatchesOrderTotal(
        OrderSummary order, PaymentResult payment);

    /// AC-2: Refund amount cannot exceed the original payment amount.
    public abstract AcceptanceCriterionResult RefundDoesNotExceedOriginal(
        PaymentResult originalPayment, Money refundAmount);

    /// AC-3: Payment failure returns a structured error with reason and suggested action.
    public abstract AcceptanceCriterionResult PaymentFailureIsStructured(
        PaymentResult failedPayment);

    /// AC-4: All payment operations are idempotent — retrying a payment with
    /// the same idempotency key does not create a duplicate charge.
    public abstract AcceptanceCriterionResult PaymentIsIdempotent(
        OrderId orderId, PaymentId firstAttempt, PaymentId retryAttempt);

    /// AC-5: PCI-DSS compliance — no raw card numbers stored or logged.
    public abstract AcceptanceCriterionResult NoPciDataStored(
        IReadOnlyList<AuditEntry> auditEntries,
        IReadOnlyList<string> logLines);
}
// MegaCorp.Requirements/Features/InventoryManagementFeature.cs
namespace MegaCorp.Requirements.Features;

public abstract record InventoryManagementFeature : Feature<ECommerceEpic>
{
    public override string Title => "Inventory Management";
    public override RequirementPriority Priority => RequirementPriority.High;
    public override string Owner => "inventory-team";

    /// AC-1: Stock levels are decremented atomically when reserved.
    public abstract AcceptanceCriterionResult StockDecrementedAtomically(
        ProductSku sku, Quantity before, Quantity after, Quantity reserved);

    /// AC-2: Concurrent reservation requests for the same SKU are serialized.
    public abstract AcceptanceCriterionResult ConcurrentReservationsSerialized(
        ProductSku sku,
        IReadOnlyList<StockReservation> concurrentRequests,
        Quantity totalStock);

    /// AC-3: Released reservations restore stock within 30 seconds.
    public abstract AcceptanceCriterionResult ReleasedReservationsRestoreStock(
        StockReservation reservation,
        Quantity stockBefore,
        Quantity stockAfter,
        TimeSpan elapsed);

    /// AC-4: Out-of-stock items return a structured response with
    /// restock estimate and alternative SKUs.
    public abstract AcceptanceCriterionResult OutOfStockReturnsAlternatives(
        ProductSku requestedSku,
        IReadOnlyList<ProductSku> alternatives,
        DateTimeOffset? restockEstimate);
}
// MegaCorp.Requirements/Features/CustomerNotificationFeature.cs
namespace MegaCorp.Requirements.Features;

public abstract record CustomerNotificationFeature : Feature<CustomerExperienceEpic>
{
    public override string Title => "Customer Notifications";
    public override RequirementPriority Priority => RequirementPriority.Medium;
    public override string Owner => "cx-team";

    /// AC-1: Order confirmation email sent within 5 seconds of payment capture.
    public abstract AcceptanceCriterionResult ConfirmationEmailTimely(
        OrderSummary order, PaymentResult payment, TimeSpan elapsed);

    /// AC-2: Notification includes order ID, total, line items, and estimated delivery.
    public abstract AcceptanceCriterionResult ConfirmationContainsOrderDetails(
        OrderSummary order, string emailBody);

    /// AC-3: Failed notification delivery is retried 3 times with exponential backoff.
    public abstract AcceptanceCriterionResult FailedNotificationRetried(
        NotificationId notification, int attemptCount, IReadOnlyList<TimeSpan> retryIntervals);

    /// AC-4: Customer can opt out of non-essential notifications.
    public abstract AcceptanceCriterionResult OptOutRespected(
        CustomerId customer, bool optedOut, bool notificationSent);
}
// MegaCorp.Requirements/Features/BillingFeature.cs
namespace MegaCorp.Requirements.Features;

public abstract record BillingFeature : Feature<ECommerceEpic>
{
    public override string Title => "Billing and Invoicing";
    public override RequirementPriority Priority => RequirementPriority.High;
    public override string Owner => "billing-team";

    /// AC-1: Invoice generated automatically after successful order.
    public abstract AcceptanceCriterionResult InvoiceGeneratedAfterOrder(
        OrderSummary order, InvoiceId invoice);

    /// AC-2: Invoice total matches order total including tax.
    public abstract AcceptanceCriterionResult InvoiceTotalMatchesOrder(
        OrderSummary order, Money invoiceTotal, Money taxAmount);

    /// AC-3: Credit note generated on refund, referencing original invoice.
    public abstract AcceptanceCriterionResult CreditNoteOnRefund(
        InvoiceId originalInvoice, InvoiceId creditNote, Money refundAmount);
}

What the Requirements Project Contains — Summary

MegaCorp.Requirements/
├── RequirementMetadata.cs           ← Base types (Epic, Feature<T>, Story<T>, Task<T>, Bug)
├── AcceptanceCriterionResult.cs     ← The AC return type
├── DomainConcepts.cs                ← Lightweight value types for AC signatures
├── Epics/
│   ├── ECommerceEpic.cs
│   ├── ComplianceEpic.cs
│   └── CustomerExperienceEpic.cs
├── Features/
│   ├── OrderProcessingFeature.cs8 ACs
│   ├── PaymentProcessingFeature.cs5 ACs
│   ├── InventoryManagementFeature.cs4 ACs
│   ├── CustomerNotificationFeature.cs4 ACs
│   ├── BillingFeature.cs3 ACs
│   ├── UserManagementFeature.cs     ← (not shown, 6 ACs)
│   ├── ReportingFeature.cs          ← (not shown, 3 ACs)
│   └── SearchFeature.cs             ← (not shown, 4 ACs)
├── Stories/
│   └── ... (stories decompose features)
└── Bugs/
    └── ... (bugs reference features)

Zero dependencies. The project compiles in isolation. It can be published as a NuGet package. It can be referenced by any project in the solution — or by projects in other solutions. It is the single source of truth for what the business requires.

The .csproj File

<!-- MegaCorp.Requirements/MegaCorp.Requirements.csproj -->
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <RootNamespace>MegaCorp.Requirements</RootNamespace>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>

    <!-- This is the most important project in the solution.
         Treat ALL warnings as errors. No exceptions. -->
    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
  </PropertyGroup>

  <!-- No ProjectReferences. No PackageReferences (beyond the SDK).
       This project depends on NOTHING. That's by design. -->
</Project>

Project 2: MegaCorp.Specifications — Contracts the Domain Must Satisfy

This project references MegaCorp.Requirements and MegaCorp.SharedKernel. It defines interfaces — one per feature boundary — that implementation projects must satisfy. Each interface method is decorated with [ForRequirement] for IDE navigability and Roslyn analyzer traceability.

The Generated Attributes

The source generator (in MegaCorp.Requirements.Analyzers) scans the Requirements project and generates attributes used by all subsequent layers:

// Generated: MegaCorp.Requirements/Generated/ForRequirementAttribute.g.cs
namespace MegaCorp.Requirements;

/// <summary>
/// Links a type, interface, or method to a requirement.
/// Generated by the Requirements source generator.
/// Ctrl+Click on typeof(Feature) in the IDE → jumps to the requirement.
/// "Find All References" on Feature → shows specs, implementations, and tests.
/// </summary>
[AttributeUsage(
    AttributeTargets.Class | AttributeTargets.Interface | AttributeTargets.Method,
    AllowMultiple = true)]
public sealed class ForRequirementAttribute : Attribute
{
    public Type RequirementType { get; }
    public string? AcceptanceCriterion { get; }

    public ForRequirementAttribute(Type requirementType, string? acceptanceCriterion = null)
    {
        RequirementType = requirementType;
        AcceptanceCriterion = acceptanceCriterion;
    }
}

/// <summary>
/// Links a test method to a specific acceptance criterion.
/// The AC is identified by nameof(Feature.ACMethod) — compiler-checked.
/// </summary>
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
public sealed class VerifiesAttribute : Attribute
{
    public Type RequirementType { get; }
    public string AcceptanceCriterionName { get; }

    public VerifiesAttribute(Type requirementType, string acceptanceCriterionName)
    {
        RequirementType = requirementType;
        AcceptanceCriterionName = acceptanceCriterionName;
    }
}

/// <summary>
/// Links a test class to a requirement type.
/// </summary>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
public sealed class TestsForAttribute : Attribute
{
    public Type RequirementType { get; }
    public TestsForAttribute(Type requirementType) => RequirementType = requirementType;
}

The Specification Interfaces

Each feature gets one or more specification interfaces — one per service boundary. A feature that spans 5 services gets 5 spec interfaces, each declaring the methods that specific service must implement for that feature.

Order Processing Specifications

The OrderProcessingFeature has 8 ACs. These ACs are implemented by different services. The specifications split accordingly:

// MegaCorp.Specifications/OrderProcessing/IOrderProcessingSpec.cs
namespace MegaCorp.Specifications.OrderProcessing;

using MegaCorp.Requirements.Features;
using MegaCorp.SharedKernel;

/// <summary>
/// Specification for the core order processing flow.
/// Implemented by: MegaCorp.OrderService.
///
/// This interface covers ACs that belong to the order orchestration logic:
/// - AC-1: OrderTotalMustBePositive
/// - AC-7: BulkOrderDiscountApplied
/// - AC-8: OrderPersistedWithFullDetail
///
/// Other ACs are covered by service-specific spec interfaces
/// (IInventoryIntegrationSpec, IPaymentIntegrationSpec, etc.)
/// </summary>
[ForRequirement(typeof(OrderProcessingFeature))]
public interface IOrderProcessingSpec
{
    /// <summary>
    /// Validates that the order total is positive.
    /// Rejection must happen before any payment or inventory interaction.
    /// </summary>
    [ForRequirement(typeof(OrderProcessingFeature),
        nameof(OrderProcessingFeature.OrderTotalMustBePositive))]
    Result ValidateOrderTotal(Order order);

    /// <summary>
    /// Applies bulk discount for orders with 10+ line items.
    /// Returns the discounted order.
    /// </summary>
    [ForRequirement(typeof(OrderProcessingFeature),
        nameof(OrderProcessingFeature.BulkOrderDiscountApplied))]
    Result<Order> ApplyBulkDiscount(Order order);

    /// <summary>
    /// Persists the order with full detail: line items, payment ref, reservations.
    /// The persisted state must be self-contained (no external lookups needed).
    /// </summary>
    [ForRequirement(typeof(OrderProcessingFeature),
        nameof(OrderProcessingFeature.OrderPersistedWithFullDetail))]
    Task<Result> PersistOrder(Order order, Payment payment,
        IReadOnlyList<Reservation> reservations);

    /// <summary>
    /// Orchestrates the full order flow: validate → reserve → pay → persist → notify.
    /// This method calls IInventoryIntegrationSpec, IPaymentIntegrationSpec, etc.
    /// </summary>
    Task<Result<OrderConfirmation>> ProcessOrder(CreateOrderCommand command);
}
// MegaCorp.Specifications/OrderProcessing/IInventoryIntegrationSpec.cs
namespace MegaCorp.Specifications.OrderProcessing;

using MegaCorp.Requirements.Features;
using MegaCorp.SharedKernel;

/// <summary>
/// Specification for inventory operations within order processing.
/// Implemented by: MegaCorp.InventoryService.
///
/// Covers:
/// - AC-2: InventoryReservedBeforePayment
/// - AC-6: FailedPaymentReleasesInventory
/// </summary>
[ForRequirement(typeof(OrderProcessingFeature))]
[ForRequirement(typeof(InventoryManagementFeature))]
public interface IInventoryIntegrationSpec
{
    [ForRequirement(typeof(OrderProcessingFeature),
        nameof(OrderProcessingFeature.InventoryReservedBeforePayment))]
    Task<Result<IReadOnlyList<Reservation>>> ReserveInventory(Order order);

    [ForRequirement(typeof(OrderProcessingFeature),
        nameof(OrderProcessingFeature.FailedPaymentReleasesInventory))]
    Task<Result> ReleaseReservations(IReadOnlyList<Reservation> reservations);
}
// MegaCorp.Specifications/OrderProcessing/IPaymentIntegrationSpec.cs
namespace MegaCorp.Specifications.OrderProcessing;

using MegaCorp.Requirements.Features;
using MegaCorp.SharedKernel;

/// <summary>
/// Specification for payment operations within order processing.
/// Implemented by: MegaCorp.PaymentGateway.
///
/// Covers:
/// - AC-3: PaymentCapturedAfterReservation
/// </summary>
[ForRequirement(typeof(OrderProcessingFeature))]
[ForRequirement(typeof(PaymentProcessingFeature))]
public interface IPaymentIntegrationSpec
{
    [ForRequirement(typeof(OrderProcessingFeature),
        nameof(OrderProcessingFeature.PaymentCapturedAfterReservation))]
    Task<Result<Payment>> CapturePayment(Order order,
        IReadOnlyList<Reservation> reservations);
}
// MegaCorp.Specifications/OrderProcessing/INotificationIntegrationSpec.cs
namespace MegaCorp.Specifications.OrderProcessing;

using MegaCorp.Requirements.Features;
using MegaCorp.SharedKernel;

/// <summary>
/// Specification for notification operations within order processing.
/// Implemented by: MegaCorp.NotificationService.
///
/// Covers:
/// - AC-4: ConfirmationSentAfterPayment
/// </summary>
[ForRequirement(typeof(OrderProcessingFeature))]
[ForRequirement(typeof(CustomerNotificationFeature))]
public interface INotificationIntegrationSpec
{
    [ForRequirement(typeof(OrderProcessingFeature),
        nameof(OrderProcessingFeature.ConfirmationSentAfterPayment))]
    Task<Result<NotificationReceipt>> SendOrderConfirmation(
        Order order, Payment payment);
}
// MegaCorp.Specifications/OrderProcessing/IAuditIntegrationSpec.cs
namespace MegaCorp.Specifications.OrderProcessing;

using MegaCorp.Requirements.Features;
using MegaCorp.SharedKernel;

/// <summary>
/// Specification for audit operations within order processing.
/// Implemented by: MegaCorp.AuditService.
///
/// Covers:
/// - AC-5: AllOperationsAudited
/// </summary>
[ForRequirement(typeof(OrderProcessingFeature))]
public interface IAuditIntegrationSpec
{
    [ForRequirement(typeof(OrderProcessingFeature),
        nameof(OrderProcessingFeature.AllOperationsAudited))]
    Task<Result> RecordAuditTrail(Order order, string operation,
        string actorId, object details);
}

The AC-to-Spec Mapping

Every AC on OrderProcessingFeature maps to exactly one specification interface method:

AC Name Spec Interface Implemented By
AC-1 OrderTotalMustBePositive IOrderProcessingSpec.ValidateOrderTotal OrderService
AC-2 InventoryReservedBeforePayment IInventoryIntegrationSpec.ReserveInventory InventoryService
AC-3 PaymentCapturedAfterReservation IPaymentIntegrationSpec.CapturePayment PaymentGateway
AC-4 ConfirmationSentAfterPayment INotificationIntegrationSpec.SendOrderConfirmation NotificationService
AC-5 AllOperationsAudited IAuditIntegrationSpec.RecordAuditTrail AuditService
AC-6 FailedPaymentReleasesInventory IInventoryIntegrationSpec.ReleaseReservations InventoryService
AC-7 BulkOrderDiscountApplied IOrderProcessingSpec.ApplyBulkDiscount OrderService
AC-8 OrderPersistedWithFullDetail IOrderProcessingSpec.PersistOrder OrderService

Every AC is accounted for. No gaps. No duplicates. The Roslyn analyzer verifies this mapping at compile time — if an AC has no corresponding spec method, it's a compiler error (REQ101).

Diagram

The .csproj File

<!-- MegaCorp.Specifications/MegaCorp.Specifications.csproj -->
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <RootNamespace>MegaCorp.Specifications</RootNamespace>
    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
  </PropertyGroup>

  <ItemGroup>
    <ProjectReference Include="..\MegaCorp.Requirements\MegaCorp.Requirements.csproj" />
    <ProjectReference Include="..\MegaCorp.SharedKernel\MegaCorp.SharedKernel.csproj" />
  </ItemGroup>

  <!-- The Roslyn analyzer that verifies AC-to-Spec mapping -->
  <ItemGroup>
    <ProjectReference Include="..\tools\MegaCorp.Requirements.Analyzers\MegaCorp.Requirements.Analyzers.csproj"
                      OutputItemType="Analyzer"
                      ReferenceOutputAssembly="false" />
  </ItemGroup>
</Project>

The Implementations — Compiler-Enforced

Each implementation project references MegaCorp.Specifications. It implements one or more spec interfaces. The colon operator IS the guarantee — if the interface has 3 methods, the class must have 3 methods. The compiler enforces it.

MegaCorp.OrderService

// MegaCorp.OrderService/OrderProcessingService.cs
namespace MegaCorp.OrderService;

using MegaCorp.Requirements.Features;
using MegaCorp.Specifications.OrderProcessing;
using MegaCorp.SharedKernel;

/// <summary>
/// Implements the core order processing specification.
/// The compiler FORCES this class to implement all IOrderProcessingSpec methods.
/// If a new AC is added to the spec, this class won't compile until it's implemented.
/// </summary>
[ForRequirement(typeof(OrderProcessingFeature))]
public class OrderProcessingService : IOrderProcessingSpec
{
    private readonly IInventoryIntegrationSpec _inventory;
    private readonly IPaymentIntegrationSpec _payment;
    private readonly INotificationIntegrationSpec _notification;
    private readonly IAuditIntegrationSpec _audit;
    private readonly IOrderRepository _orderRepo;

    public OrderProcessingService(
        IInventoryIntegrationSpec inventory,
        IPaymentIntegrationSpec payment,
        INotificationIntegrationSpec notification,
        IAuditIntegrationSpec audit,
        IOrderRepository orderRepo)
    {
        _inventory = inventory;
        _payment = payment;
        _notification = notification;
        _audit = audit;
        _orderRepo = orderRepo;
    }

    [ForRequirement(typeof(OrderProcessingFeature),
        nameof(OrderProcessingFeature.OrderTotalMustBePositive))]
    public Result ValidateOrderTotal(Order order)
    {
        if (order.Total.Amount <= 0)
            return Result.Failure("Order total must be positive");

        if (order.Lines.Any(l => l.Quantity.Value <= 0))
            return Result.Failure("All line item quantities must be positive");

        if (order.Lines.Any(l => l.UnitPrice.Amount < 0))
            return Result.Failure("Line item prices cannot be negative");

        return Result.Success();
    }

    [ForRequirement(typeof(OrderProcessingFeature),
        nameof(OrderProcessingFeature.BulkOrderDiscountApplied))]
    public Result<Order> ApplyBulkDiscount(Order order)
    {
        if (order.Lines.Count < 10)
            return Result<Order>.Success(order); // No discount for < 10 items

        var discountRate = 0.05m; // 5% bulk discount (configurable per category)
        var discountedTotal = order.Total.Amount * (1 - discountRate);
        var discountedOrder = order with
        {
            Total = new Money(discountedTotal, order.Total.Currency),
            DiscountApplied = true
        };

        return Result<Order>.Success(discountedOrder);
    }

    [ForRequirement(typeof(OrderProcessingFeature),
        nameof(OrderProcessingFeature.OrderPersistedWithFullDetail))]
    public async Task<Result> PersistOrder(Order order, Payment payment,
        IReadOnlyList<Reservation> reservations)
    {
        var persistableOrder = order with
        {
            PaymentReference = payment.Id,
            ReservationReferences = reservations.Select(r => r.Id).ToList(),
            PersistedAt = DateTimeOffset.UtcNow
        };

        await _orderRepo.Save(persistableOrder);
        return Result.Success();
    }

    public async Task<Result<OrderConfirmation>> ProcessOrder(CreateOrderCommand command)
    {
        // Build the order
        var order = Order.FromCommand(command);

        // AC-1: Validate total
        var validation = ValidateOrderTotal(order);
        if (!validation.IsSuccess)
            return Result<OrderConfirmation>.Failure(validation.Reason!);

        // AC-7: Apply bulk discount
        var discountResult = ApplyBulkDiscount(order);
        if (!discountResult.IsSuccess)
            return Result<OrderConfirmation>.Failure(discountResult.Reason!);
        order = discountResult.Value;

        // AC-2: Reserve inventory (IInventoryIntegrationSpec)
        var reservationResult = await _inventory.ReserveInventory(order);
        if (!reservationResult.IsSuccess)
            return Result<OrderConfirmation>.Failure(reservationResult.Reason!);
        var reservations = reservationResult.Value;

        try
        {
            // AC-3: Capture payment (IPaymentIntegrationSpec)
            var paymentResult = await _payment.CapturePayment(order, reservations);
            if (!paymentResult.IsSuccess)
            {
                // AC-6: Release reservations on payment failure
                await _inventory.ReleaseReservations(reservations);
                return Result<OrderConfirmation>.Failure(paymentResult.Reason!);
            }
            var payment = paymentResult.Value;

            // AC-8: Persist with full detail
            var persistResult = await PersistOrder(order, payment, reservations);
            if (!persistResult.IsSuccess)
                return Result<OrderConfirmation>.Failure(persistResult.Reason!);

            // AC-5: Audit trail
            await _audit.RecordAuditTrail(order, "OrderCreated",
                command.ActorId, new { payment.Id, ReservationCount = reservations.Count });

            // AC-4: Send confirmation (INotificationIntegrationSpec)
            await _notification.SendOrderConfirmation(order, payment);

            return Result<OrderConfirmation>.Success(new OrderConfirmation(
                order.Id, payment.Id, reservations.Count, order.Total));
        }
        catch
        {
            // AC-6: Release on any failure
            await _inventory.ReleaseReservations(reservations);
            throw;
        }
    }
}

Key observations:

  1. Constructor injection — not ServiceLocator. Every dependency is visible. The compiler checks them.
  2. Typed spec interfacesIInventoryIntegrationSpec, not IServiceProvider.GetService<IInventoryChecker>(). The interface IS the contract.
  3. Each method is linked to its AC via [ForRequirement(typeof(...), nameof(...))]. Ctrl+Click navigable. Refactor-safe.
  4. The ProcessOrder orchestrator calls other spec interfaces. The flow is explicit: validate → discount → reserve → pay → persist → audit → notify. No hidden ServiceLocator hops.

MegaCorp.InventoryService

// MegaCorp.InventoryService/InventoryReservationService.cs
namespace MegaCorp.InventoryService;

using MegaCorp.Requirements.Features;
using MegaCorp.Specifications.OrderProcessing;
using MegaCorp.SharedKernel;

[ForRequirement(typeof(OrderProcessingFeature))]
[ForRequirement(typeof(InventoryManagementFeature))]
public class InventoryReservationService : IInventoryIntegrationSpec
{
    private readonly IStockRepository _stockRepo;
    private readonly IReservationRepository _reservationRepo;

    public InventoryReservationService(
        IStockRepository stockRepo,
        IReservationRepository reservationRepo)
    {
        _stockRepo = stockRepo;
        _reservationRepo = reservationRepo;
    }

    [ForRequirement(typeof(OrderProcessingFeature),
        nameof(OrderProcessingFeature.InventoryReservedBeforePayment))]
    public async Task<Result<IReadOnlyList<Reservation>>> ReserveInventory(Order order)
    {
        var reservations = new List<Reservation>();

        foreach (var line in order.Lines)
        {
            var stock = await _stockRepo.GetBySku(line.Sku);
            if (stock is null || stock.Available < line.Quantity)
                return Result<IReadOnlyList<Reservation>>.Failure(
                    $"Insufficient stock for {line.Sku}: need {line.Quantity}, have {stock?.Available ?? 0}");

            var reservation = await _reservationRepo.Create(new Reservation(
                Id: ReservationId.New(),
                Sku: line.Sku,
                Quantity: line.Quantity,
                OrderId: order.Id,
                ExpiresAt: DateTimeOffset.UtcNow.AddMinutes(15)));

            await _stockRepo.Decrement(line.Sku, line.Quantity);
            reservations.Add(reservation);
        }

        return Result<IReadOnlyList<Reservation>>.Success(reservations);
    }

    [ForRequirement(typeof(OrderProcessingFeature),
        nameof(OrderProcessingFeature.FailedPaymentReleasesInventory))]
    public async Task<Result> ReleaseReservations(IReadOnlyList<Reservation> reservations)
    {
        foreach (var reservation in reservations)
        {
            await _stockRepo.Increment(reservation.Sku, reservation.Quantity);
            await _reservationRepo.Cancel(reservation.Id);
        }

        return Result.Success();
    }
}

MegaCorp.PaymentGateway

// MegaCorp.PaymentGateway/StripePaymentService.cs
namespace MegaCorp.PaymentGateway;

using MegaCorp.Requirements.Features;
using MegaCorp.Specifications.OrderProcessing;
using MegaCorp.SharedKernel;

[ForRequirement(typeof(OrderProcessingFeature))]
[ForRequirement(typeof(PaymentProcessingFeature))]
public class StripePaymentService : IPaymentIntegrationSpec
{
    private readonly IStripeClient _stripe;

    public StripePaymentService(IStripeClient stripe) => _stripe = stripe;

    [ForRequirement(typeof(OrderProcessingFeature),
        nameof(OrderProcessingFeature.PaymentCapturedAfterReservation))]
    public async Task<Result<Payment>> CapturePayment(Order order,
        IReadOnlyList<Reservation> reservations)
    {
        if (reservations.Count == 0)
            return Result<Payment>.Failure(
                "Cannot capture payment without inventory reservations");

        var charge = await _stripe.CreateCharge(new StripeChargeRequest
        {
            Amount = (long)(order.Total.Amount * 100), // Stripe uses cents
            Currency = order.Total.Currency.ToLower(),
            CustomerId = order.Customer.StripeCustomerId,
            IdempotencyKey = $"order-{order.Id.Value}",
            Metadata = new Dictionary<string, string>
            {
                ["order_id"] = order.Id.Value.ToString(),
                ["reservation_count"] = reservations.Count.ToString()
            }
        });

        if (!charge.Succeeded)
            return Result<Payment>.Failure(
                $"Payment failed: {charge.FailureReason}");

        return Result<Payment>.Success(new Payment(
            Id: PaymentId.New(),
            OrderId: order.Id,
            Amount: order.Total,
            StripeChargeId: charge.Id,
            CapturedAt: DateTimeOffset.UtcNow));
    }
}

MegaCorp.NotificationService

// MegaCorp.NotificationService/OrderNotificationService.cs
namespace MegaCorp.NotificationService;

using MegaCorp.Requirements.Features;
using MegaCorp.Specifications.OrderProcessing;
using MegaCorp.SharedKernel;

[ForRequirement(typeof(OrderProcessingFeature))]
[ForRequirement(typeof(CustomerNotificationFeature))]
public class OrderNotificationService : INotificationIntegrationSpec
{
    private readonly IEmailSender _email;
    private readonly ITemplateEngine _templates;

    public OrderNotificationService(IEmailSender email, ITemplateEngine templates)
    {
        _email = email;
        _templates = templates;
    }

    [ForRequirement(typeof(OrderProcessingFeature),
        nameof(OrderProcessingFeature.ConfirmationSentAfterPayment))]
    public async Task<Result<NotificationReceipt>> SendOrderConfirmation(
        Order order, Payment payment)
    {
        var body = _templates.Render("order-confirmation", new
        {
            OrderId = order.Id.Value,
            Total = $"{order.Total.Amount:N2} {order.Total.Currency}",
            Items = order.Lines.Select(l => new
            {
                Sku = l.Sku.Value,
                Qty = l.Quantity.Value,
                Price = $"{l.UnitPrice.Amount:N2}"
            }),
            PaymentRef = payment.StripeChargeId
        });

        var receipt = await _email.Send(new EmailMessage
        {
            To = order.Customer.Email,
            Subject = $"Order Confirmation — {order.Id.Value}",
            Body = body
        });

        return Result<NotificationReceipt>.Success(new NotificationReceipt(
            NotificationId.New(), receipt.MessageId, DateTimeOffset.UtcNow));
    }
}

The Tests — Type-Linked Verification

Every test class declares which feature it tests. Every test method declares which AC it verifies. The compiler checks that the referenced Feature and AC actually exist.

// MegaCorp.OrderService.Tests/OrderProcessingTests.cs
namespace MegaCorp.OrderService.Tests;

using MegaCorp.Requirements.Features;
using MegaCorp.Specifications.OrderProcessing;

[TestFixture]
[TestsFor(typeof(OrderProcessingFeature))]
public class OrderProcessingTests
{
    private OrderProcessingService _service;
    private InMemoryStockRepository _stockRepo;
    private InMemoryReservationRepository _reservationRepo;
    private FakeStripeClient _stripeClient;
    private InMemoryOrderRepository _orderRepo;
    private InMemoryAuditRepository _auditRepo;
    private FakeEmailSender _emailSender;

    [SetUp]
    public void Setup()
    {
        _stockRepo = new InMemoryStockRepository();
        _reservationRepo = new InMemoryReservationRepository();
        _stripeClient = new FakeStripeClient();
        _orderRepo = new InMemoryOrderRepository();
        _auditRepo = new InMemoryAuditRepository();
        _emailSender = new FakeEmailSender();

        // Wire real spec implementations with in-memory infrastructure
        var inventory = new InventoryReservationService(_stockRepo, _reservationRepo);
        var payment = new StripePaymentService(_stripeClient);
        var notification = new OrderNotificationService(_emailSender, new FakeTemplateEngine());
        var audit = new AuditService(_auditRepo);

        _service = new OrderProcessingService(
            inventory, payment, notification, audit, _orderRepo);

        // Seed inventory
        _stockRepo.Seed("SKU-001", available: 100);
        _stockRepo.Seed("SKU-002", available: 50);
    }

    // ─── AC-1: OrderTotalMustBePositive ────────────────────────────────

    [Test]
    [Verifies(typeof(OrderProcessingFeature),
        nameof(OrderProcessingFeature.OrderTotalMustBePositive))]
    public void Positive_total_is_accepted()
    {
        var order = CreateOrder(total: 99.99m);
        var result = _service.ValidateOrderTotal(order);
        Assert.That(result.IsSuccess, Is.True);
    }

    [Test]
    [Verifies(typeof(OrderProcessingFeature),
        nameof(OrderProcessingFeature.OrderTotalMustBePositive))]
    public void Zero_total_is_rejected()
    {
        var order = CreateOrder(total: 0m);
        var result = _service.ValidateOrderTotal(order);
        Assert.That(result.IsSuccess, Is.False);
        Assert.That(result.Reason, Does.Contain("positive"));
    }

    [Test]
    [Verifies(typeof(OrderProcessingFeature),
        nameof(OrderProcessingFeature.OrderTotalMustBePositive))]
    public void Negative_total_is_rejected()
    {
        var order = CreateOrder(total: -50m);
        var result = _service.ValidateOrderTotal(order);
        Assert.That(result.IsSuccess, Is.False);
    }

    [Test]
    [Verifies(typeof(OrderProcessingFeature),
        nameof(OrderProcessingFeature.OrderTotalMustBePositive))]
    public void Negative_line_item_quantity_is_rejected()
    {
        var order = CreateOrderWithLines(new[] { ("SKU-001", -1, 10m) });
        var result = _service.ValidateOrderTotal(order);
        Assert.That(result.IsSuccess, Is.False);
        Assert.That(result.Reason, Does.Contain("quantities"));
    }

    // ─── AC-2: InventoryReservedBeforePayment ──────────────────────────

    [Test]
    [Verifies(typeof(OrderProcessingFeature),
        nameof(OrderProcessingFeature.InventoryReservedBeforePayment))]
    public async Task Inventory_reserved_before_payment_attempt()
    {
        _stripeClient.Configure(succeed: true);
        var command = CreateOrderCommand(items: new[] { ("SKU-001", 5) });

        var result = await _service.ProcessOrder(command);

        Assert.That(result.IsSuccess, Is.True);
        // Verify reservation happened before payment
        Assert.That(_reservationRepo.Reservations.Count, Is.GreaterThan(0));
        Assert.That(_stripeClient.ChargeAttempts.Count, Is.EqualTo(1));
        Assert.That(_reservationRepo.Reservations.First().CreatedAt,
            Is.LessThan(_stripeClient.ChargeAttempts.First().AttemptedAt));
    }

    [Test]
    [Verifies(typeof(OrderProcessingFeature),
        nameof(OrderProcessingFeature.InventoryReservedBeforePayment))]
    public async Task Insufficient_stock_rejects_without_payment()
    {
        var command = CreateOrderCommand(items: new[] { ("SKU-001", 999) });

        var result = await _service.ProcessOrder(command);

        Assert.That(result.IsSuccess, Is.False);
        Assert.That(result.Reason, Does.Contain("Insufficient stock"));
        Assert.That(_stripeClient.ChargeAttempts.Count, Is.EqualTo(0),
            "Payment should not be attempted when stock is insufficient");
    }

    // ─── AC-3: PaymentCapturedAfterReservation ─────────────────────────

    [Test]
    [Verifies(typeof(OrderProcessingFeature),
        nameof(OrderProcessingFeature.PaymentCapturedAfterReservation))]
    public async Task Payment_captured_after_successful_reservation()
    {
        _stripeClient.Configure(succeed: true);
        var command = CreateOrderCommand(items: new[] { ("SKU-001", 2) });

        var result = await _service.ProcessOrder(command);

        Assert.That(result.IsSuccess, Is.True);
        Assert.That(_stripeClient.ChargeAttempts.Count, Is.EqualTo(1));
        var charge = _stripeClient.ChargeAttempts.First();
        Assert.That(charge.Amount, Is.EqualTo(
            (long)(CreateOrder(items: new[] { ("SKU-001", 2) }).Total.Amount * 100)));
    }

    [Test]
    [Verifies(typeof(OrderProcessingFeature),
        nameof(OrderProcessingFeature.PaymentCapturedAfterReservation))]
    public async Task Payment_amount_matches_order_total()
    {
        _stripeClient.Configure(succeed: true);
        var command = CreateOrderCommand(items: new[] { ("SKU-001", 3) },
            unitPrice: 25.00m);

        var result = await _service.ProcessOrder(command);

        Assert.That(result.IsSuccess, Is.True);
        Assert.That(_stripeClient.ChargeAttempts.First().Amount,
            Is.EqualTo(7500L)); // 3 * 25.00 * 100 cents
    }

    // ─── AC-4: ConfirmationSentAfterPayment ────────────────────────────

    [Test]
    [Verifies(typeof(OrderProcessingFeature),
        nameof(OrderProcessingFeature.ConfirmationSentAfterPayment))]
    public async Task Confirmation_email_sent_after_payment()
    {
        _stripeClient.Configure(succeed: true);
        var command = CreateOrderCommand(items: new[] { ("SKU-001", 1) });

        var result = await _service.ProcessOrder(command);

        Assert.That(result.IsSuccess, Is.True);
        Assert.That(_emailSender.SentMessages.Count, Is.EqualTo(1));
        Assert.That(_emailSender.SentMessages.First().Subject,
            Does.Contain("Order Confirmation"));
    }

    // ─── AC-5: AllOperationsAudited ────────────────────────────────────

    [Test]
    [Verifies(typeof(OrderProcessingFeature),
        nameof(OrderProcessingFeature.AllOperationsAudited))]
    public async Task All_operations_recorded_in_audit_log()
    {
        _stripeClient.Configure(succeed: true);
        var command = CreateOrderCommand(items: new[] { ("SKU-001", 1) });

        await _service.ProcessOrder(command);

        Assert.That(_auditRepo.Entries.Count, Is.GreaterThan(0));
        Assert.That(_auditRepo.Entries.Any(e => e.Operation == "OrderCreated"),
            Is.True);
    }

    // ─── AC-6: FailedPaymentReleasesInventory ──────────────────────────

    [Test]
    [Verifies(typeof(OrderProcessingFeature),
        nameof(OrderProcessingFeature.FailedPaymentReleasesInventory))]
    public async Task Failed_payment_releases_inventory()
    {
        _stripeClient.Configure(succeed: false, failureReason: "Card declined");
        var initialStock = _stockRepo.GetAvailable("SKU-001");
        var command = CreateOrderCommand(items: new[] { ("SKU-001", 5) });

        var result = await _service.ProcessOrder(command);

        Assert.That(result.IsSuccess, Is.False);
        Assert.That(result.Reason, Does.Contain("Card declined"));

        // Stock should be restored
        var finalStock = _stockRepo.GetAvailable("SKU-001");
        Assert.That(finalStock, Is.EqualTo(initialStock),
            "Inventory should be fully restored after payment failure");
    }

    // ─── AC-7: BulkOrderDiscountApplied ────────────────────────────────

    [Test]
    [Verifies(typeof(OrderProcessingFeature),
        nameof(OrderProcessingFeature.BulkOrderDiscountApplied))]
    public void Bulk_order_receives_discount()
    {
        var order = CreateOrderWithLines(
            Enumerable.Range(1, 12).Select(i => ($"SKU-{i:000}", 1, 10.00m)).ToArray());

        var result = _service.ApplyBulkDiscount(order);

        Assert.That(result.IsSuccess, Is.True);
        Assert.That(result.Value.Total.Amount, Is.LessThan(order.Total.Amount));
        Assert.That(result.Value.Total.Amount, Is.EqualTo(114.00m)); // 120 * 0.95
    }

    [Test]
    [Verifies(typeof(OrderProcessingFeature),
        nameof(OrderProcessingFeature.BulkOrderDiscountApplied))]
    public void Small_order_no_discount()
    {
        var order = CreateOrderWithLines(new[] { ("SKU-001", 3, 10.00m) });

        var result = _service.ApplyBulkDiscount(order);

        Assert.That(result.IsSuccess, Is.True);
        Assert.That(result.Value.Total.Amount, Is.EqualTo(order.Total.Amount));
    }

    // ─── AC-8: OrderPersistedWithFullDetail ────────────────────────────

    [Test]
    [Verifies(typeof(OrderProcessingFeature),
        nameof(OrderProcessingFeature.OrderPersistedWithFullDetail))]
    public async Task Order_persisted_with_payment_and_reservation_refs()
    {
        _stripeClient.Configure(succeed: true);
        var command = CreateOrderCommand(items: new[] { ("SKU-001", 2), ("SKU-002", 3) });

        var result = await _service.ProcessOrder(command);

        Assert.That(result.IsSuccess, Is.True);
        var persisted = _orderRepo.GetById(result.Value.OrderId);
        Assert.That(persisted, Is.Not.Null);
        Assert.That(persisted!.PaymentReference, Is.Not.EqualTo(default(PaymentId)));
        Assert.That(persisted.ReservationReferences.Count, Is.EqualTo(2));
        Assert.That(persisted.Lines.Count, Is.EqualTo(2));
    }

    // ─── Helpers ───────────────────────────────────────────────────────

    private static Order CreateOrder(decimal total = 100m) => /* ... */;
    private static Order CreateOrderWithLines(
        (string sku, int qty, decimal price)[] lines) => /* ... */;
    private static CreateOrderCommand CreateOrderCommand(
        (string sku, int qty)[] items, decimal unitPrice = 10m) => /* ... */;
}

19 test methods. Each one has a [Verifies] attribute linking it to a specific AC. The Roslyn analyzer knows:

  • AC-1 has 4 tests (positive, zero, negative, negative line item)
  • AC-2 has 2 tests (reservation before payment, insufficient stock rejects)
  • AC-3 has 2 tests (captured after reservation, amount matches)
  • AC-4 has 1 test (email sent)
  • AC-5 has 1 test (audit log recorded)
  • AC-6 has 1 test (failed payment releases)
  • AC-7 has 2 tests (bulk discount applied, small order no discount)
  • AC-8 has 1 test (persisted with refs)

8/8 ACs covered. The traceability matrix reports 100% acceptance criterion coverage.


The Generated Traceability

The Roslyn source generator cross-references all [ForRequirement], [TestsFor], and [Verifies] attributes and produces:

RequirementRegistry

// Generated: RequirementRegistry.g.cs
public static class RequirementRegistry
{
    public static IReadOnlyDictionary<Type, RequirementInfo> All { get; } =
        new Dictionary<Type, RequirementInfo>
        {
            [typeof(OrderProcessingFeature)] = new(
                Type: typeof(OrderProcessingFeature),
                Kind: RequirementKind.Feature,
                Title: "Order Processing",
                Parent: typeof(ECommerceEpic),
                AcceptanceCriteria: new[]
                {
                    nameof(OrderProcessingFeature.OrderTotalMustBePositive),
                    nameof(OrderProcessingFeature.InventoryReservedBeforePayment),
                    nameof(OrderProcessingFeature.PaymentCapturedAfterReservation),
                    nameof(OrderProcessingFeature.ConfirmationSentAfterPayment),
                    nameof(OrderProcessingFeature.AllOperationsAudited),
                    nameof(OrderProcessingFeature.FailedPaymentReleasesInventory),
                    nameof(OrderProcessingFeature.BulkOrderDiscountApplied),
                    nameof(OrderProcessingFeature.OrderPersistedWithFullDetail),
                }),
            [typeof(PaymentProcessingFeature)] = new(/* ... */),
            [typeof(InventoryManagementFeature)] = new(/* ... */),
            [typeof(CustomerNotificationFeature)] = new(/* ... */),
            [typeof(BillingFeature)] = new(/* ... */),
        };
}

TraceabilityMatrix

// Generated: TraceabilityMatrix.g.cs
public static class TraceabilityMatrix
{
    public static IReadOnlyDictionary<Type, TraceabilityEntry> Entries { get; } =
        new Dictionary<Type, TraceabilityEntry>
        {
            [typeof(OrderProcessingFeature)] = new TraceabilityEntry(
                RequirementType: typeof(OrderProcessingFeature),
                Implementations: new[]
                {
                    new ImplementationRef(
                        typeof(OrderProcessingService), typeof(IOrderProcessingSpec)),
                    new ImplementationRef(
                        typeof(InventoryReservationService), typeof(IInventoryIntegrationSpec)),
                    new ImplementationRef(
                        typeof(StripePaymentService), typeof(IPaymentIntegrationSpec)),
                    new ImplementationRef(
                        typeof(OrderNotificationService), typeof(INotificationIntegrationSpec)),
                    new ImplementationRef(
                        typeof(AuditService), typeof(IAuditIntegrationSpec)),
                },
                Tests: new[]
                {
                    new TestRef(typeof(OrderProcessingTests),
                        "Positive_total_is_accepted",
                        nameof(OrderProcessingFeature.OrderTotalMustBePositive)),
                    new TestRef(typeof(OrderProcessingTests),
                        "Zero_total_is_rejected",
                        nameof(OrderProcessingFeature.OrderTotalMustBePositive)),
                    new TestRef(typeof(OrderProcessingTests),
                        "Negative_total_is_rejected",
                        nameof(OrderProcessingFeature.OrderTotalMustBePositive)),
                    new TestRef(typeof(OrderProcessingTests),
                        "Negative_line_item_quantity_is_rejected",
                        nameof(OrderProcessingFeature.OrderTotalMustBePositive)),
                    new TestRef(typeof(OrderProcessingTests),
                        "Inventory_reserved_before_payment_attempt",
                        nameof(OrderProcessingFeature.InventoryReservedBeforePayment)),
                    // ... (14 more test refs, all 19 total)
                },
                AcceptanceCriteriaCoverage: new Dictionary<string, int>
                {
                    [nameof(OrderProcessingFeature.OrderTotalMustBePositive)] = 4,
                    [nameof(OrderProcessingFeature.InventoryReservedBeforePayment)] = 2,
                    [nameof(OrderProcessingFeature.PaymentCapturedAfterReservation)] = 2,
                    [nameof(OrderProcessingFeature.ConfirmationSentAfterPayment)] = 1,
                    [nameof(OrderProcessingFeature.AllOperationsAudited)] = 1,
                    [nameof(OrderProcessingFeature.FailedPaymentReleasesInventory)] = 1,
                    [nameof(OrderProcessingFeature.BulkOrderDiscountApplied)] = 2,
                    [nameof(OrderProcessingFeature.OrderPersistedWithFullDetail)] = 1,
                }),
        };
}

Compiler Diagnostics

The Roslyn analyzer emits diagnostics at compile time:

info  REQ103: OrderProcessingFeature — all 8 acceptance criteria have specifications (100% specified)
info  REQ203: OrderProcessingFeature — all 5 spec implementations found (100% implemented)
info  REQ303: OrderProcessingFeature — all 8 acceptance criteria have tests (100% tested)

info  REQ103: PaymentProcessingFeature — all 5 acceptance criteria have specifications (100% specified)
info  REQ203: PaymentProcessingFeature — all spec implementations found (100% implemented)
warn  REQ301: PaymentProcessingFeature.NoPciDataStored — no test with [Verifies] for this AC
  → Add a test method with [Verifies(typeof(PaymentProcessingFeature),
    nameof(PaymentProcessingFeature.NoPciDataStored))]

warn  REQ102: BillingFeature.CreditNoteOnRefund — no specification method foundAdd a method with [ForRequirement(typeof(BillingFeature),
    nameof(BillingFeature.CreditNoteOnRefund))] to a spec interface

These diagnostics appear in the IDE Error List and in CI build output. The build can be configured to treat them as errors in CI.


The DI Wiring — Clean and Explicit

The host project (MegaCorp.Web) wires spec implementations to their interfaces. No ServiceLocator. No IServiceProvider injection. Just standard constructor injection through typed interfaces:

// MegaCorp.Web/Program.cs
var builder = WebApplication.CreateBuilder(args);

// ─── Order Processing chain ────────────────────────────────────────
builder.Services.AddScoped<IOrderProcessingSpec, OrderProcessingService>();
builder.Services.AddScoped<IInventoryIntegrationSpec, InventoryReservationService>();
builder.Services.AddScoped<IPaymentIntegrationSpec, StripePaymentService>();
builder.Services.AddScoped<INotificationIntegrationSpec, OrderNotificationService>();
builder.Services.AddScoped<IAuditIntegrationSpec, AuditService>();

// ─── Payment Processing chain ──────────────────────────────────────
builder.Services.AddScoped<IPaymentProcessingSpec, PaymentService>();

// ─── Inventory Management chain ────────────────────────────────────
builder.Services.AddScoped<IInventoryManagementSpec, InventoryService>();

// ─── Billing chain ─────────────────────────────────────────────────
builder.Services.AddScoped<IBillingSpec, BillingService>();

// ─── Infrastructure (repositories, external clients) ───────────────
builder.Services.AddScoped<IOrderRepository, EfOrderRepository>();
builder.Services.AddScoped<IStockRepository, EfStockRepository>();
builder.Services.AddScoped<IReservationRepository, EfReservationRepository>();
builder.Services.AddScoped<IAuditRepository, EfAuditRepository>();
builder.Services.AddScoped<IStripeClient, StripeClient>();
builder.Services.AddScoped<IEmailSender, SendGridEmailSender>();
builder.Services.AddScoped<ITemplateEngine, ScribanTemplateEngine>();

// ─── Startup compliance check ──────────────────────────────────────
builder.Services.AddHostedService<RequirementComplianceCheck>();

var app = builder.Build();
// NO ServiceLocator.Initialize(app.Services) — it's gone.

The Program.cs is organized by feature chain, not by technical layer. You can see at a glance which class implements which specification. There's no ServiceLocator.Initialize() — it's been removed.


The IDE Experience: Find All References

From any layer, "Find All References" on OrderProcessingFeature shows the entire traceability chain:

OrderProcessingFeature                                     ← Layer 1: Definition
├── IOrderProcessingSpec                                   ← [ForRequirement(typeof(OrderProcessingFeature))]
│   ├── .ValidateOrderTotal[ForRequirement(..., "OrderTotalMustBePositive")]
│   ├── .ApplyBulkDiscount[ForRequirement(..., "BulkOrderDiscountApplied")]
│   └── .PersistOrder[ForRequirement(..., "OrderPersistedWithFullDetail")]
├── IInventoryIntegrationSpec                              ← [ForRequirement(typeof(OrderProcessingFeature))]
│   ├── .ReserveInventory[ForRequirement(..., "InventoryReservedBeforePayment")]
│   └── .ReleaseReservations[ForRequirement(..., "FailedPaymentReleasesInventory")]
├── IPaymentIntegrationSpec                                ← [ForRequirement(typeof(OrderProcessingFeature))]
│   └── .CapturePayment[ForRequirement(..., "PaymentCapturedAfterReservation")]
├── INotificationIntegrationSpec                           ← [ForRequirement(typeof(OrderProcessingFeature))]
│   └── .SendOrderConfirmation[ForRequirement(..., "ConfirmationSentAfterPayment")]
├── IAuditIntegrationSpec                                  ← [ForRequirement(typeof(OrderProcessingFeature))]
│   └── .RecordAuditTrail[ForRequirement(..., "AllOperationsAudited")]
├── OrderProcessingService : IOrderProcessingSpec          ← Implementation
├── InventoryReservationService : IInventoryIntegrationSpec ← Implementation
├── StripePaymentService : IPaymentIntegrationSpec         ← Implementation
├── OrderNotificationService : INotificationIntegrationSpec ← Implementation
├── AuditService : IAuditIntegrationSpec                   ← Implementation
└── OrderProcessingTests                                   ← [TestsFor(typeof(OrderProcessingFeature))]
    ├── .Positive_total_is_accepted[Verifies(..., "OrderTotalMustBePositive")]
    ├── .Zero_total_is_rejected[Verifies(..., "OrderTotalMustBePositive")]
    ├── .Negative_total_is_rejected[Verifies(..., "OrderTotalMustBePositive")]
    ├── .Negative_line_item_quantity_is_rejected[Verifies(..., "OrderTotalMustBePositive")]
    ├── .Inventory_reserved_before_payment_attempt[Verifies(..., "InventoryReservedBeforePayment")]
    ├── .Insufficient_stock_rejects_without_payment[Verifies(..., "InventoryReservedBeforePayment")]
    ├── .Payment_captured_after_successful_reservation[Verifies(..., "PaymentCapturedAfterReservation")]
    ├── .Payment_amount_matches_order_total[Verifies(..., "PaymentCapturedAfterReservation")]
    ├── .Confirmation_email_sent_after_payment[Verifies(..., "ConfirmationSentAfterPayment")]
    ├── .All_operations_recorded_in_audit_log[Verifies(..., "AllOperationsAudited")]
    ├── .Failed_payment_releases_inventory[Verifies(..., "FailedPaymentReleasesInventory")]
    ├── .Bulk_order_receives_discount[Verifies(..., "BulkOrderDiscountApplied")]
    ├── .Small_order_no_discount[Verifies(..., "BulkOrderDiscountApplied")]
    └── .Order_persisted_with_payment_and_reservation_refs[Verifies(..., "OrderPersistedWithFullDetail")]

One click. Five spec interfaces. Five implementation classes. Nineteen tests. Eight acceptance criteria. Complete traceability from business requirement to verified production code — across 10+ projects in the monorepo.

Every link is a typeof() or nameof() — Ctrl+Click navigable, refactor-safe, compiler-checked.


The Full Typed Chain — From Requirement to Production

Diagram

The Roslyn Analyzer Pipeline

The source generator follows the CMF's multi-stage pipeline:

Stage 0: Metamodel Registration

Register requirement base types (Epic, Feature<T>, Story<T>, Task<T>, Bug) as meta-concepts. This tells the generator what to scan for.

Stage 1: Requirement Collection

Walk the compilation for all types inheriting RequirementMetadata. Build the hierarchy graph (Epic → Feature → Story → Task). Validate constraints: no cycles, valid parent-child types, unique AC method names.

Stage 2: Registry Generation

Generate RequirementRegistry.g.cs — the type catalog with hierarchy, AC method names, and metadata. This is the phone book of requirements.

Stage 3: Attribute Generation

Generate ForRequirementAttribute, VerifiesAttribute, TestsForAttribute. These are the glue attributes used by Specifications, Implementations, and Tests.

Stage 4: Traceability Matrix

Cross-reference all [ForRequirement], [TestsFor], and [Verifies] attributes across all referenced assemblies. Generate TraceabilityMatrix.g.cs. Emit compiler diagnostics for coverage gaps:

Diagnostic Severity Trigger
REQ100 Error Feature has no spec interface with [ForRequirement]
REQ101 Error AC method has no matching spec method
REQ102 Warning Story has no specification (acceptable for small stories)
REQ103 Info Feature fully specified
REQ200 Error Spec interface has no implementing class
REQ201 Warning Implementation class missing [ForRequirement] attribute
REQ300 Error Feature has zero [TestsFor] test classes
REQ301 Warning AC has no [Verifies] test
REQ302 Error [Verifies] references an AC that doesn't exist (stale test)
REQ303 Info Feature fully tested

Stage 5: Reports

Generate markdown hierarchy reports, JSON export for external tools, and CSV traceability matrices.


The SharedKernel — Domain Types Shared Across Layers

The MegaCorp.SharedKernel project sits between Requirements and Specifications. It contains domain types used by specification interface signatures — types with behavior, not just the lightweight value types in Requirements.

// MegaCorp.SharedKernel/Order.cs
namespace MegaCorp.SharedKernel;

using MegaCorp.Requirements;

public record Order
{
    public OrderId Id { get; init; }
    public CustomerId Customer { get; init; }
    public IReadOnlyList<OrderLine> Lines { get; init; } = Array.Empty<OrderLine>();
    public Money Total { get; init; }
    public ShippingAddress ShippingAddress { get; init; } = null!;
    public bool DiscountApplied { get; init; }
    public PaymentId? PaymentReference { get; init; }
    public IReadOnlyList<ReservationId> ReservationReferences { get; init; } = Array.Empty<ReservationId>();
    public DateTimeOffset? PersistedAt { get; init; }

    public static Order FromCommand(CreateOrderCommand cmd) => new()
    {
        Id = new OrderId(Guid.NewGuid()),
        Customer = cmd.CustomerId,
        Lines = cmd.Items.Select(i => new OrderLine(i.Sku, i.Quantity, i.UnitPrice)).ToList(),
        Total = new Money(cmd.Items.Sum(i => i.Quantity.Value * i.UnitPrice.Amount), cmd.Currency),
        ShippingAddress = cmd.ShippingAddress
    };
}

public record OrderLine(ProductSku Sku, Quantity Quantity, Money UnitPrice);

public record CreateOrderCommand(
    CustomerId CustomerId,
    IReadOnlyList<OrderLineCommand> Items,
    string Currency,
    ShippingAddress ShippingAddress,
    string ActorId);

public record OrderLineCommand(ProductSku Sku, Quantity Quantity, Money UnitPrice);
// MegaCorp.SharedKernel/Payment.cs
namespace MegaCorp.SharedKernel;

using MegaCorp.Requirements;

public record Payment(
    PaymentId Id,
    OrderId OrderId,
    Money Amount,
    string StripeChargeId,
    DateTimeOffset CapturedAt);
// MegaCorp.SharedKernel/Reservation.cs
namespace MegaCorp.SharedKernel;

using MegaCorp.Requirements;

public record Reservation(
    ReservationId Id,
    ProductSku Sku,
    Quantity Quantity,
    OrderId OrderId,
    DateTimeOffset ExpiresAt)
{
    public DateTimeOffset CreatedAt { get; init; } = DateTimeOffset.UtcNow;
}
// MegaCorp.SharedKernel/Result.cs
namespace MegaCorp.SharedKernel;

/// <summary>
/// Strongly-typed result for domain operations.
/// Every specification method returns this — not bool, not void, not exceptions.
/// </summary>
public record Result
{
    public bool IsSuccess { get; }
    public string? Reason { get; }

    private Result(bool success, string? reason) { IsSuccess = success; Reason = reason; }

    public static Result Success() => new(true, null);
    public static Result Failure(string reason) => new(false, reason);
}

public record Result<T>
{
    public bool IsSuccess { get; }
    public T Value { get; }
    public string? Reason { get; }

    private Result(bool success, T value, string? reason)
    {
        IsSuccess = success;
        Value = value;
        Reason = reason;
    }

    public static Result<T> Success(T value) => new(true, value, null);
    public static Result<T> Failure(string reason) => new(false, default!, reason);
}
// MegaCorp.SharedKernel/NotificationReceipt.cs
namespace MegaCorp.SharedKernel;

using MegaCorp.Requirements;

public record NotificationReceipt(
    NotificationId Id,
    string ExternalMessageId,
    DateTimeOffset SentAt);

public record OrderConfirmation(
    OrderId OrderId,
    PaymentId PaymentId,
    int ReservationCount,
    Money Total);

The dependency chain:

MegaCorp.Requirements      ← Value types (OrderId, Money, etc.) — no behavior
       ↓
MegaCorp.SharedKernel      ← Domain types (Order, Payment, Result) — has behavior, uses value types
       ↓
MegaCorp.Specifications    ← Interfaces use both value types and domain types
       ↓
MegaCorp.OrderService (etc.) ← Implementations use domain types

SharedKernel references Requirements (for value types). Specifications references both. Implementation projects reference Specifications (and transitively, everything above). This keeps all layers using the same Order, Payment, Result types without duplication.


The Validator Bridge — AC Evaluation at Runtime

For compliance-sensitive operations (audit, regulatory), you may need to evaluate acceptance criteria at runtime — not just at compile time. The Specifications project includes validator bridges that connect the abstract Feature AC methods to the spec interface:

// MegaCorp.Specifications/OrderProcessing/OrderProcessingValidator.cs
namespace MegaCorp.Specifications.OrderProcessing;

using MegaCorp.Requirements.Features;
using MegaCorp.Requirements;
using MegaCorp.SharedKernel;

/// <summary>
/// Validator bridge: connects the abstract Feature AC methods to the ISpec interface.
/// Used for runtime AC evaluation in compliance mode.
///
/// In normal production flow, code calls IOrderProcessingSpec directly.
/// For audit/regulatory operations, the validator evaluates ACs explicitly.
/// </summary>
[ForRequirement(typeof(OrderProcessingFeature))]
public record OrderProcessingValidator : OrderProcessingFeature
{
    private readonly IOrderProcessingSpec _spec;

    public OrderProcessingValidator(IOrderProcessingSpec spec) => _spec = spec;

    public override string Title => "Order Processing";
    public override RequirementPriority Priority => RequirementPriority.Critical;
    public override string Owner => "order-team";

    public override AcceptanceCriterionResult OrderTotalMustBePositive(OrderSummary order)
    {
        var domainOrder = MapToDomain(order);
        var result = _spec.ValidateOrderTotal(domainOrder);
        return result.IsSuccess
            ? AcceptanceCriterionResult.Satisfied()
            : AcceptanceCriterionResult.Failed(result.Reason!);
    }

    public override AcceptanceCriterionResult BulkOrderDiscountApplied(
        OrderSummary order, Money originalTotal, Money discountedTotal)
    {
        var domainOrder = MapToDomain(order);
        var result = _spec.ApplyBulkDiscount(domainOrder);
        if (!result.IsSuccess)
            return AcceptanceCriterionResult.Failed(result.Reason!);

        return result.Value.Total.Amount < originalTotal.Amount
            ? AcceptanceCriterionResult.Satisfied()
            : AcceptanceCriterionResult.Failed("No discount was applied");
    }

    public override AcceptanceCriterionResult InventoryReservedBeforePayment(
        OrderSummary order, IReadOnlyList<StockReservation> reservations)
    {
        return reservations.Count > 0
            ? AcceptanceCriterionResult.Satisfied()
            : AcceptanceCriterionResult.Failed("No inventory reservations found");
    }

    public override AcceptanceCriterionResult PaymentCapturedAfterReservation(
        OrderSummary order, IReadOnlyList<StockReservation> reservations, PaymentResult payment)
    {
        if (!payment.Success)
            return AcceptanceCriterionResult.Failed($"Payment failed: {payment.FailureReason}");
        if (reservations.Count == 0)
            return AcceptanceCriterionResult.Failed("Payment captured without reservations");
        if (payment.Amount.Amount != order.Total.Amount)
            return AcceptanceCriterionResult.Failed(
                $"Payment amount {payment.Amount.Amount} != order total {order.Total.Amount}");
        return AcceptanceCriterionResult.Satisfied();
    }

    public override AcceptanceCriterionResult ConfirmationSentAfterPayment(
        OrderSummary order, PaymentResult payment, NotificationId notification)
    {
        return notification.Value != Guid.Empty
            ? AcceptanceCriterionResult.Satisfied()
            : AcceptanceCriterionResult.Failed("No confirmation notification sent");
    }

    public override AcceptanceCriterionResult AllOperationsAudited(
        OrderSummary order, IReadOnlyList<AuditEntry> auditEntries)
    {
        var requiredOps = new[] { "OrderCreated", "PaymentCaptured", "InventoryReserved" };
        var missing = requiredOps.Except(auditEntries.Select(e => e.Operation)).ToList();
        return missing.Count == 0
            ? AcceptanceCriterionResult.Satisfied()
            : AcceptanceCriterionResult.Failed($"Missing audit entries: {string.Join(", ", missing)}");
    }

    public override AcceptanceCriterionResult FailedPaymentReleasesInventory(
        OrderSummary order, PaymentResult failedPayment,
        IReadOnlyList<StockReservation> releasedReservations)
    {
        return releasedReservations.Count > 0
            ? AcceptanceCriterionResult.Satisfied()
            : AcceptanceCriterionResult.Failed("Reservations not released after payment failure");
    }

    public override AcceptanceCriterionResult OrderPersistedWithFullDetail(
        OrderSummary order, PaymentResult payment,
        IReadOnlyList<StockReservation> reservations)
    {
        // Check that all required data is present
        if (order.Lines.Count == 0)
            return AcceptanceCriterionResult.Failed("Order has no line items");
        if (!payment.Success)
            return AcceptanceCriterionResult.Failed("Cannot persist with failed payment");
        if (reservations.Count == 0)
            return AcceptanceCriterionResult.Failed("Cannot persist without reservations");
        return AcceptanceCriterionResult.Satisfied();
    }

    private static Order MapToDomain(OrderSummary summary) => new()
    {
        Id = summary.Id,
        Customer = summary.Customer,
        Lines = summary.Lines.Select(l =>
            new OrderLine(l.Sku, l.Quantity, l.UnitPrice)).ToList(),
        Total = summary.Total
    };
}

Two runtime modes:

// Mode 1: Normal production flow — call ISpec directly (most cases)
public class OrderController : ControllerBase
{
    private readonly IOrderProcessingSpec _spec;

    [HttpPost("orders")]
    [ForRequirement(typeof(OrderProcessingFeature))]
    public async Task<IActionResult> CreateOrder([FromBody] CreateOrderCommand cmd)
    {
        var result = await _spec.ProcessOrder(cmd);
        return result.IsSuccess
            ? Ok(result.Value)
            : BadRequest(new { error = result.Reason });
    }
}

// Mode 2: Compliance mode — validate ACs explicitly (audit, regulatory)
public class ComplianceService
{
    private readonly OrderProcessingValidator _validator;
    private readonly IAuditIntegrationSpec _audit;

    public async Task<ComplianceReport> AuditOrder(OrderSummary order,
        IReadOnlyList<StockReservation> reservations,
        PaymentResult payment,
        IReadOnlyList<AuditEntry> auditEntries)
    {
        var results = new List<(string AC, AcceptanceCriterionResult Result)>
        {
            ("AC-1", _validator.OrderTotalMustBePositive(order)),
            ("AC-2", _validator.InventoryReservedBeforePayment(order, reservations)),
            ("AC-3", _validator.PaymentCapturedAfterReservation(order, reservations, payment)),
            ("AC-5", _validator.AllOperationsAudited(order, auditEntries)),
            ("AC-8", _validator.OrderPersistedWithFullDetail(order, payment, reservations)),
        };

        foreach (var (ac, result) in results)
        {
            await _audit.RecordAuditTrail(
                new Order { Id = order.Id },
                $"ComplianceCheck:{ac}",
                "compliance-service",
                new { Satisfied = result.IsSatisfied, result.FailureReason });
        }

        return new ComplianceReport(
            Feature: nameof(OrderProcessingFeature),
            AcsChecked: results.Count,
            AcsPassed: results.Count(r => r.Result.IsSatisfied),
            Violations: results.Where(r => !r.Result.IsSatisfied)
                .Select(r => $"{r.AC}: {r.Result.FailureReason}").ToList());
    }
}

The API Host — OpenAPI Integration

The [ForRequirement] attributes on controllers and action methods can be used to enrich the OpenAPI schema:

// MegaCorp.Web/Controllers/OrderController.cs
namespace MegaCorp.Web.Controllers;

using MegaCorp.Requirements.Features;
using MegaCorp.Specifications.OrderProcessing;
using MegaCorp.SharedKernel;

[ApiController]
[Route("api/orders")]
[ForRequirement(typeof(OrderProcessingFeature))]
public class OrderController : ControllerBase
{
    private readonly IOrderProcessingSpec _spec;

    public OrderController(IOrderProcessingSpec spec) => _spec = spec;

    [HttpPost]
    [ForRequirement(typeof(OrderProcessingFeature),
        nameof(OrderProcessingFeature.OrderTotalMustBePositive))]
    [ForRequirement(typeof(OrderProcessingFeature),
        nameof(OrderProcessingFeature.InventoryReservedBeforePayment))]
    [ForRequirement(typeof(OrderProcessingFeature),
        nameof(OrderProcessingFeature.PaymentCapturedAfterReservation))]
    [ForRequirement(typeof(OrderProcessingFeature),
        nameof(OrderProcessingFeature.ConfirmationSentAfterPayment))]
    [ForRequirement(typeof(OrderProcessingFeature),
        nameof(OrderProcessingFeature.AllOperationsAudited))]
    [ForRequirement(typeof(OrderProcessingFeature),
        nameof(OrderProcessingFeature.OrderPersistedWithFullDetail))]
    public async Task<IActionResult> CreateOrder([FromBody] CreateOrderCommand command)
    {
        var result = await _spec.ProcessOrder(command);
        return result.IsSuccess
            ? CreatedAtAction(nameof(GetOrder), new { id = result.Value.OrderId.Value }, result.Value)
            : BadRequest(new ProblemDetails
            {
                Title = "Order creation failed",
                Detail = result.Reason,
                Status = 400
            });
    }

    [HttpGet("{id:guid}")]
    [ForRequirement(typeof(OrderProcessingFeature),
        nameof(OrderProcessingFeature.OrderPersistedWithFullDetail))]
    public async Task<IActionResult> GetOrder(Guid id)
    {
        // ...
    }
}

The generated OpenAPI schema includes requirement metadata:

{
  "paths": {
    "/api/orders": {
      "post": {
        "x-requirement": "OrderProcessingFeature",
        "x-acceptance-criteria": [
          "OrderTotalMustBePositive",
          "InventoryReservedBeforePayment",
          "PaymentCapturedAfterReservation",
          "ConfirmationSentAfterPayment",
          "AllOperationsAudited",
          "OrderPersistedWithFullDetail"
        ],
        "summary": "Create a new order",
        "description": "Processes a new order through the full chain: validation, inventory reservation, payment capture, persistence, audit, and notification.",
        "operationId": "CreateOrder",
        "requestBody": { ... },
        "responses": {
          "201": { "description": "Order created successfully" },
          "400": { "description": "Order creation failed — AC violation" }
        }
      }
    }
  }
}

API consumers can see which acceptance criteria each endpoint satisfies. This is especially valuable for:

  • API documentation — consumers understand what business rules the endpoint enforces
  • Contract testing — consumers can write tests against specific ACs
  • Change impact analysis — when an AC changes, affected endpoints are documented

Startup Compliance Check

The API host can validate requirement compliance at startup:

// MegaCorp.Web/RequirementComplianceCheck.cs
namespace MegaCorp.Web;

using MegaCorp.Requirements;

public class RequirementComplianceCheck : IHostedService
{
    private readonly ILogger<RequirementComplianceCheck> _logger;

    public RequirementComplianceCheck(ILogger<RequirementComplianceCheck> logger)
        => _logger = logger;

    public Task StartAsync(CancellationToken ct)
    {
        _logger.LogInformation("Checking requirement compliance...");

        foreach (var (type, entry) in TraceabilityMatrix.Entries)
        {
            var info = RequirementRegistry.All[type];
            var totalACs = info.AcceptanceCriteria.Length;
            var testedACs = entry.AcceptanceCriteriaCoverage.Count;
            var implementations = entry.Implementations.Length;

            if (implementations == 0)
            {
                _logger.LogError("{Feature}: NO IMPLEMENTATION FOUND",
                    info.Title);
            }
            else if (testedACs < totalACs)
            {
                var missing = info.AcceptanceCriteria
                    .Except(entry.AcceptanceCriteriaCoverage.Keys)
                    .ToList();
                _logger.LogWarning(
                    "{Feature}: {Tested}/{Total} ACs tested ({Missing} missing: {MissingList})",
                    info.Title, testedACs, totalACs, totalACs - testedACs,
                    string.Join(", ", missing));
            }
            else
            {
                _logger.LogInformation(
                    "{Feature}: all {Total} ACs covered, {Impls} implementations, {Tests} tests",
                    info.Title, totalACs, implementations, entry.Tests.Length);
            }
        }

        _logger.LogInformation("Requirement compliance check complete.");
        return Task.CompletedTask;
    }

    public Task StopAsync(CancellationToken ct) => Task.CompletedTask;
}

On startup, the application logs:

info: RequirementComplianceCheck[0]
      Checking requirement compliance...
info: RequirementComplianceCheck[0]
      Order Processing: all 8 ACs covered, 5 implementations, 19 tests
info: RequirementComplianceCheck[0]
      Payment Processing: all 5 ACs covered, 2 implementations, 12 tests
warn: RequirementComplianceCheck[0]
      Billing and Invoicing: 2/3 ACs tested (1 missing: CreditNoteOnRefund)
info: RequirementComplianceCheck[0]
      Inventory Management: all 4 ACs covered, 1 implementation, 8 tests
info: RequirementComplianceCheck[0]
      Customer Notifications: all 4 ACs covered, 1 implementation, 6 tests
info: RequirementComplianceCheck[0]
      Requirement compliance check complete.

The Structured Dependency Graph — After

Compare this to the spaghetti diagram from Part 1:

Diagram

The dependencies flow downward from Requirements through Specifications to Implementations. No spaghetti. No ServiceLocator star pattern. Every implementation project depends on Specifications (and transitively on Requirements). The dependency graph is a DAG with a clear direction.

The ServiceLocator is gone. There is no static god-object mediating between services. Each service receives its dependencies through constructor injection of typed specification interfaces. The DI container wires them — but the DI container is configured once in Program.cs, not scattered across 47 files via ServiceLocator.GetService<>().


Cross-Feature Specifications

Some features share specification interfaces. For example, IInventoryIntegrationSpec is used by both OrderProcessingFeature (for inventory reservation) and InventoryManagementFeature (for stock management). The interface is decorated with [ForRequirement] for both:

[ForRequirement(typeof(OrderProcessingFeature))]
[ForRequirement(typeof(InventoryManagementFeature))]
public interface IInventoryIntegrationSpec
{
    [ForRequirement(typeof(OrderProcessingFeature),
        nameof(OrderProcessingFeature.InventoryReservedBeforePayment))]
    [ForRequirement(typeof(InventoryManagementFeature),
        nameof(InventoryManagementFeature.StockDecrementedAtomically))]
    Task<Result<IReadOnlyList<Reservation>>> ReserveInventory(Order order);

    [ForRequirement(typeof(OrderProcessingFeature),
        nameof(OrderProcessingFeature.FailedPaymentReleasesInventory))]
    [ForRequirement(typeof(InventoryManagementFeature),
        nameof(InventoryManagementFeature.ReleasedReservationsRestoreStock))]
    Task<Result> ReleaseReservations(IReadOnlyList<Reservation> reservations);
}

One method satisfies ACs from two features. The traceability matrix records both links. "Find All References" on InventoryManagementFeature.StockDecrementedAtomically shows this spec method AND the ReserveInventory method on InventoryReservationService AND the tests that verify it. Cross-feature traceability — something that's impossible with string-based requirements in Jira.


The CI Pipeline — Three Enforcement Points

# .github/workflows/build.yml (or equivalent local build script)
jobs:
  build:
    steps:
      # 1. Compile — Roslyn analyzers run
      - run: dotnet build --warnaserror
        # REQ1xx: all features have specifications
        # REQ2xx: all specifications have implementations
        # REQ3xx: all ACs have tests
        # Any gap → build fails

      # 2. Test — verify implementations
      - run: dotnet test --collect:"XPlat Code Coverage" --logger:trx
        # Tests with [Verifies] execute against real implementations
        # Any test failure → build fails

      # 3. Quality gates — post-test analysis
      - run: dotnet quality-gates check
          --trx TestResults/*.trx
          --coverage TestResults/**/coverage.cobertura.xml
          --traceability obj/**/TraceabilityMatrix.g.cs
          --min-pass-rate 100
          --min-coverage 80
        # Checks:
        # - All [Verifies] tests passed
        # - Code coverage per AC meets threshold
        # - No stale tests referencing deleted ACs
        # Any gate failure → build fails

Three enforcement points, one build:

  1. dotnet build — Roslyn analyzers catch structural gaps (missing specs, missing implementations, missing tests)
  2. dotnet test — tests execute against implementations
  3. dotnet quality-gates — post-test analysis ensures tests are passing, coverage is sufficient

If any point fails, the build fails. The chain is enforced from requirement to quality-verified production code.


What This Achieves — The Before/After Comparison

Aspect Before (ServiceLocator Monorepo) After (Requirements as Projects)
"What is Order Processing?" Search Jira, read 6 tickets from 5 years Read OrderProcessingFeature — 8 ACs with typed signatures
"What code implements it?" Grep for "Order" — 247 files, 34 classes "Find All References" — 5 spec interfaces, 5 implementations
"What tests cover it?" Unknown — tests reference classes, not ACs 19 tests with [Verifies], 8/8 ACs covered
"Is it complete?" Ask the team, check Jira manually Compiler diagnostic: REQ303: 100% tested
Dependencies Hidden in ServiceLocator calls (520 runtime) Explicit in constructor injection (visible)
Add a new AC Add text to Jira, hope someone implements it Add abstract method → build breaks everywhere → teams implement
Rename an AC Find-replace in Jira + code + tests (fragile) Rename method → refactoring tools update all references
Delete an AC Remove from Jira, forget to remove code Remove abstract method → [Verifies] references break → stale tests removed
Onboarding 2.5 hours to understand one feature 1 minute: read Feature → Find All References
Audit compliance 3-day manual spreadsheet dotnet buildTraceabilityMatrix.g.cs

Why These Are Projects, Not Files

The .Requirements and .Specifications are separate .csproj projects — not folders within MegaCorp.Core, not namespaces, not attribute decorations on existing code. This is deliberate:

1. Zero-Dependency Root

MegaCorp.Requirements has zero project references. This means:

  • It compiles first, always
  • It never breaks because of changes in other projects
  • It can be published as a standalone NuGet package
  • It can be shared across multiple solutions
  • It is the stable root of the dependency tree

2. Compilation Unit Enforcement

A separate project means the compiler enforces visibility:

  • Implementation projects MUST add <ProjectReference> to see the requirements
  • The reference is explicit and auditable in the .csproj file
  • internal types in Requirements are truly internal — not accidentally leaked

3. Build Isolation

Changes to MegaCorp.Requirements trigger rebuilds only in projects that reference it. This is the correct behavior — a change in requirements SHOULD trigger a rebuild of all implementations. But changes to MegaCorp.OrderService do NOT trigger a rebuild of MegaCorp.Requirements.

4. Ownership Clarity

The project has an owner (the architecture team or the product team). Changes go through PR review. This is the most important project in the solution — it defines what the system does. It deserves the same review rigor as a database migration.

5. Versioning

As a separate project (or NuGet package), requirements can be versioned. Major version = breaking AC changes. Minor version = new features. Patch = metadata updates. Teams can update their implementations on their own schedule (within a build window).


Summary

This post introduced the two missing projects that transform an industrial monorepo from a physical-only pile of DLLs into a compiler-enforced architecture:

  1. MegaCorp.Requirements — Features as abstract records, ACs as abstract methods. Zero dependencies. The single source of truth for what the business requires.

  2. MegaCorp.Specifications — Interfaces per feature boundary, decorated with [ForRequirement]. The contracts that every implementation project must satisfy.

Together, they create a typed chain:

Requirement (abstract type)
  → Specification (interface with [ForRequirement])
    → Implementation (class : ISpec with [ForRequirement])
      → Test ([Verifies] with typeof + nameof)
        → Compiler diagnostics (REQ1xx, REQ2xx, REQ3xx)
          → Traceability matrix (generated)

Every link is compile-time. Every reference is typed. Every gap is a compiler diagnostic.


Side by Side: The Same Flow, Two Architectures

Let's trace the exact same operation — "create an order" — through both architectures. Same business logic, same services, same outcome. Different structures.

ServiceLocator Architecture (Before)

// 1. Controller resolves via IServiceProvider
[HttpPost("orders")]
public async Task<IActionResult> CreateOrder([FromBody] CreateOrderDto dto)
{
    var service = _sp.GetRequiredService<IOrderService>();
    var result = await service.ProcessOrder(dto);
    return result.Success ? Ok(result) : BadRequest(result);
}

// 2. OrderService resolves 9 dependencies at runtime
public class OrderService : IOrderService
{
    public async Task<OrderResult> ProcessOrder(CreateOrderDto dto)
    {
        var validator = ServiceLocator.GetService<IOrderValidator>();
        var inventory = ServiceLocator.GetService<IInventoryChecker>();
        var pricing = ServiceLocator.GetService<IPricingEngine>();
        var reserver = ServiceLocator.GetService<IInventoryReserver>();
        var payment = ServiceLocator.GetService<IPaymentProcessor>();
        var repo = ServiceLocator.GetService<IOrderRepository>();
        var eventBus = ServiceLocator.GetService<IEventBus>();
        var notifier = ServiceLocator.GetService<INotificationSender>();
        var audit = ServiceLocator.GetService<IAuditLogger>();

        // Validate
        var valid = validator.Validate(dto);
        if (!valid.IsValid) return OrderResult.Failed(valid.Errors);

        // Check inventory
        var stock = await inventory.CheckAvailability(dto.Items);
        if (!stock.AllAvailable) return OrderResult.Failed("Insufficient stock");

        // Calculate pricing
        var priced = pricing.Calculate(dto, stock.Reservations);

        // Reserve
        var reservationId = await reserver.Reserve(stock.Reservations);

        try
        {
            // Pay
            var payResult = await payment.Capture(new PaymentRequest
            {
                Amount = priced.Total, CustomerId = dto.CustomerId
            });
            if (!payResult.Success)
            {
                await reserver.Release(reservationId);
                return OrderResult.Failed(payResult.Error);
            }

            // Persist
            var order = Order.Create(dto, priced, payResult, reservationId);
            await repo.Save(order);

            // Event
            await eventBus.Publish(new OrderCreatedEvent { OrderId = order.Id });

            // Notify
            await notifier.Send(new OrderConfirmation { OrderId = order.Id });

            // Audit
            audit.Log("OrderCreated", new { order.Id });

            return OrderResult.Success(order.Id);
        }
        catch
        {
            await reserver.Release(reservationId);
            throw;
        }
    }
}

// 3. Test: 90 lines of mock setup, no AC linkage
[Test]
public async Task ProcessOrder_ValidInput_ReturnsSuccess()
{
    // Mock 9 services... (see Part 2 for full example)
    // No [Verifies] attribute. No AC reference.
    // This test exists in a vacuum.
}

What the compiler sees: OrderService has no dependencies. CreateOrder calls IOrderService. Everything else is invisible.

What breaks silently: Rename IPaymentProcessor.Capture → runtime explosion. Remove IAuditLogger registration → runtime explosion. Change IPricingEngine behavior → mocks hide the change.

Requirements-as-Projects Architecture (After)

// 1. Controller calls typed spec interface
[HttpPost("orders")]
[ForRequirement(typeof(OrderProcessingFeature))]
public async Task<IActionResult> CreateOrder([FromBody] CreateOrderCommand cmd)
{
    var result = await _spec.ProcessOrder(cmd);
    return result.IsSuccess
        ? CreatedAtAction(nameof(GetOrder), new { id = result.Value.OrderId.Value }, result.Value)
        : BadRequest(new ProblemDetails { Detail = result.Reason });
}

// 2. OrderProcessingService receives typed spec dependencies
[ForRequirement(typeof(OrderProcessingFeature))]
public class OrderProcessingService : IOrderProcessingSpec
{
    private readonly IInventoryIntegrationSpec _inventory;
    private readonly IPaymentIntegrationSpec _payment;
    private readonly INotificationIntegrationSpec _notification;
    private readonly IAuditIntegrationSpec _audit;
    private readonly IOrderRepository _orderRepo;

    public OrderProcessingService(
        IInventoryIntegrationSpec inventory,
        IPaymentIntegrationSpec payment,
        INotificationIntegrationSpec notification,
        IAuditIntegrationSpec audit,
        IOrderRepository orderRepo)
    {
        _inventory = inventory;
        _payment = payment;
        _notification = notification;
        _audit = audit;
        _orderRepo = orderRepo;
    }

    public async Task<Result<OrderConfirmation>> ProcessOrder(CreateOrderCommand command)
    {
        var order = Order.FromCommand(command);

        // AC-1
        var validation = ValidateOrderTotal(order);
        if (!validation.IsSuccess) return Result<OrderConfirmation>.Failure(validation.Reason!);

        // AC-7
        var discountResult = ApplyBulkDiscount(order);
        order = discountResult.Value;

        // AC-2
        var reservations = await _inventory.ReserveInventory(order);
        if (!reservations.IsSuccess) return Result<OrderConfirmation>.Failure(reservations.Reason!);

        try
        {
            // AC-3
            var payment = await _payment.CapturePayment(order, reservations.Value);
            if (!payment.IsSuccess)
            {
                // AC-6
                await _inventory.ReleaseReservations(reservations.Value);
                return Result<OrderConfirmation>.Failure(payment.Reason!);
            }

            // AC-8
            await PersistOrder(order, payment.Value, reservations.Value);

            // AC-5
            await _audit.RecordAuditTrail(order, "OrderCreated", command.ActorId, new { });

            // AC-4
            await _notification.SendOrderConfirmation(order, payment.Value);

            return Result<OrderConfirmation>.Success(new OrderConfirmation(
                order.Id, payment.Value.Id, reservations.Value.Count, order.Total));
        }
        catch
        {
            await _inventory.ReleaseReservations(reservations.Value);
            throw;
        }
    }

    // ... AC-1, AC-7, AC-8 method implementations (shown earlier)
}

// 3. Test: 15 lines, AC-linked, no mock explosion
[Test]
[Verifies(typeof(OrderProcessingFeature),
    nameof(OrderProcessingFeature.PaymentCapturedAfterReservation))]
public async Task Payment_captured_after_reservation()
{
    _stripeClient.Configure(succeed: true);
    var command = CreateOrderCommand(items: new[] { ("SKU-001", 2) });

    var result = await _service.ProcessOrder(command);

    Assert.That(result.IsSuccess, Is.True);
    Assert.That(_stripeClient.ChargeAttempts.Count, Is.EqualTo(1));
}

What the compiler sees: OrderProcessingService depends on 5 typed interfaces. Each method is linked to an AC via [ForRequirement]. The test is linked to AC-3 via [Verifies].

What breaks at compile time: Rename IPaymentIntegrationSpec.CapturePayment → compile error in OrderProcessingService. Remove IAuditIntegrationSpec → compile error (missing constructor parameter). Change IOrderProcessingSpec → compile error in every implementing class.

The Difference, Summarized

ServiceLocator:
  Controller → IServiceProvider → (runtime) → OrderService
  OrderService → ServiceLocator → (runtime) → 9 services → (runtime) → concrete implementations
  Test → Mock<IServiceProvider> → (runtime) → 9 mock objects

Requirements as Projects:
  Controller → IOrderProcessingSpec → (compile-time) → OrderProcessingService
  OrderProcessingService → IInventoryIntegrationSpec → (compile-time) → InventoryReservationService
                         → IPaymentIntegrationSpec → (compile-time) → StripePaymentService
                         → INotificationIntegrationSpec → (compile-time) → OrderNotificationService
                         → IAuditIntegrationSpec → (compile-time) → AuditService
  Test → [Verifies(typeof, nameof)] → (compile-time) → AC-3 of OrderProcessingFeature

Everything that was runtime-resolved is now compile-time-checked. Everything that was hidden is now visible. Everything that was a string is now a type.


Severity Configuration

The Roslyn analyzer severity is configurable per-project via .editorconfig:

# .editorconfig
[*.cs]

# ─── Requirements → Specifications ──────────────────────────────────
# Missing spec for a Feature: always an error
dotnet_diagnostic.REQ100.severity = error
# Missing spec method for an AC: always an error
dotnet_diagnostic.REQ101.severity = error
# Missing spec for a Story: warning (acceptable for small stories)
dotnet_diagnostic.REQ102.severity = warning
# Feature fully specified: informational
dotnet_diagnostic.REQ103.severity = suggestion

# ─── Specifications → Implementations ───────────────────────────────
# Spec interface with no implementation: error
dotnet_diagnostic.REQ200.severity = error
# Implementation missing [ForRequirement] class attribute: warning (loses traceability)
dotnet_diagnostic.REQ201.severity = warning
# Implementation missing [ForRequirement] method attributes: suggestion
dotnet_diagnostic.REQ202.severity = suggestion
# Spec fully implemented: informational
dotnet_diagnostic.REQ203.severity = suggestion

# ─── Implementations → Tests ────────────────────────────────────────
# Feature with zero [TestsFor] test classes: error in CI, warning in dev
dotnet_diagnostic.REQ300.severity = warning
# AC with no [Verifies] test: warning
dotnet_diagnostic.REQ301.severity = warning
# [Verifies] references a nonexistent AC: always an error (stale test)
dotnet_diagnostic.REQ302.severity = error
# Feature fully tested: informational
dotnet_diagnostic.REQ303.severity = suggestion

For CI, override to strict:

<!-- Directory.Build.props -->
<PropertyGroup Condition="'$(CI)' == 'true'">
  <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
  <WarningsAsErrors>REQ100;REQ101;REQ200;REQ300;REQ301</WarningsAsErrors>
</PropertyGroup>

This means:

  • Local development: warnings for missing tests (you're still working on it)
  • CI: errors for missing tests (the build fails if any AC is untested)

The Full Solution Structure — After

MegaCorp.sln
├── src/
│   ├── MegaCorp.Requirements/                    ← Zero deps. Features as types.
│   │   ├── RequirementMetadata.cs
│   │   ├── AcceptanceCriterionResult.cs
│   │   ├── DomainConcepts.cs
│   │   ├── Epics/
│   │   │   ├── ECommerceEpic.cs
│   │   │   ├── ComplianceEpic.cs
│   │   │   └── CustomerExperienceEpic.cs
│   │   ├── Features/
│   │   │   ├── OrderProcessingFeature.cs8 ACs
│   │   │   ├── PaymentProcessingFeature.cs5 ACs
│   │   │   ├── InventoryManagementFeature.cs4 ACs
│   │   │   ├── CustomerNotificationFeature.cs4 ACs
│   │   │   ├── BillingFeature.cs3 ACs
│   │   │   ├── UserManagementFeature.cs6 ACs
│   │   │   ├── ReportingFeature.cs3 ACs
│   │   │   └── SearchFeature.cs4 ACs
│   │   ├── Stories/
│   │   │   └── ...
│   │   └── Bugs/
│   │       └── ...
│   │
│   ├── MegaCorp.SharedKernel/                     ← Domain types (Order, Payment, Result)
│   │   ├── Order.cs
│   │   ├── Payment.cs
│   │   ├── Reservation.cs
│   │   ├── Result.cs
│   │   └── ...
│   │
│   ├── MegaCorp.Specifications/                   ← Interfaces per feature boundary
│   │   ├── OrderProcessing/
│   │   │   ├── IOrderProcessingSpec.cs
│   │   │   ├── IInventoryIntegrationSpec.cs
│   │   │   ├── IPaymentIntegrationSpec.cs
│   │   │   ├── INotificationIntegrationSpec.cs
│   │   │   ├── IAuditIntegrationSpec.cs
│   │   │   └── OrderProcessingValidator.cs        ← Validator bridge
│   │   ├── Payment/
│   │   │   └── IPaymentProcessingSpec.cs
│   │   ├── Inventory/
│   │   │   └── IInventoryManagementSpec.cs
│   │   ├── Notification/
│   │   │   └── ICustomerNotificationSpec.cs
│   │   ├── Billing/
│   │   │   └── IBillingSpec.cs
│   │   ├── UserManagement/
│   │   │   └── IUserManagementSpec.cs
│   │   ├── Reporting/
│   │   │   └── IReportingSpec.cs
│   │   └── Search/
│   │       └── ISearchSpec.cs
│   │
│   ├── MegaCorp.OrderService/                     ← : IOrderProcessingSpec
│   │   ├── OrderProcessingService.cs
│   │   └── MegaCorp.OrderService.csproj
│   │
│   ├── MegaCorp.PaymentGateway/                   ← : IPaymentIntegrationSpec
│   │   ├── StripePaymentService.cs
│   │   └── MegaCorp.PaymentGateway.csproj
│   │
│   ├── MegaCorp.InventoryService/                 ← : IInventoryIntegrationSpec
│   │   ├── InventoryReservationService.cs
│   │   └── MegaCorp.InventoryService.csproj
│   │
│   ├── MegaCorp.NotificationService/              ← : INotificationIntegrationSpec
│   │   ├── OrderNotificationService.cs
│   │   └── MegaCorp.NotificationService.csproj
│   │
│   ├── MegaCorp.BillingService/                   ← : IBillingSpec
│   │   ├── BillingService.cs
│   │   └── MegaCorp.BillingService.csproj
│   │
│   ├── MegaCorp.AuditService/                     ← : IAuditIntegrationSpec
│   │   ├── AuditService.cs
│   │   └── MegaCorp.AuditService.csproj
│   │
│   ├── MegaCorp.UserService/                      ← : IUserManagementSpec
│   │   └── ...
│   │
│   ├── MegaCorp.ReportingService/                 ← : IReportingSpec
│   │   └── ...
│   │
│   ├── MegaCorp.SearchService/                    ← : ISearchSpec
│   │   └── ...
│   │
│   ├── MegaCorp.Data/                             ← EF Core repositories
│   │   └── ...
│   │
│   ├── MegaCorp.EventBus/                         ← Message broker
│   │   └── ...
│   │
│   ├── MegaCorp.CacheService/                     ← Redis caching
│   │   └── ...
│   │
│   ├── MegaCorp.Web/                              ← API host + DI wiring
│   │   ├── Controllers/
│   │   │   ├── OrderController.cs[ForRequirement(typeof(OrderProcessingFeature))]
│   │   │   ├── PaymentController.cs
│   │   │   └── ...
│   │   ├── Program.cs                             ← Clean DI wiring (no ServiceLocator)
│   │   ├── RequirementComplianceCheck.cs          ← Startup compliance
│   │   └── ...
│   │
│   └── MegaCorp.Worker/                           ← Background job host
│       └── ...
│
├── test/
│   ├── MegaCorp.OrderService.Tests/               ← [TestsFor(typeof(OrderProcessingFeature))]
│   │   └── OrderProcessingTests.cs19 tests, 8/8 ACs covered
│   ├── MegaCorp.PaymentGateway.Tests/             ← [TestsFor(typeof(PaymentProcessingFeature))]
│   │   └── ...
│   ├── MegaCorp.InventoryService.Tests/           ← [TestsFor(typeof(InventoryManagementFeature))]
│   │   └── ...
│   ├── MegaCorp.NotificationService.Tests/
│   │   └── ...
│   ├── MegaCorp.BillingService.Tests/
│   │   └── ...
│   ├── MegaCorp.TestHelpers/                      ← In-memory repositories, fakes
│   │   ├── InMemoryStockRepository.cs
│   │   ├── InMemoryOrderRepository.cs
│   │   ├── FakeStripeClient.cs
│   │   ├── FakeEmailSender.cs
│   │   └── FakeTemplateEngine.cs
│   └── MegaCorp.Integration.Tests/
│       └── ...
│
├── tools/
│   └── MegaCorp.Requirements.Analyzers/           ← Roslyn source generator + analyzers
│       ├── RequirementCollector.cs                 ← Stage 1
│       ├── RegistryGenerator.cs                    ← Stage 2
│       ├── AttributeGenerator.cs                   ← Stage 3
│       ├── TraceabilityMatrixGenerator.cs          ← Stage 4
│       ├── ReportGenerator.cs                      ← Stage 5
│       └── Diagnostics/
│           ├── RequirementCoverageDiagnostics.cs   ← REQ1xx
│           ├── ImplementationDiagnostics.cs        ← REQ2xx
│           └── TestCoverageDiagnostics.cs          ← REQ3xx
│
├── .editorconfig                                   ← Analyzer severity per-project
├── Directory.Build.props                           ← CI strictness overrides
└── MegaCorp.sln

The Numbers — After

Metric Before (ServiceLocator) After (Requirements as Projects)
Total .csproj files 50 52 (+Requirements, +Specifications)
<ProjectReference> edges ~120 ~135 (+15 for spec references)
ServiceLocator.GetService<> calls ~340 0
IServiceProvider.GetRequiredService<> calls ~180 0
Total hidden dependencies ~520 0
Constructor-injected typed dependencies ~80 ~250 (all visible)
Features defined as types 0 8
Total acceptance criteria 0 (in Jira) 37 (in code)
ACs with spec methods 0 37
ACs with tests 0 (unknown) 37
Compiler diagnostics for requirement gaps 0 REQ1xx, REQ2xx, REQ3xx
TraceabilityMatrix.g.cs Does not exist Auto-generated, CI-verified

The Generated Reports

Markdown Hierarchy Report

# MegaCorp Requirement Hierarchy

## Epic: E-Commerce Platform
- Owner: platform-team
- Priority: Critical

### Feature: Order Processing (8 ACs)
- Owner: order-team
- Priority: Critical
- Implementations: 5 (OrderProcessingService, InventoryReservationService,
  StripePaymentService, OrderNotificationService, AuditService)
- Tests: 19 (8/8 ACs covered, 100%)

| AC | Name | Spec | Impl | Tests |
|----|------|------|------|-------|
| 1 | OrderTotalMustBePositive | IOrderProcessingSpec.ValidateOrderTotal | OrderProcessingService | 4 |
| 2 | InventoryReservedBeforePayment | IInventoryIntegrationSpec.ReserveInventory | InventoryReservationService | 2 |
| 3 | PaymentCapturedAfterReservation | IPaymentIntegrationSpec.CapturePayment | StripePaymentService | 2 |
| 4 | ConfirmationSentAfterPayment | INotificationIntegrationSpec.SendOrderConfirmation | OrderNotificationService | 1 |
| 5 | AllOperationsAudited | IAuditIntegrationSpec.RecordAuditTrail | AuditService | 1 |
| 6 | FailedPaymentReleasesInventory | IInventoryIntegrationSpec.ReleaseReservations | InventoryReservationService | 1 |
| 7 | BulkOrderDiscountApplied | IOrderProcessingSpec.ApplyBulkDiscount | OrderProcessingService | 2 |
| 8 | OrderPersistedWithFullDetail | IOrderProcessingSpec.PersistOrder | OrderProcessingService | 1 |

### Feature: Payment Processing (5 ACs)
- Owner: payment-team
- Tests: 12 (4/5 ACs covered, 80%)
- ⚠ Missing test: NoPciDataStored

### Feature: Inventory Management (4 ACs)
- Owner: inventory-team
- Tests: 8 (4/4 ACs covered, 100%)

### Feature: Customer Notifications (4 ACs)
- Owner: cx-team
- Tests: 6 (4/4 ACs covered, 100%)

### Feature: Billing and Invoicing (3 ACs)
- Owner: billing-team
- Tests: 4 (2/3 ACs covered, 67%)
- ⚠ Missing spec: CreditNoteOnRefund
- ⚠ Missing test: CreditNoteOnRefund

## Epic: Customer Experience
### Feature: ...

## Epic: Regulatory Compliance
### Feature: ...

---
Summary: 8 features, 37 ACs, 35/37 tested (94.6%)
⚠ 2 gaps: PaymentProcessingFeature.NoPciDataStored, BillingFeature.CreditNoteOnRefund

JSON Export (for CI/CD integration)

{
  "generated": "2026-03-26T14:30:00Z",
  "summary": {
    "features": 8,
    "totalACs": 37,
    "testedACs": 35,
    "coveragePercent": 94.6,
    "implementations": 12,
    "totalTests": 68
  },
  "features": [
    {
      "type": "OrderProcessingFeature",
      "title": "Order Processing",
      "owner": "order-team",
      "priority": "Critical",
      "parent": "ECommerceEpic",
      "acceptanceCriteria": [
        {
          "name": "OrderTotalMustBePositive",
          "spec": "IOrderProcessingSpec.ValidateOrderTotal",
          "implementation": "OrderProcessingService.ValidateOrderTotal",
          "tests": 4,
          "status": "covered"
        },
        {
          "name": "InventoryReservedBeforePayment",
          "spec": "IInventoryIntegrationSpec.ReserveInventory",
          "implementation": "InventoryReservationService.ReserveInventory",
          "tests": 2,
          "status": "covered"
        }
      ],
      "metrics": {
        "acCount": 8,
        "acTested": 8,
        "coveragePercent": 100,
        "implementations": 5,
        "tests": 19
      }
    }
  ],
  "violations": [
    {
      "feature": "PaymentProcessingFeature",
      "ac": "NoPciDataStored",
      "type": "missing_test",
      "severity": "warning"
    },
    {
      "feature": "BillingFeature",
      "ac": "CreditNoteOnRefund",
      "type": "missing_spec",
      "severity": "error"
    }
  ]
}

This JSON can be consumed by:

  • CI dashboards — display coverage trends over time
  • Jira integration — sync requirement status back to tickets
  • Slack notifications — alert when coverage drops below threshold
  • Management reports — "94.6% of acceptance criteria are verified"

How Features Compose — The Epic View

Individual features are useful. But the real power appears when you look at the Epic level — the strategic goal that aggregates multiple features.

The ECommerceEpic has 5 features under it:

// All of these inherit Feature<ECommerceEpic>:
public abstract record OrderProcessingFeature : Feature<ECommerceEpic> { ... }    // 8 ACs
public abstract record PaymentProcessingFeature : Feature<ECommerceEpic> { ... }  // 5 ACs
public abstract record InventoryManagementFeature : Feature<ECommerceEpic> { ... } // 4 ACs
public abstract record BillingFeature : Feature<ECommerceEpic> { ... }            // 3 ACs
public abstract record SearchFeature : Feature<ECommerceEpic> { ... }             // 4 ACs

The generic constraint Feature<ECommerceEpic> is compiler-checked. You cannot accidentally parent a Feature under the wrong Epic:

// This won't compile:
public abstract record OrderProcessingFeature : Feature<ComplianceEpic> { ... }
// The constraint is: where TParent : Epic
// ComplianceEpic IS an Epic, so this would compile syntactically,
// but the organizational meaning is wrong.
// The code review catches this — the feature type is the most reviewed code in the repo.

The generated RequirementRegistry includes the hierarchy, so you can query it:

// Find all features under the E-Commerce Epic
var ecommerceFeatures = RequirementRegistry.All.Values
    .Where(r => r.Kind == RequirementKind.Feature && r.Parent == typeof(ECommerceEpic))
    .ToList();

// Result: OrderProcessing, PaymentProcessing, InventoryManagement, Billing, Search
// Total ACs: 8 + 5 + 4 + 3 + 4 = 24

// Find all features across all Epics with their coverage
var coverage = RequirementRegistry.All.Values
    .Where(r => r.Kind == RequirementKind.Feature)
    .Select(r => new
    {
        Feature = r.Title,
        TotalACs = r.AcceptanceCriteria.Length,
        TestedACs = TraceabilityMatrix.Entries.TryGetValue(r.Type, out var entry)
            ? entry.AcceptanceCriteriaCoverage.Count : 0
    })
    .ToList();

// This is what the management dashboard shows:
// E-Commerce Platform: 24 ACs, 22 tested (91.7%)
// Customer Experience: 4 ACs, 4 tested (100%)
// Regulatory Compliance: 9 ACs, 9 tested (100%)

The Epic Dashboard

The generated JSON can feed a dashboard that shows requirement coverage at the Epic level:

Epic: E-Commerce Platform
  ████████████████████░░ 91.7% (22/24 ACs tested)

  ✓ Order Processing          ████████████████████ 100% (8/8)
  ⚠ Payment Processing        ████████████████░░░░  80% (4/5)
  ✓ Inventory Management      ████████████████████ 100% (4/4)
  ⚠ Billing and Invoicing     █████████████░░░░░░░  67% (2/3)
  ✓ Search                    ████████████████████ 100% (4/4)

Epic: Customer Experience
  ████████████████████ 100% (4/4 ACs tested)

  ✓ Customer Notifications    ████████████████████ 100% (4/4)

Epic: Regulatory Compliance
  ████████████████████ 100% (9/9 ACs tested)

This is generated from the TraceabilityMatrix.g.cs — not from Jira, not from a spreadsheet, not from a manual audit. It's compiler-verified. It updates on every build.


The Requirement Lifecycle

Requirements are not static. They move through a lifecycle: from Draft through Approval to Implementation to Done. The lifecycle is modeled as a state machine, generated by the source generator:

// MegaCorp.Requirements/RequirementLifecycleState.cs
namespace MegaCorp.Requirements;

public enum RequirementLifecycleState : byte
{
    Draft = 0,        // Initial state — PM is writing the feature
    Proposed = 1,     // Feature submitted for review
    Approved = 2,     // Stakeholders approved — ready for implementation
    InProgress = 3,   // Development has started
    Quality = 4,      // Implementation done — QA reviewing
    Translation = 5,  // Localization (if applicable)
    Review = 6,       // Final review before release
    Done = 7          // Released to production
}
// Generated: RequirementLifecycleStateMachine.g.cs
namespace MegaCorp.Requirements;

public static class RequirementLifecycleStateMachine
{
    private static readonly Dictionary<RequirementLifecycleState, RequirementLifecycleState[]>
        _validTransitions = new()
    {
        [RequirementLifecycleState.Draft] = new[]
            { RequirementLifecycleState.Proposed },
        [RequirementLifecycleState.Proposed] = new[]
            { RequirementLifecycleState.Approved, RequirementLifecycleState.Draft },
        [RequirementLifecycleState.Approved] = new[]
            { RequirementLifecycleState.InProgress, RequirementLifecycleState.Proposed },
        [RequirementLifecycleState.InProgress] = new[]
            { RequirementLifecycleState.Quality, RequirementLifecycleState.Approved },
        [RequirementLifecycleState.Quality] = new[]
            { RequirementLifecycleState.Translation, RequirementLifecycleState.Review,
              RequirementLifecycleState.InProgress },
        [RequirementLifecycleState.Translation] = new[]
            { RequirementLifecycleState.Review, RequirementLifecycleState.Quality },
        [RequirementLifecycleState.Review] = new[]
            { RequirementLifecycleState.Done, RequirementLifecycleState.InProgress },
        [RequirementLifecycleState.Done] = Array.Empty<RequirementLifecycleState>(),
    };

    public static bool CanTransition(
        RequirementLifecycleState from, RequirementLifecycleState to)
        => _validTransitions.TryGetValue(from, out var valid) && valid.Contains(to);

    public static IReadOnlyList<RequirementLifecycleState> ValidTransitions(
        RequirementLifecycleState from)
        => _validTransitions.TryGetValue(from, out var valid)
            ? valid
            : Array.Empty<RequirementLifecycleState>();
}

The lifecycle state can be tracked per-feature in the source generator's metadata:

// In a requirements configuration file or attribute
[RequirementLifecycle(RequirementLifecycleState.InProgress)]
public abstract record OrderProcessingFeature : Feature<ECommerceEpic> { ... }

[RequirementLifecycle(RequirementLifecycleState.Quality)]
public abstract record PaymentProcessingFeature : Feature<ECommerceEpic> { ... }

[RequirementLifecycle(RequirementLifecycleState.Draft)]
public abstract record LoyaltyFeature : Feature<CustomerExperienceEpic> { ... }

The generated reports include lifecycle state:

Feature                    State        ACs  Tested  Coverage
─────────────────────────────────────────────────────────────
Order Processing           InProgress    8     8      100%
Payment Processing         Quality       5     4       80%  ⚠
Inventory Management       Review        4     4      100%
Billing and Invoicing      InProgress    3     2       67%  ⚠
Customer Notifications     Done          4     4      100%
Search                     InProgress    4     4      100%
Loyalty (NEW)              Draft         0     0        —
User Management            Done          6     6      100%

Features in Draft state don't generate compiler errors for missing specs or tests — they're not ready for implementation yet. Features in Quality or Review state generate errors for any gap — they must be fully specified, implemented, and tested before progressing.


Integration with DDD DSL

For teams using the CMF's DDD DSL, the Requirements chain integrates naturally:

// A DDD aggregate that also implements a requirement spec
[ForRequirement(typeof(OrderProcessingFeature))]
[AggregateRoot]
public partial class OrderAggregate : IOrderProcessingSpec
{
    [ForRequirement(typeof(OrderProcessingFeature),
        nameof(OrderProcessingFeature.OrderTotalMustBePositive))]
    public Result ValidateOrderTotal(Order order)
    {
        // The DDD generator produces Entity, Repository, and CQRS handlers
        // The Requirements analyzer generates traceability
        // Both work together — both are Roslyn source generators
    }
}

The DDD generator and the Requirements analyzer coexist as Roslyn analyzers. They see the same syntax tree. They produce complementary artifacts:

  • DDD generator: OrderAggregateRepository.g.cs, CreateOrderCommandHandler.g.cs
  • Requirements analyzer: RequirementRegistry.g.cs, TraceabilityMatrix.g.cs

The [ForRequirement] attribute doesn't conflict with DDD attributes. They compose. A single class can be both an aggregate root AND a requirement implementation.


Integration with Workflow DSL

For features that need approval workflows:

[ForRequirement(typeof(OrderProcessingFeature))]
[Workflow(Name = "FeatureApproval",
    Stages = new[] { "Design", "Implement", "Test", "Review" })]
public partial class OrderProcessingWorkflow { }

// Workflow rules reference requirement compliance:
[WorkflowRule(Stage = "Test",
    Condition = "All ACs have [Verifies] tests")]
[WorkflowRule(Stage = "Review",
    Condition = "Test coverage >= 80%")]

The Workflow DSL generates the workflow engine. The Requirements analyzer provides the compliance data. The workflow rule "All ACs have [Verifies] tests" is checked against the TraceabilityMatrix — not a manual assertion.


The M3/M2/M1/M0 Mapping

For those familiar with the CMF metamodeling architecture, here's how Requirements as Projects maps to the meta-metamodel:

Level In CMF In Requirements as Projects
M3 MetaConcept, MetaProperty RequirementMetadata, Feature<T>, Story<T>, AcceptanceCriterionResult — the base types
M2 [AggregateRoot], [Entity] OrderProcessingFeature, PaymentProcessingFeature — concrete requirement types
M1 Generated entities, repos, CQRS RequirementRegistry.g.cs, TraceabilityMatrix.g.cs, ForRequirementAttribute.g.cs
M0 Runtime instances OrderProcessingService processing actual Order objects at runtime

The source generator operates between M2 and M1: it reads the concrete requirement types (M2) and generates the registry, traceability matrix, and compiler diagnostics (M1). The implementations (M0) are written by developers and verified by the M1 artifacts.


Summary

This post introduced the two missing projects that transform an industrial monorepo from a physical-only pile of DLLs into a compiler-enforced architecture:

  1. MegaCorp.Requirements — Features as abstract records, ACs as abstract methods. Zero dependencies. The single source of truth for what the business requires.

  2. MegaCorp.Specifications — Interfaces per feature boundary, decorated with [ForRequirement]. The contracts that every implementation project must satisfy.

Together with SharedKernel (domain types), Requirements.Analyzers (Roslyn source generator), and the generated artifacts (RequirementRegistry.g.cs, TraceabilityMatrix.g.cs), they create a typed chain:

Requirement (abstract type)
  → Specification (interface with [ForRequirement])
    → Implementation (class : ISpec with [ForRequirement])
      → Test ([Verifies] with typeof + nameof)
        → Compiler diagnostics (REQ1xx, REQ2xx, REQ3xx)
          → Traceability matrix (generated)
            → Reports (markdown, JSON, CSV)

Every link is compile-time. Every reference is typed. Every gap is a compiler diagnostic. The ServiceLocator is gone. The hidden dependency graph is gone. The phantom chains are gone.

What remains is architecture — real architecture, enforced by the compiler, navigable in the IDE, and verifiable by the build system.

The next post shows what happens when you apply this to 50+ projects with 15 teams: the AC cascade, feature traceability across the entire monorepo, and the compiler as cross-team coordination mechanism.


Previous: Part 2 — Physical Boundaries Are Not Architecture

Next: Part 4 — What Changes at 50+ Projects