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

What Changes at 50+ Projects

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

Add one abstract method to a Feature type. The build breaks across 5 teams. The compiler just became your cross-team coordination mechanism.


Part 3 showed how .Requirements and .Specifications projects create typed boundaries in a monorepo. This post answers the question that matters at industrial scale: what actually happens when 15 teams, 50 projects, and 200 features operate under this architecture?

The answer centers on three phenomena:

  1. The AC Cascade — how a single acceptance criterion change propagates through the entire monorepo
  2. Feature Traceability — how you trace a business feature across 15+ projects
  3. Multi-Team Ownership — how teams coordinate through the compiler instead of Jira tickets

The AC Cascade

This is the defining characteristic of Requirements as Projects at scale. When a PM adds a new acceptance criterion to a Feature, the build breaks — deliberately, precisely, and informatively — across every project that needs to change.

Scenario: PM Adds AC-9 to Order Processing

The PM says: "We need loyalty points credited after every order." The developer adds an abstract method to OrderProcessingFeature:

// MegaCorp.Requirements/Features/OrderProcessingFeature.cs
// New AC added by the order-team:

/// <summary>
/// AC-9: Customer loyalty points are credited after successful order.
/// Points = floor(order total / 10). Credited within 5 seconds of payment capture.
/// </summary>
public abstract AcceptanceCriterionResult LoyaltyPointsCredited(
    OrderSummary order,
    CustomerId customer,
    int pointsEarned,
    TimeSpan elapsed);

The developer commits this change to a PR. The CI build runs. Here's what happens:

Diagram

The Build Output

Build started...

MegaCorp.Requirements → Build succeeded.
MegaCorp.SharedKernel → Build succeeded.

MegaCorp.Specifications →
  error REQ101: OrderProcessingFeature.LoyaltyPointsCredited has no matching
                specification method with [ForRequirement(typeof(OrderProcessingFeature),
                nameof(OrderProcessingFeature.LoyaltyPointsCredited))]
                → Add a method to a spec interface (e.g., IOrderProcessingSpec or a new
                  ILoyaltyIntegrationSpec) with the matching [ForRequirement] attribute.
  Build FAILED.

MegaCorp.OrderService(skipped — depends on MegaCorp.Specifications which failed)

MegaCorp.OrderService.Tests(skipped — depends on MegaCorp.OrderService which was skipped)

Build: 2 succeeded, 1 failed, 3 skipped.

The build fails in MegaCorp.Specifications — the first project downstream of MegaCorp.Requirements. The error is precise: it names the feature, the AC, and tells you exactly what to do.

Step-by-Step Resolution

Step 1: Create the spec method.

The team decides loyalty points need a new spec interface (because the implementation will be in a separate service):

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

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

/// <summary>
/// Specification for loyalty point operations within order processing.
/// Implemented by: MegaCorp.UserService (loyalty is a user-domain concept).
/// </summary>
[ForRequirement(typeof(OrderProcessingFeature))]
public interface ILoyaltyIntegrationSpec
{
    [ForRequirement(typeof(OrderProcessingFeature),
        nameof(OrderProcessingFeature.LoyaltyPointsCredited))]
    Task<Result> CreditLoyaltyPoints(Order order, int points);
}

Build again:

MegaCorp.Specifications → Build succeeded. (REQ101 resolved)

MegaCorp.OrderService →
  error CS0535: 'OrderProcessingService' does not implement interface member
                'IOrderProcessingSpec.ProcessOrder' — wait, that's already implemented.

  Actually, the build succeeds for OrderService because it doesn't implement
  ILoyaltyIntegrationSpec. But:

  error REQ200: ILoyaltyIntegrationSpec has no implementing class.
                → Create a class that implements ILoyaltyIntegrationSpec.
  Build FAILED.

Step 2: Implement the spec.

The user-team creates the implementation:

// MegaCorp.UserService/LoyaltyPointService.cs
namespace MegaCorp.UserService;

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

[ForRequirement(typeof(OrderProcessingFeature))]
public class LoyaltyPointService : ILoyaltyIntegrationSpec
{
    private readonly ILoyaltyRepository _repo;

    public LoyaltyPointService(ILoyaltyRepository repo) => _repo = repo;

    [ForRequirement(typeof(OrderProcessingFeature),
        nameof(OrderProcessingFeature.LoyaltyPointsCredited))]
    public async Task<Result> CreditLoyaltyPoints(Order order, int points)
    {
        if (points <= 0)
            return Result.Success(); // No points to credit

        await _repo.AddPoints(order.Customer, points, $"Order {order.Id.Value}");
        return Result.Success();
    }
}

