.NET Modular Monolith Architecture
Build a single-deployable .NET solution made of strongly isolated modules (e.g. Users, Billing, Catalog), each internally structured as vertical slices, communicating across module boundaries only through async, durable messaging (outbox → inbox) or thin public API contracts — never through shared database access or direct project references into another module's internals.
This skill is opinionated. It is the synthesis of patterns from production .NET systems (the kind referenced in running_tracker/MFI-style codebases): Guard clauses, Value Objects, Result pattern, typed CQRS markers, decorator-based cross-cutting behaviors (no MediatR required), rich domain models with Raise, transactional outbox, idempotent inbox, and module-isolation architecture tests.
CQRS dispatch — read this before generating handler/behavior code. This skill defaults to a library-free CQRS implementation: your own ICommandHandler<>/IQueryHandler<,> interfaces (NOT extending MediatR.IRequest), handlers registered by assembly scan (Scrutor Scan), endpoints injecting the handler interface directly, and cross-cutting concerns (validation, logging, caching) implemented as decorators wrapping the handler and chained via Scrutor Decorate. This is the approach demonstrated in the host repository (src/Application/...). MediatR (IRequest/IPipelineBehavior/ISender/IPublisher) remains a fully supported alternative — every code reference shows the decorator form first and the MediatR form as a clearly marked alternative. Pick one per solution and stay consistent; do not mix a MediatR pipeline with hand-rolled decorators for the same handlers.
When to go deep vs. stay light
Not every project needs all twelve patterns from the foundational list. Triage with the user first:
- Single module / simple CRUD service → Tier 1 + Tier 2 patterns only (Guard, Value Objects, Result, CQRS markers, behaviors). Skip outbox/inbox/saga entirely — there's no cross-module boundary to protect.
- Multiple modules, but synchronous/in-process calls are acceptable (no eventual consistency requirement) → Add Tier 4 (module API contracts
*.Api projects + architecture tests) but skip the outbox/saga machinery.
- Multiple modules with eventual consistency / cross-module workflows that must survive crashes → Full stack: domain events (
Raise), transactional outbox, integration events, idempotent inbox, and a saga/process manager for anything spanning more than one event hop.
- Multi-tenant → Layer on top of whichever tier above applies. See
references/multi-tenancy.md. Don't design tenancy in from scratch; retrofit it onto the isolation boundaries that already exist, because tenancy itself is just another cross-cutting concern enforced at the same seams (EF Core query filters, pipeline behaviors, outbox/inbox payloads).
Ask which tier applies before generating a large scaffold — it changes what gets built by an order of magnitude.
Reality check: what the host repo demonstrates vs. what this skill scales to
The repository this skill ships in is a single-module clean architecture (src/Domain, src/Application, src/Infrastructure, src/Web.Api, src/SharedKernel) — not yet a multi-module solution. It is the canonical, working demonstration of the Tier 1–2 patterns (Result, Value Objects, EF Core config-per-entity, typed CQRS markers, decorator behaviors, Result→HTTP mapping) and a simplified after-SaveChanges in-process domain-event dispatch. The Modules/<Module>.{Domain,Application,Infrastructure,Api,Presentation} layout and the Tier 3/4 outbox/inbox/saga + *.Api isolation machinery described below are the scale-up target, not code that already exists in this repo. When adapting: copy the Tier 1–2 shapes verbatim from src/..., and treat the modular/outbox sections as the design to grow into — don't claim files exist that don't.
House style (enforced by .cursorrules in this repo)
Generated code must match the repo conventions: C# 12 / primary constructors for DI; explicit typing (only use var when the type is evident from the right-hand side); internal sealed by default unless a wider surface is required; is null / is not null (never == null); Guid identifiers; records for immutable data; async/await for I/O. The decorator and handler examples below follow these — keep them that way when extending.
Core principle: module isolation is a project-reference rule, not a folder convention
The single most common failure mode is "modules" that are just folders inside one project, with classes reaching across into each other's repositories or DbContexts. Real isolation means:
- Each module is its own set of projects:
Module.Domain, Module.Application, Module.Infrastructure, Module.Api (thin contracts), and a slice of the host (Module.Presentation or endpoint registration).
- Other modules may only reference
Module.Api. Never Module.Application, never Module.Infrastructure, never Module.Domain.
- Each module owns its own database schema (same physical DB is fine for a monolith; separate schemas with separate
DbContexts per module — never a shared DbContext, never cross-module joins/queries).
- Cross-module communication is either:
- A call through the other module's
*.Api interface (synchronous, in-process, fine for queries that need an immediate answer and where eventual consistency is not required), or
- An integration event delivered via outbox → message bus/in-process dispatcher → inbox (asynchronous, durable, required whenever a workflow must survive a crash mid-flight or fan out to multiple consumers).
- Enforce both #2 and #3 with
NetArchTest architecture tests that fail the build if violated. See references/architecture-tests.md.
The pattern stack (build in this order)
Each pattern below is described briefly; full code for all of them together, wired into one running example module, is in references/full-example.md. Read that file before generating any code — it shows how the pieces actually connect, which is the part people get wrong when implementing each pattern in isolation.
Tier 1 — Foundational, in every module regardless of scale
- Guard / Ensure pattern — centralized invariant checks at construction/entry, using
[CallerArgumentExpression] for self-documenting exceptions. One static Ensure class in the shared kernel; everything else calls into it instead of scattering if (...) throw.
- Value Objects —
private constructor + static Create(...) factory returning Result<T>, never a public constructor or public setter. Validation lives in the type. This is non-negotiable for money, identifiers, and anything with a domain-meaningful format (emails, codes, rates).
IDateTimeProvider abstraction — banish DateTime.UtcNow from domain/handlers so time is injectable and tests are deterministic.
Tier 2 — Application cross-cutting, per module
- Typed CQRS markers + handler interfaces (library-free by default) — your own
ICommand, ICommand<TResponse>, IQuery<TResponse> marker interfaces and ICommandHandler<>, ICommandHandler<,>, IQueryHandler<,> handler interfaces whose Handle method returns Result/Result<T>. The default approach does not extend MediatR.IRequest/IRequestHandler: handlers are auto-registered with a Scrutor Scan, and endpoints inject the handler interface directly (IQueryHandler<GetUserByIdQuery, UserResponse> handler → handler.Handle(...)) rather than going through ISender. This makes "every handler returns Result" a compile-time fact, resolves handlers at compile time (no reflection dispatch), and makes decorators target precisely. Alternative: the same markers can extend MediatR.IRequest<Result<T>> if you want MediatR's mediator + IPipelineBehavior — see references/full-example.md §5/§7 for both forms side by side.
- Cross-cutting concerns live in decorators (default) or pipeline behaviors (MediatR alternative) — validation, logging, exception handling, and caching (item 5a) are layered around handlers, never copy-pasted into them. Handlers stay pure: load aggregate, call domain method, save, done.
- Default (no library): each concern is a decorator class implementing the same handler interface, taking the inner handler as a constructor dependency, registered with
services.Decorate(typeof(ICommandHandler<,>), typeof(...)). Registration order = nesting order (last registered runs outermost). The host repo wires Validation then Logging, yielding Logging → Validation → Handler at runtime. Note the natural read/write asymmetry: validation typically decorates commands only; queries get logging (and caching) but not validation.
- Alternative (MediatR): the same concerns as
IPipelineBehavior<,> implementations registered via AddOpenBehavior.
5a. Caching as a query decorator — wrap IQueryHandler<,> with a CachingDecorator that depends on IDistributedCache (provider-agnostic; Redis via AddStackExchangeRedisCache is the recommended production backend, MemoryDistributedCache for dev/test). Key on query type name + serialized query; only cache successful results; let cacheable queries opt in via an ICachedQuery marker (with CacheKey/Expiration) so you don't blanket-cache everything. See references/full-example.md §7.
- Option pattern for configuration — strongly-typed, validated-on-startup options classes (
IOptions<T> / IOptionsSnapshot<T> / IOptionsMonitor<T>) instead of IConfiguration["Some:Key"] scattered through the codebase. Validate with IValidateOptions<T> or data annotations + ValidateOnStart().
- EF Core configuration pattern — every entity gets its own
IEntityTypeConfiguration<T> class (never fluent config inline in OnModelCreating, never data-annotation attributes on domain entities), applied via ApplyConfigurationsFromAssembly. Keeps the domain model persistence-ignorant.
- Result → HTTP mapping — one
result.Match(Results.Ok, CustomResults.Problem) at the endpoint, not per-endpoint status-code logic.
Tier 3 — Required once you have more than one module with eventual consistency
- Rich domain model +
Raise pattern — aggregates expose behavior methods (LoanApplication.Approve()), not public setters; those methods call Raise(IDomainEvent) to record what happened. This is the trigger source for everything below — without it there's nothing for the outbox to pick up. (Raise is protected if only the aggregate raises its own events — the stricter, preferred default; the host repo currently exposes it as public on the Entity base. Either compiles; prefer protected for new code and flag the public form in review.) Domain events are dispatched library-free by default — a small reflection-wrapper DomainEventsDispatcher resolving IDomainEventHandler<> implementations (see src/Infrastructure/DomainEvents/DomainEventsDispatcher.cs), not MediatR's IPublisher/INotificationHandler.
- Outbox via
SaveChanges interceptor — domain events raised during a unit of work are serialized into an OutboxMessage row in the same transaction as the data change, closing the dual-write gap. A background processor later reads unprocessed outbox rows and publishes them as integration events.
- Inbox / idempotency table — the flip side of outbox. At-least-once delivery means consumers can receive the same integration event twice; every consumer checks (and records) the event ID in an inbox table before processing, inside the same transaction as its side effects.
- Saga / process manager for anything spanning more than one event hop — e.g. "loan approved → reserve funds → notify client → close application" across three modules. A saga is a small stateful entity (its own table) that reacts to one integration event, performs its step, and either raises the next event or a compensating one on failure. Don't build a generic saga framework up front; start with an explicit state machine per workflow and only abstract once you have two or three real ones to compare.
- Public module API contracts (
*.Api projects) — thin assemblies containing only interfaces + DTOs (e.g. IUsersApi, UserSummaryDto). This is what Tier 4's architecture test enforces other modules to depend on.
Full wiring of 9–13 together — domain event → outbox interceptor → processor → integration event → inbox consumer → saga step — is in references/outbox-inbox-saga.md, including the idempotency table schema and the failure/retry semantics.
Workflow for this skill
- Clarify scope — ask which tier (above) actually applies, and how many modules exist or are planned. Don't scaffold sagas for a two-module CRUD app.
- Read the relevant reference file(s) before writing code:
references/full-example.md — complete worked example: one module (Orders) fully built out with all Tier 1–2 patterns, including project layout, every file, and how they reference each other.
references/outbox-inbox-saga.md — Tier 3 wiring: outbox interceptor, processor, inbox, and a worked saga example (Orders → Inventory → Shipping).
references/architecture-tests.md — NetArchTest rules for module isolation, no-public-setters, no-DateTime.UtcNow, etc., ready to paste into a *.ArchitectureTests project.
references/multi-tenancy.md — how to layer tenancy onto this architecture: tenant resolution, EF Core global query filters per module, outbox/inbox payload changes, and the "shared schema vs. schema-per-tenant vs. database-per-tenant" tradeoff.
- Generate code matching the user's actual module names and domain, not generic placeholders — adapt the
Orders example's shape (folder layout, naming, file boundaries) to their domain, don't just rename Orders to their term and call it done; check whether their aggregate's actual invariants change which Value Objects or guard clauses are needed.
- Always include the architecture test for whatever isolation boundary was just introduced. A module-isolation pattern without a test enforcing it tends to erode within a few sprints — say this to the user if they want to skip the test.
- For multi-tenancy specifically, ask early which isolation model they want (shared schema + filter, schema-per-tenant, or database-per-tenant) before generating code — it changes the EF Core configuration, the outbox/inbox table shape, and the migration strategy. See the decision table in
references/multi-tenancy.md.
Common mistakes to flag proactively
- Mixing MediatR and hand-rolled decorators for the same handlers — pick one dispatch model per solution. If handlers don't extend
IRequest, you cannot use IPipelineBehavior; if they do, don't also register Scrutor Decorate wrappers on the same interface. The default in this repo is decorators with no MediatR — match it unless the user explicitly wants the library.
- Validating in the handler — a handler that runs FluentValidation (or
if (string.IsNullOrEmpty(...)) business-input checks) inside Handle instead of in a validation decorator/behavior has leaked a cross-cutting concern into business logic. Keep handlers to: load aggregate → call domain method → save.
- Caching the wrong thing — caching commands, or caching failed/
Result.Failure query results, or blanket-caching every query. Cache only successful query results, and only for queries that opt in (ICachedQuery).
- Reflection-heavy validation result construction — don't reconstruct a failed
Result<T> via MakeGenericMethod; use a typed ValidationError aggregate (Error[]) and Result.Failure<T>(validationError) as the host repo does (src/Application/Abstractions/Behaviors/ValidationDecorator.cs).
- Shared
DbContext across modules — defeats the entire point of isolation; even in one physical database, each module needs its own DbContext scoped to its own schema/tables.
- Anemic domain model with rich-looking names — a
LoanApplication.Approve() method that just sets Status = Approved without raising a domain event isn't the Raise pattern, it's a setter with extra steps. The event is the point.
- In-memory event bus for cross-module integration events — fine for same-process domain events within one module's unit of work, but integration events between modules must go through the outbox or they're lost on crash/restart (this was explicitly flagged as a bug to fix, not copy, from the
running_tracker reference).
- Sagas without compensating actions — a saga that only handles the happy path isn't a saga, it's a longer pipeline behavior. Every step that has a side effect needs a defined compensation for when a later step fails.
- Value Objects with public constructors "for EF Core" — use private constructors + backing fields and configure EF Core to use them (see
references/full-example.md for the EF Core configuration that makes this work without exposing a public ctor).
- Multi-tenancy bolted on via a
WHERE TenantId = @id the developer has to remember to add — must be a global query filter enforced at the DbContext level, plus an architecture test or analyzer that fails the build if a query bypasses it (e.g. via IgnoreQueryFilters() without an explicit justification).
1---2name: dotnet-modular-monolith3description: .NET Modular Monolith Architecture4---56# .NET Modular Monolith Architecture78Build a single-deployable .NET solution made of strongly isolated modules (e.g. `Users`, `Billing`, `Catalog`), each internally structured as vertical slices, communicating across module boundaries only through async, durable messaging (outbox → inbox) or thin public API contracts — never through shared database access or direct project references into another module's internals.910This skill is opinionated. It is the synthesis of patterns from production .NET systems (the kind referenced in `running_tracker`/MFI-style codebases): Guard clauses, Value Objects, Result pattern, typed CQRS markers, **decorator-based cross-cutting behaviors (no MediatR required)**, rich domain models with `Raise`, transactional outbox, idempotent inbox, and module-isolation architecture tests.1112> **CQRS dispatch — read this before generating handler/behavior code.** This skill defaults to a **library-free** CQRS implementation: your own `ICommandHandler<>`/`IQueryHandler<,>` interfaces (NOT extending `MediatR.IRequest`), handlers registered by assembly scan (Scrutor `Scan`), endpoints injecting the handler interface directly, and cross-cutting concerns (validation, logging, caching) implemented as **decorators** wrapping the handler and chained via Scrutor `Decorate`. This is the approach demonstrated in the host repository (`src/Application/...`). MediatR (`IRequest`/`IPipelineBehavior`/`ISender`/`IPublisher`) remains a fully supported alternative — every code reference shows the decorator form first and the MediatR form as a clearly marked alternative. Pick one per solution and stay consistent; do not mix a MediatR pipeline with hand-rolled decorators for the same handlers.1314## When to go deep vs. stay light1516Not every project needs all twelve patterns from the foundational list. Triage with the user first:1718- **Single module / simple CRUD service** → Tier 1 + Tier 2 patterns only (Guard, Value Objects, Result, CQRS markers, behaviors). Skip outbox/inbox/saga entirely — there's no cross-module boundary to protect.19- **Multiple modules, but synchronous/in-process calls are acceptable (no eventual consistency requirement)** → Add Tier 4 (module API contracts `*.Api` projects + architecture tests) but skip the outbox/saga machinery.20- **Multiple modules with eventual consistency / cross-module workflows that must survive crashes** → Full stack: domain events (`Raise`), transactional outbox, integration events, idempotent inbox, and a saga/process manager for anything spanning more than one event hop.21- **Multi-tenant** → Layer on top of whichever tier above applies. See `references/multi-tenancy.md`. Don't design tenancy in from scratch; retrofit it onto the isolation boundaries that already exist, because tenancy itself is just another cross-cutting concern enforced at the same seams (EF Core query filters, pipeline behaviors, outbox/inbox payloads).2223Ask which tier applies before generating a large scaffold — it changes what gets built by an order of magnitude.2425## Reality check: what the host repo demonstrates vs. what this skill scales to2627The repository this skill ships in is a **single-module clean architecture** (`src/Domain`, `src/Application`, `src/Infrastructure`, `src/Web.Api`, `src/SharedKernel`) — not yet a multi-module solution. It is the canonical, working demonstration of the **Tier 1–2** patterns (Result, Value Objects, EF Core config-per-entity, typed CQRS markers, **decorator behaviors**, Result→HTTP mapping) and a simplified **after-`SaveChanges` in-process** domain-event dispatch. The `Modules/<Module>.{Domain,Application,Infrastructure,Api,Presentation}` layout and the Tier 3/4 outbox/inbox/saga + `*.Api` isolation machinery described below are the **scale-up target**, not code that already exists in this repo. When adapting: copy the Tier 1–2 shapes verbatim from `src/...`, and treat the modular/outbox sections as the design to grow into — don't claim files exist that don't.2829## House style (enforced by `.cursorrules` in this repo)3031Generated code must match the repo conventions: C# 12 / primary constructors for DI; **explicit typing** (only use `var` when the type is evident from the right-hand side); `internal sealed` by default unless a wider surface is required; `is null` / `is not null` (never `== null`); `Guid` identifiers; records for immutable data; async/await for I/O. The decorator and handler examples below follow these — keep them that way when extending.3233## Core principle: module isolation is a project-reference rule, not a folder convention3435The single most common failure mode is "modules" that are just folders inside one project, with classes reaching across into each other's repositories or DbContexts. Real isolation means:36371. Each module is its own set of projects: `Module.Domain`, `Module.Application`, `Module.Infrastructure`, `Module.Api` (thin contracts), and a slice of the host (`Module.Presentation` or endpoint registration).382. **Other modules may only reference `Module.Api`.** Never `Module.Application`, never `Module.Infrastructure`, never `Module.Domain`.393. Each module owns its own database schema (same physical DB is fine for a monolith; separate schemas with separate `DbContext`s per module — never a shared `DbContext`, never cross-module joins/queries).404. Cross-module communication is either:41 - A call through the other module's `*.Api` interface (synchronous, in-process, fine for queries that need an immediate answer and where eventual consistency is not required), or42 - An integration event delivered via outbox → message bus/in-process dispatcher → inbox (asynchronous, durable, required whenever a workflow must survive a crash mid-flight or fan out to multiple consumers).435. Enforce both #2 and #3 with `NetArchTest` architecture tests that fail the build if violated. See `references/architecture-tests.md`.4445## The pattern stack (build in this order)4647Each pattern below is described briefly; full code for all of them together, wired into one running example module, is in `references/full-example.md`. Read that file before generating any code — it shows how the pieces actually connect, which is the part people get wrong when implementing each pattern in isolation.4849### Tier 1 — Foundational, in every module regardless of scale50511. **Guard / Ensure pattern** — centralized invariant checks at construction/entry, using `[CallerArgumentExpression]` for self-documenting exceptions. One static `Ensure` class in the shared kernel; everything else calls into it instead of scattering `if (...) throw`.522. **Value Objects** — `private` constructor + static `Create(...)` factory returning `Result<T>`, never a public constructor or public setter. Validation lives in the type. This is non-negotiable for money, identifiers, and anything with a domain-meaningful format (emails, codes, rates).533. **`IDateTimeProvider`** abstraction — banish `DateTime.UtcNow` from domain/handlers so time is injectable and tests are deterministic.5455### Tier 2 — Application cross-cutting, per module56574. **Typed CQRS markers + handler interfaces (library-free by default)** — your own `ICommand`, `ICommand<TResponse>`, `IQuery<TResponse>` marker interfaces and `ICommandHandler<>`, `ICommandHandler<,>`, `IQueryHandler<,>` handler interfaces whose `Handle` method returns `Result`/`Result<T>`. The default approach does **not** extend `MediatR.IRequest`/`IRequestHandler`: handlers are auto-registered with a Scrutor `Scan`, and endpoints inject the handler interface directly (`IQueryHandler<GetUserByIdQuery, UserResponse> handler` → `handler.Handle(...)`) rather than going through `ISender`. This makes "every handler returns Result" a compile-time fact, resolves handlers at compile time (no reflection dispatch), and makes decorators target precisely. *Alternative:* the same markers can extend `MediatR.IRequest<Result<T>>` if you want MediatR's mediator + `IPipelineBehavior` — see `references/full-example.md` §5/§7 for both forms side by side.585. **Cross-cutting concerns live in decorators (default) or pipeline behaviors (MediatR alternative)** — validation, logging, exception handling, and caching (item 5a) are layered *around* handlers, never copy-pasted into them. Handlers stay pure: load aggregate, call domain method, save, done.59 - **Default (no library):** each concern is a decorator class implementing the same handler interface, taking the inner handler as a constructor dependency, registered with `services.Decorate(typeof(ICommandHandler<,>), typeof(...))`. Registration order = nesting order (last registered runs outermost). The host repo wires `Validation` then `Logging`, yielding `Logging → Validation → Handler` at runtime. Note the natural read/write asymmetry: validation typically decorates **commands only**; queries get logging (and caching) but not validation.60 - **Alternative (MediatR):** the same concerns as `IPipelineBehavior<,>` implementations registered via `AddOpenBehavior`.615a. **Caching as a query decorator** — wrap `IQueryHandler<,>` with a `CachingDecorator` that depends on `IDistributedCache` (provider-agnostic; Redis via `AddStackExchangeRedisCache` is the recommended production backend, `MemoryDistributedCache` for dev/test). Key on query type name + serialized query; only cache successful results; let cacheable queries opt in via an `ICachedQuery` marker (with `CacheKey`/`Expiration`) so you don't blanket-cache everything. See `references/full-example.md` §7.626. **Option pattern** for configuration — strongly-typed, validated-on-startup options classes (`IOptions<T>` / `IOptionsSnapshot<T>` / `IOptionsMonitor<T>`) instead of `IConfiguration["Some:Key"]` scattered through the codebase. Validate with `IValidateOptions<T>` or data annotations + `ValidateOnStart()`.637. **EF Core configuration pattern** — every entity gets its own `IEntityTypeConfiguration<T>` class (never fluent config inline in `OnModelCreating`, never data-annotation attributes on domain entities), applied via `ApplyConfigurationsFromAssembly`. Keeps the domain model persistence-ignorant.648. **Result → HTTP mapping** — one `result.Match(Results.Ok, CustomResults.Problem)` at the endpoint, not per-endpoint status-code logic.6566### Tier 3 — Required once you have more than one module with eventual consistency67689. **Rich domain model + `Raise` pattern** — aggregates expose behavior methods (`LoanApplication.Approve()`), not public setters; those methods call `Raise(IDomainEvent)` to record what happened. This is the trigger source for everything below — without it there's nothing for the outbox to pick up. (`Raise` is `protected` if only the aggregate raises its own events — the stricter, preferred default; the host repo currently exposes it as `public` on the `Entity` base. Either compiles; prefer `protected` for new code and flag the `public` form in review.) Domain events are dispatched **library-free** by default — a small reflection-wrapper `DomainEventsDispatcher` resolving `IDomainEventHandler<>` implementations (see `src/Infrastructure/DomainEvents/DomainEventsDispatcher.cs`), not MediatR's `IPublisher`/`INotificationHandler`.6910. **Outbox via `SaveChanges` interceptor** — domain events raised during a unit of work are serialized into an `OutboxMessage` row in the _same transaction_ as the data change, closing the dual-write gap. A background processor later reads unprocessed outbox rows and publishes them as integration events.7011. **Inbox / idempotency table** — the flip side of outbox. At-least-once delivery means consumers can receive the same integration event twice; every consumer checks (and records) the event ID in an inbox table before processing, inside the same transaction as its side effects.7112. **Saga / process manager** for anything spanning more than one event hop — e.g. "loan approved → reserve funds → notify client → close application" across three modules. A saga is a small stateful entity (its own table) that reacts to one integration event, performs its step, and either raises the next event or a compensating one on failure. Don't build a generic saga framework up front; start with an explicit state machine per workflow and only abstract once you have two or three real ones to compare.7213. **Public module API contracts (`*.Api` projects)** — thin assemblies containing only interfaces + DTOs (e.g. `IUsersApi`, `UserSummaryDto`). This is what Tier 4's architecture test enforces other modules to depend on.7374Full wiring of 9–13 together — domain event → outbox interceptor → processor → integration event → inbox consumer → saga step — is in `references/outbox-inbox-saga.md`, including the idempotency table schema and the failure/retry semantics.7576## Workflow for this skill77781. **Clarify scope** — ask which tier (above) actually applies, and how many modules exist or are planned. Don't scaffold sagas for a two-module CRUD app.792. **Read the relevant reference file(s) before writing code**:80 - `references/full-example.md` — complete worked example: one module (`Orders`) fully built out with all Tier 1–2 patterns, including project layout, every file, and how they reference each other.81 - `references/outbox-inbox-saga.md` — Tier 3 wiring: outbox interceptor, processor, inbox, and a worked saga example (`Orders` → `Inventory` → `Shipping`).82 - `references/architecture-tests.md` — NetArchTest rules for module isolation, no-public-setters, no-DateTime.UtcNow, etc., ready to paste into a `*.ArchitectureTests` project.83 - `references/multi-tenancy.md` — how to layer tenancy onto this architecture: tenant resolution, EF Core global query filters per module, outbox/inbox payload changes, and the "shared schema vs. schema-per-tenant vs. database-per-tenant" tradeoff.843. **Generate code matching the user's actual module names and domain**, not generic placeholders — adapt the `Orders` example's shape (folder layout, naming, file boundaries) to their domain, don't just rename `Orders` to their term and call it done; check whether their aggregate's actual invariants change which Value Objects or guard clauses are needed.854. **Always include the architecture test** for whatever isolation boundary was just introduced. A module-isolation pattern without a test enforcing it tends to erode within a few sprints — say this to the user if they want to skip the test.865. **For multi-tenancy specifically**, ask early which isolation model they want (shared schema + filter, schema-per-tenant, or database-per-tenant) before generating code — it changes the EF Core configuration, the outbox/inbox table shape, and the migration strategy. See the decision table in `references/multi-tenancy.md`.8788## Common mistakes to flag proactively8990- **Mixing MediatR and hand-rolled decorators for the same handlers** — pick one dispatch model per solution. If handlers don't extend `IRequest`, you cannot use `IPipelineBehavior`; if they do, don't also register Scrutor `Decorate` wrappers on the same interface. The default in this repo is decorators with no MediatR — match it unless the user explicitly wants the library.91- **Validating in the handler** — a handler that runs FluentValidation (or `if (string.IsNullOrEmpty(...))` business-input checks) inside `Handle` instead of in a validation decorator/behavior has leaked a cross-cutting concern into business logic. Keep handlers to: load aggregate → call domain method → save.92- **Caching the wrong thing** — caching commands, or caching failed/`Result.Failure` query results, or blanket-caching every query. Cache only successful query results, and only for queries that opt in (`ICachedQuery`).93- **Reflection-heavy validation result construction** — don't reconstruct a failed `Result<T>` via `MakeGenericMethod`; use a typed `ValidationError` aggregate (`Error[]`) and `Result.Failure<T>(validationError)` as the host repo does (`src/Application/Abstractions/Behaviors/ValidationDecorator.cs`).94- **Shared `DbContext` across modules** — defeats the entire point of isolation; even in one physical database, each module needs its own `DbContext` scoped to its own schema/tables.95- **Anemic domain model with rich-looking names** — a `LoanApplication.Approve()` method that just sets `Status = Approved` without raising a domain event isn't the `Raise` pattern, it's a setter with extra steps. The event is the point.96- **In-memory event bus for cross-module integration events** — fine for same-process domain events within one module's unit of work, but integration events between modules must go through the outbox or they're lost on crash/restart (this was explicitly flagged as a bug to fix, not copy, from the `running_tracker` reference).97- **Sagas without compensating actions** — a saga that only handles the happy path isn't a saga, it's a longer pipeline behavior. Every step that has a side effect needs a defined compensation for when a later step fails.98- **Value Objects with public constructors "for EF Core"** — use private constructors + backing fields and configure EF Core to use them (see `references/full-example.md` for the EF Core configuration that makes this work without exposing a public ctor).99- **Multi-tenancy bolted on via a `WHERE TenantId = @id` the developer has to remember to add** — must be a global query filter enforced at the `DbContext` level, plus an architecture test or analyzer that fails the build if a query bypasses it (e.g. via `IgnoreQueryFilters()` without an explicit justification).