Ensuring the final feature is implemented as seamlessly as the first.
- The Tragedy of Good Intentions
- The "Vault" Architecture
- The 3 Laws of Vault
- How It Works
- Getting Started (In 60 Seconds)
- Implementation Steps
- The Enforcer (Analyzers)
- The Magician (Generators & DX)
- Project Structure
- The Escape Hatch:
[ArchitectureBypass] - Pipeline Behavior
At some point, every team realizes they aren't fighting bugs or performance issues anymore. They are fighting the architecture.
What makes this especially painful is that the architecture usually looked perfect at the beginning. It followed best practices. It used the right patterns. The diagram had colorful boxes with neat arrows. It passed code reviews. Nothing was obviously wrong—until months of lost time made the cost impossible to ignore.
In .NET projects, architectural mistakes rarely show up as broken code or compile errors. They show up as hesitation. They show up as 4 PM Friday meetings about "circular dependencies." They show up as fear of change. They turn capable teams into cautious ones and productive systems into fragile artifacts.
Faster.Modulith exists because we got tired of fighting.
It’s not about choosing Clean Architecture over Vertical Slices. It’s about taking those architectural decisions that feel safe early on, and locking them in a vault guarded by the compiler eliminating the possibility of architectural drift.
We stop the "Big Ball of Mud" by making the path of least resistance—tight coupling—physically impossible, ensuring that the last feature you build is just as easy to ship as the first.
Faster.Modulith is not just a library; it implements a strict version of the Modular Monolith that we call "Vault."
Most modular monoliths are "soft." They rely on namespaces and discipline to keep modules separate. But discipline fails when deadlines loom. Vault Architecture is hard. It treats every Module as a secure, impenetrable vault.
graph TD
subgraph "Host Application"
Orchestrator[Global Orchestrator]
end
subgraph "Module: HumanResources"
direction TB
Key["Public Key (Contract)"]:::green
Vault["Module (The Vault)"]:::red
Airlock["The Api (Generated)"]:::yellow
Key --> Airlock
Airlock -->|Secure Dispatch| Orchestrator
Orchestrator -->|Route| Vault
end
classDef green fill:#d5f5e3,stroke:#2ecc71,color:black;
classDef red fill:#fadbd8,stroke:#e74c3c,color:black;
classDef yellow fill:#fcf3cf,stroke:#f1c40f,color:black;
Inside the Module (.Module project), everything is internal. Your Domain Entities, your EF Core Context, your Services, and your Logic. Nothing escapes the vault. The compiler physically prevents other modules from seeing your internal implementation details.
The only way to interact with the Vault is through a specific "Key" defined in the .Api project. These are pure, simple DTOs (Records). They have no logic. They are just the request to open the door.
You never talk to the Vault directly. You enter through The Airlock (the Generated I{ModuleName}). The Airlock accepts your Key, sanitizes the transaction, and securely passes the message to the internal Dispatcher.
It does not rely on polite agreements or wiki pages that no one reads. It does not rely on you reading another blog post from Uncle Bob, or watching a 4-hour YouTube tutorial to figure out where to put a file.
It relies on the only thing that actually stops a developer: The Red Squiggly Line.
| Action | Result |
|---|---|
| Injecting a Repository into a Controller | Build Failed |
| Referencing internal Domain logic from another module | Build Failed |
| Calling a UseCase | Success (Code is generated) |
We do not believe in manual setup. We believe in scripts. Before you write a single line of code, understand the goal: moving from a tangled "Spaghetti Monolith" to a system of Unbreakable Modules where boundaries are enforced by the compiler.
Do not create projects manually. Use our CLI helper to scaffold the Vault structure perfectly every time. This ensures your projects are born with the correct "DNA" and analyzer references [cite: 2026-01-28].
# Scaffolds Module.HumanResources, Module.HumanResources.Api, and Module.HumanResources.Tests
.\CreateModule.cmd HumanResourcesIf you choose not to use the batch file, you must manually add the following references to both your .Api and .Module projects to enable the generator, contracts, and architectural guardrails:
<ItemGroup>
<PackageReference Include="Faster.Modulith.Analyzers" Version="1.0.0" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
<PackageReference Include="Faster.Modulith.Contracts" Version="1.0.0" />
</ItemGroup>Open your new .Api project and create a record. This record serves as your Key, defining the entry point to your module. Ensure you use the required contracts namespace for all IUsecase types.
using Faster.Modulith.Contracts;
namespace Module.HumanResources.Api;
public record HireEmployeeUseCase(string Name, decimal Salary) : IUsecase<Result<Guid>>;Open the .Module project and create a class. This class acts as your Vault. To maintain strict architectural boundaries, the class must be marked as internal. Do not manually type the interface; utilize the CodeFix to scaffold the implementation.
using Module.HumanResources.Api;
using Faster.Modulith.Contracts;
namespace Module.HumanResources;
internal sealed class HireEmployeeHandler : IUseCaseHandler<HireEmployeeUseCase, Result<Guid>>
{
/// <summary>
/// Handles the HireEmployeeUseCase request.
/// </summary>
/// <param name="usecase">The usecase data.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A Result containing the Guid of the hired employee.</returns>
public async Task<Result<Guid>> Handle(HireEmployeeUseCase usecase, CancellationToken cancellationToken)
{
// 2026-01-31 13:10:36 - Implementation logic initiated
return await Task.FromResult(Result<Guid>.Success(Guid.NewGuid()));
}
}Registration is simplified to one line per module. These extension methods are generated automatically to ensure all internal services and dispatchers are correctly registered—even if no usecases currently exist.
Crucial: To ensure consistency, every piece of generated code—from dispatchers to extension methods—resides strictly in the Faster.Modulith namespace.
// 2026-01-31 13:11:30 - Registering module services
using Faster.Modulith;
// The generated extension method resides in the Faster.Modulith namespace
builder.Services.AddHumanResourcesModule();The Source Generator acts as the architect, automatically splitting your logic into the correct structural files. This ensures the public Facade remains the only entry point, while the Dispatcher handles internal traffic.
What goes where? Module.g.cs (Public Entrypoint): Contains the HumanResourceModule : IHumanResourceModule. This is the "Front Door." It handles IUsecaseHandler mappings and IEventHandler publishing logic. [cite: 2026-01-10]
Dispatcher.g.cs (Internal Communication): An internal class that handles private cross-module routing. This is where ICommandHandler implementations are registered. It is inaccessible from outside the module and is generated even if the module is empty to ensure stable DI [cite: 2026-01-08].
Extensions.g.cs: Contains the IServiceCollection logic to register all handlers and generated types automatically. All code in this file is generated into the Faster.Modulith namespace.
We include a suite of Roslyn Analyzers that act as your strict architectural bodyguard. They catch "invisible mistakes" during development before the code is even committed.
| Code | Severity | Rule Name | Description |
|---|---|---|---|
| MOD005 | 🔴 Error | The "Vault" Rule | Cross-Vault Injection. You cannot inject IFacilitiesService into HumanResources. You must use the Orchestrator. |
| MOD001 | 🔴 Error | Leaking Internals | Your Domain Events and Commands must be internal. If it is public, it belongs in the .Api. |
| MOD018 | 🟡 Warning | Public Entities | Stop making your EF Core entities public. They are for your internal logic, not your neighbors. |
| MOD033 | 🔴 Error | Sneaky Controllers | You cannot hide an ASP.NET Controller inside a Module DLL. Keep the protocol out of the domain. |
| MOD034 | 🔴 Error | DTO Leakage | Your public API cannot return an internal Domain Entity. Map it or wrap it. |
The image captures the MOD005 analyzer in action, blocking an attempt to use a type that belongs to a different module's internal scope. By triggering a hard compiler error, the framework physically prevents cross-module coupling before the code can even be built. This ensures the "Vault" remains impenetrable, forcing developers to use the official API rather than taking the path of least resistance.
In most Modular Monoliths, calling another module is a nightmare of "Search Hell." You have to search through 50 folders to find the exact class name of the message. Is it CreateUserCommand? AddUser? UserRegistrationRequest?
Faster.Modulith solves this by generating Friendly APIs that provide instant discovery via IntelliSense.
sequenceDiagram
participant Controller as Web Controller
participant Airlock as IHumanResourcesApi (Airlock)
participant Orch as Orchestrator
participant Handler as Internal Handler
Controller->>Airlock: HireEmployee("John", 5000)
Note right of Controller: 1. Clean, typed API call
Airlock->>Orch: Dispatch(new HireEmployeeUseCase(...))
Note right of Airlock: 2. Secure Transition
Orch->>Handler: Handle(Message)
Note right of Orch: 3. Enters Vault
Handler-->>Controller: Returns Result
Instead of interacting with the raw engine, the Source Generator creates a Public API Wrapper (I{ModuleName}) for every module. This acts as the Airlock for cross-module communication.
Why this enriches your DX:
- No "Search Hell": You do not need to hunt for specific message classes.
- IntelliSense Discovery: Simply type
_moduleNameand your IDE lists exactly what Human Resources can do. - Strong Typing: The method signatures are generated directly from your contracts.
Inside the module, you often have dozens of internal Commands (CalculateTax, ValidateVisa, SendWelcomeEmail) that should never be exposed publicly. The Source Generator creates an Internal Dispatcher (I{Module}Dispatcher) specifically for your module's internal coordination.
Testing "Internal" code is usually a nightmare of AssemblyInfo.cs edits. Our Source Generator automatically detects your Module.X.Tests project and injects the [InternalsVisibleTo] attribute into your main module.
The framework enforces a specific physical structure to ensure the "Vault" stays locked.
| Path | Purpose | Visibility |
|---|---|---|
/src/Modules/Module.HumanResources |
Internal Vault / Domain | 🔴 Internal |
/src/Modules/Module.HumanResources.Api |
Public Keys / Contracts | 🟢 Public |
/src/Modules/Module.HumanResources.Tests |
Integration Tests | 🟡 Internal |
- If you try to put a UseCase in the
Moduleproject? Error MOD030. - If you try to put a Handler in the
Apiproject? Error MOD031.
We know that real life is messy. Sometimes you have a legacy migration or a circular dependency that you cannot fix today. Instead of suppressing warnings (which hides the problem), we force you to be honest.
// This will suppress the Analyzer Error, but it documents your shame.
[ArchitectureBypass("MOD005", "We need to fix the circular dependency in Q3 2026")]
private readonly IFacilitiesDispatcher _legacyInjection;The Modulith source generator supports pipeline behaviors (middleware) for handling cross-cutting concerns like validation, logging, and transactions. These behaviors wrap your command execution pipeline, allowing you to intercept requests before and after they reach the handler.
Use the [EnrichWith] attribute to apply specific behaviors to individual Commands or Handlers. This is useful for specific policies (e.g., validation) that apply only to certain use cases.
using Faster.Modulith.Contracts;
// Apply directly to the Command (Recommended)
[EnrichWith(typeof(ValidationBehavior<,>))]
[EnrichWith(typeof(LoggingBehavior<,>))]
public record CreateEmployeeCommand(string Name) : ICommand<int>;The generator will automatically:
- Detect these attributes.
- Register the specific generic service for that command type in the DI container.
- Generate the dispatcher code to execute these behaviors.
For behaviors that apply to all commands (global policies) or if you prefer to avoid attributes, you can register them manually in the AddInfrastructure partial method.
The source generator inspects your AddInfrastructure method body. If it detects the string IPipelineBehavior, it automatically generates the necessary pipeline wrapping logic in the dispatcher to support your manual registrations.
// HumanResourcesExtensions.Infrastructure.cs
namespace Faster.Modulith;
public static partial class HumanResourcesExtensions
{
static partial void AddInfrastructure(IServiceCollection services)
{
// Global Registration: Applies to all commands in this module
// Note: Using open generics matches all requests
services.AddScoped(typeof(IPipelineBehavior<,>), typeof(GlobalLoggingBehavior<,>));
// Manual/Conditional Registration
if (Environment.GetEnvironmentVariable("ENABLE_METRICS") == "true")
{
services.AddScoped(typeof(IPipelineBehavior<,>), typeof(MetricsBehavior<,>));
}
}
}The dispatcher resolves all registered behaviors for a request and executes them in reverse registration order (Outer → Inner), creating a "Russian Doll" structure.
- Outer: Global Behaviors (Manual Registration in
AddInfrastructure) - Inner: Attribute Behaviors (
[EnrichWith]) - Core: Command Handler
Behaviors must implement the IPipelineBehavior<TRequest, TResponse> interface.
using Faster.Modulith.Contracts;
using Microsoft.Extensions.Logging;
public class LoggingBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
where TRequest : notnull
{
private readonly ILogger<LoggingBehavior<TRequest, TResponse>> _logger;
public LoggingBehavior(ILogger<LoggingBehavior<TRequest, TResponse>> logger)
{
_logger = logger;
}
public async ValueTask<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken ct)
{
_logger.LogInformation("Starting Request: {Name}", typeof(TRequest).Name);
// Call the next step in the pipeline (or the handler)
var response = await next();
_logger.LogInformation("Completed Request: {Name}", typeof(TRequest).Name);
return response;
}
}Ensuring the final feature is implemented as seamlessly as the first.