Step 3: Wire it into the orchestrator.

The order-team updates OrderProcessingService to call the new spec:

// MegaCorp.OrderService/OrderProcessingService.cs — updated constructor
public OrderProcessingService(
    IInventoryIntegrationSpec inventory,
    IPaymentIntegrationSpec payment,
    INotificationIntegrationSpec notification,
    IAuditIntegrationSpec audit,
    ILoyaltyIntegrationSpec loyalty,  // ← NEW
    IOrderRepository orderRepo)
{
    // ...
    _loyalty = loyalty;
}

// In ProcessOrder, after payment capture:
// AC-9: Credit loyalty points
var points = (int)Math.Floor(order.Total.Amount / 10m);
await _loyalty.CreditLoyaltyPoints(order, points);

Step 4: Register in DI.

// MegaCorp.Web/Program.cs
builder.Services.AddScoped<ILoyaltyIntegrationSpec, LoyaltyPointService>();

Step 5: Write the test.

// MegaCorp.OrderService.Tests/OrderProcessingTests.cs
[Test]
[Verifies(typeof(OrderProcessingFeature),
    nameof(OrderProcessingFeature.LoyaltyPointsCredited))]
public async Task Loyalty_points_credited_after_order()
{
    _stripeClient.Configure(succeed: true);
    var command = CreateOrderCommand(items: new[] { ("SKU-001", 5) },
        unitPrice: 20.00m); // Total: 100 → 10 points

    var result = await _service.ProcessOrder(command);

    Assert.That(result.IsSuccess, Is.True);
    Assert.That(_loyaltyRepo.GetPoints(command.CustomerId), Is.EqualTo(10));
}

[Test]
[Verifies(typeof(OrderProcessingFeature),
    nameof(OrderProcessingFeature.LoyaltyPointsCredited))]
public async Task No_loyalty_points_for_small_order()
{
    _stripeClient.Configure(succeed: true);
    var command = CreateOrderCommand(items: new[] { ("SKU-001", 1) },
        unitPrice: 5.00m); // Total: 5 → 0 points

    var result = await _service.ProcessOrder(command);

    Assert.That(result.IsSuccess, Is.True);
    Assert.That(_loyaltyRepo.GetPoints(command.CustomerId), Is.EqualTo(0));
}

Final build:

MegaCorp.Requirements     → Build succeeded.
MegaCorp.SharedKernel     → Build succeeded.
MegaCorp.Specifications   → Build succeeded. info REQ103: OrderProcessingFeature — 9/9 ACs specified
MegaCorp.UserService      → Build succeeded. info REQ203: ILoyaltyIntegrationSpec — implemented
MegaCorp.OrderService     → Build succeeded.
MegaCorp.OrderService.Tests → Build succeeded. info REQ303: OrderProcessingFeature — 9/9 ACs tested

Build: 6 succeeded, 0 failed.

The Cascade Timeline

Diagram

Total time from PM request to verified production code: ~2 hours. Two teams worked in parallel (user-team on LoyaltyPointService, order-team on OrderProcessingService update). The compiler coordinated them — no Jira ticket handoffs, no Slack messages asking "are you done with your part?"

What Didn't Happen

In the ServiceLocator monorepo, the same change would have gone like this:

  1. PM creates Jira ticket MEGA-6001 assigned to order-team
  2. Order-team adds loyalty logic to OrderService.ProcessOrder via ServiceLocator
  3. Order-team resolves ILoyaltyService — but who implements it?
  4. Order-team creates MEGA-6002 sub-task for user-team to implement LoyaltyService
  5. User-team picks it up next sprint (1-2 week delay)
  6. User-team implements it, doesn't know about the mock in OrderService tests
  7. QA finds that the test mocks ILoyaltyService and always returns success
  8. Integration bug discovered in staging 3 weeks later
  9. Hotfix, rollback, post-mortem

Total time: 3-4 weeks. With compiler coordination: 2 hours.


Feature Traceability Across 50+ Projects

In a 50-project monorepo, a single feature like Order Processing touches many projects. With Requirements as Projects, every touch point is compiler-verified and IDE-navigable.

The Full Traceability Chain for Order Processing

Diagram

One feature. Six specification interfaces. Six implementation classes in six different projects. One API controller. One background worker. Three test projects. Three generated artifacts. 15 touch points across the monorepo — every one of them linked by typeof() and nameof(), compiler-verified, IDE-navigable.

"Find All References" at Scale

In a 50-project monorepo with 8 features and 37 ACs, "Find All References" on a single Feature type produces:

OrderProcessingFeature (37 references across 12 files in 8 projects)
│
├── MegaCorp.Specifications/OrderProcessing/IOrderProcessingSpec.cs
│   ├── [ForRequirement(typeof(OrderProcessingFeature))] on interface
│   ├── [ForRequirement(..., nameof(...OrderTotalMustBePositive))] on method
│   ├── [ForRequirement(..., nameof(...BulkOrderDiscountApplied))] on method
│   └── [ForRequirement(..., nameof(...OrderPersistedWithFullDetail))] on method
│
├── MegaCorp.Specifications/OrderProcessing/IInventoryIntegrationSpec.cs
│   ├── [ForRequirement(typeof(OrderProcessingFeature))] on interface
│   ├── [ForRequirement(..., nameof(...InventoryReservedBeforePayment))] on method
│   └── [ForRequirement(..., nameof(...FailedPaymentReleasesInventory))] on method
│
├── MegaCorp.Specifications/OrderProcessing/IPaymentIntegrationSpec.cs
│   ├── [ForRequirement(typeof(OrderProcessingFeature))] on interface
│   └── [ForRequirement(..., nameof(...PaymentCapturedAfterReservation))] on method
│
├── MegaCorp.Specifications/OrderProcessing/INotificationIntegrationSpec.cs
│   └── [ForRequirement(..., nameof(...ConfirmationSentAfterPayment))] on method
│
├── MegaCorp.Specifications/OrderProcessing/IAuditIntegrationSpec.cs
│   └── [ForRequirement(..., nameof(...AllOperationsAudited))] on method
│
├── MegaCorp.Specifications/OrderProcessing/ILoyaltyIntegrationSpec.cs
│   └── [ForRequirement(..., nameof(...LoyaltyPointsCredited))] on method
│
├── MegaCorp.OrderService/OrderProcessingService.cs
│   └── [ForRequirement(typeof(OrderProcessingFeature))] on class
│
├── MegaCorp.InventoryService/InventoryReservationService.cs
│   └── [ForRequirement(typeof(OrderProcessingFeature))] on class
│
├── MegaCorp.PaymentGateway/StripePaymentService.cs
│   └── [ForRequirement(typeof(OrderProcessingFeature))] on class
│
├── MegaCorp.NotificationService/OrderNotificationService.cs
│   └── [ForRequirement(typeof(OrderProcessingFeature))] on class
│
├── MegaCorp.UserService/LoyaltyPointService.cs
│   └── [ForRequirement(typeof(OrderProcessingFeature))] on class
│
├── MegaCorp.Web/Controllers/OrderController.cs
│   └── [ForRequirement(typeof(OrderProcessingFeature))] on controller + methods
│
└── MegaCorp.OrderService.Tests/OrderProcessingTests.cs
    ├── [TestsFor(typeof(OrderProcessingFeature))] on class
    ├── [Verifies(..., nameof(...OrderTotalMustBePositive))] × 4 tests
    ├── [Verifies(..., nameof(...InventoryReservedBeforePayment))] × 2 tests
    ├── [Verifies(..., nameof(...PaymentCapturedAfterReservation))] × 2 tests
    ├── [Verifies(..., nameof(...ConfirmationSentAfterPayment))] × 1 test
    ├── [Verifies(..., nameof(...AllOperationsAudited))] × 1 test
    ├── [Verifies(..., nameof(...FailedPaymentReleasesInventory))] × 1 test
    ├── [Verifies(..., nameof(...BulkOrderDiscountApplied))] × 2 tests
    ├── [Verifies(..., nameof(...OrderPersistedWithFullDetail))] × 1 test
    └── [Verifies(..., nameof(...LoyaltyPointsCredited))] × 2 tests

37 references. 12 files. 8 projects. One click.

In the ServiceLocator monorepo, the same question ("what implements Order Processing?") required 2.5 hours of manual investigation. Now it takes 1 second.


Multi-Team Ownership

In a 15-team organization, who owns what? The Requirements project makes this explicit.

Team-to-Feature Mapping

// The `Owner` property on each Feature type declares ownership:
public abstract record OrderProcessingFeature : Feature<ECommerceEpic>
{
    public override string Owner => "order-team";
    // ...
}

public abstract record PaymentProcessingFeature : Feature<ECommerceEpic>
{
    public override string Owner => "payment-team";
    // ...
}

public abstract record InventoryManagementFeature : Feature<ECommerceEpic>
{
    public override string Owner => "inventory-team";
    // ...
}

The generated reports include ownership:

Team Ownership Report
═══════════════════════════════════════════════════════

order-team:
  Features: OrderProcessingFeature (9 ACs)
  Spec implementations: OrderProcessingService
  Spec dependencies:
    IInventoryIntegrationSpec → inventory-team
    IPaymentIntegrationSpec → payment-team
    INotificationIntegrationSpec → cx-team
    IAuditIntegrationSpec → compliance-team
    ILoyaltyIntegrationSpec → user-team

payment-team:
  Features: PaymentProcessingFeature (5 ACs)
  Spec implementations: StripePaymentService
  Also implements for: OrderProcessingFeature (IPaymentIntegrationSpec)

inventory-team:
  Features: InventoryManagementFeature (4 ACs)
  Spec implementations: InventoryReservationService
  Also implements for: OrderProcessingFeature (IInventoryIntegrationSpec)

user-team:
  Features: UserManagementFeature (6 ACs)
  Spec implementations: UserService, LoyaltyPointService
  Also implements for: OrderProcessingFeature (ILoyaltyIntegrationSpec)

Cross-Team Dependencies Are Visible

When the order-team's OrderProcessingService depends on IPaymentIntegrationSpec (owned by payment-team), this is visible in:

  1. The constructorOrderProcessingService(IPaymentIntegrationSpec payment, ...)
  2. The spec interface[ForRequirement(typeof(OrderProcessingFeature))] on IPaymentIntegrationSpec
  3. The generated report — "order-team depends on payment-team for AC-3"

If the payment-team changes IPaymentIntegrationSpec, the order-team's build breaks. The compiler tells both teams. No Slack message needed.

The Requirements Project Is PR-Gated

Changes to MegaCorp.Requirements affect the entire monorepo. It's the most impactful project in the solution. It should be:

  • PR-gated — no direct pushes to main
  • Multi-team review — changes require approval from all affected teams
  • Architecture-team owned — a dedicated team or role manages the requirement hierarchy
# .github/CODEOWNERS (or equivalent)
/src/MegaCorp.Requirements/      @megacorp/architecture-team
/src/MegaCorp.Specifications/    @megacorp/architecture-team
/src/MegaCorp.Requirements/Features/OrderProcessing*  @megacorp/order-team
/src/MegaCorp.Requirements/Features/Payment*           @megacorp/payment-team
/src/MegaCorp.Requirements/Features/Inventory*         @megacorp/inventory-team

Feature owners can modify their own features. But adding a new feature or changing the hierarchy requires architecture-team approval. This is the governance model that scales — it's enforced by the Git hosting platform, not by human discipline.


The Compiler as Coordination Mechanism

In a traditional monorepo, cross-team coordination happens through:

Mechanism Speed Reliability Scales?
Slack messages Minutes Low (messages get buried) No
Jira tickets Days Medium (tickets get stale) Somewhat
Email Hours Low (unread) No
Standup meetings 1 day Medium (verbal, not tracked) No
Sprint planning 2 weeks Medium (plans change) Somewhat
Architecture docs Weeks Low (docs drift from code) No

With Requirements as Projects, cross-team coordination happens through:

Mechanism Speed Reliability Scales?
Compiler error Instant 100% (it's a build failure) Yes

When the order-team adds AC-9, the compiler tells every affected team immediately, precisely, and unforgettably. The build is red until everyone has responded. No message gets buried. No ticket gets stale. No plan changes unnoticed.

The compiler is the fastest, most reliable, and most scalable coordination mechanism in software engineering. It doesn't take vacations. It doesn't forget. It doesn't have a backlog. It runs on every build, every PR, every commit.


Before and After: The 50-Project View

Let's compare the full 50-project monorepo before and after adopting Requirements as Projects.

Before: The ServiceLocator Monorepo

50 projects, ~520 hidden dependencies, 0 feature types.

Developer question: "What implements Order Processing?"
Answer: Unknown. Grep for "Order" → 247 files. Trace ServiceLocator calls → 2.5 hours.

PM question: "Is Order Processing complete?"
Answer: Unknown. Check Jira → 6 tickets over 5 years. Some ACs updated, some not.

QA question: "Are all ACs for Order Processing tested?"
Answer: Unknown. 87% code coverage. 0% requirement coverage.

New developer question: "What does this codebase do?"
Answer: Open the solution. Read 50 project names. Ask a senior developer. Hope they remember.

Auditor question: "Prove that all payment ACs are tested."
Answer: 3-day manual spreadsheet exercise. Results may be incomplete.

Incident response: "OrderPricingEngine was changed. What's the blast radius?"
Answer: Unknown. PricingEngine is in MegaCorp.Core, referenced by everything.
ServiceLocator hides the actual callers. Run all tests and hope.

After: Requirements as Projects

52 projects (+Requirements, +Specifications), 0 hidden dependencies, 8 feature types, 37 ACs.

Developer question: "What implements Order Processing?"
Answer: Find All References on OrderProcessingFeature → 37 refs, 12 files, 8 projects. 1 second.

PM question: "Is Order Processing complete?"
Answer: dotnet build → "REQ303: OrderProcessingFeature — 9/9 ACs tested (100%)". Instant.

QA question: "Are all ACs for Order Processing tested?"
Answer: TraceabilityMatrix.g.cs → 21 tests, 9/9 ACs covered. Generated on every build.

New developer question: "What does this codebase do?"
Answer: Open MegaCorp.Requirements/Features/. See 8 feature types. Read them.
Each one has ACs with typed signatures. Ctrl+Click to implementations and tests.

Auditor question: "Prove that all payment ACs are tested."
Answer: dotnet build → TraceabilityMatrix.g.cs → PaymentProcessingFeature:
5/5 ACs specified, 5/5 implemented, 4/5 tested. Missing: NoPciDataStored.
Total time: 30 seconds.

Incident response: "OrderPricingEngine was changed. What's the blast radius?"
Answer: OrderPricingEngine implements IOrderProcessingSpec.ApplyBulkDiscount
→ [ForRequirement(typeof(OrderProcessingFeature), nameof(BulkOrderDiscountApplied))]
→ Affects AC-7. Tests: Bulk_order_receives_discount, Small_order_no_discount.
→ Run those 2 tests. If they pass, AC-7 is still satisfied.

The Numbers

Metric Before After Change
Projects 50 52 +2 (Requirements, Specifications)
Hidden dependencies (ServiceLocator) ~520 0 -100%
Visible dependencies (constructor injection) ~80 ~250 +213% (all visible now)
Feature types 0 8 +8 (from nothing)
Acceptance criteria (in code) 0 37 +37 (were only in Jira)
ACs with specification methods 0 37 100% coverage
ACs with tests unknown 35 94.6% coverage
Time to understand a feature ~2.5 hours ~1 minute -97.5%
Time for compliance audit ~3 days ~30 seconds -99.9%
Cross-team coordination Jira + Slack Compiler errors Instant
Dead code detection Unreliable Reliable No hidden refs

Scaling Characteristics

Build Time

Adding .Requirements and .Specifications to a 50-project monorepo adds negligible build time:

  • MegaCorp.Requirements: ~20 source files, no dependencies → compiles in < 1 second
  • MegaCorp.Specifications: ~30 source files, 2 dependencies → compiles in < 1 second
  • Roslyn analyzer: runs incrementally per-project, caches results → < 0.5 seconds per project

Total additional build time: ~2-3 seconds on a 50-project solution that already takes 2-3 minutes.

The Roslyn analyzer is the key to scaling. It doesn't re-analyze the entire solution on every build — it uses Roslyn's incremental generation API:

// The analyzer uses ForAttributeWithMetadataName for incremental generation
[Generator]
public class RequirementAnalyzer : IIncrementalGenerator
{
    public void Initialize(IncrementalGeneratorInitializationContext context)
    {
        // Only re-runs when types with [ForRequirement] change
        var requirementTypes = context.SyntaxProvider
            .ForAttributeWithMetadataName(
                "MegaCorp.Requirements.ForRequirementAttribute",
                predicate: (node, _) => node is ClassDeclarationSyntax
                    or InterfaceDeclarationSyntax
                    or MethodDeclarationSyntax,
                transform: (ctx, _) => /* extract metadata */);

        // Combine with requirement registry
        var combined = requirementTypes.Collect()
            .Combine(context.CompilationProvider);

        // Generate only when inputs change
        context.RegisterSourceOutput(combined, (ctx, source) =>
        {
            // Generate TraceabilityMatrix.g.cs
            // Emit diagnostics
        });
    }
}

If no [ForRequirement] attributes changed, the analyzer produces cached output. If only one attribute changed, it re-generates only the affected entries. The incremental API means the analyzer scales with the size of the change, not the size of the solution.

Memory Footprint

The Roslyn analyzer keeps metadata in memory during compilation:

Data Size (50-project monorepo) Size (200-project monorepo)
Requirement types ~8 records × 200 bytes = 1.6 KB ~40 records × 200 bytes = 8 KB
AC methods ~37 entries × 100 bytes = 3.7 KB ~200 entries × 100 bytes = 20 KB
[ForRequirement] attributes ~100 × 150 bytes = 15 KB ~500 × 150 bytes = 75 KB
[Verifies] attributes ~70 × 100 bytes = 7 KB ~400 × 100 bytes = 40 KB
TraceabilityMatrix entries ~8 × 2 KB = 16 KB ~40 × 2 KB = 80 KB
Total ~43 KB ~223 KB

Negligible. The analyzer's memory footprint is smaller than a single high-resolution image. It doesn't matter whether the monorepo has 50 projects or 500.

IDE Responsiveness

"Find All References" on a Feature type in a 50-project monorepo:

  • Before (grep for "Order"): 5-15 seconds (text search across all files)
  • After (Roslyn symbol search): < 0.5 seconds (Roslyn's symbol index is pre-built)

The typeof() and nameof() references are symbols, not strings. Roslyn indexes symbols at solution load time. "Find All References" on a symbol is a hash table lookup, not a text search. It scales to any solution size.


What Doesn't Change

It's important to be honest about what Requirements as Projects does NOT change at scale:

1. Build complexity

You still have 50+ projects. The build graph is still complex. MSBuild still needs to compile them in dependency order. Adding 2 projects doesn't simplify this.

2. Deployment complexity

You still deploy multiple services. Docker containers, Kubernetes manifests, CI/CD pipelines — none of that changes. Requirements as Projects is a compile-time architecture, not a deployment architecture.

3. Runtime behavior

At runtime, the application behaves identically. The [ForRequirement] attributes are metadata — they don't affect execution speed, memory usage, or API latency. The only runtime addition is the optional startup compliance check.

4. Team politics

If two teams disagree about who owns a feature, the compiler can't resolve that. If a PM writes bad acceptance criteria, the compiler enforces bad acceptance criteria. The tool is only as good as the humans defining the requirements.

5. Legacy code

Existing code that uses ServiceLocator continues to work. The 520 hidden dependencies don't disappear overnight. Migration is incremental (covered in Part 5).


The Scale-Independent Guarantee

Here is what DOES hold at any scale:

  1. Every feature is a type. Whether you have 5 features or 500, each one is an abstract record with ACs as abstract methods.

  2. Every AC has a spec. The Roslyn analyzer verifies this at compile time. Missing spec → build error.

  3. Every spec has an implementation. The C# compiler verifies this. Missing interface method → compile error.

  4. Every AC has a test. The Roslyn analyzer verifies this. Missing [Verifies] → build warning (error in CI).

  5. The traceability matrix is generated. From 8 features to 800, the matrix is source-generated on every build.

  6. "Find All References" works. Type references are indexed by Roslyn. Symbol lookup is O(1), not O(n).

  7. The AC cascade propagates. Add an AC → build breaks → teams implement → build green. Same at 5 teams or 50 teams.

These guarantees are structural — they come from the C# type system and the Roslyn analyzer, not from human discipline. They don't degrade with scale. They don't require more meetings. They don't need a "requirements sync" ceremony. The compiler enforces them on every build, forever.


Summary

At 50+ projects with 15 teams:

Aspect ServiceLocator Monorepo Requirements as Projects
Add a new AC Jira ticket → Slack → wait → implement → hope Abstract method → compiler cascade → all teams implement → build green
Cross-team coordination Jira, Slack, standups, sprint planning Compiler errors
Feature traceability 2.5 hours manual investigation 1 second (Find All References)
Compliance audit 3-day spreadsheet 30-second build output
Dead code detection Unreliable (ServiceLocator hides refs) Reliable (all refs are typed)
Onboarding 5 days to understand one feature Read Feature type → Ctrl+Click
Build time overhead N/A +2-3 seconds
Memory overhead N/A ~43 KB

The compiler is the coordination mechanism. The type system is the architecture. The traceability matrix is the audit trail. They scale because they're structural, not procedural.

The next post shows how to get from the ServiceLocator monorepo to this architecture — incrementally, one feature at a time, without a big-bang rewrite.


Previous: Part 3 — Requirements and Specifications ARE Projects

Next: Part 5 — Migration: Space and Time