<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>Milan Jovanović: .NET &amp; Software Architecture Articles</title>
        <link>https://milanjovanovic.tech/articles</link>
        <description>In-depth, evergreen guides on .NET, ASP.NET Core, EF Core, distributed systems, testing, and software architecture.</description>
        <lastBuildDate>Thu, 13 Aug 2026 00:00:00 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>Feed for milanjovanovic.tech articles</generator>
        <image>
            <title>Milan Jovanović: .NET &amp; Software Architecture Articles</title>
            <url>https://milanjovanovic.tech/profile.png</url>
            <link>https://milanjovanovic.tech/articles</link>
        </image>
        <copyright>All rights reserved 2026, Milan Jovanović</copyright>
        <atom:link href="https://milanjovanovic.tech/rss/articles.xml" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[When to Choose Vertical Slice Architecture Over Layered Architecture]]></title>
            <link>https://milanjovanovic.tech/blog/when-to-choose-vertical-slice-architecture</link>
            <guid isPermaLink="false">https://milanjovanovic.tech/blog/when-to-choose-vertical-slice-architecture</guid>
            <pubDate>Thu, 13 Aug 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Four files across four folders for a query that returns one object: that is the layered tax on every feature.]]></description>
            <content:encoded><![CDATA[<p>Layered architecture is the default choice in .NET, and defaults rarely get questioned.
But an architecture you picked by inertia is still an architecture decision, just one made without looking at the alternatives.
This article lays out the concrete signals for when Vertical Slice Architecture beats layers, when layers still win, and what to do when the signals point both ways.</p>
<h2>The Problem With Layers</h2>
<p>In a layered architecture, a simple &quot;Get Order by ID&quot; feature touches:</p>
<ol>
<li><code>OrdersController</code> (Presentation)</li>
<li><code>IOrderService</code> + <code>OrderService</code> (Application)</li>
<li><code>IOrderRepository</code> + <code>OrderRepository</code> (Infrastructure)</li>
<li><code>OrderDto</code>, <code>OrderResponse</code> (Mapping)</li>
</ol>
<p>Four files across four folders for a database query that returns one object. Adding a new feature means touching every layer. Modifying a feature means jumping between folders.</p>
<h2>What Changes Together Should Live Together</h2>
<p><a href="https://milanjovanovic.tech/blog/vertical-slice-architecture-dotnet"><strong>Vertical Slice Architecture</strong></a> organizes code by feature instead of layer, and it's <a href="https://milanjovanovic.tech/blog/vertical-slice-architecture-is-easier-than-you-think"><strong>easier to adopt than most people think</strong></a>:</p>
<pre><code>// Layered: related code is scattered
Controllers/OrdersController.cs    ← GetOrder, CreateOrder, DeleteOrder
Services/OrderService.cs           ← GetOrder, CreateOrder, DeleteOrder
Repositories/OrderRepository.cs    ← GetOrder, CreateOrder, DeleteOrder

// VSA: related code is co-located
Features/Orders/GetOrder.cs        ← everything for GetOrder
Features/Orders/CreateOrder.cs     ← everything for CreateOrder
Features/Orders/DeleteOrder.cs     ← everything for DeleteOrder
</code></pre>
<p>When you work on &quot;Get Order,&quot; you open one file. When you review a pull request, the diff shows one file per feature.</p>
<h2>Choose VSA When...</h2>
<h3>Your Features Are Independent</h3>
<p>If most features don't share logic, VSA reduces unnecessary abstractions:</p>
<pre><code class="language-csharp">// This feature needs no repository, no service layer
public static class GetOrder
{
    public sealed record Query(Guid Id);

    public sealed record OrderResponse(
        Guid Id, string Status, decimal TotalAmount, DateTime CreatedAt);

    public sealed class Handler(ApplicationDbContext db)
    {
        public async Task&lt;OrderResponse?&gt; Handle(
            Query query, CancellationToken ct)
        {
            return await db.Orders
                .Where(o =&gt; o.Id == query.Id)
                .Select(o =&gt; new OrderResponse(
                    o.Id, o.Status, o.TotalAmount, o.CreatedAt))
                .FirstOrDefaultAsync(ct);
        }
    }
}
</code></pre>
<p>No interface, no repository, no service. Just a query that returns data.</p>
<h3>Your Team Is Growing</h3>
<p>With layers, two developers working on separate features often edit the same files - the same controller, the same service. Merge conflicts happen frequently.</p>
<p>With VSA, each developer works in separate files. Feature A doesn't touch Feature B's code.</p>
<h3>You Want Fast Iteration</h3>
<p>VSA has less ceremony. Adding a new feature:</p>
<ol>
<li>Create one file</li>
<li>Define the request, handler, and endpoint</li>
<li>Done</li>
</ol>
<p>No interface to define, no repository to implement, and assembly scanning handles the registration.</p>
<h3>Your Application Is CRUD-Heavy</h3>
<p>Many business applications are variations of create-read-update-delete. VSA handles this cleanly:</p>
<pre><code>Features/
  Products/
    CreateProduct.cs      ← 60 lines
    GetProduct.cs         ← 40 lines
    GetProducts.cs        ← 50 lines
    UpdateProduct.cs      ← 70 lines
    DeleteProduct.cs      ← 30 lines
</code></pre>
<p>Each file is small and self-contained. No layered abstractions adding complexity without value.</p>
<h3>You're Using CQRS</h3>
<p><a href="https://milanjovanovic.tech/blog/cqrs-pattern-the-way-it-should-have-been-from-the-start"><strong>CQRS</strong></a> and VSA are a natural pair. Commands and queries are already separate operations - putting each in its own file is the logical next step:</p>
<pre><code>Features/
  Orders/
    Commands/
      PlaceOrder.cs
      CancelOrder.cs
    Queries/
      GetOrder.cs
      GetOrders.cs
</code></pre>
<h2>Choose Layers When...</h2>
<h3>You Have Complex Shared Business Logic</h3>
<p>If 10 features all need the same pricing calculation, a <code>PricingService</code> in a service layer makes sense. Duplicating that logic across 10 slices is worse.</p>
<h3>You Need Strict Architectural Boundaries</h3>
<p>Layers enforce compile-time boundaries. The presentation layer physically cannot reference the database. VSA in a single project doesn't prevent a handler from doing whatever it wants.</p>
<h3>Your Team Is Familiar With Layers</h3>
<p>Architecture decisions are team decisions. If your team knows layered architecture well and productivity is good, switching to VSA for the sake of switching creates churn without value.</p>
<h3>You Have a Rich Domain Model</h3>
<p><strong>Domain-Driven Design</strong> with a <strong>rich domain model</strong> benefits from a dedicated domain layer. The domain layer contains complex business rules that multiple features share. <a href="https://milanjovanovic.tech/blog/clean-architecture-vs-onion-vs-hexagonal"><strong>Clean Architecture</strong></a> is a better fit here.</p>
<h2>The Middle Ground</h2>
<p>You don't have to be all-in on either approach. Many successful projects combine both:</p>
<pre><code>src/
  MyApp.Api/
    Features/           ← Vertical slices for individual operations
      Orders/
        PlaceOrder.cs
        GetOrder.cs
    Domain/             ← Shared domain entities (from Clean Architecture)
      Order.cs
      Customer.cs
    Shared/             ← Cross-cutting concerns
      Behaviors/
        ValidationBehavior.cs
</code></pre>
<p>Use slices for application logic. Use a domain layer for shared business rules. Use pipeline behaviors for cross-cutting concerns.</p>
<h2>Migrating From Layers, Incrementally</h2>
<p>Choosing VSA doesn't mean rewriting your layered application. The migration path I recommend:</p>
<ol>
<li><strong>New features go in a <code>Features</code> folder</strong> as self-contained slices. Don't touch the existing layers yet.</li>
<li><strong>When you modify an existing feature</strong>, consider moving it into a slice as part of the change. The controller action, service method, and repository method collapse into one handler.</li>
<li><strong>Leave stable code alone.</strong> A feature nobody has touched in a year gains nothing from being restructured.</li>
<li><strong>Delete layers as they empty out.</strong> When <code>OrderService</code> has one method left, inline it and remove the class.</li>
</ol>
<p>After a few months you have a codebase that's mostly slices with a small legacy core, and you got there without a big-bang rewrite or a feature freeze.</p>
<h2>Decision Matrix</h2>
<img src="https://milanjovanovic.tech/blogs/articles/when-to-choose-vertical-slice-architecture/architecture-decision.png" alt="A decision flow: rich shared domain logic or strict compile-time boundaries point to layered or Clean Architecture; independent, CRUD-heavy, or CQRS features point to vertical slices, otherwise a hybrid">
<p>Signals that point toward <strong>Vertical Slice Architecture</strong>:</p>
<ul>
<li>Features are largely independent of each other</li>
<li>The application is CRUD-heavy</li>
<li>You need fast iteration with minimal ceremony</li>
<li>The team is growing and works on many features in parallel</li>
<li>You're already using CQRS</li>
</ul>
<p>Signals that point toward <strong>layers</strong>:</p>
<ul>
<li>Heavy shared business logic across features</li>
<li>A rich domain model with DDD</li>
<li>You need strict compile-time boundaries</li>
<li>The team is experienced and productive with layers</li>
</ul>
<p>If your signals land on both sides, that's normal. It usually means the hybrid approach (slices plus a shared domain layer) is your answer.</p>
<h2>Making the Call</h2>
<p>Choose <a href="https://milanjovanovic.tech/blog/vertical-slice-architecture-dotnet"><strong>Vertical Slice Architecture</strong></a> when:</p>
<ol>
<li><strong>Features are independent</strong> and rarely share logic</li>
<li><strong>Your team needs parallel development</strong> without merge conflicts</li>
<li><strong>You value simplicity</strong> - one file per feature, no unnecessary abstractions</li>
<li><strong>You're already using CQRS</strong> (with or without MediatR)</li>
<li><strong>The application is CRUD-heavy</strong> without complex domain logic</li>
</ol>
<p>Choose layers when shared business logic, strict boundaries, or DDD richness justifies the overhead.</p>
<p>The best architecture is the one your team can maintain and evolve. Start with what fits your problem, not what's trending.</p>
<p>Thanks for reading, and stay awesome!</p>
<hr>
]]></content:encoded>
            <author>milan@milanjovanovic.tech (Milan Jovanović)</author>
        </item>
        <item>
            <title><![CDATA[Vertical Slice Architecture vs Clean Architecture]]></title>
            <link>https://milanjovanovic.tech/blog/vertical-slice-vs-clean-architecture</link>
            <guid isPermaLink="false">https://milanjovanovic.tech/blog/vertical-slice-vs-clean-architecture</guid>
            <pubDate>Thu, 13 Aug 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Clean Architecture manages complexity through layer discipline. Vertical Slice Architecture manages it through feature isolation.]]></description>
            <content:encoded><![CDATA[<p>Clean Architecture and Vertical Slice Architecture are the two most debated ways to structure a .NET application.
The debate is usually framed as a battle where one side must be wrong.
That framing fails you: the two approaches optimize for different things, and each one wins in different projects.
Here is how they compare, when to pick which, and the hybrid that many real projects land on.</p>
<h2>Two Different Philosophies</h2>
<p><a href="https://milanjovanovic.tech/blog/clean-architecture-and-the-benefits-of-structured-software-design"><strong>Clean Architecture</strong></a> organizes code by <strong>technical layer</strong> - Domain, Application, Infrastructure, Presentation. Each layer has clear responsibilities, and dependencies point inward.</p>
<p><a href="https://milanjovanovic.tech/blog/vertical-slice-architecture"><strong>Vertical Slice Architecture</strong></a> organizes code by <strong>feature</strong> - each feature is a self-contained slice that cuts through all layers from UI to database. You don't share code between slices unless it's a genuine cross-cutting concern.</p>
<p>Both are valid. But they optimize for different things.</p>
<img src="https://milanjovanovic.tech/blogs/articles/vertical-slice-vs-clean-architecture/clean-vs-vsa.png" alt="Clean Architecture groups code into Presentation, Application, Domain, and Infrastructure layers, while Vertical Slice Architecture groups code into self-contained feature slices">
<h2>Clean Architecture in Brief</h2>
<p>Clean Architecture separates your solution into concentric layers:</p>
<pre><code>Presentation → Application → Domain ← Infrastructure
</code></pre>
<p>The dependency rule: inner layers define abstractions, outer layers implement them.</p>
<p>A typical project structure:</p>
<pre><code>MyApp.Domain/         ← Entities, Value Objects, Interfaces
MyApp.Application/    ← Use Cases, DTOs, Validators
MyApp.Infrastructure/ ← EF Core, External APIs
MyApp.Api/            ← Controllers, Endpoints
</code></pre>
<p>Adding a new feature means touching <strong>multiple projects</strong>: define the entity in Domain, add a command/handler in Application, configure persistence in Infrastructure, add an endpoint in Presentation.</p>
<h2>Vertical Slice Architecture in Brief</h2>
<p>Vertical Slice Architecture groups everything for a feature together:</p>
<pre><code>Features/
  PlaceOrder/
    PlaceOrderEndpoint.cs
    PlaceOrderCommand.cs
    PlaceOrderHandler.cs
    PlaceOrderValidator.cs
  GetOrderById/
    GetOrderByIdEndpoint.cs
    GetOrderByIdQuery.cs
    GetOrderByIdHandler.cs
    OrderResponse.cs
</code></pre>
<p>Adding a new feature means creating a <strong>new folder</strong> with all the code for that feature. You don't modify existing features.</p>
<h2>Key Differences</h2>
<h3>Coupling Direction</h3>
<p><strong>Clean Architecture</strong> couples by <em>layer</em>. All repositories live together. All entities live together. Changing how repositories work could affect many features.</p>
<p><strong>Vertical Slices</strong> couple by <em>feature</em>. Each feature is independent. Changing the PlaceOrder feature doesn't affect GetOrderById.</p>
<h3>Code Reuse</h3>
<p><strong>Clean Architecture</strong> encourages code reuse across features. A shared <code>OrderRepository</code> serves every use case that needs orders.</p>
<p><strong>Vertical Slices</strong> minimize shared code. Each feature can query the database differently. PlaceOrder might use a repository; GetOrderById might use raw Dapper.</p>
<h3>Consistency</h3>
<p><strong>Clean Architecture</strong> enforces consistency. Every feature follows the same patterns - same handler structure, same validation approach, same repository layer.</p>
<p><strong>Vertical Slices</strong> allow variation. Simple CRUD features can be simple. Complex features can use rich domain models. Each slice uses what it needs.</p>
<h3>Indirection</h3>
<p><strong>Clean Architecture</strong> adds layers of indirection. To trace a request from endpoint to database, you pass through multiple abstractions: endpoint → handler → repository → DbContext.</p>
<p><strong>Vertical Slices</strong> minimize indirection. A simple query handler might go straight to the database with no intermediate abstractions.</p>
<h2>Side-by-Side Comparison</h2>
<p>Here's how the two approaches stack up, dimension by dimension:</p>
<ul>
<li><strong>Organization</strong>: Clean Architecture groups by technical layer; vertical slices group by feature.</li>
<li><strong>Coupling</strong>: Clean Architecture couples code within layers; VSA couples code within features.</li>
<li><strong>Code reuse</strong>: high in Clean Architecture (shared services and repositories); intentionally low in VSA.</li>
<li><strong>Consistency</strong>: enforced by Clean Architecture's uniform patterns; optional in VSA, where each slice picks its own level of abstraction.</li>
<li><strong>New feature effort</strong>: Clean Architecture touches multiple projects; VSA adds one folder.</li>
<li><strong>Learning curve</strong>: moderate to high for Clean Architecture; low for VSA.</li>
<li><strong>Best for</strong>: complex domain logic (Clean Architecture) versus features with varied complexity (VSA).</li>
<li><strong>Main risk</strong>: over-engineering simple features (Clean Architecture) versus duplication between features (VSA).</li>
</ul>
<h2>When to Choose Clean Architecture</h2>
<p><strong>Choose Clean Architecture when:</strong></p>
<ol>
<li>
<p><strong>Your domain is complex.</strong> If you have rich business rules, invariants, and domain events, Clean Architecture gives you the structure to manage that complexity.</p>
</li>
<li>
<p><strong>Multiple features share domain logic.</strong> If your Order entity is used by PlaceOrder, CancelOrder, RefundOrder, and ShipOrder - sharing it through a Domain layer makes sense.</p>
</li>
<li>
<p><strong>You want strict architectural boundaries.</strong> Clean Architecture's layers can be enforced with <a href="https://milanjovanovic.tech/blog/enforcing-software-architecture-with-architecture-tests"><strong>architecture tests</strong></a>, giving you confidence that infrastructure doesn't leak into your domain.</p>
</li>
<li>
<p><strong>Your team is large and values uniformity.</strong> One enforced pattern for every use case keeps dozens of developers producing code that looks the same, which pays off in reviews and onboarding.</p>
</li>
<li>
<p><strong>The project is long-lived.</strong> The upfront investment in structure pays off over years as the codebase grows.</p>
</li>
</ol>
<h2>When to Choose Vertical Slice Architecture</h2>
<p><strong>Choose Vertical Slices when:</strong></p>
<ol>
<li>
<p><strong>Features have different complexity.</strong> Some endpoints are simple CRUD, others have complex workflows. Vertical slices let each feature use the appropriate level of abstraction.</p>
</li>
<li>
<p><strong>You want fast feature delivery.</strong> New features are self-contained. You don't need to understand the entire repository layer to add a new query.</p>
</li>
<li>
<p><strong>Your team is small.</strong> Less infrastructure to maintain. Fewer abstractions to navigate.</p>
</li>
<li>
<p><strong>You're building a CRUD-heavy API.</strong> If most features are thin wrappers around database operations, layers add overhead without value.</p>
</li>
<li>
<p><strong>You want to minimize coupling.</strong> Features that don't share code can be modified and tested in isolation, without touching the rest of the system.</p>
</li>
</ol>
<h2>Can You Combine Them?</h2>
<p>Yes. And many teams do.</p>
<p>A common approach: use <strong>Vertical Slices for feature organization</strong> inside a <strong>Clean Architecture solution structure</strong>.</p>
<pre><code>MyApp.Application/
  Features/
    Orders/
      PlaceOrder/
        PlaceOrderCommand.cs
        PlaceOrderCommandHandler.cs
      GetOrderById/
        GetOrderByIdQuery.cs
        GetOrderByIdQueryHandler.cs
    Customers/
      RegisterCustomer/
        RegisterCustomerCommand.cs
        RegisterCustomerCommandHandler.cs
</code></pre>
<p>You get:</p>
<ul>
<li>Feature-based organization (vertical slices)</li>
<li>Layer boundaries enforced at the project level (Clean Architecture)</li>
<li>Shared domain model for complex business rules</li>
<li>Independent use cases that don't affect each other</li>
</ul>
<p>This hybrid approach is what I use in my <a href="https://milanjovanovic.tech/pragmatic-clean-architecture">Pragmatic Clean Architecture course</a>. It gives you the best of both worlds.</p>
<h2>Common Mistakes</h2>
<p><strong>1. Using Clean Architecture for everything.</strong> A simple API with five CRUD endpoints doesn't need four projects and a dozen abstractions.</p>
<p><strong>2. Duplicating everything in Vertical Slices.</strong> If three features need the same validation logic, extract it. &quot;Minimize code sharing&quot; doesn't mean &quot;never share code.&quot;</p>
<p><strong>3. Choosing based on popularity, not fit.</strong> Clean Architecture is more popular in .NET. That doesn't make it the right choice for every project.</p>
<p><strong>4. Thinking it's permanent.</strong> You can start with Vertical Slices and introduce layer boundaries as complexity grows. Architecture should evolve with your project. If you're unsure where your project falls, I've written about <a href="https://milanjovanovic.tech/blog/when-to-choose-vertical-slice-architecture"><strong>when to choose Vertical Slice Architecture</strong></a> in more detail.</p>
<h2>Takeaway</h2>
<p>Clean Architecture and Vertical Slice Architecture solve different problems:</p>
<ul>
<li><strong>Clean Architecture</strong> manages complexity through <strong>layer discipline</strong></li>
<li><strong>Vertical Slices</strong> manage complexity through <strong>feature isolation</strong></li>
</ul>
<p>For complex domains with shared business rules → Clean Architecture.
For varied features with different complexity levels → Vertical Slices.
For most real projects → a pragmatic combination of both.</p>
<p>Thanks for reading, and stay awesome!</p>
<hr>
]]></content:encoded>
            <author>milan@milanjovanovic.tech (Milan Jovanović)</author>
        </item>
        <item>
            <title><![CDATA[Vertical Slice Architecture Folder Structure: From 5 to 50+ Features]]></title>
            <link>https://milanjovanovic.tech/blog/vertical-slice-project-structure-dotnet</link>
            <guid isPermaLink="false">https://milanjovanovic.tech/blog/vertical-slice-project-structure-dotnet</guid>
            <pubDate>Thu, 13 Aug 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[A concrete folder and solution layout for Vertical Slice Architecture in .NET, and how it evolves as you grow from 5 features to 50+.]]></description>
            <content:encoded><![CDATA[<p>Vertical Slice Architecture sounds simple until you create the solution and have to decide where everything goes.
Where do domain entities live?
Does the DbContext get its own project?
What happens when the Features folder hits 50 slices?
This article answers those questions with a concrete layout, and shows how it evolves as the project grows.</p>
<h2>The Problem With Layered Folders</h2>
<p>In a traditional layered project, you get folders like this:</p>
<pre><code>Controllers/
  OrdersController.cs
  CustomersController.cs
  ProductsController.cs
Services/
  OrderService.cs
  CustomerService.cs
  ProductService.cs
Repositories/
  OrderRepository.cs
  CustomerRepository.cs
  ProductRepository.cs
Models/
  Order.cs
  Customer.cs
  Product.cs
</code></pre>
<p>To understand how &quot;Place Order&quot; works, you jump between 4+ folders. Adding a feature means touching multiple folders. Related code is scattered.</p>
<p><a href="https://milanjovanovic.tech/blog/vertical-slice-architecture-dotnet"><strong>Vertical Slice Architecture</strong></a> fixes this by organizing code around features. This article is about the practical part: the actual folder and solution layout, and how it holds up as the feature count grows.</p>
<p>For what goes <strong>inside</strong> a slice (the command, handler, and validator structure), see my newsletter issue on <a href="https://milanjovanovic.tech/blog/vertical-slice-architecture-structuring-vertical-slices"><strong>structuring vertical slices</strong></a>. Here we stay at the folder level.</p>
<h2>The Starting Layout: 5-15 Features</h2>
<p>One project, one <code>Features</code> folder, one file per use case:</p>
<pre><code>Features/
  Orders/
    PlaceOrder.cs
    GetOrder.cs
    GetOrders.cs
    CancelOrder.cs
    OrdersModule.cs
  Customers/
    RegisterCustomer.cs
    GetCustomer.cs
    UpdateCustomer.cs
    CustomersModule.cs
  Products/
    CreateProduct.cs
    GetProducts.cs
    SearchProducts.cs
    ProductsModule.cs
</code></pre>
<p>Everything for &quot;Place Order&quot; is in one file. Everything for orders is in one folder. The <code>*Module.cs</code> file per folder registers that feature group's endpoints (a <a href="https://milanjovanovic.tech/blog/vertical-slice-architecture-carter-dotnet"><strong>Carter</strong></a> module or a plain extension method, your choice).</p>
<p>Two naming rules keep this navigable:</p>
<ul>
<li><strong>Files are verbs</strong>: <code>PlaceOrder.cs</code>, not <code>OrderService.cs</code>. The folder listing reads like a feature list.</li>
<li><strong>One use case per file</strong>: if a file handles two operations, it's two slices pretending to be one.</li>
</ul>
<h2>Full Project Structure</h2>
<p>Here's the complete single-project layout I use:</p>
<pre><code>src/
  MyApp.Api/
    Features/
      Orders/
        PlaceOrder.cs
        GetOrder.cs
        GetOrders.cs
        CancelOrder.cs
        UpdateOrderStatus.cs
        OrdersModule.cs
      Customers/
        RegisterCustomer.cs
        GetCustomer.cs
        GetCustomers.cs
        UpdateCustomer.cs
        CustomersModule.cs
      Products/
        CreateProduct.cs
        GetProducts.cs
        SearchProducts.cs
        ProductsModule.cs
    Domain/
      Order.cs
      Customer.cs
      Product.cs
      Common/
        Entity.cs
        Result.cs
        Error.cs
    Data/
      ApplicationDbContext.cs
      Configurations/
        OrderConfiguration.cs
        CustomerConfiguration.cs
        ProductConfiguration.cs
      Migrations/
    Shared/
      Behaviors/
        ValidationBehavior.cs
        LoggingBehavior.cs
      Middleware/
        ExceptionHandlingMiddleware.cs
    Program.cs
tests/
  MyApp.Api.Tests/
    Features/
      Orders/
        PlaceOrderTests.cs
        GetOrderTests.cs
      Customers/
        RegisterCustomerTests.cs
</code></pre>
<p>Note that the test project mirrors the <code>Features</code> tree exactly. Finding the tests for a slice should never require a search.</p>
<h2>Key Decisions</h2>
<h3>One Project or Multiple?</h3>
<p><strong>Single project</strong> - the default for VSA. Keep it simple:</p>
<pre><code>MyApp.Api/
  Features/
  Domain/
  Data/
  Shared/
</code></pre>
<p><strong>Multiple projects</strong> - only when you need strict compile-time enforcement:</p>
<pre><code>MyApp.Api/          ← entry point
MyApp.Features/     ← all features
MyApp.Domain/       ← domain entities
MyApp.Data/         ← EF Core, migrations
</code></pre>
<p>Start with one project. Split when you have a reason. Folder boundaries are cheap to change; project boundaries are not. If you want boundary enforcement without extra projects, <strong>architecture tests</strong> on namespaces get you most of the way.</p>
<h3>Where Do Domain Entities Live?</h3>
<p>In a <code>Domain/</code> folder within the same project:</p>
<pre><code>Domain/
  Order.cs
  LineItem.cs
  Customer.cs
  Product.cs
  Common/
    Entity.cs
    AggregateRoot.cs
    IDomainEvent.cs
</code></pre>
<p>Domain entities are shared across features. An <code>Order</code> entity is used by <code>PlaceOrder</code>, <code>GetOrder</code>, and <code>CancelOrder</code>. Slices own their request and response types; they share the domain model underneath.</p>
<h3>Where Does the DbContext Live?</h3>
<p>In a <code>Data/</code> folder:</p>
<pre><code>Data/
  ApplicationDbContext.cs
  Configurations/
    OrderConfiguration.cs
    CustomerConfiguration.cs
  Migrations/
</code></pre>
<p>EF Core configurations are separate from features - they're infrastructure, not business logic.</p>
<h3>Shared Code (Cross-Cutting Concerns)</h3>
<p>Pipeline behaviors, middleware, and shared abstractions in <code>Shared/</code>:</p>
<pre><code>Shared/
  Behaviors/
    ValidationBehavior.cs
    LoggingBehavior.cs
    CachingBehavior.cs
  Middleware/
    ExceptionHandlingMiddleware.cs
    RequestLoggingMiddleware.cs
  Abstractions/
    ICommand.cs
    IQuery.cs
    ICacheable.cs
  Extensions/
    ResultExtensions.cs
</code></pre>
<p>Keep this folder small and boring. If <code>Shared</code> starts accumulating business logic, a slice boundary is leaking; see <a href="https://milanjovanovic.tech/blog/cross-cutting-concerns-in-vertical-slice-architecture"><strong>cross-cutting concerns in Vertical Slice Architecture</strong></a> for what belongs here and what doesn't.</p>
<h2>When Features Get Complex</h2>
<p>A simple feature fits in one file. A complex feature graduates to a folder:</p>
<pre><code>Features/
  Orders/
    PlaceOrder/
      PlaceOrderCommand.cs
      PlaceOrderHandler.cs
      PlaceOrderValidator.cs
      PlaceOrderResponse.cs
    GetOrder/
      GetOrderQuery.cs
      GetOrderHandler.cs
    Shared/
      OrderResponse.cs
    OrdersModule.cs
</code></pre>
<p>My threshold: split into a folder when the single file grows past roughly 150-200 lines, or when a slice needs private helper classes that would pollute the file.</p>
<p>A feature-local <code>Shared/</code> folder (like <code>Orders/Shared/</code>) is fine for DTOs reused by two or three sibling slices, like the <code>OrderResponse</code> that both <code>GetOrder</code> and <code>GetOrders</code> return. It's still inside the feature boundary, which is very different from a global shared folder.</p>
<h2>Scaling to 50+ Features</h2>
<p>A flat <code>Features</code> folder stops working around a few dozen slices. The fix is one more level: group by domain area.</p>
<img src="https://milanjovanovic.tech/blogs/articles/vertical-slice-project-structure-dotnet/structure-evolution.png" alt="The folder structure evolves from a flat Features folder at 5-15 features, to domain-area grouping at 50+ features, to a modular monolith where each area becomes a module">
<pre><code>Features/
  Ordering/
    PlaceOrder.cs
    GetOrder.cs
    CancelOrder.cs
    OrderingModule.cs
  Catalog/
    CreateProduct.cs
    SearchProducts.cs
    CatalogModule.cs
  Identity/
    RegisterUser.cs
    Login.cs
    RefreshToken.cs
    IdentityModule.cs
  Shipping/
    CreateShipment.cs
    TrackShipment.cs
    ShippingModule.cs
</code></pre>
<p>These groups aren't arbitrary. They mirror <a href="https://milanjovanovic.tech/blog/bounded-context-ddd-explained"><strong>bounded contexts</strong></a>, and each one is a candidate module if you later evolve toward a <a href="https://milanjovanovic.tech/blog/modular-monolith-architecture-dotnet"><strong>modular monolith</strong></a>. At that point each domain area gets its own project (or set of projects), and the folder structure you already have becomes the module structure. I've written about exactly <a href="https://milanjovanovic.tech/blog/where-vertical-slices-fit-inside-the-modular-monolith-architecture"><strong>where vertical slices fit inside a modular monolith</strong></a>.</p>
<p>The practical signals that you've hit this stage:</p>
<ul>
<li>You scroll to find anything in <code>Features/</code></li>
<li>Two domain areas keep reaching into each other's entities</li>
<li>Different teams own different feature groups and step on each other in PRs</li>
</ul>
<p>Restructuring is mechanical: create the domain-area folders, move files, fix namespaces. Do it in one PR before the pain compounds.</p>
<h2>Conventions That Keep the Structure Healthy</h2>
<p>A folder structure only stays clean if a few conventions back it up:</p>
<ul>
<li><strong>Namespace mirrors folder.</strong> <code>MyApp.Api.Features.Ordering.PlaceOrder</code> tells you exactly where the file lives. Most IDEs enforce this automatically.</li>
<li><strong>Handlers are <code>internal</code>.</strong> Nothing outside the slice should call a handler directly. The endpoint (or dispatcher) is the only entry point.</li>
<li><strong>One route prefix per module.</strong> <code>OrdersModule</code> owns <code>/api/orders</code>; no other module maps routes under it.</li>
<li><strong>No slice-to-slice references.</strong> If <code>PlaceOrder</code> needs something from <code>Shipping</code>, that's a domain service or a domain event, not a <code>using</code> statement. A couple of <a href="https://milanjovanovic.tech/blog/5-architecture-tests-you-should-add-to-your-dotnet-projects"><strong>architecture tests</strong></a> will hold this line for you.</li>
</ul>
<h2>Takeaway</h2>
<p>A practical VSA folder structure:</p>
<ol>
<li><strong>Features folder</strong> - one file per use case, named as a verb</li>
<li><strong>Domain folder</strong> - shared entities and value objects</li>
<li><strong>Data folder</strong> - DbContext, configurations, migrations</li>
<li><strong>Shared folder</strong> - pipeline behaviors, middleware, abstractions (keep it boring)</li>
<li><strong>Single project</strong> to start, split only when you need compile-time boundaries</li>
<li><strong>Single-file slices</strong> first, folders past ~150-200 lines</li>
<li><strong>Domain-area grouping</strong> at 50+ features, mirroring bounded contexts</li>
</ol>
<p>The structure should make it obvious what your application does by reading the feature folder names.</p>
<p>Thanks for reading, and stay awesome!</p>
<hr>
]]></content:encoded>
            <author>milan@milanjovanovic.tech (Milan Jovanović)</author>
        </item>
        <item>
            <title><![CDATA[Vertical Slice Architecture With Carter in .NET]]></title>
            <link>https://milanjovanovic.tech/blog/vertical-slice-architecture-carter-dotnet</link>
            <guid isPermaLink="false">https://milanjovanovic.tech/blog/vertical-slice-architecture-carter-dotnet</guid>
            <pubDate>Thu, 13 Aug 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Carter turns Minimal API endpoints into self-registering modules: each feature defines its routes, and app.MapCarter() wires them up at startup.]]></description>
            <content:encoded><![CDATA[<p>Minimal APIs are great until <code>Program.cs</code> hits 500 lines.
Carter fixes that with one convention: each feature defines its routes in a module, and the library discovers and maps them at startup.
That convention happens to be a perfect fit for Vertical Slice Architecture, where each feature already owns everything else.
Here is how I combine the two.</p>
<h2>What Is Carter?</h2>
<p><a href="https://github.com/CarterCommunity/Carter">Carter</a> is a library that adds convention-based routing to ASP.NET Core <a href="https://milanjovanovic.tech/blog/minimal-apis-dotnet"><strong>Minimal APIs</strong></a>. It lets you define endpoints in self-contained modules instead of one giant <code>Program.cs</code>.</p>
<p>Combined with <a href="https://milanjovanovic.tech/blog/vertical-slice-architecture-dotnet"><strong>Vertical Slice Architecture</strong></a>, Carter gives each feature its own endpoint module, handler, and model - all in one place.</p>
<h2>Setting Up Carter</h2>
<p>Install the package:</p>
<pre><code class="language-bash">dotnet add package Carter
</code></pre>
<p>Register it:</p>
<pre><code class="language-csharp">var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCarter();

builder.Services.AddMediatR(config =&gt;
{
    config.RegisterServicesFromAssembly(typeof(Program).Assembly);
    config.AddOpenBehavior(typeof(ValidationBehavior&lt;,&gt;));
});

builder.Services.AddValidatorsFromAssembly(typeof(Program).Assembly);

var app = builder.Build();

app.MapCarter();
app.Run();
</code></pre>
<p><code>MapCarter()</code> automatically discovers all <code>ICarterModule</code> implementations and registers their routes.</p>
<p><code>ValidationBehavior</code> is the FluentValidation pipeline behavior that runs each slice's validator before the handler and returns failures as a failed <code>Result</code>.
I break it down in <a href="https://milanjovanovic.tech/blog/validation-vertical-slice-architecture"><strong>validation in Vertical Slice Architecture</strong></a>.</p>
<img src="https://milanjovanovic.tech/blogs/articles/vertical-slice-architecture-carter-dotnet/carter-module-discovery.png" alt="app.MapCarter scans the assembly for ICarterModule implementations and registers the routes each module owns, one module per feature area">
<h2>A Feature Slice With Carter</h2>
<p>Here's a complete <a href="https://milanjovanovic.tech/blog/feature-folders-dotnet"><strong>feature slice</strong></a> for placing an order:</p>
<pre><code class="language-csharp">// Features/Orders/PlaceOrder.cs
public static class PlaceOrder
{
    public sealed record Command(
        Guid CustomerId,
        List&lt;OrderItemRequest&gt; Items) : IRequest&lt;Result&lt;Guid&gt;&gt;;

    public sealed record OrderItemRequest(Guid ProductId, int Quantity);

    public sealed class Validator : AbstractValidator&lt;Command&gt;
    {
        public Validator()
        {
            RuleFor(x =&gt; x.CustomerId).NotEmpty();
            RuleFor(x =&gt; x.Items).NotEmpty();
            RuleForEach(x =&gt; x.Items).ChildRules(item =&gt;
            {
                item.RuleFor(x =&gt; x.ProductId).NotEmpty();
                item.RuleFor(x =&gt; x.Quantity).GreaterThan(0);
            });
        }
    }

    public sealed class Handler : IRequestHandler&lt;Command, Result&lt;Guid&gt;&gt;
    {
        private readonly ApplicationDbContext _db;

        public Handler(ApplicationDbContext db) =&gt; _db = db;

        public async Task&lt;Result&lt;Guid&gt;&gt; Handle(
            Command request, CancellationToken ct)
        {
            var customer = await _db.Customers
                .FirstOrDefaultAsync(c =&gt; c.Id == request.CustomerId, ct);

            if (customer is null)
            {
                return Result.Failure&lt;Guid&gt;(
                    new Error(&quot;Customer.NotFound&quot;, &quot;Customer not found.&quot;));
            }

            var order = new Order
            {
                Id = Guid.NewGuid(),
                CustomerId = request.CustomerId,
                Items = request.Items.Select(i =&gt; new OrderItem
                {
                    ProductId = i.ProductId,
                    Quantity = i.Quantity
                }).ToList(),
                CreatedAt = DateTime.UtcNow
            };

            _db.Orders.Add(order);
            await _db.SaveChangesAsync(ct);

            return order.Id;
        }
    }
}
</code></pre>
<p>Now the Carter module:</p>
<pre><code class="language-csharp">// Features/Orders/OrdersModule.cs
public class OrdersModule : ICarterModule
{
    public void AddRoutes(IEndpointRouteBuilder app)
    {
        var group = app.MapGroup(&quot;/api/orders&quot;)
            .WithTags(&quot;Orders&quot;);

        group.MapPost(&quot;&quot;, async (
            PlaceOrder.Command command,
            ISender sender,
            CancellationToken ct) =&gt;
        {
            var result = await sender.Send(command, ct);

            return result.IsSuccess
                ? Results.Created($&quot;/api/orders/{result.Value}&quot;, result.Value)
                : result.ToProblemDetails();
        });

        group.MapGet(&quot;{id:guid}&quot;, async (
            Guid id,
            ISender sender,
            CancellationToken ct) =&gt;
        {
            var result = await sender.Send(new GetOrder.Query(id), ct);

            return result.IsSuccess
                ? Results.Ok(result.Value)
                : result.ToProblemDetails();
        });

        group.MapGet(&quot;&quot;, async (
            int page,
            int pageSize,
            ISender sender,
            CancellationToken ct) =&gt;
        {
            var result = await sender.Send(
                new GetOrders.Query(page, pageSize), ct);

            return Results.Ok(result.Value);
        });
    }
}
</code></pre>
<p><code>ToProblemDetails()</code> is a small extension method that maps a failed <code>Result</code> to <code>Results.Problem</code> or <code>Results.ValidationProblem</code>, so every endpoint returns consistent Problem Details responses.</p>
<h2>Project Structure</h2>
<pre><code>Features/
  Orders/
    PlaceOrder.cs
    GetOrder.cs
    GetOrders.cs
    CancelOrder.cs
    OrdersModule.cs
  Customers/
    RegisterCustomer.cs
    GetCustomer.cs
    CustomersModule.cs
  Products/
    CreateProduct.cs
    GetProducts.cs
    ProductsModule.cs
</code></pre>
<p>Each feature folder contains:</p>
<ul>
<li><strong>One file per use case</strong> (command/query + handler + validator)</li>
<li><strong>One Carter module</strong> for routing all endpoints in that domain</li>
</ul>
<p>Everything for orders lives in <code>Features/Orders/</code>. No jumping between Controllers, Services, Models, and Repositories folders.</p>
<h2>Route Groups and Common Configuration</h2>
<p>Carter modules support route groups with shared configuration:</p>
<pre><code class="language-csharp">public class OrdersModule : ICarterModule
{
    public void AddRoutes(IEndpointRouteBuilder app)
    {
        var group = app.MapGroup(&quot;/api/orders&quot;)
            .WithTags(&quot;Orders&quot;)
            .RequireAuthorization();

        group.MapPost(&quot;&quot;, HandlePlaceOrder);
        group.MapGet(&quot;{id:guid}&quot;, HandleGetOrder);
        group.MapDelete(&quot;{id:guid}&quot;, HandleCancelOrder)
            .RequireAuthorization(&quot;Admin&quot;);
    }

    private static async Task&lt;IResult&gt; HandlePlaceOrder(
        PlaceOrder.Command command,
        ISender sender,
        CancellationToken ct)
    {
        var result = await sender.Send(command, ct);
        return result.IsSuccess
            ? Results.Created($&quot;/api/orders/{result.Value}&quot;, result.Value)
            : result.ToProblemDetails();
    }

    private static async Task&lt;IResult&gt; HandleGetOrder(
        Guid id,
        ISender sender,
        CancellationToken ct)
    {
        var result = await sender.Send(new GetOrder.Query(id), ct);
        return result.IsSuccess
            ? Results.Ok(result.Value)
            : result.ToProblemDetails();
    }

    private static async Task&lt;IResult&gt; HandleCancelOrder(
        Guid id,
        ISender sender,
        CancellationToken ct)
    {
        var result = await sender.Send(new CancelOrder.Command(id), ct);
        return result.IsSuccess
            ? Results.NoContent()
            : result.ToProblemDetails();
    }
}
</code></pre>
<p>Extract handler methods to keep <code>AddRoutes</code> readable.</p>
<h2>Endpoint Filters</h2>
<p>If you'd rather handle <a href="https://milanjovanovic.tech/blog/cross-cutting-concerns-in-vertical-slice-architecture"><strong>cross-cutting concerns</strong></a> at the HTTP boundary instead of inside the MediatR pipeline, use endpoint filters.
Here's a validation filter that resolves the slice's FluentValidation validator from DI:</p>
<pre><code class="language-csharp">public class ValidationFilter&lt;TRequest&gt; : IEndpointFilter
{
    public async ValueTask&lt;object?&gt; InvokeAsync(
        EndpointFilterInvocationContext context,
        EndpointFilterDelegate next)
    {
        var request = context.Arguments
            .OfType&lt;TRequest&gt;()
            .FirstOrDefault();

        if (request is null)
        {
            return await next(context);
        }

        var validator = context.HttpContext.RequestServices
            .GetService&lt;IValidator&lt;TRequest&gt;&gt;();

        if (validator is not null)
        {
            var result = await validator.ValidateAsync(request);
            if (!result.IsValid)
            {
                return Results.ValidationProblem(result.ToDictionary());
            }
        }

        return await next(context);
    }
}
</code></pre>
<p>The filter is generic over the request type, so you apply it per endpoint:</p>
<pre><code class="language-csharp">group.MapPost(&quot;&quot;, HandlePlaceOrder)
    .AddEndpointFilter&lt;ValidationFilter&lt;PlaceOrder.Command&gt;&gt;();
</code></pre>
<p>Pick one place to validate (pipeline behavior or endpoint filter), not both.</p>
<h2>Testing Carter Modules</h2>
<p>Because Carter modules are just Minimal API routes, they test like any other endpoint through <strong>WebApplicationFactory</strong>:</p>
<pre><code class="language-csharp">public class OrdersModuleTests
    : IClassFixture&lt;WebApplicationFactory&lt;Program&gt;&gt;
{
    private readonly HttpClient _client;

    public OrdersModuleTests(WebApplicationFactory&lt;Program&gt; factory)
    {
        _client = factory.CreateClient();
    }

    [Fact]
    public async Task PlaceOrder_WithInvalidBody_ReturnsProblemDetails()
    {
        var response = await _client.PostAsJsonAsync(&quot;/api/orders&quot;, new
        {
            CustomerId = Guid.Empty,
            Items = Array.Empty&lt;object&gt;()
        });

        response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
    }
}
</code></pre>
<p><code>MapCarter()</code> runs during test host startup, so all modules are discovered exactly as in production. No special test setup for Carter itself.</p>
<h2>Why Carter + VSA Works</h2>
<p>What the combination buys you:</p>
<ul>
<li><strong>Auto-discovery</strong>: Carter finds modules automatically, no manual registration in <code>Program.cs</code></li>
<li><strong>Feature isolation</strong>: each module encapsulates a bounded set of endpoints</li>
<li><strong>Clean Program.cs</strong>: just <code>app.MapCarter()</code> instead of dozens of <code>MapGet</code>/<code>MapPost</code> calls</li>
<li><strong>Route grouping</strong>: share authorization, filters, and tags across related endpoints</li>
<li><strong>Testability</strong>: each handler is independent and easily unit-tested</li>
</ul>
<h2>Carter vs. Plain Minimal APIs</h2>
<p>Without Carter, endpoints pile up in <code>Program.cs</code> or require manual extension methods:</p>
<pre><code class="language-csharp">// Without Carter - gets messy fast
app.MapPost(&quot;/api/orders&quot;, HandlePlaceOrder);
app.MapGet(&quot;/api/orders/{id}&quot;, HandleGetOrder);
app.MapGet(&quot;/api/orders&quot;, HandleGetOrders);
app.MapPost(&quot;/api/customers&quot;, HandleRegisterCustomer);
app.MapGet(&quot;/api/customers/{id}&quot;, HandleGetCustomer);
// ... 50 more lines
</code></pre>
<p>With Carter, each module owns its routes. <code>Program.cs</code> stays clean.</p>
<p>To be fair, Carter isn't the only way to get there. You can build the same auto-discovery yourself with an <code>IEndpoint</code> interface and assembly scanning, which I showed in <a href="https://milanjovanovic.tech/blog/automatically-register-minimal-apis-in-aspnetcore"><strong>automatically registering Minimal APIs</strong></a>. Choose Carter if you want the convention ready-made and don't mind a third-party dependency in your API layer; roll your own if you'd rather own those 30 lines of reflection.</p>
<h2>Carter or Roll Your Own?</h2>
<p>Carter doesn't change what a vertical slice is.
It standardizes how a slice exposes its routes: each feature folder gets a module, each module owns its endpoints, and <code>Program.cs</code> shrinks to <code>app.MapCarter()</code>.</p>
<p>If you want that convention ready-made, use Carter.
If you'd rather avoid the dependency, the <code>IEndpoint</code> approach gets you the same result for 30 lines of your own code.
Either way, if you're building with Minimal APIs and <a href="https://milanjovanovic.tech/blog/vertical-slice-architecture-is-easier-than-you-think"><strong>vertical slices</strong></a>, the endpoints should end up where they belong: inside the slice.</p>
<p>Thanks for reading, and stay awesome!</p>
<hr>
]]></content:encoded>
            <author>milan@milanjovanovic.tech (Milan Jovanović)</author>
        </item>
        <item>
            <title><![CDATA[Validation in Vertical Slice Architecture]]></title>
            <link>https://milanjovanovic.tech/blog/validation-vertical-slice-architecture</link>
            <guid isPermaLink="false">https://milanjovanovic.tech/blog/validation-vertical-slice-architecture</guid>
            <pubDate>Thu, 13 Aug 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Where does validation go in Vertical Slice Architecture? Co-locate validators with features and use a pipeline behavior to run them automatically.]]></description>
            <content:encoded><![CDATA[<p>In a layered codebase, validation rules end up far from the feature they protect.
Vertical Slice Architecture puts the validator right next to the command and handler it guards.
Here is the full setup with FluentValidation and MediatR: co-located validators, a pipeline behavior that runs them automatically, and a clean split between input validation and domain validation.</p>
<h2>Validation Belongs in the Slice</h2>
<p>In <a href="https://milanjovanovic.tech/blog/vertical-slice-architecture-dotnet"><strong>Vertical Slice Architecture</strong></a>, each feature is self-contained. The validator lives next to the handler - not in a separate &quot;Validators&quot; folder across the project.</p>
<pre><code>Features/
  Orders/
    PlaceOrder.cs        ← Command + Handler + Validator
    GetOrder.cs
    CancelOrder.cs
</code></pre>
<h2>The Validator</h2>
<p>Use FluentValidation to define rules:</p>
<pre><code class="language-csharp">public static class PlaceOrder
{
    public sealed record Command(
        Guid CustomerId,
        List&lt;OrderItemRequest&gt; Items) : IRequest&lt;Result&lt;Guid&gt;&gt;;

    public sealed record OrderItemRequest(
        Guid ProductId,
        int Quantity,
        decimal UnitPrice);

    public sealed class Validator : AbstractValidator&lt;Command&gt;
    {
        public Validator()
        {
            RuleFor(x =&gt; x.CustomerId)
                .NotEmpty()
                .WithMessage(&quot;Customer ID is required.&quot;);

            RuleFor(x =&gt; x.Items)
                .NotEmpty()
                .WithMessage(&quot;At least one item is required.&quot;);

            RuleForEach(x =&gt; x.Items).ChildRules(item =&gt;
            {
                item.RuleFor(x =&gt; x.ProductId).NotEmpty();
                item.RuleFor(x =&gt; x.Quantity)
                    .GreaterThan(0)
                    .WithMessage(&quot;Quantity must be positive.&quot;);
                item.RuleFor(x =&gt; x.UnitPrice)
                    .GreaterThan(0)
                    .WithMessage(&quot;Price must be positive.&quot;);
            });
        }
    }

    public sealed class Handler : IRequestHandler&lt;Command, Result&lt;Guid&gt;&gt;
    {
        private readonly ApplicationDbContext _db;

        public Handler(ApplicationDbContext db) =&gt; _db = db;

        public async Task&lt;Result&lt;Guid&gt;&gt; Handle(
            Command request, CancellationToken ct)
        {
            // No input validation here - the pipeline already ran it
            var order = Order.Create(request.CustomerId, request.Items);

            _db.Orders.Add(order);
            await _db.SaveChangesAsync(ct);

            return order.Id;
        }
    }
}
</code></pre>
<p>The command, validator, and handler are all in one file. Everything about &quot;Place Order&quot; is in one place.</p>
<h2>Automatic Validation With a Pipeline Behavior</h2>
<p>Instead of calling the validator manually in every handler, use a MediatR pipeline behavior. This is the same approach I showed for <a href="https://milanjovanovic.tech/blog/cqrs-validation-with-mediatr-pipeline-and-fluentvalidation"><strong>CQRS validation with MediatR and FluentValidation</strong></a>:</p>
<pre><code class="language-csharp">public sealed class ValidationBehavior&lt;TRequest, TResponse&gt;
    : IPipelineBehavior&lt;TRequest, TResponse&gt;
    where TRequest : IRequest&lt;TResponse&gt;
{
    private readonly IEnumerable&lt;IValidator&lt;TRequest&gt;&gt; _validators;

    public ValidationBehavior(
        IEnumerable&lt;IValidator&lt;TRequest&gt;&gt; validators)
    {
        _validators = validators;
    }

    public async Task&lt;TResponse&gt; Handle(
        TRequest request,
        RequestHandlerDelegate&lt;TResponse&gt; next,
        CancellationToken ct)
    {
        if (!_validators.Any())
            return await next();

        var context = new ValidationContext&lt;TRequest&gt;(request);

        var validationResults = await Task.WhenAll(
            _validators.Select(v =&gt; v.ValidateAsync(context, ct)));

        var failures = validationResults
            .SelectMany(r =&gt; r.Errors)
            .Where(f =&gt; f is not null)
            .ToList();

        if (failures.Count != 0)
            throw new ValidationException(failures);

        return await next();
    }
}
</code></pre>
<p>Register it:</p>
<pre><code class="language-csharp">builder.Services.AddMediatR(cfg =&gt;
{
    cfg.RegisterServicesFromAssembly(typeof(Program).Assembly);
    cfg.AddOpenBehavior(typeof(ValidationBehavior&lt;,&gt;));
});

builder.Services.AddValidatorsFromAssembly(
    typeof(Program).Assembly);
</code></pre>
<p>Every request that has a matching <code>IValidator&lt;T&gt;</code> is validated automatically before reaching the handler.
Pair this throwing variant with a <a href="https://milanjovanovic.tech/blog/global-error-handling-in-aspnetcore-from-middleware-to-modern-handlers"><strong>global exception handler</strong></a> that turns <code>ValidationException</code> into a 400 Problem Details response.</p>
<h2>Result-Based Validation</h2>
<p>Instead of throwing exceptions, return validation errors as a <code>Result</code>:</p>
<pre><code class="language-csharp">public sealed class ValidationBehavior&lt;TRequest, TResponse&gt;
    : IPipelineBehavior&lt;TRequest, TResponse&gt;
    where TRequest : IRequest&lt;TResponse&gt;
    where TResponse : Result
{
    private readonly IEnumerable&lt;IValidator&lt;TRequest&gt;&gt; _validators;

    public ValidationBehavior(
        IEnumerable&lt;IValidator&lt;TRequest&gt;&gt; validators)
    {
        _validators = validators;
    }

    public async Task&lt;TResponse&gt; Handle(
        TRequest request,
        RequestHandlerDelegate&lt;TResponse&gt; next,
        CancellationToken ct)
    {
        if (!_validators.Any())
            return await next();

        var context = new ValidationContext&lt;TRequest&gt;(request);

        var validationResults = await Task.WhenAll(
            _validators.Select(v =&gt; v.ValidateAsync(context, ct)));

        var errors = validationResults
            .SelectMany(r =&gt; r.Errors)
            .Where(f =&gt; f is not null)
            .Select(f =&gt; new Error(f.PropertyName, f.ErrorMessage))
            .ToArray();

        if (errors.Length != 0)
            return (TResponse)(object)Result.Failure(
                new ValidationError(errors));

        return await next();
    }
}
</code></pre>
<p>The handler never runs if validation fails. The endpoint returns a 400 response with the validation errors.</p>
<p>One wrinkle to be aware of: the cast at the end only works when <code>TResponse</code> is the non-generic <code>Result</code>. For handlers returning <code>Result&lt;T&gt;</code>, a complete implementation creates the typed failure through a static factory or a small piece of reflection. It's a one-time cost in the behavior, and every slice benefits.</p>
<h2>Input Validation vs Domain Validation</h2>
<p>There are two layers of validation in any application:</p>
<img src="https://milanjovanovic.tech/blogs/articles/validation-vertical-slice-architecture/input-vs-domain-validation.png" alt="Input validation with FluentValidation runs before the handler and rejects bad format; domain validation runs inside the handler and enforces business rules like stock and customer status">
<p><strong>Input validation</strong> (FluentValidation) - checks data format and presence:</p>
<ul>
<li>Is the email format valid?</li>
<li>Is the quantity positive?</li>
<li>Is the required field present?</li>
</ul>
<p><strong>Domain validation</strong> (<strong>domain invariants</strong>) - checks business rules:</p>
<ul>
<li>Can this customer place an order?</li>
<li>Is this product in stock?</li>
<li>Does the discount code apply?</li>
</ul>
<pre><code class="language-csharp">// Input validation (FluentValidation) - runs BEFORE the handler
public sealed class Validator : AbstractValidator&lt;Command&gt;
{
    public Validator()
    {
        RuleFor(x =&gt; x.CustomerId).NotEmpty();
        RuleForEach(x =&gt; x.Items).ChildRules(item =&gt;
        {
            item.RuleFor(x =&gt; x.ProductId).NotEmpty();
            item.RuleFor(x =&gt; x.Quantity).GreaterThan(0);
        });
    }
}

// Domain validation - runs INSIDE the handler
public sealed class Handler : IRequestHandler&lt;Command, Result&lt;Guid&gt;&gt;
{
    private readonly ApplicationDbContext _db;

    public Handler(ApplicationDbContext db) =&gt; _db = db;

    public async Task&lt;Result&lt;Guid&gt;&gt; Handle(
        Command request, CancellationToken ct)
    {
        var productIds = request.Items.Select(i =&gt; i.ProductId).ToList();

        var products = await _db.Products
            .Where(p =&gt; productIds.Contains(p.Id))
            .ToDictionaryAsync(p =&gt; p.Id, ct);

        foreach (var item in request.Items)
        {
            if (!products.TryGetValue(item.ProductId, out var product))
                return Result.Failure&lt;Guid&gt;(ProductErrors.NotFound);

            if (product.StockQuantity &lt; item.Quantity)
                return Result.Failure&lt;Guid&gt;(ProductErrors.InsufficientStock);
        }

        // Domain checks passed - create the order
        var order = Order.Create(request.CustomerId, request.Items);

        _db.Orders.Add(order);
        await _db.SaveChangesAsync(ct);

        return order.Id;
    }
}
</code></pre>
<p>Input validation rejects obviously bad data. Domain validation enforces business rules.</p>
<h2>Async Validators</h2>
<p>Some validation requires database access:</p>
<pre><code class="language-csharp">public sealed class Validator : AbstractValidator&lt;Command&gt;
{
    public Validator(ApplicationDbContext db)
    {
        RuleFor(x =&gt; x.Email)
            .NotEmpty()
            .EmailAddress()
            .MustAsync(async (email, ct) =&gt;
                !await db.Users.AnyAsync(u =&gt; u.Email == email, ct))
            .WithMessage(&quot;Email is already registered.&quot;);
    }
}
</code></pre>
<p>Use async validators sparingly. Most input validation should be synchronous. Save database checks for the handler when possible.</p>
<h2>Testing Your Validators</h2>
<p>Co-located validators are trivially testable with FluentValidation's built-in <code>TestHelper</code>:</p>
<pre><code class="language-csharp">public class PlaceOrderValidatorTests
{
    private readonly PlaceOrder.Validator _validator = new();

    [Fact]
    public void Should_Fail_When_Items_Are_Empty()
    {
        var command = new PlaceOrder.Command(Guid.NewGuid(), []);

        var result = _validator.TestValidate(command);

        result.ShouldHaveValidationErrorFor(x =&gt; x.Items);
    }

    [Fact]
    public void Should_Pass_For_Valid_Command()
    {
        var command = new PlaceOrder.Command(
            Guid.NewGuid(),
            [new PlaceOrder.OrderItemRequest(Guid.NewGuid(), 2, 10m)]);

        var result = _validator.TestValidate(command);

        result.ShouldNotHaveAnyValidationErrors();
    }
}
</code></pre>
<p><code>TestValidate</code> gives you assertion helpers that point at the exact rule that failed. These tests run in microseconds, so cover every rule. More on the broader strategy in <a href="https://milanjovanovic.tech/blog/testing-vertical-slices-dotnet"><strong>testing vertical slices</strong></a>.</p>
<h2>Endpoint Error Mapping</h2>
<p>Map validation errors to Problem Details:</p>
<pre><code class="language-csharp">app.MapPost(&quot;/api/orders&quot;, async (
    PlaceOrder.Command command,
    ISender sender) =&gt;
{
    var result = await sender.Send(command);

    return result.Match(
        onSuccess: id =&gt; Results.Created($&quot;/api/orders/{id}&quot;, id),
        onFailure: error =&gt; error switch
        {
            ValidationError ve =&gt; Results.ValidationProblem(
                ve.Errors.GroupBy(e =&gt; e.Code)
                    .ToDictionary(
                        g =&gt; g.Key,
                        g =&gt; g.Select(e =&gt; e.Description).ToArray())),
            _ =&gt; Results.Problem(
                detail: error.Description,
                statusCode: StatusCodes.Status400BadRequest)
        });
});
</code></pre>
<h2>Takeaway</h2>
<p>Validation in Vertical Slice Architecture:</p>
<ol>
<li><strong>Co-locate validators with features</strong> - same file as the command and handler</li>
<li><strong>Pipeline behavior</strong> validates automatically before the handler runs</li>
<li><strong>Input validation</strong> (format, presence) goes in FluentValidation</li>
<li><strong>Domain validation</strong> (business rules) stays in the handler or domain model</li>
<li><strong>Throw or return Results</strong> - both work, pick one convention and stay consistent</li>
<li><strong>Map to Problem Details</strong> for consistent API error responses</li>
</ol>
<p>Validation is a <a href="https://milanjovanovic.tech/blog/cross-cutting-concerns-in-vertical-slice-architecture"><strong>cross-cutting concern</strong></a> - handle it once in the pipeline, not in every handler.</p>
<p>Thanks for reading, and stay awesome!</p>
<hr>
]]></content:encoded>
            <author>milan@milanjovanovic.tech (Milan Jovanović)</author>
        </item>
        <item>
            <title><![CDATA[Testing Vertical Slices in .NET]]></title>
            <link>https://milanjovanovic.tech/blog/testing-vertical-slices-dotnet</link>
            <guid isPermaLink="false">https://milanjovanovic.tech/blog/testing-vertical-slices-dotnet</guid>
            <pubDate>Thu, 13 Aug 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Vertical slices change the shape of your tests. Instead of repository, service, and controller tests held together by mocks, you test one feature at a time…]]></description>
            <content:encoded><![CDATA[<p>Layered architectures produce layered tests: repository tests, service tests, controller tests, and a mock for every seam between them.
Vertical slices collapse those seams, so the tests change shape too.
You test features, not layers, and the most valuable test runs one slice end to end.
Here is how I structure that, from validator unit tests to Testcontainers.</p>
<h2>Testing Slices, Not Layers</h2>
<p>In <a href="https://milanjovanovic.tech/blog/vertical-slice-architecture-dotnet"><strong>Vertical Slice Architecture</strong></a>, each feature is self-contained - request, handler, validation, persistence. This means your tests should be organized by feature, not by layer.</p>
<p>Instead of:</p>
<ul>
<li><code>OrderRepositoryTests</code></li>
<li><code>OrderServiceTests</code></li>
<li><code>OrderControllerTests</code></li>
</ul>
<p>You write:</p>
<ul>
<li><code>PlaceOrderTests</code></li>
<li><code>GetOrderTests</code></li>
<li><code>CancelOrderTests</code></li>
</ul>
<p>Each test covers one slice from input to output.</p>
<h2>Unit Testing a Handler</h2>
<p>The simplest test targets the handler directly:</p>
<pre><code class="language-csharp">public class PlaceOrderTests
{
    private readonly ApplicationDbContext _db;
    private readonly PlaceOrder.Handler _handler;

    public PlaceOrderTests()
    {
        _db = CreateInMemoryDbContext();
        _handler = new PlaceOrder.Handler(_db);
    }

    [Fact]
    public async Task Should_Create_Order_With_Valid_Request()
    {
        // Arrange
        var customer = new Customer { Id = Guid.NewGuid(), Name = &quot;John&quot; };
        _db.Customers.Add(customer);
        await _db.SaveChangesAsync();

        var command = new PlaceOrder.Command(
            customer.Id,
            [new PlaceOrder.OrderItemRequest(Guid.NewGuid(), 2)]);

        // Act
        var result = await _handler.Handle(command, CancellationToken.None);

        // Assert
        result.IsSuccess.Should().BeTrue();
        var order = await _db.Orders.FirstAsync();
        order.CustomerId.Should().Be(customer.Id);
        order.Items.Should().HaveCount(1);
    }

    [Fact]
    public async Task Should_Fail_When_Customer_Not_Found()
    {
        var command = new PlaceOrder.Command(
            Guid.NewGuid(),
            [new PlaceOrder.OrderItemRequest(Guid.NewGuid(), 1)]);

        var result = await _handler.Handle(command, CancellationToken.None);

        result.IsSuccess.Should().BeFalse();
        result.Error.Code.Should().Be(&quot;Customer.NotFound&quot;);
    }

    private static ApplicationDbContext CreateInMemoryDbContext()
    {
        var options = new DbContextOptionsBuilder&lt;ApplicationDbContext&gt;()
            .UseInMemoryDatabase(Guid.NewGuid().ToString())
            .Options;

        return new ApplicationDbContext(options);
    }
}
</code></pre>
<p>This tests business logic without HTTP, serialization, or middleware.</p>
<p>One caveat: the in-memory <code>DbContext</code> keeps these tests fast, but it doesn't enforce constraints or translate real SQL. That's an acceptable trade for handler logic tests. For query-heavy slices, prefer the Testcontainers approach below (I compare the options in <strong>testing EF Core repositories</strong>).</p>
<h2>Testing Validation</h2>
<p>Test validators separately - they're fast and don't need infrastructure:</p>
<pre><code class="language-csharp">public class PlaceOrderValidatorTests
{
    private readonly PlaceOrder.Validator _validator = new();

    [Fact]
    public void Should_Fail_When_CustomerId_Is_Empty()
    {
        var command = new PlaceOrder.Command(
            Guid.Empty,
            [new PlaceOrder.OrderItemRequest(Guid.NewGuid(), 1)]);

        var result = _validator.Validate(command);

        result.IsValid.Should().BeFalse();
        result.Errors.Should().Contain(e =&gt;
            e.PropertyName == nameof(PlaceOrder.Command.CustomerId));
    }

    [Fact]
    public void Should_Fail_When_Items_Empty()
    {
        var command = new PlaceOrder.Command(Guid.NewGuid(), []);

        var result = _validator.Validate(command);

        result.IsValid.Should().BeFalse();
    }

    [Fact]
    public void Should_Pass_With_Valid_Command()
    {
        var command = new PlaceOrder.Command(
            Guid.NewGuid(),
            [new PlaceOrder.OrderItemRequest(Guid.NewGuid(), 3)]);

        var result = _validator.Validate(command);

        result.IsValid.Should().BeTrue();
    }
}
</code></pre>
<h2>Integration Testing With WebApplicationFactory</h2>
<p>For end-to-end slice testing, use <strong>WebApplicationFactory</strong>:</p>
<pre><code class="language-csharp">public class PlaceOrderEndpointTests
    : IClassFixture&lt;WebApplicationFactory&lt;Program&gt;&gt;
{
    private readonly WebApplicationFactory&lt;Program&gt; _factory;
    private readonly HttpClient _client;

    public PlaceOrderEndpointTests(
        WebApplicationFactory&lt;Program&gt; factory)
    {
        _factory = factory.WithWebHostBuilder(builder =&gt;
        {
            builder.ConfigureServices(services =&gt;
            {
                // Replace real DB with test container or in-memory
                services.RemoveAll&lt;DbContextOptions&lt;ApplicationDbContext&gt;&gt;();
                services.AddDbContext&lt;ApplicationDbContext&gt;(options =&gt;
                    options.UseInMemoryDatabase(&quot;test&quot;));
            });
        });

        _client = _factory.CreateClient();
    }

    [Fact]
    public async Task PlaceOrder_Returns_Created()
    {
        // The handler rejects unknown customers, so seed one first
        var customerId = await SeedCustomerAsync();

        var request = new
        {
            CustomerId = customerId,
            Items = new[]
            {
                new { ProductId = Guid.NewGuid(), Quantity = 2 }
            }
        };

        var response = await _client.PostAsJsonAsync(
            &quot;/api/orders&quot;, request);

        response.StatusCode.Should().Be(HttpStatusCode.Created);
    }

    [Fact]
    public async Task PlaceOrder_Returns_BadRequest_For_Empty_Items()
    {
        var request = new
        {
            CustomerId = Guid.NewGuid(),
            Items = Array.Empty&lt;object&gt;()
        };

        var response = await _client.PostAsJsonAsync(
            &quot;/api/orders&quot;, request);

        response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
    }

    private async Task&lt;Guid&gt; SeedCustomerAsync()
    {
        using var scope = _factory.Services.CreateScope();
        var db = scope.ServiceProvider
            .GetRequiredService&lt;ApplicationDbContext&gt;();

        var customer = new Customer { Id = Guid.NewGuid(), Name = &quot;Test&quot; };
        db.Customers.Add(customer);
        await db.SaveChangesAsync();

        return customer.Id;
    }
}
</code></pre>
<p>This tests the full HTTP pipeline - routing, model binding, validation, handler, persistence, and response serialization.</p>
<h2>Integration Testing With Testcontainers</h2>
<p>For realistic tests against a real database, use <a href="https://milanjovanovic.tech/blog/testcontainers-integration-testing-using-docker-in-dotnet"><strong>Testcontainers</strong></a>:</p>
<pre><code class="language-csharp">public class OrderApiTests : IAsyncLifetime
{
    private readonly PostgreSqlContainer _postgres = new PostgreSqlBuilder()
        .WithImage(&quot;postgres:16-alpine&quot;)
        .Build();

    private WebApplicationFactory&lt;Program&gt; _factory = null!;
    private HttpClient _client = null!;

    public async Task InitializeAsync()
    {
        await _postgres.StartAsync();

        _factory = new WebApplicationFactory&lt;Program&gt;()
            .WithWebHostBuilder(builder =&gt;
            {
                builder.ConfigureServices(services =&gt;
                {
                    services.RemoveAll&lt;DbContextOptions&lt;ApplicationDbContext&gt;&gt;();
                    services.AddDbContext&lt;ApplicationDbContext&gt;(options =&gt;
                        options.UseNpgsql(_postgres.GetConnectionString()));
                });
            });

        _client = _factory.CreateClient();

        // Apply migrations
        using var scope = _factory.Services.CreateScope();
        var db = scope.ServiceProvider
            .GetRequiredService&lt;ApplicationDbContext&gt;();
        await db.Database.MigrateAsync();
    }

    [Fact]
    public async Task Full_Order_Lifecycle()
    {
        var customerId = await SeedCustomerAsync();

        // Place order
        var placeResponse = await _client.PostAsJsonAsync(
            &quot;/api/orders&quot;,
            new
            {
                CustomerId = customerId,
                Items = new[]
                {
                    new { ProductId = Guid.NewGuid(), Quantity = 2 }
                }
            });
        placeResponse.StatusCode.Should().Be(HttpStatusCode.Created);

        var orderId = await placeResponse.Content
            .ReadFromJsonAsync&lt;Guid&gt;();

        // Get order
        var getResponse = await _client.GetAsync(
            $&quot;/api/orders/{orderId}&quot;);
        getResponse.StatusCode.Should().Be(HttpStatusCode.OK);

        // Cancel order
        var cancelResponse = await _client.DeleteAsync(
            $&quot;/api/orders/{orderId}&quot;);
        cancelResponse.StatusCode.Should().Be(HttpStatusCode.NoContent);
    }

    private async Task&lt;Guid&gt; SeedCustomerAsync()
    {
        using var scope = _factory.Services.CreateScope();
        var db = scope.ServiceProvider
            .GetRequiredService&lt;ApplicationDbContext&gt;();

        var customer = new Customer { Id = Guid.NewGuid(), Name = &quot;Test&quot; };
        db.Customers.Add(customer);
        await db.SaveChangesAsync();

        return customer.Id;
    }

    public async Task DisposeAsync()
    {
        await _factory.DisposeAsync();
        await _postgres.DisposeAsync();
    }
}
</code></pre>
<h2>Where Mocks Still Fit</h2>
<p>Slices reduce the need for mocking, but they don't eliminate it. External systems (payment gateways, email providers, third-party APIs) should still be replaced with <strong>test doubles</strong>, even in integration tests:</p>
<pre><code class="language-csharp">var factory = new WebApplicationFactory&lt;Program&gt;()
    .WithWebHostBuilder(builder =&gt;
    {
        builder.ConfigureTestServices(services =&gt;
        {
            services.RemoveAll&lt;IPaymentGateway&gt;();
            services.AddScoped&lt;IPaymentGateway, FakePaymentGateway&gt;();
        });
    });
</code></pre>
<p>The rule I follow: fake what you don't own (external services), keep what you do own (your database, your handlers, your validation) real. That way a passing slice test means the feature genuinely works, minus only the third-party call you can't control anyway.</p>
<h2>Guarding Slice Independence</h2>
<p>One more test category worth having: a few architecture tests that keep slices from quietly coupling to each other. A <code>PlaceOrder</code> handler reaching into <code>Features.Shipping</code> internals is exactly the kind of erosion that's invisible in code review. I cover the setup in <strong>architecture testing in .NET</strong>; two or three rules per feature group are enough.</p>
<h2>Test Organization</h2>
<p>Mirror the feature structure:</p>
<pre><code>Features/
  Orders/
    PlaceOrder.cs
    GetOrder.cs
    CancelOrder.cs
tests/
  Features/
    Orders/
      PlaceOrderTests.cs
      PlaceOrderValidatorTests.cs
      GetOrderTests.cs
      CancelOrderTests.cs
</code></pre>
<p>Each test file tests one slice. Finding the tests for a feature is trivial.</p>
<h2>What to Test at Each Level</h2>
<p>Three levels, each with a distinct job:</p>
<img src="https://milanjovanovic.tech/blogs/articles/testing-vertical-slices-dotnet/slice-test-levels.png" alt="An integration test covers the whole slice from HTTP through routing, validation, handler, and database, while validator tests and handler unit tests target individual stages">
<ul>
<li><strong>Validator tests</strong>: input validation rules. Pure logic, run in microseconds.</li>
<li><strong>Handler unit tests</strong>: business logic in isolation. Fast, no HTTP.</li>
<li><strong>Integration tests</strong>: the full HTTP pipeline against a real database. Slower, but they prove the slice actually works.</li>
</ul>
<p>Because a slice is a complete feature, integration tests here carry more weight than in layered architectures. Don't be afraid to have plenty of them; that's <a href="https://milanjovanovic.tech/blog/the-test-pyramid-is-a-lie-and-what-i-do-instead"><strong>what I do instead of the classic test pyramid</strong></a>. A slice test that goes HTTP-to-database catches serialization bugs, validation wiring, and query errors in one shot.</p>
<h2>Takeaway</h2>
<p>Testing vertical slices in .NET:</p>
<ol>
<li><strong>Organize tests by feature</strong>, not by layer</li>
<li><strong>Unit test handlers</strong> with in-memory DbContext for fast feedback</li>
<li><strong>Unit test validators</strong> separately - they're pure logic</li>
<li><strong>Integration test with WebApplicationFactory</strong> for full HTTP pipeline</li>
<li><strong>Use Testcontainers</strong> for realistic database tests</li>
<li><strong>Lean on integration tests</strong> - a slice test proves the whole feature works</li>
</ol>
<p>Each slice is independently testable. That's the power of vertical slices.</p>
<p>Thanks for reading, and stay awesome!</p>
<hr>
]]></content:encoded>
            <author>milan@milanjovanovic.tech (Milan Jovanović)</author>
        </item>
        <item>
            <title><![CDATA[Feature Folders in .NET: Organizing Code by Feature]]></title>
            <link>https://milanjovanovic.tech/blog/feature-folders-dotnet</link>
            <guid isPermaLink="false">https://milanjovanovic.tech/blog/feature-folders-dotnet</guid>
            <pubDate>Thu, 13 Aug 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Stop organizing code by technical concern (Controllers, Services, Models). Organize by feature instead - so everything related to placing an order lives in one…]]></description>
            <content:encoded><![CDATA[<p>Open almost any .NET solution and you can guess the top-level folders before it loads: Controllers, Services, Models.
That structure tells you which framework the team used, but nothing about what the application does.
Feature folders flip it: all the code for one feature lives in one folder, and the solution reads like a list of capabilities.
Here is how to implement them in .NET, and when they pay off.</p>
<h2>The Problem With Layer-Based Organization</h2>
<p>Most .NET projects start like this:</p>
<pre><code>Controllers/
    OrdersController.cs
    CustomersController.cs
    ProductsController.cs
Services/
    OrderService.cs
    CustomerService.cs
    ProductService.cs
Models/
    Order.cs
    Customer.cs
    Product.cs
DTOs/
    OrderRequest.cs
    OrderResponse.cs
    CustomerRequest.cs
Validators/
    OrderValidator.cs
    CustomerValidator.cs
</code></pre>
<p>To work on one feature (placing an order), you touch files in 5+ folders. To understand a feature, you jump between directories piecing together how <code>OrdersController</code> calls <code>OrderService</code> which uses <code>Order</code> and <code>OrderRequest</code>.</p>
<p>This is <strong>organizing by layer</strong> - it groups files by what they are (controller, service, model), not by what they do.</p>
<img src="https://milanjovanovic.tech/blogs/articles/feature-folders-dotnet/layer-vs-feature.png" alt="By layer, one feature is scattered across Controllers, Services, and Models folders; by feature, everything for Place Order lives in a single folder">
<h2>Feature Folders: Organize by What Code Does</h2>
<p>Feature folders flip the structure. Everything related to a feature lives together:</p>
<pre><code>Features/
    Orders/
        PlaceOrder/
            PlaceOrderEndpoint.cs
            PlaceOrderCommand.cs
            PlaceOrderCommandHandler.cs
            PlaceOrderRequest.cs
            PlaceOrderResponse.cs
            PlaceOrderValidator.cs
        CancelOrder/
            CancelOrderEndpoint.cs
            CancelOrderCommand.cs
            CancelOrderCommandHandler.cs
        GetOrderById/
            GetOrderByIdEndpoint.cs
            GetOrderByIdQuery.cs
            GetOrderByIdQueryHandler.cs
            OrderResponse.cs
    Customers/
        RegisterCustomer/
            RegisterCustomerEndpoint.cs
            RegisterCustomerCommand.cs
            RegisterCustomerCommandHandler.cs
        GetCustomer/
            GetCustomerEndpoint.cs
            GetCustomerQuery.cs
</code></pre>
<p>To work on &quot;Place Order,&quot; you open one folder. Everything is there. No jumping between directories.</p>
<p>This is how <a href="https://milanjovanovic.tech/blog/vertical-slice-architecture"><strong>Vertical Slice Architecture</strong></a> and <a href="https://milanjovanovic.tech/blog/screaming-architecture"><strong>Screaming Architecture</strong></a> naturally organize code.</p>
<h2>Implementing Feature Folders</h2>
<h3>Step 1: Create the Feature Structure</h3>
<p>Each feature gets its own folder with everything it needs:</p>
<pre><code class="language-csharp">// Features/Orders/PlaceOrder/PlaceOrderCommand.cs
public sealed record PlaceOrderCommand(
    Guid CustomerId,
    List&lt;OrderItemRequest&gt; Items) : ICommand&lt;Guid&gt;;

// Features/Orders/PlaceOrder/PlaceOrderCommandHandler.cs
public sealed class PlaceOrderCommandHandler(
    IOrderRepository orderRepository,
    IUnitOfWork unitOfWork)
    : ICommandHandler&lt;PlaceOrderCommand, Guid&gt;
{
    public async Task&lt;Result&lt;Guid&gt;&gt; Handle(
        PlaceOrderCommand command, CancellationToken ct)
    {
        var order = Order.Create(command.CustomerId, command.Items);
        orderRepository.Add(order);
        await unitOfWork.SaveChangesAsync(ct);
        return order.Id;
    }
}

// Features/Orders/PlaceOrder/PlaceOrderEndpoint.cs
public static class PlaceOrderEndpoint
{
    public static void Map(IEndpointRouteBuilder app)
    {
        app.MapPost(&quot;/api/orders&quot;, async (
            PlaceOrderRequest request,
            ICommandHandler&lt;PlaceOrderCommand, Guid&gt; handler,
            CancellationToken ct) =&gt;
        {
            var command = new PlaceOrderCommand(request.CustomerId, request.Items);
            var result = await handler.Handle(command, ct);

            return result.IsSuccess
                ? Results.Created($&quot;/api/orders/{result.Value}&quot;, result.Value)
                : result.ToProblemDetails();
        });
    }
}

// Features/Orders/PlaceOrder/PlaceOrderValidator.cs
public sealed class PlaceOrderValidator : AbstractValidator&lt;PlaceOrderCommand&gt;
{
    public PlaceOrderValidator()
    {
        RuleFor(x =&gt; x.CustomerId).NotEmpty();
        RuleFor(x =&gt; x.Items).NotEmpty();
    }
}
</code></pre>
<p>The <code>ICommand</code> and <code>ICommandHandler</code> abstractions are the thin CQRS interfaces I defined in <a href="https://milanjovanovic.tech/blog/cqrs-pattern-the-way-it-should-have-been-from-the-start"><strong>CQRS Pattern: The Way It Should Have Been From the Start</strong></a>.</p>
<h3>Step 2: Auto-Register Endpoints</h3>
<p>Scan for all endpoint classes and register them:</p>
<pre><code class="language-csharp">public static class EndpointRegistration
{
    public static void MapFeatureEndpoints(this IEndpointRouteBuilder app)
    {
        var endpointTypes = typeof(Program).Assembly
            .GetTypes()
            .Where(t =&gt; t.GetMethods(BindingFlags.Public | BindingFlags.Static)
                .Any(m =&gt; m.Name == &quot;Map&quot; &amp;&amp;
                    m.GetParameters().Length == 1 &amp;&amp;
                    m.GetParameters()[0].ParameterType == typeof(IEndpointRouteBuilder)));

        foreach (var type in endpointTypes)
        {
            var method = type.GetMethod(&quot;Map&quot;,
                BindingFlags.Public | BindingFlags.Static,
                [typeof(IEndpointRouteBuilder)]);

            method?.Invoke(null, [app]);
        }
    }
}
</code></pre>
<pre><code class="language-csharp">// Program.cs
app.MapFeatureEndpoints();
</code></pre>
<h3>Step 3: Register Handlers</h3>
<p>Use assembly scanning with the Scrutor library, so a new slice never means editing <code>Program.cs</code>:</p>
<pre><code class="language-bash">dotnet add package Scrutor
</code></pre>
<pre><code class="language-csharp">builder.Services.Scan(scan =&gt; scan
    .FromAssemblyOf&lt;PlaceOrderCommandHandler&gt;()
    .AddClasses(c =&gt; c.AssignableTo(typeof(ICommandHandler&lt;,&gt;)))
    .AsImplementedInterfaces()
    .WithScopedLifetime()
    .AddClasses(c =&gt; c.AssignableTo(typeof(IQueryHandler&lt;,&gt;)))
    .AsImplementedInterfaces()
    .WithScopedLifetime());
</code></pre>
<h2>One File or One Folder per Feature?</h2>
<p>There are two popular granularities, and both are fine:</p>
<p><strong>Folder per operation</strong> (shown above): <code>PlaceOrder/</code> contains five or six small files. Best when slices carry validators, mappers, and multiple DTOs.</p>
<p><strong>Single file per operation</strong>: the whole slice lives in <code>PlaceOrder.cs</code> as a static class with nested types:</p>
<pre><code class="language-csharp">// Features/Orders/PlaceOrder.cs
public static class PlaceOrder
{
    public sealed record Command(
        Guid CustomerId, List&lt;OrderItemRequest&gt; Items) : ICommand&lt;Guid&gt;;

    public sealed class Validator : AbstractValidator&lt;Command&gt; { /* ... */ }

    internal sealed class Handler : ICommandHandler&lt;Command, Guid&gt; { /* ... */ }

    public static void Map(IEndpointRouteBuilder app) { /* ... */ }
}
</code></pre>
<p>The single-file style keeps the entire feature on one screen and makes names collision-free (<code>PlaceOrder.Command</code>, <code>CancelOrder.Command</code>).
The static <code>Map</code> method keeps the same shape as before, so the endpoint scanner from Step 2 picks it up unchanged. It's the style I lean toward for small-to-medium slices, and I've written more about <a href="https://milanjovanovic.tech/blog/vertical-slice-architecture-structuring-vertical-slices"><strong>structuring vertical slices</strong></a> if you want the full reasoning.</p>
<p>Start with one file. Split into a folder when the file gets uncomfortable to scroll.</p>
<h2>Feature Folders in Clean Architecture</h2>
<p>You can combine feature folders with <a href="https://milanjovanovic.tech/blog/clean-architecture-and-the-benefits-of-structured-software-design"><strong>Clean Architecture</strong></a>:</p>
<pre><code>src/
  MyApp.Domain/
    Orders/
      Order.cs
      OrderLineItem.cs
      IOrderRepository.cs
    Customers/
      Customer.cs
  MyApp.Application/
    Orders/
      PlaceOrder/
        PlaceOrderCommand.cs
        PlaceOrderCommandHandler.cs
        PlaceOrderValidator.cs
      CancelOrder/
        CancelOrderCommand.cs
        CancelOrderCommandHandler.cs
      GetOrderById/
        GetOrderByIdQuery.cs
        GetOrderByIdQueryHandler.cs
        OrderResponse.cs
  MyApp.Infrastructure/
    Persistence/
      Repositories/
        OrderRepository.cs
  MyApp.Api/
    Endpoints/
      Orders/
        PlaceOrderEndpoint.cs
        CancelOrderEndpoint.cs
        GetOrderByIdEndpoint.cs
</code></pre>
<p>The Application layer uses feature folders. The Domain layer groups entities by aggregate. The Presentation layer mirrors the Application structure.</p>
<h2>When to Use Feature Folders</h2>
<p><strong>Feature folders work well when:</strong></p>
<ul>
<li>Features are relatively independent</li>
<li>The team works on features, not layers</li>
<li>You want code locality (related code close together)</li>
<li>You're using <a href="https://milanjovanovic.tech/blog/cqrs-pattern-the-way-it-should-have-been-from-the-start"><strong>CQRS</strong></a> (commands and queries naturally form features)</li>
</ul>
<p><strong>Stick with layers when:</strong></p>
<ul>
<li>There's extensive code sharing between features</li>
<li>The project is very small (5-10 files total)</li>
<li>Your team is more comfortable with the traditional structure</li>
</ul>
<h2>Shared Code</h2>
<p>Some code is truly shared - domain entities, base classes, common helpers. Put these in a <code>Common</code> or <code>Shared</code> folder:</p>
<pre><code>Features/
    Orders/
        PlaceOrder/...
        CancelOrder/...
    Customers/...
Common/
    Domain/
        Entity.cs
        ValueObject.cs
        AggregateRoot.cs
    Results/
        Result.cs
        Error.cs
</code></pre>
<p>Keep the shared folder minimal. If code is only used by one feature, it belongs in that feature's folder. When two features start needing the same logic, resist the reflex to abstract immediately; I've written about <a href="https://milanjovanovic.tech/blog/vertical-slice-architecture-where-does-the-shared-logic-live"><strong>where shared logic should live</strong></a>, and the short answer is: extract when the duplication hurts, not when it merely exists.</p>
<h2>Migrating an Existing Codebase</h2>
<p>You don't need a rewrite to adopt feature folders. The incremental path:</p>
<ol>
<li><strong>Create the <code>Features</code> folder</strong> next to your existing <code>Controllers</code>/<code>Services</code> folders.</li>
<li><strong>Move one feature end to end.</strong> Pick a small, actively developed one. Pull its controller action, service method, DTOs, and validator into a feature folder, collapsing the service and repository indirection where it adds nothing.</li>
<li><strong>Ship it.</strong> The old layers and the new folder coexist fine; routing doesn't care where files live.</li>
<li><strong>Repeat opportunistically.</strong> Migrate features when you touch them for other reasons. Untouched code stays where it is.</li>
</ol>
<p>The biggest friction is usually psychological, not technical: the codebase looks &quot;inconsistent&quot; during the transition. That's fine. A consistent structure that hides features is worse than a mixed one that's converging on clarity.</p>
<h2>Stop Grouping by Layer</h2>
<p>Feature folders organize code by business capability instead of technical concern. Every file related to placing an order lives in the <code>PlaceOrder</code> folder.</p>
<p>The result: faster navigation, fewer merge conflicts, and code that screams what the application does.</p>
<p>Stop grouping by layer. Start grouping by feature.</p>
<p>Thanks for reading, and stay awesome!</p>
<hr>
]]></content:encoded>
            <author>milan@milanjovanovic.tech (Milan Jovanović)</author>
        </item>
        <item>
            <title><![CDATA[Cross-Cutting Concerns in Vertical Slice Architecture]]></title>
            <link>https://milanjovanovic.tech/blog/cross-cutting-concerns-in-vertical-slice-architecture</link>
            <guid isPermaLink="false">https://milanjovanovic.tech/blog/cross-cutting-concerns-in-vertical-slice-architecture</guid>
            <pubDate>Thu, 13 Aug 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[One criticism of Vertical Slice Architecture is code duplication across slices. Here is how to handle cross-cutting concerns like validation, logging, and…]]></description>
            <content:encoded><![CDATA[<p>Vertical slices promise isolation, and then reality shows up.
Every slice needs validation, logging, transactions, and caching, and copy-pasting those into every handler is how the pattern gets a bad name.
Here is how I keep cross-cutting concerns in one place while each slice stays focused on its feature.</p>
<h2>The Duplication Problem</h2>
<p>In <a href="https://milanjovanovic.tech/blog/vertical-slice-architecture-dotnet"><strong>Vertical Slice Architecture</strong></a>, each feature is self-contained. But certain behaviors cut across every slice - validation, logging, caching, authorization, transaction management.</p>
<p>You don't want to copy-paste these into every handler. That would defeat the purpose. And this question is really a special case of a broader one I keep coming back to: <a href="https://milanjovanovic.tech/blog/vertical-slice-architecture-where-does-the-shared-logic-live"><strong>where does the shared logic live</strong></a> in a sliced codebase?</p>
<h2>MediatR Pipeline Behaviors</h2>
<p>The most common solution is <a href="https://milanjovanovic.tech/blog/mediatr-pipeline-behaviors"><strong>MediatR pipeline behaviors</strong></a>. They wrap every request handler automatically.</p>
<h3>Validation Behavior</h3>
<p>Validate every command before the handler executes:</p>
<pre><code class="language-csharp">public class ValidationBehavior&lt;TRequest, TResponse&gt;
    : IPipelineBehavior&lt;TRequest, TResponse&gt;
    where TRequest : IRequest&lt;TResponse&gt;
{
    private readonly IEnumerable&lt;IValidator&lt;TRequest&gt;&gt; _validators;

    public ValidationBehavior(IEnumerable&lt;IValidator&lt;TRequest&gt;&gt; validators)
    {
        _validators = validators;
    }

    public async Task&lt;TResponse&gt; Handle(
        TRequest request,
        RequestHandlerDelegate&lt;TResponse&gt; next,
        CancellationToken ct)
    {
        if (!_validators.Any())
        {
            return await next();
        }

        var context = new ValidationContext&lt;TRequest&gt;(request);

        var failures = _validators
            .Select(v =&gt; v.Validate(context))
            .SelectMany(r =&gt; r.Errors)
            .Where(f =&gt; f is not null)
            .ToList();

        if (failures.Count != 0)
        {
            throw new ValidationException(failures);
        }

        return await next();
    }
}
</code></pre>
<p>Each slice just defines a validator:</p>
<pre><code class="language-csharp">// Features/Orders/PlaceOrder.cs
public sealed class Validator : AbstractValidator&lt;Command&gt;
{
    public Validator()
    {
        RuleFor(x =&gt; x.CustomerId).NotEmpty();
        RuleFor(x =&gt; x.Items).NotEmpty();
    }
}
</code></pre>
<p>The behavior picks it up automatically - zero wiring per slice.</p>
<h3>Logging Behavior</h3>
<p>Log every request entry, exit, and duration:</p>
<pre><code class="language-csharp">public class LoggingBehavior&lt;TRequest, TResponse&gt;
    : IPipelineBehavior&lt;TRequest, TResponse&gt;
    where TRequest : IRequest&lt;TResponse&gt;
{
    private readonly ILogger&lt;LoggingBehavior&lt;TRequest, TResponse&gt;&gt; _logger;

    public LoggingBehavior(
        ILogger&lt;LoggingBehavior&lt;TRequest, TResponse&gt;&gt; logger)
    {
        _logger = logger;
    }

    public async Task&lt;TResponse&gt; Handle(
        TRequest request,
        RequestHandlerDelegate&lt;TResponse&gt; next,
        CancellationToken ct)
    {
        var requestName = typeof(TRequest).Name;
        _logger.LogInformation(&quot;Handling {RequestName}&quot;, requestName);

        var sw = Stopwatch.StartNew();
        var response = await next();
        sw.Stop();

        _logger.LogInformation(
            &quot;Handled {RequestName} in {ElapsedMs}ms&quot;,
            requestName, sw.ElapsedMilliseconds);

        return response;
    }
}
</code></pre>
<h3>Transaction Behavior</h3>
<p>Wrap commands in a database transaction.
The <code>ICommand</code> constraint uses the marker interface from <a href="https://milanjovanovic.tech/blog/combining-vertical-slices-cqrs"><strong>combining vertical slices with CQRS</strong></a>:</p>
<pre><code class="language-csharp">public class TransactionBehavior&lt;TRequest, TResponse&gt;
    : IPipelineBehavior&lt;TRequest, TResponse&gt;
    where TRequest : ICommand&lt;TResponse&gt;
{
    private readonly ApplicationDbContext _db;

    public TransactionBehavior(ApplicationDbContext db)
    {
        _db = db;
    }

    public async Task&lt;TResponse&gt; Handle(
        TRequest request,
        RequestHandlerDelegate&lt;TResponse&gt; next,
        CancellationToken ct)
    {
        await using var transaction =
            await _db.Database.BeginTransactionAsync(ct);

        try
        {
            var response = await next();
            await transaction.CommitAsync(ct);
            return response;
        }
        catch
        {
            await transaction.RollbackAsync(ct);
            throw;
        }
    }
}
</code></pre>
<p>Notice the constraint <code>ICommand&lt;TResponse&gt;</code> - this behavior only wraps commands, not queries. Queries don't need transactions.</p>
<h3>Caching Behavior</h3>
<p>Cache query results:</p>
<pre><code class="language-csharp">public interface ICacheable
{
    string CacheKey { get; }
    TimeSpan? Expiration { get; }
}

public class CachingBehavior&lt;TRequest, TResponse&gt;
    : IPipelineBehavior&lt;TRequest, TResponse&gt;
    where TRequest : IRequest&lt;TResponse&gt;, ICacheable
{
    private readonly IDistributedCache _cache;

    public CachingBehavior(IDistributedCache cache)
    {
        _cache = cache;
    }

    public async Task&lt;TResponse&gt; Handle(
        TRequest request,
        RequestHandlerDelegate&lt;TResponse&gt; next,
        CancellationToken ct)
    {
        var cachedResult = await _cache.GetStringAsync(request.CacheKey, ct);
        if (cachedResult is not null)
        {
            return JsonSerializer.Deserialize&lt;TResponse&gt;(cachedResult)!;
        }

        var response = await next();

        await _cache.SetStringAsync(
            request.CacheKey,
            JsonSerializer.Serialize(response),
            new DistributedCacheEntryOptions
            {
                AbsoluteExpirationRelativeToNow =
                    request.Expiration ?? TimeSpan.FromMinutes(5)
            },
            ct);

        return response;
    }
}
</code></pre>
<p>Opt in per query:</p>
<pre><code class="language-csharp">public sealed record Query(Guid Id)
    : IRequest&lt;ProductResponse&gt;, ICacheable
{
    public string CacheKey =&gt; $&quot;product-{Id}&quot;;
    public TimeSpan? Expiration =&gt; TimeSpan.FromMinutes(10);
}
</code></pre>
<p>Only queries that implement <code>ICacheable</code> get cached. The behavior ignores everything else.</p>
<h2>Registration</h2>
<p>Register all behaviors in order:</p>
<pre><code class="language-csharp">services.AddMediatR(config =&gt;
{
    config.RegisterServicesFromAssembly(typeof(Program).Assembly);
    config.AddOpenBehavior(typeof(LoggingBehavior&lt;,&gt;));
    config.AddOpenBehavior(typeof(ValidationBehavior&lt;,&gt;));
    config.AddOpenBehavior(typeof(TransactionBehavior&lt;,&gt;));
    config.AddOpenBehavior(typeof(CachingBehavior&lt;,&gt;));
});
</code></pre>
<p>Order matters. Logging wraps validation, which wraps transaction, which wraps caching.</p>
<img src="https://milanjovanovic.tech/blogs/articles/cross-cutting-concerns-in-vertical-slice-architecture/behavior-pipeline.png" alt="A request passing through nested Logging, Validation, Transaction, and Caching behaviors before reaching the handler and the database">
<h2>Pitfalls to Watch For</h2>
<p>A few ways this setup bites in practice:</p>
<ul>
<li><strong>Behavior order bugs are silent.</strong> If you register <code>CachingBehavior</code> before <code>TransactionBehavior</code>, a cached response can be returned without the transaction ever opening - which is correct for queries but masks a misconfigured command that accidentally implements <code>ICacheable</code>. Review the registration order whenever you add a behavior.</li>
<li><strong>Caching failures, not just successes.</strong> The caching behavior above serializes whatever the handler returns, including a failed <code>Result</code>. Add a check so only successful responses get cached, or you'll serve a cached error for ten minutes.</li>
<li><strong>Transactions around everything.</strong> Wrapping every command in an explicit transaction is redundant when the handler makes a single <code>SaveChangesAsync</code> call (EF Core already wraps that in a transaction). Reserve the transaction behavior for handlers that perform multiple save operations.</li>
<li><strong>Behavior sprawl.</strong> Every behavior runs on every matching request. Ten behaviors deep, debugging a request means stepping through ten wrappers. Keep the pipeline short and boring.</li>
</ul>
<h2>Pipeline Behaviors Without MediatR</h2>
<p>If you've moved off MediatR (or never adopted it), the same pattern works as decorators over your own handler interfaces. Define <code>ICommandHandler&lt;TCommand, TResponse&gt;</code>, then register decorators (Scrutor's <code>Decorate</code> makes this one line per behavior). The mechanics change; the idea (cross-cutting logic wraps the handler, slices stay clean) doesn't.</p>
<h2>Endpoint Filters (Alternative)</h2>
<p>If you prefer not to use MediatR, ASP.NET Core <strong>endpoint filters</strong> handle cross-cutting concerns at the HTTP layer:</p>
<pre><code class="language-csharp">public class ValidationFilter&lt;TRequest&gt; : IEndpointFilter
{
    public async ValueTask&lt;object?&gt; InvokeAsync(
        EndpointFilterInvocationContext context,
        EndpointFilterDelegate next)
    {
        var request = context.Arguments
            .OfType&lt;TRequest&gt;()
            .FirstOrDefault();

        if (request is null)
        {
            return await next(context);
        }

        var validator = context.HttpContext.RequestServices
            .GetService&lt;IValidator&lt;TRequest&gt;&gt;();

        if (validator is not null)
        {
            var result = await validator.ValidateAsync(request);
            if (!result.IsValid)
            {
                return Results.ValidationProblem(
                    result.ToDictionary());
            }
        }

        return await next(context);
    }
}
</code></pre>
<h2>Base Handler Classes (Use Sparingly)</h2>
<p>Another option - a base class for shared handler logic:</p>
<pre><code class="language-csharp">public abstract class BaseHandler
{
    protected readonly ApplicationDbContext Db;
    protected readonly ICurrentUserService CurrentUser;

    protected BaseHandler(
        ApplicationDbContext db,
        ICurrentUserService currentUser)
    {
        Db = db;
        CurrentUser = currentUser;
    }
}
</code></pre>
<p>I generally avoid this. It creates coupling and makes the inheritance hierarchy grow. Pipeline behaviors are more flexible.</p>
<h2>The Pattern Summary</h2>
<p>Here's the mapping I use, concern by concern:</p>
<ul>
<li><strong>Validation</strong>: pipeline behavior + FluentValidation. Applies to every request that has a validator; slices without one pass through untouched.</li>
<li><strong>Logging</strong>: pipeline behavior. Applies to all requests.</li>
<li><strong>Transactions</strong>: pipeline behavior constrained to <code>ICommand</code>. Write operations only.</li>
<li><strong>Caching</strong>: pipeline behavior + <code>ICacheable</code> marker. Opt-in per query.</li>
<li><strong>Authorization</strong>: pipeline behavior or endpoint filter, declared per request.</li>
<li><strong>Error handling</strong>: a <a href="https://milanjovanovic.tech/blog/global-error-handling-in-aspnetcore-from-middleware-to-modern-handlers"><strong>global exception handler</strong></a> at the HTTP boundary, so handlers never need try-catch blocks for presentation concerns.</li>
</ul>
<h2>Takeaway</h2>
<p>Cross-cutting concerns in Vertical Slice Architecture are solved with:</p>
<ol>
<li><strong>Pipeline behaviors</strong> - wrap every handler automatically</li>
<li><strong>Marker interfaces</strong> - opt in to specific behaviors (<code>ICacheable</code>, <code>ICommand</code>)</li>
<li><strong>Endpoint filters</strong> - HTTP-level concerns</li>
<li><strong>Convention over configuration</strong> - validators are discovered, not registered manually</li>
</ol>
<p>Each slice stays focused on its feature. The pipeline handles everything else.</p>
<p>Thanks for reading, and stay awesome!</p>
<hr>
]]></content:encoded>
            <author>milan@milanjovanovic.tech (Milan Jovanović)</author>
        </item>
        <item>
            <title><![CDATA[Combining Vertical Slices With CQRS in .NET]]></title>
            <link>https://milanjovanovic.tech/blog/combining-vertical-slices-cqrs</link>
            <guid isPermaLink="false">https://milanjovanovic.tech/blog/combining-vertical-slices-cqrs</guid>
            <pubDate>Thu, 13 Aug 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[CQRS and Vertical Slice Architecture are a natural pair. Commands and queries are already separate - putting each in its own slice makes them independent and…]]></description>
            <content:encoded><![CDATA[<p>CQRS and Vertical Slice Architecture answer different questions.
CQRS decides how reads and writes are modeled.
Vertical slices decide where the code lives.
Put them together and every command and query becomes a small, self-contained unit that you can optimize on its own.
Here is how the combination works in practice, from folder structure to endpoints and separate read stores.</p>
<h2>Why CQRS + VSA Works</h2>
<p><a href="https://milanjovanovic.tech/blog/cqrs-pattern-the-way-it-should-have-been-from-the-start"><strong>CQRS</strong></a> separates reads from writes. <a href="https://milanjovanovic.tech/blog/vertical-slice-architecture-dotnet"><strong>Vertical Slice Architecture</strong></a> organizes code by feature. Together, each command and query becomes an independent slice with its own data access strategy.</p>
<img src="https://milanjovanovic.tech/blogs/articles/combining-vertical-slices-cqrs/command-query-slices.png" alt="An HTTP request routed by whether it is a command or a query: commands go to a command slice using EF Core with change tracking, queries go to a query slice using Dapper and raw SQL, both hitting the same database">
<ul>
<li>Commands use EF Core with the change tracker</li>
<li>Queries use Dapper or raw SQL for performance</li>
<li>Each slice picks the tool that fits</li>
</ul>
<h2>Project Structure</h2>
<pre><code>Features/
  Orders/
    Commands/
      PlaceOrder.cs
      CancelOrder.cs
      UpdateOrderStatus.cs
    Queries/
      GetOrder.cs
      GetOrders.cs
      SearchOrders.cs
    OrdersModule.cs
</code></pre>
<p>Or flatten it when the feature count is small:</p>
<pre><code>Features/
  Orders/
    PlaceOrder.cs          ← Command
    CancelOrder.cs         ← Command
    GetOrder.cs            ← Query
    GetOrders.cs           ← Query
</code></pre>
<h2>Command Slice</h2>
<p>A command changes state:</p>
<pre><code class="language-csharp">public static class PlaceOrder
{
    public sealed record Command(
        Guid CustomerId,
        List&lt;ItemRequest&gt; Items) : IRequest&lt;Result&lt;Guid&gt;&gt;;

    public sealed record ItemRequest(
        Guid ProductId, int Quantity);

    public sealed class Validator : AbstractValidator&lt;Command&gt;
    {
        public Validator()
        {
            RuleFor(x =&gt; x.CustomerId).NotEmpty();
            RuleFor(x =&gt; x.Items).NotEmpty();
            RuleForEach(x =&gt; x.Items).ChildRules(item =&gt;
            {
                item.RuleFor(x =&gt; x.ProductId).NotEmpty();
                item.RuleFor(x =&gt; x.Quantity).GreaterThan(0);
            });
        }
    }

    public sealed class Handler : IRequestHandler&lt;Command, Result&lt;Guid&gt;&gt;
    {
        private readonly ApplicationDbContext _db;

        public Handler(ApplicationDbContext db) =&gt; _db = db;

        public async Task&lt;Result&lt;Guid&gt;&gt; Handle(
            Command request, CancellationToken ct)
        {
            var order = Order.Create(
                request.CustomerId,
                request.Items.Select(i =&gt;
                    new LineItem(i.ProductId, i.Quantity)).ToList());

            _db.Orders.Add(order);
            await _db.SaveChangesAsync(ct);

            return order.Id;
        }
    }
}
</code></pre>
<p>Commands go through EF Core - the change tracker handles inserts, updates, and domain event dispatch.</p>
<h2>Query Slice</h2>
<p>A query reads data without side effects:</p>
<pre><code class="language-csharp">public static class GetOrder
{
    public sealed record Query(Guid OrderId) : IRequest&lt;OrderResponse?&gt;;

    public sealed record OrderResponse(
        Guid Id,
        string Status,
        decimal TotalAmount,
        DateTime CreatedAt,
        List&lt;LineItemResponse&gt; Items);

    public sealed record LineItemResponse(
        Guid ProductId,
        int Quantity,
        decimal UnitPrice);

    // Intermediate row type - Dapper maps columns to this,
    // since OrderResponse's constructor also expects Items
    private sealed record OrderRow(
        Guid Id,
        string Status,
        decimal TotalAmount,
        DateTime CreatedAt);

    public sealed class Handler : IRequestHandler&lt;Query, OrderResponse?&gt;
    {
        private readonly IDbConnection _connection;

        public Handler(IDbConnection connection) =&gt;
            _connection = connection;

        public async Task&lt;OrderResponse?&gt; Handle(
            Query query, CancellationToken ct)
        {
            const string sql = &quot;&quot;&quot;
                SELECT o.Id, o.Status, o.TotalAmount, o.CreatedAt
                FROM Orders o
                WHERE o.Id = @OrderId;

                SELECT li.ProductId, li.Quantity, li.UnitPrice
                FROM LineItems li
                WHERE li.OrderId = @OrderId;
                &quot;&quot;&quot;;

            using var multi = await _connection
                .QueryMultipleAsync(sql, new { query.OrderId });

            var order = await multi
                .ReadSingleOrDefaultAsync&lt;OrderRow&gt;();

            if (order is null) return null;

            var items = (await multi
                .ReadAsync&lt;LineItemResponse&gt;()).ToList();

            return new OrderResponse(
                order.Id,
                order.Status,
                order.TotalAmount,
                order.CreatedAt,
                items);
        }
    }
}
</code></pre>
<p>Queries use Dapper for fast, lightweight reads. No change tracking overhead. No mapping layers.</p>
<h2>Typed Abstractions</h2>
<p>One caveat before you build everything on <code>IRequest</code>: <a href="https://milanjovanovic.tech/blog/stop-conflating-cqrs-and-mediatr"><strong>CQRS is not MediatR</strong></a>. The pattern is the read/write separation; the library is one way to dispatch it. That said, marker interfaces make the separation explicit and unlock selective behaviors:</p>
<pre><code class="language-csharp">public interface ICommand&lt;TResponse&gt; : IRequest&lt;TResponse&gt;;
public interface IQuery&lt;TResponse&gt; : IRequest&lt;TResponse&gt;;

public interface ICommandHandler&lt;TCommand, TResponse&gt;
    : IRequestHandler&lt;TCommand, TResponse&gt;
    where TCommand : ICommand&lt;TResponse&gt;;

public interface IQueryHandler&lt;TQuery, TResponse&gt;
    : IRequestHandler&lt;TQuery, TResponse&gt;
    where TQuery : IQuery&lt;TResponse&gt;;
</code></pre>
<p>Now you can apply behaviors selectively:</p>
<pre><code class="language-csharp">// Validation only runs for commands
public sealed class ValidationBehavior&lt;TRequest, TResponse&gt;
    : IPipelineBehavior&lt;TRequest, TResponse&gt;
    where TRequest : ICommand&lt;TResponse&gt;
{
    // ...
}

// Caching only runs for queries
public sealed class CachingBehavior&lt;TRequest, TResponse&gt;
    : IPipelineBehavior&lt;TRequest, TResponse&gt;
    where TRequest : IQuery&lt;TResponse&gt;
{
    // ...
}
</code></pre>
<h2>Endpoint Registration</h2>
<p>Map commands and queries to HTTP methods:</p>
<pre><code class="language-csharp">public class OrdersModule : ICarterModule
{
    public void AddRoutes(IEndpointRouteBuilder app)
    {
        var group = app.MapGroup(&quot;/api/orders&quot;).WithTags(&quot;Orders&quot;);

        // Commands → POST, PUT, DELETE
        group.MapPost(&quot;/&quot;, async (
            PlaceOrder.Command command, ISender sender) =&gt;
        {
            var result = await sender.Send(command);
            return result.Match(
                id =&gt; Results.Created($&quot;/api/orders/{id}&quot;, id),
                error =&gt; Results.Problem(error.Description));
        });

        group.MapDelete(&quot;/{id:guid}&quot;, async (
            Guid id, ISender sender) =&gt;
        {
            var result = await sender.Send(
                new CancelOrder.Command(id));
            return result.Match(
                () =&gt; Results.NoContent(),
                error =&gt; Results.Problem(error.Description));
        });

        // Queries → GET
        group.MapGet(&quot;/{id:guid}&quot;, async (
            Guid id, ISender sender) =&gt;
        {
            var order = await sender.Send(new GetOrder.Query(id));
            return order is not null
                ? Results.Ok(order)
                : Results.NotFound();
        });

        group.MapGet(&quot;/&quot;, async (
            [AsParameters] GetOrders.Query query, ISender sender) =&gt;
        {
            var result = await sender.Send(query);
            return Results.Ok(result);
        });
    }
}
</code></pre>
<h2>Separate Read and Write Models</h2>
<p>CQRS lets commands and queries use different data shapes:</p>
<pre><code class="language-csharp">// Write model (rich domain entity)
public class Order : AggregateRoot
{
    private readonly List&lt;LineItem&gt; _lineItems = [];
    public OrderStatus Status { get; private set; }
    public Money TotalAmount { get; private set; }

    public void AddLineItem(Guid productId, int quantity, Money price)
    {
        // Business rules...
    }
}

// Read model (flat DTO optimized for display)
public sealed record OrderListItem(
    Guid Id,
    string CustomerName,   // Joined from Customers table
    string Status,
    decimal TotalAmount,
    int ItemCount,          // Computed
    DateTime CreatedAt);
</code></pre>
<p>Write models enforce <strong>domain invariants</strong>. Read models are flat DTOs optimized for the UI.</p>
<h2>Scaling: Separate Read Database</h2>
<p>For high-traffic applications, CQRS enables separate read and write stores.
The write side doesn't change: commands keep going through EF Core to the normalized schema.
The read side gets its own denormalized table (or a read replica connection), and the query slice is the only code that needs to know:</p>
<pre><code class="language-csharp">public static class GetOrderSummary
{
    // Flat read model, so Dapper maps it directly
    public sealed record OrderSummary(
        Guid Id,
        string Status,
        decimal TotalAmount,
        DateTime CreatedAt);

    public sealed record Query(Guid OrderId) : IQuery&lt;OrderSummary?&gt;;

    public sealed class Handler : IQueryHandler&lt;Query, OrderSummary?&gt;
    {
        private readonly IDbConnection _readDb; // Dapper → read replica

        public Handler(IDbConnection readDb) =&gt; _readDb = readDb;

        public async Task&lt;OrderSummary?&gt; Handle(
            Query query, CancellationToken ct)
        {
            const string sql = &quot;&quot;&quot;
                SELECT Id, Status, TotalAmount, CreatedAt
                FROM OrderSummaries
                WHERE Id = @OrderId;
                &quot;&quot;&quot;;

            return await _readDb.QuerySingleOrDefaultAsync&lt;OrderSummary&gt;(
                sql, new { query.OrderId });
        }
    }
}
</code></pre>
<p>Start with a single database. Split when performance demands it.</p>
<h2>Where This Combination Struggles</h2>
<p>Two honest caveats before you commit:</p>
<ul>
<li><strong>Eventual consistency creeps in early.</strong> The moment queries read from a cache or replica, a command's result may not be immediately visible to the next query. Design your UI for it (return the created resource from the command, don't re-query).</li>
<li><strong>Slice independence is a discipline, not a guarantee.</strong> It's tempting to share DTOs between a command and its neighboring query &quot;because they look the same.&quot; Resist it. The whole point is that each side can evolve without breaking the other. When slices genuinely need shared logic, pull it out deliberately, which I covered in <a href="https://milanjovanovic.tech/blog/vertical-slice-architecture-where-does-the-shared-logic-live"><strong>where the shared logic lives in VSA</strong></a>.</li>
</ul>
<h2>Takeaway</h2>
<p>CQRS + Vertical Slice Architecture:</p>
<ol>
<li><strong>Each command and query is its own slice</strong> - independent and self-contained</li>
<li><strong>Commands use EF Core</strong> for change tracking and domain events</li>
<li><strong>Queries use Dapper</strong> for fast, lightweight reads</li>
<li><strong>Pipeline behaviors</strong> can target commands or queries selectively</li>
<li><strong>Read and write models</strong> are separate - optimize each independently</li>
<li><strong>Start simple</strong> - one database, split read/write stores when needed</li>
</ol>
<p>The combination gives you the organizational clarity of VSA with the optimization potential of CQRS.</p>
<p>Thanks for reading, and stay awesome!</p>
<hr>
]]></content:encoded>
            <author>milan@milanjovanovic.tech (Milan Jovanović)</author>
        </item>
        <item>
            <title><![CDATA[When to Extract a Module Into a Microservice]]></title>
            <link>https://milanjovanovic.tech/blog/when-to-extract-module-to-microservice</link>
            <guid isPermaLink="false">https://milanjovanovic.tech/blog/when-to-extract-module-to-microservice</guid>
            <pubDate>Thu, 13 Aug 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Extract a module too early and you lock unstable boundaries into network contracts. Wait too long and extraction becomes a multi-month rewrite.]]></description>
            <content:encoded><![CDATA[<p>&quot;We can always extract it into a microservice later&quot; is the promise that sells the modular monolith.
It's a real promise, but it depends on two things the sales pitch skips: knowing when &quot;later&quot; has arrived, and knowing how to extract without breaking what already works.
This article covers both.</p>
<h2>The Extraction Promise</h2>
<p>One of the biggest selling points of a <a href="https://milanjovanovic.tech/blog/modular-monolith-architecture-dotnet"><strong>modular monolith</strong></a> is the ability to extract modules into independent microservices when the need arises. But &quot;when the need arises&quot; is dangerously vague.</p>
<p>Extract too early, before the module boundaries are stable, and you end up with a distributed monolith. Wait too long, and extraction becomes a multi-month rewrite.</p>
<p>The key is understanding the signals that tell you extraction is the right move.</p>
<img src="https://milanjovanovic.tech/blogs/articles/when-to-extract-module-to-microservice/extraction-decision.png" alt="Decision flowchart: if a module lacks a clean API, isolated data, and event-based communication, fix the monolith first; if it has them but no concrete driver, keep it an in-process module; only with a concrete driver like scaling or fault isolation do you extract it via strangler fig">
<h2>Signs a Module Should Be Extracted</h2>
<h3>Independent Scaling Requirements</h3>
<p>If one module needs 10x the compute resources of the rest, deploying everything together wastes resources. A notification module sending millions of emails shouldn't force you to scale the entire application.</p>
<p>Before reaching for extraction, check whether <a href="https://milanjovanovic.tech/blog/scaling-monoliths-a-practical-guide-for-growing-systems"><strong>scaling the whole monolith</strong></a> horizontally is cheaper. It often is, until the load asymmetry gets extreme.</p>
<pre><code>Module                   | Avg Load      | Peak Load
-------------------------|---------------|----------
Catalog                  | Low           | Low
Ordering                 | Medium        | Medium
NotificationProcessing   | Low           | Very High  ← extraction candidate
UserManagement           | Low           | Low
</code></pre>
<h3>Different Deployment Cadences</h3>
<p>If one team needs to deploy their module five times a day while the rest deploys weekly, they're blocked by the monolith's deployment cycle. Independent deployment is a microservice benefit that matters here.</p>
<h3>Technology Requirements</h3>
<p>A module might benefit from a different technology stack. Maybe the search module needs Elasticsearch-native code, or the ML pipeline needs Python. In a monolith, you're locked to .NET for everything.</p>
<h3>Fault Isolation</h3>
<p>If a bug in one module crashes the entire application, extraction provides fault isolation. The notification module throwing an out-of-memory exception shouldn't take down the ordering module.</p>
<h2>Signs You Should NOT Extract</h2>
<h3>The Boundaries Are Still Shifting</h3>
<p>If module boundaries are still changing - endpoints move between modules, shared types keep growing - extraction will lock in the wrong boundaries as network contracts.</p>
<h3>Strong Data Coupling</h3>
<p>If two modules frequently join each other's data, separating them means replacing SQL joins with API calls. That's a performance and reliability hit. Fix the <a href="https://milanjovanovic.tech/blog/schema-per-module-vs-database-per-module"><strong>data isolation</strong></a> first.</p>
<h3>You Want to &quot;Try Microservices&quot;</h3>
<p>Extracting a module to learn microservices is expensive learning. The modular monolith gives you most of the modularity benefits without the operational complexity.</p>
<h3>Small Team</h3>
<p>A modular monolith suits teams of roughly 2-15 developers. If your engineering team is still inside that range, the coordination overhead of microservices usually outweighs the benefits. A modular monolith with clear <a href="https://milanjovanovic.tech/blog/modular-monolith-communication-patterns"><strong>module communication</strong></a> is simpler to operate.</p>
<h2>Readiness Checklist</h2>
<p>Before extracting, verify these preconditions:</p>
<pre><code class="language-csharp">// Your module should already have:

// 1. Clean public API (no direct database access from other modules)
public interface ICatalogModule
{
    Task&lt;ProductResponse&gt; GetProductAsync(Guid productId);
    Task&lt;bool&gt; CheckAvailabilityAsync(Guid productId, int quantity);
    Task ReserveStockAsync(Guid orderId, List&lt;OrderItem&gt; items);
}

// 2. Own database/schema (data isolation enforced)
public class CatalogDbContext : DbContext
{
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.HasDefaultSchema(&quot;catalog&quot;);
    }
}

// 3. Event-based communication (not synchronous method calls)
public class OrderPlacedEventHandler
    : IIntegrationEventHandler&lt;OrderPlacedIntegrationEvent&gt;
{
    public async Task HandleAsync(
        OrderPlacedIntegrationEvent @event,
        CancellationToken cancellationToken = default) { /* ... */ }
}
</code></pre>
<p>The <code>IIntegrationEvent</code> contracts and handler abstractions come from the <a href="https://milanjovanovic.tech/blog/shared-kernel-pattern-modular-monolith"><strong>shared kernel</strong></a>.</p>
<p>If any of these are missing, fix them first. Extraction without these foundations creates a distributed monolith.</p>
<h2>The Extraction Process</h2>
<h3>Step 1: Verify Data Isolation</h3>
<p>Ensure the module uses its own <a href="https://milanjovanovic.tech/blog/modular-monolith-data-isolation"><strong>database or schema</strong></a>. Query the database to find any cross-module references:</p>
<pre><code class="language-sql">-- Find foreign keys crossing module boundaries (PostgreSQL)
SELECT
    con.conname AS constraint_name,
    child_ns.nspname AS child_schema,
    child.relname AS child_table,
    parent_ns.nspname AS referenced_schema,
    parent.relname AS referenced_table
FROM pg_constraint con
JOIN pg_class child ON con.conrelid = child.oid
JOIN pg_namespace child_ns ON child.relnamespace = child_ns.oid
JOIN pg_class parent ON con.confrelid = parent.oid
JOIN pg_namespace parent_ns ON parent.relnamespace = parent_ns.oid
WHERE con.contype = 'f'
  AND child_ns.nspname &lt;&gt; parent_ns.nspname;
</code></pre>
<p>If you find cross-module foreign keys, remove them and replace them with eventual consistency through <a href="https://milanjovanovic.tech/blog/event-driven-communication-modules"><strong>integration events</strong></a>.</p>
<h3>Step 2: Replace In-Process Communication With HTTP/Messaging</h3>
<p>Module interfaces that were resolved via <strong>dependency injection</strong> now need to go over the network.</p>
<pre><code class="language-csharp">// Before: In-process call
public class OrderService
{
    private readonly ICatalogModule _catalog;

    public async Task PlaceOrder(PlaceOrderCommand command)
    {
        var available = await _catalog.CheckAvailabilityAsync(
            command.ProductId, command.Quantity);
    }
}

// After: HTTP client call
public class CatalogApiClient : ICatalogModule
{
    private readonly HttpClient _httpClient;

    public CatalogApiClient(HttpClient httpClient)
    {
        _httpClient = httpClient;
    }

    public async Task&lt;bool&gt; CheckAvailabilityAsync(
        Guid productId, int quantity)
    {
        var response = await _httpClient.GetAsync(
            $&quot;/api/catalog/products/{productId}/availability?quantity={quantity}&quot;);

        response.EnsureSuccessStatusCode();

        return await response.Content.ReadFromJsonAsync&lt;bool&gt;();
    }

    // GetProductAsync and ReserveStockAsync follow the same pattern
}
</code></pre>
<p>The key insight: the <code>ICatalogModule</code> interface doesn't change. Only the implementation switches from in-process to HTTP. This is why clean module APIs matter.</p>
<p>The new network hop also brings network failure modes. Wrap the HTTP client with retries and a <strong>circuit breaker</strong>, and set explicit timeouts. An in-process call could never time out; this one can.</p>
<h3>Step 3: Replace In-Memory Events With a Message Broker</h3>
<p>If you were using an in-memory event bus, switch to a real broker (RabbitMQ, Azure Service Bus) through a library like MassTransit.
The publishing call barely changes (<code>_publishEndpoint</code> is MassTransit's <code>IPublishEndpoint</code>):</p>
<pre><code class="language-csharp">// Before: in-process event bus
await _eventBus.PublishAsync(integrationEvent, ct);

// After: message broker publish with MassTransit
await _publishEndpoint.Publish(integrationEvent, ct);
</code></pre>
<p>The <a href="https://milanjovanovic.tech/blog/outbox-pattern-dual-write-problem"><strong>outbox pattern</strong></a> becomes critical here since network calls to the broker can fail.</p>
<h3>Step 4: Deploy Independently</h3>
<p>Create a separate deployment pipeline for the extracted module. It gets its own:</p>
<ul>
<li>Repository (or folder in a monorepo)</li>
<li>CI/CD pipeline</li>
<li>Container/hosting environment</li>
<li>Database instance</li>
</ul>
<h2>Strangler Fig Approach</h2>
<p>Don't extract everything at once. Use the strangler fig pattern:</p>
<ol>
<li>Deploy the new microservice alongside the monolith</li>
<li>Route traffic for the extracted module to the new service</li>
<li>Keep both running until the new service is proven stable</li>
<li>Remove the module from the monolith</li>
</ol>
<p>This gives you a rollback path if the extraction causes issues.</p>
<p>I walk through this migration in detail in <a href="https://milanjovanovic.tech/blog/breaking-it-down-how-to-migrate-your-modular-monolith-to-microservices"><strong>Breaking It Down: How to Migrate Your Modular Monolith to Microservices</strong></a>.</p>
<h2>Takeaway</h2>
<ol>
<li>Extract when you have concrete scaling, deployment, or fault-isolation needs - not because microservices are trendy.</li>
<li>Verify your module has clean APIs, isolated data, and event-based communication before extracting.</li>
<li>The module interface stays the same; only the implementation changes from in-process to network calls.</li>
<li>Replace in-memory events with a message broker and use the outbox pattern for reliability.</li>
<li>Use the strangler fig approach for safe, incremental extraction.</li>
<li>If boundaries are unstable or data is tightly coupled, fix the monolith first.</li>
</ol>
<p>Thanks for reading, and stay awesome!</p>
<hr>
]]></content:encoded>
            <author>milan@milanjovanovic.tech (Milan Jovanović)</author>
        </item>
        <item>
            <title><![CDATA[Strangler Fig Pattern for Modular Monolith Migration]]></title>
            <link>https://milanjovanovic.tech/blog/strangler-fig-modular-monolith-migration</link>
            <guid isPermaLink="false">https://milanjovanovic.tech/blog/strangler-fig-modular-monolith-migration</guid>
            <pubDate>Thu, 13 Aug 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Big-bang rewrites fail for predictable reasons: they take longer than planned, the legacy system keeps changing underneath them, and the business sees nothing…]]></description>
            <content:encoded><![CDATA[<p>Legacy monolith to modular monolith is the migration most .NET teams actually need, far more often than monolith to microservices.
The hard part is getting there without pausing feature work for a year.
The strangler fig pattern solves exactly that: you replace the legacy system incrementally, one bounded context at a time, while everything keeps running.
Here is the playbook, from the routing seam to the final cutover.</p>
<h2>The Big Rewrite Trap</h2>
<p>You have a legacy monolith - spaghetti code, shared database, tangled dependencies. The temptation is to rewrite everything from scratch.</p>
<p>This almost never works. Big rewrites take longer than expected, introduce new bugs, and often get abandoned.</p>
<p>The <a href="https://martinfowler.com/bliki/StranglerFigApplication.html">Strangler Fig pattern</a> offers a better approach. Just like the strangler fig tree gradually envelops its host tree, you gradually replace the legacy system with a <a href="https://milanjovanovic.tech/blog/modular-monolith-architecture-dotnet"><strong>Modular Monolith</strong></a> - one module at a time.</p>
<p>The same technique also works for the next step, <a href="https://milanjovanovic.tech/blog/breaking-it-down-how-to-migrate-your-modular-monolith-to-microservices"><strong>migrating a modular monolith to microservices</strong></a>. Here I'll focus on the first move: legacy monolith to modular monolith.</p>
<h2>How the Strangler Fig Pattern Works</h2>
<p>The strategy is simple:</p>
<ol>
<li><strong>Identify a bounded context</strong> in the legacy system</li>
<li><strong>Build a new module</strong> that handles the same functionality</li>
<li><strong>Route traffic</strong> to the new module instead of the legacy code</li>
<li><strong>Remove the old code</strong> once migration is complete</li>
<li><strong>Repeat</strong> for the next bounded context</li>
</ol>
<p>At any point, the system is fully functional. Some requests go to the new modules, others still hit the legacy code.</p>
<img src="https://milanjovanovic.tech/blogs/articles/strangler-fig-modular-monolith-migration/strangler-fig-cycle.png" alt="Cyclical flow of the strangler fig migration: identify a bounded context, build a new module, route traffic via a feature flag, run in parallel to verify, cut over and remove the legacy code, then repeat for the next context">
<h2>Step 1: Add a Routing Layer</h2>
<p>You need a seam where each request decides whether the legacy code or the new module handles it.</p>
<p>The cleanest seam is an interface with two implementations, switched by a <strong>feature flag</strong>:</p>
<pre><code class="language-csharp">public class OrderServiceRouter : IOrderService
{
    private readonly LegacyOrderService _legacy;
    private readonly NewOrderModule _newModule;
    private readonly IFeatureManager _features;

    public OrderServiceRouter(
        LegacyOrderService legacy,
        NewOrderModule newModule,
        IFeatureManager features)
    {
        _legacy = legacy;
        _newModule = newModule;
        _features = features;
    }

    public async Task&lt;OrderDto&gt; GetOrderAsync(Guid id)
    {
        if (await _features.IsEnabledAsync(&quot;NewOrdersModule&quot;))
        {
            return await _newModule.GetOrderAsync(id);
        }

        return await _legacy.GetOrderAsync(id);
    }
}
</code></pre>
<p>Register both implementations and the router in <code>Program.cs</code>:</p>
<pre><code class="language-csharp">builder.Services.AddFeatureManagement();

builder.Services.AddScoped&lt;LegacyOrderService&gt;();
builder.Services.AddScoped&lt;NewOrderModule&gt;();
builder.Services.AddScoped&lt;IOrderService, OrderServiceRouter&gt;();
</code></pre>
<p>The flag check happens at call time, so flipping the flag takes effect immediately - no redeploy needed.
And because the same check routes in both directions, rollback is the same flag flipped back.</p>
<p>If the legacy system is a separate application (not code you can share a process with), the routing layer becomes a reverse proxy like YARP: migrated routes go to the new application, everything else is forwarded to the legacy one. The principle is identical; only the seam moves from an interface to an HTTP route table.</p>
<h2>Step 2: Build the First Module</h2>
<p>Choose the least coupled, highest-value bounded context. Common good candidates:</p>
<ul>
<li><strong>Notifications</strong> - clear boundary, few dependencies</li>
<li><strong>Payments</strong> - well-defined interface, critical for business</li>
<li><strong>User profiles</strong> - self-contained data</li>
</ul>
<p>Build the module with proper <a href="https://milanjovanovic.tech/blog/module-boundaries-bounded-contexts"><strong>module boundaries</strong></a>:</p>
<pre><code>Modules/
  Orders/
    Orders.Application/
    Orders.Domain/
    Orders.Infrastructure/
    Orders.Contracts/
LegacyCode/
  OrdersLegacy/         ← still running
  CustomersLegacy/      ← still running
  ShippingLegacy/       ← still running
</code></pre>
<p>The new module has its own:</p>
<ul>
<li>Database schema (or separate tables)</li>
<li>Domain model</li>
<li>Public API (contracts)</li>
</ul>
<h2>Step 3: Data Migration</h2>
<p>The hardest part. You need to migrate data from the legacy schema to the new module's schema.</p>
<h3>Option A: Shared Database, New Schema</h3>
<pre><code class="language-sql">-- Legacy table (default schema)
SELECT * FROM public.orders;

-- New module table (module schema)
SELECT * FROM orders.orders;
</code></pre>
<p>Both schemas coexist in the same database. The new module reads from <code>orders.orders</code> - its own schema. During migration, you sync data between schemas:</p>
<pre><code class="language-csharp">public class OrderDataMigrationJob : BackgroundService
{
    private readonly IServiceScopeFactory _scopeFactory;
    private DateTime _lastSync = DateTime.MinValue;

    public OrderDataMigrationJob(IServiceScopeFactory scopeFactory)
    {
        _scopeFactory = scopeFactory;
    }

    protected override async Task ExecuteAsync(CancellationToken ct)
    {
        while (!ct.IsCancellationRequested)
        {
            using var scope = _scopeFactory.CreateScope();
            var legacyDb = scope.ServiceProvider
                .GetRequiredService&lt;LegacyDbContext&gt;();
            var newDb = scope.ServiceProvider
                .GetRequiredService&lt;OrderDbContext&gt;();

            DateTime syncStartedAt = DateTime.UtcNow;

            // Sync legacy orders modified since the last run
            var legacyOrders = await legacyDb.Orders
                .Where(o =&gt; o.ModifiedAt &gt; _lastSync)
                .ToListAsync(ct);

            foreach (var legacyOrder in legacyOrders)
            {
                var existing = await newDb.Orders
                    .FirstOrDefaultAsync(o =&gt; o.Id == legacyOrder.Id, ct);

                if (existing is null)
                {
                    newDb.Orders.Add(MapToNewModel(legacyOrder));
                }
                else
                {
                    existing.UpdateFrom(legacyOrder);
                }
            }

            await newDb.SaveChangesAsync(ct);
            _lastSync = syncStartedAt;

            await Task.Delay(TimeSpan.FromSeconds(30), ct);
        }
    }
}
</code></pre>
<p>A few details that matter here.
<code>BackgroundService</code> is a singleton, so the job resolves the scoped <code>DbContext</code> instances through <code>IServiceScopeFactory</code> on every iteration (injecting them directly into the constructor throws at startup).
The job records <code>syncStartedAt</code> before querying, so rows modified mid-sync are picked up on the next pass instead of being skipped.
<code>MapToNewModel</code> and <code>UpdateFrom</code> are plain mapping helpers that translate legacy columns into the new domain model.
Register the job with <code>builder.Services.AddHostedService&lt;OrderDataMigrationJob&gt;();</code>.</p>
<h3>Option B: Event-Based Sync</h3>
<p>Use <a href="https://milanjovanovic.tech/blog/how-to-use-domain-events-to-build-loosely-coupled-systems"><strong>domain events</strong></a> or change data capture to keep both systems in sync.
Since the legacy code and the new module share a process during the migration, MediatR notifications work well as the sync channel:</p>
<pre><code class="language-csharp">// The sync event
public sealed record OrderCreatedEvent(
    Guid OrderId,
    Guid CustomerId,
    decimal Total,
    DateTime CreatedAt) : INotification;

// The legacy code calls this hook after every write
public class LegacyOrderEventPublisher
{
    private readonly IPublisher _publisher;

    public LegacyOrderEventPublisher(IPublisher publisher)
    {
        _publisher = publisher;
    }

    public async Task OnOrderCreatedAsync(LegacyOrder order, CancellationToken ct)
    {
        await _publisher.Publish(
            new OrderCreatedEvent(
                order.Id,
                order.CustomerId,
                order.Total,
                order.CreatedAt),
            ct);
    }
}

// New module subscribes
public class OrderCreatedEventHandler
    : INotificationHandler&lt;OrderCreatedEvent&gt;
{
    private readonly OrderDbContext _db;

    public OrderCreatedEventHandler(OrderDbContext db)
    {
        _db = db;
    }

    public async Task Handle(
        OrderCreatedEvent notification, CancellationToken ct)
    {
        var order = Order.Create(
            notification.OrderId,
            notification.CustomerId,
            notification.Total);

        _db.Orders.Add(order);
        await _db.SaveChangesAsync(ct);
    }
}
</code></pre>
<h2>Step 4: Parallel Running</h2>
<p>Run both implementations simultaneously to verify the new module produces correct results:</p>
<pre><code class="language-csharp">public class ParallelVerificationService : IOrderService
{
    private readonly LegacyOrderService _legacy;
    private readonly NewOrderModule _newModule;
    private readonly ILogger&lt;ParallelVerificationService&gt; _logger;

    public ParallelVerificationService(
        LegacyOrderService legacy,
        NewOrderModule newModule,
        ILogger&lt;ParallelVerificationService&gt; logger)
    {
        _legacy = legacy;
        _newModule = newModule;
        _logger = logger;
    }

    public async Task&lt;OrderDto&gt; GetOrderAsync(Guid id)
    {
        var legacyResult = await _legacy.GetOrderAsync(id);
        var newResult = await _newModule.GetOrderAsync(id);

        // OrderDto is a record, so this compares values
        if (!legacyResult.Equals(newResult))
        {
            _logger.LogWarning(
                &quot;Mismatch for order {OrderId}. Legacy: {@Legacy}, New: {@New}&quot;,
                id, legacyResult, newResult);
        }

        // Return the legacy result until verified
        return legacyResult;
    }
}
</code></pre>
<p>It implements the same <code>IOrderService</code> seam, so during the parallel-run phase you register it in place of the router.
Compare on read paths like this one; doubling up writes would create duplicate side effects.</p>
<p>Once you're confident the new module is correct, flip the feature flag.</p>
<h2>Step 5: Cut Over and Clean Up</h2>
<p>After the new module is proven:</p>
<ol>
<li>Point all traffic to the new module</li>
<li>Keep the legacy code around for a rollback period</li>
<li>Stop the data sync</li>
<li>Delete the legacy code</li>
<li>Drop the legacy database tables</li>
</ol>
<pre><code class="language-json">{
  &quot;FeatureManagement&quot;: {
    &quot;NewOrdersModule&quot;: true,
    &quot;NewPaymentsModule&quot;: true,
    &quot;NewShippingModule&quot;: false
  }
}
</code></pre>
<p>Here, Orders and Payments are fully migrated, and Shipping is next up.</p>
<h2>Migration Timeline</h2>
<p>A realistic per-module timeline looks like this:</p>
<ul>
<li><strong>Module scoping</strong> (1-2 weeks): identify boundaries, define contracts</li>
<li><strong>Build the new module</strong> (2-4 weeks): domain model, use cases, persistence</li>
<li><strong>Data migration</strong> (1-2 weeks): sync data, verify consistency</li>
<li><strong>Parallel run</strong> (1-2 weeks): verify correctness under production load</li>
<li><strong>Cut over</strong> (1 day): flip the feature flag, monitor closely</li>
<li><strong>Clean up</strong> (1 week): remove legacy code, drop old tables</li>
</ul>
<p>That adds up to roughly 6 to 11 weeks per module.
Repeat for each one.
Later modules go faster because the routing seam, sync tooling, and team habits already exist, and you can pipeline the work (scope the next module while the previous one is in its parallel run).
A system with 5-6 modules usually lands somewhere between six months and a year.</p>
<p>That sounds long, but compare it honestly with a rewrite: the same scope rewritten from scratch takes at least as long, delivers nothing until the end, and carries the risk of never shipping. The strangler fig delivers a migrated, verified module every few weeks.</p>
<h2>Common Mistakes</h2>
<p><strong>Migrating too much at once.</strong> One module at a time. Don't try to migrate orders, payments, and shipping simultaneously.</p>
<p><strong>Skipping the parallel run.</strong> You need proof the new module is correct before cutting over. Data inconsistencies are expensive to fix.</p>
<p><strong>Not investing in the routing layer.</strong> The ability to route between old and new implementations per-request is crucial. Feature flags make this safe.</p>
<p><strong>Ignoring data ownership.</strong> The legacy system and new module should not read from each other's tables directly. Communicate through events or APIs.</p>
<h2>No Big Bang Required</h2>
<p>The Strangler Fig pattern for migrating to a Modular Monolith:</p>
<ol>
<li>Add a routing layer (feature flags)</li>
<li>Build one module at a time</li>
<li>Sync data between old and new</li>
<li>Run both in parallel to verify</li>
<li>Cut over and clean up</li>
<li>Repeat</li>
</ol>
<p>No big bang rewrites. No weekends of downtime. Just steady, incremental progress toward a better architecture.</p>
<p>And when the modular monolith itself needs to evolve further, the same playbook applies to <a href="https://milanjovanovic.tech/blog/when-to-extract-module-to-microservice"><strong>extracting modules into microservices</strong></a>.</p>
<p>Thanks for reading, and stay awesome!</p>
<hr>
]]></content:encoded>
            <author>milan@milanjovanovic.tech (Milan Jovanović)</author>
        </item>
        <item>
            <title><![CDATA[Shared Kernel Pattern in Modular Monoliths]]></title>
            <link>https://milanjovanovic.tech/blog/shared-kernel-pattern-modular-monolith</link>
            <guid isPermaLink="false">https://milanjovanovic.tech/blog/shared-kernel-pattern-modular-monolith</guid>
            <pubDate>Thu, 13 Aug 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Modules need to share some code without becoming coupled. The shared kernel pattern defines a small, explicit set of shared types that all modules can depend…]]></description>
            <content:encoded><![CDATA[<p>How much code should modules share?
It's one of the first questions that comes up when you build a modular monolith, and both instinctive answers (share nothing, share everything) are wrong.
The shared kernel pattern is the middle path: a small, explicit, governed set of types that every module can safely depend on.
This article covers what goes in, what stays out, and how to keep it from growing into a &quot;Common&quot; project.</p>
<h2>The Sharing Dilemma</h2>
<p>In a <a href="https://milanjovanovic.tech/blog/modular-monolith-architecture-dotnet"><strong>modular monolith</strong></a>, modules should be independent. But they inevitably need to share some things - base entity classes, common interfaces, integration event contracts, and <a href="https://milanjovanovic.tech/blog/value-objects-in-dotnet-ddd-fundamentals"><strong>value objects</strong></a>.</p>
<p>The question isn't whether to share, but how much and how.</p>
<p>Teams tend toward one of two extremes: modules that share nothing (duplicating hundreds of lines of identical code) and modules that share everything (a giant &quot;Common&quot; project that defeats the purpose of modularity). The shared kernel pattern sits in the middle.</p>
<h2>What Is a Shared Kernel?</h2>
<p>The shared kernel is a <a href="https://milanjovanovic.tech/blog/bounded-context-ddd-explained"><strong>DDD concept</strong></a> - a small, well-defined set of code that two or more bounded contexts agree to share. It's not a dumping ground for convenience. Every type in the shared kernel is there because multiple modules genuinely need it.</p>
<p>In a .NET modular monolith, the shared kernel is typically a single project that all modules reference:</p>
<img src="https://milanjovanovic.tech/blogs/articles/shared-kernel-pattern-modular-monolith/shared-kernel-dependencies.png" alt="Dependency diagram showing the Catalog, Ordering, and Shipping modules all referencing a single Shared Kernel project that holds base types, the Result and Error types, IDomainEvent, and strongly typed IDs">
<pre><code>src/
  SharedKernel/
    SharedKernel.csproj
  Modules/
    Catalog/
    Ordering/
    Shipping/
</code></pre>
<h2>What Belongs in the Shared Kernel</h2>
<h3>Base Domain Types</h3>
<p>Abstract base classes for <strong>entities</strong>, <strong>aggregate roots</strong>, and value objects:</p>
<pre><code class="language-csharp">public abstract class Entity
{
    public Guid Id { get; protected init; }

    private readonly List&lt;IDomainEvent&gt; _domainEvents = [];

    public IReadOnlyList&lt;IDomainEvent&gt; DomainEvents =&gt; _domainEvents.AsReadOnly();

    public void AddDomainEvent(IDomainEvent domainEvent)
    {
        _domainEvents.Add(domainEvent);
    }

    public void ClearDomainEvents()
    {
        _domainEvents.Clear();
    }
}

public abstract class AggregateRoot : Entity
{
    // Aggregate-specific behavior
}
</code></pre>
<h3>Common Interfaces</h3>
<p>Interfaces that define cross-cutting contracts:</p>
<pre><code class="language-csharp">public interface IDomainEvent
{
    Guid EventId { get; }
    DateTime OccurredOnUtc { get; }
}

public interface IIntegrationEvent
{
    Guid EventId { get; }
    DateTime OccurredOnUtc { get; }
}

public interface IUnitOfWork
{
    Task&lt;int&gt; SaveChangesAsync(CancellationToken cancellationToken = default);
}
</code></pre>
<h3>The Result Type</h3>
<p>A <a href="https://milanjovanovic.tech/blog/functional-error-handling-in-dotnet-with-the-result-pattern"><strong>result pattern</strong></a> implementation that all modules use for error handling:</p>
<pre><code class="language-csharp">public class Result
{
    public bool IsSuccess { get; }
    public bool IsFailure =&gt; !IsSuccess;
    public Error Error { get; }

    protected Result(bool isSuccess, Error error)
    {
        IsSuccess = isSuccess;
        Error = error;
    }

    public static Result Success() =&gt; new(true, Error.None);
    public static Result Failure(Error error) =&gt; new(false, error);
    public static Result&lt;T&gt; Success&lt;T&gt;(T value) =&gt; new(value, true, Error.None);
    public static Result&lt;T&gt; Failure&lt;T&gt;(Error error) =&gt; new(default!, false, error);
}

public class Result&lt;T&gt; : Result
{
    public T Value { get; }

    protected internal Result(T value, bool isSuccess, Error error)
        : base(isSuccess, error)
    {
        Value = value;
    }
}

public record Error(string Code, string Description)
{
    public static readonly Error None = new(string.Empty, string.Empty);
}
</code></pre>
<h3>Integration Event Contracts</h3>
<p>The event types that define the contract between modules for <a href="https://milanjovanovic.tech/blog/event-driven-communication-modules"><strong>event-driven communication</strong></a>:</p>
<pre><code class="language-csharp">// These are the contracts between modules
public sealed record OrderPlacedIntegrationEvent(
    Guid EventId,
    DateTime OccurredOnUtc,
    Guid OrderId,
    Guid CustomerId,
    decimal TotalAmount) : IIntegrationEvent;

public sealed record PaymentCompletedIntegrationEvent(
    Guid EventId,
    DateTime OccurredOnUtc,
    Guid OrderId,
    decimal Amount) : IIntegrationEvent;
</code></pre>
<p>An alternative worth considering: keep only the <code>IIntegrationEvent</code> interface in the shared kernel and put each module's event records in that module's <code>Contracts</code> project (so <code>OrderPlacedIntegrationEvent</code> lives in <code>Ordering.Contracts</code>).
That scopes each contract to its owning module, at the cost of consumers referencing multiple Contracts projects.
Both work; just pick one convention and stick to it.</p>
<h3>Strongly Typed IDs</h3>
<p>If you're using <strong>strongly typed IDs</strong> that cross module boundaries:</p>
<pre><code class="language-csharp">public readonly record struct CustomerId(Guid Value)
{
    public static CustomerId New() =&gt; new(Guid.NewGuid());
}

public readonly record struct ProductId(Guid Value)
{
    public static ProductId New() =&gt; new(Guid.NewGuid());
}
</code></pre>
<h2>What Does NOT Belong in the Shared Kernel</h2>
<p>This is where teams go wrong. The shared kernel should not contain:</p>
<ul>
<li><strong>Module-specific domain logic</strong> - An <code>Order</code> entity belongs in the Ordering module, not the shared kernel</li>
<li><strong>Infrastructure concerns</strong> - Database configurations, HTTP clients, logging helpers</li>
<li><strong>Utility classes</strong> - String helpers, date formatters, extension methods that only one module uses</li>
<li><strong>DTOs/ViewModels</strong> - These are API concerns, not domain concepts</li>
</ul>
<p>A good rule of thumb: if removing a type from the shared kernel breaks only one module, it doesn't belong there.</p>
<h2>Project Structure</h2>
<p>Keep the shared kernel minimal and well-organized:</p>
<pre><code class="language-xml">&lt;!-- SharedKernel.csproj --&gt;
&lt;Project Sdk=&quot;Microsoft.NET.Sdk&quot;&gt;
  &lt;PropertyGroup&gt;
    &lt;TargetFramework&gt;net10.0&lt;/TargetFramework&gt;
  &lt;/PropertyGroup&gt;

  &lt;!-- Zero external dependencies! --&gt;
&lt;/Project&gt;
</code></pre>
<p>The shared kernel should have zero external NuGet dependencies. It's pure domain code. If you need EF Core or MediatR types in the shared kernel, you've gone too far.</p>
<pre><code>SharedKernel/
  Domain/
    Entity.cs
    AggregateRoot.cs
    IDomainEvent.cs
    IUnitOfWork.cs
  Results/
    Result.cs
    Error.cs
  Events/
    IIntegrationEvent.cs
    OrderPlacedIntegrationEvent.cs
    PaymentCompletedIntegrationEvent.cs
  Ids/
    CustomerId.cs
    ProductId.cs
</code></pre>
<h2>Governance</h2>
<p>The shared kernel is shared code, which means changes to it affect all modules. You need governance:</p>
<ol>
<li><strong>Code review required</strong> - Any change to the shared kernel must be reviewed by representatives of all consuming modules</li>
<li><strong>Backward compatibility</strong> - Don't remove or rename types. Add new ones instead</li>
<li><strong>Small surface area</strong> - Resist the urge to add convenience methods. Keep it minimal</li>
<li><strong>Versioning</strong> - In a monorepo this is less critical, but treat the shared kernel as a contract</li>
</ol>
<pre><code class="language-csharp">// BAD: Adding a helper because it's convenient
public static class StringExtensions
{
    public static string ToSlug(this string input) =&gt;
        input.Trim().ToLowerInvariant().Replace(' ', '-');
}

// GOOD: Adding a type that genuinely defines a cross-module contract
public interface IIntegrationEventHandler&lt;in TEvent&gt;
    where TEvent : IIntegrationEvent
{
    Task HandleAsync(TEvent @event, CancellationToken cancellationToken = default);
}
</code></pre>
<h2>Registering Shared Kernel Services</h2>
<p>If the shared kernel provides services (like a <a href="https://milanjovanovic.tech/blog/how-to-use-domain-events-to-build-loosely-coupled-systems"><strong>domain event dispatcher</strong></a>), register them in a single extension method:</p>
<pre><code class="language-csharp">public static class SharedKernelServiceExtensions
{
    public static IServiceCollection AddSharedKernel(
        this IServiceCollection services)
    {
        services.AddScoped&lt;IDomainEventDispatcher, DomainEventDispatcher&gt;();

        return services;
    }
}

// In Program.cs
builder.Services.AddSharedKernel();
builder.Services.AddCatalogModule();
builder.Services.AddOrderingModule();
</code></pre>
<p>Keep the zero-dependency rule intact: the shared kernel defines <code>IDomainEventDispatcher</code>, but the implementation and this registration extension live in a shared <em>infrastructure</em> project (which is allowed to reference <code>Microsoft.Extensions.DependencyInjection.Abstractions</code>).</p>
<h2>Takeaway</h2>
<ol>
<li>The shared kernel is a small, explicit set of shared types - not a dumping ground for convenience code.</li>
<li>Include base domain types, common interfaces, the result pattern, integration event contracts, and shared IDs.</li>
<li>Exclude module-specific logic, infrastructure, utilities, and DTOs.</li>
<li>The shared kernel project should have zero external NuGet dependencies.</li>
<li>Require code reviews for any shared kernel change since it affects all modules.</li>
<li>Keep the surface area as small as possible - when in doubt, don't share it.</li>
</ol>
<p>Thanks for reading, and stay awesome!</p>
<hr>
]]></content:encoded>
            <author>milan@milanjovanovic.tech (Milan Jovanović)</author>
        </item>
        <item>
            <title><![CDATA[Schema-Per-Module vs Database-Per-Module: Which Data Isolation Strategy Should You Pick?]]></title>
            <link>https://milanjovanovic.tech/blog/schema-per-module-vs-database-per-module</link>
            <guid isPermaLink="false">https://milanjovanovic.tech/blog/schema-per-module-vs-database-per-module</guid>
            <pubDate>Thu, 13 Aug 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Schema-per-module and database-per-module both enforce module boundaries at the data layer, but they differ sharply on transactions, operations, and your path…]]></description>
            <content:encoded><![CDATA[<p>When you build a <a href="https://milanjovanovic.tech/blog/modular-monolith-architecture-dotnet"><strong>modular monolith</strong></a>, one of the earliest decisions you'll face is how to isolate data between modules. Two strategies dominate the conversation: <strong>schema-per-module</strong> and <strong>database-per-module</strong>.</p>
<p>They're the two strongest levels on the <a href="https://milanjovanovic.tech/blog/modular-monolith-data-isolation"><strong>data isolation spectrum</strong></a>, which also includes table prefixes at the weak end.</p>
<p>Both enforce module boundaries at the data layer. But they make very different trade-offs around complexity, consistency, and your future migration path. The right choice depends on where your system is today - not where you hope it'll be in two years.</p>
<p>Let me break down each strategy, show you how to wire them up in EF Core, and compare them head to head.</p>
<img src="https://milanjovanovic.tech/blogs/articles/schema-per-module-vs-database-per-module/isolation-strategies.png" alt="Diagram contrasting schema-per-module, where one database holds a catalog schema and an ordering schema, with database-per-module, where the catalog and ordering modules each get their own separate database">
<h2>Schema-Per-Module</h2>
<p>The schema-per-module strategy keeps all modules inside a single database but assigns each module its own schema. The <code>catalog</code> module owns <code>catalog.products</code>, while the <code>ordering</code> module owns <code>ordering.orders</code>. They share a database server and connection string but have logically separated table namespaces.</p>
<p>In EF Core, you configure this with <code>HasDefaultSchema</code> on each <code>DbContext</code>:</p>
<pre><code class="language-csharp">public class CatalogDbContext : DbContext
{
    public DbSet&lt;Product&gt; Products =&gt; Set&lt;Product&gt;();

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.HasDefaultSchema(&quot;catalog&quot;);

        modelBuilder.ApplyConfigurationsFromAssembly(
            typeof(CatalogDbContext).Assembly);
    }
}

public class OrderingDbContext : DbContext
{
    public DbSet&lt;Order&gt; Orders =&gt; Set&lt;Order&gt;();

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.HasDefaultSchema(&quot;ordering&quot;);

        modelBuilder.ApplyConfigurationsFromAssembly(
            typeof(OrderingDbContext).Assembly);
    }
}
</code></pre>
<p>Both contexts share the same connection string. Each one also gets its own migrations history table, inside its own schema, so the modules can add and apply migrations independently:</p>
<pre><code class="language-csharp">var connectionString = builder.Configuration
    .GetConnectionString(&quot;DefaultConnection&quot;);

builder.Services.AddDbContext&lt;CatalogDbContext&gt;(options =&gt;
    options.UseNpgsql(connectionString, npgsql =&gt;
        npgsql.MigrationsHistoryTable(
            &quot;__EFMigrationsHistory&quot;, &quot;catalog&quot;)));

builder.Services.AddDbContext&lt;OrderingDbContext&gt;(options =&gt;
    options.UseNpgsql(connectionString, npgsql =&gt;
        npgsql.MigrationsHistoryTable(
            &quot;__EFMigrationsHistory&quot;, &quot;ordering&quot;)));
</code></pre>
<p>Without the <code>MigrationsHistoryTable</code> call, both contexts record their migrations in the same default <code>__EFMigrationsHistory</code> table, which couples the modules' migration workflows.</p>
<p>This strategy works well when you're starting out or when your modules share traffic patterns and don't need independent scaling. One database means one backup strategy, one monitoring target, and one connection string in your configuration.</p>
<h2>Database-Per-Module</h2>
<p>The database-per-module strategy gives each module its own database entirely. The <code>catalog</code> module connects to <code>myshop_catalog</code>, and the <code>ordering</code> module connects to <code>myshop_ordering</code>. There's no way to accidentally join across module boundaries because the tables live on separate databases.</p>
<p>The EF Core setup uses distinct connection strings:</p>
<pre><code class="language-csharp">builder.Services.AddDbContext&lt;CatalogDbContext&gt;(options =&gt;
    options.UseNpgsql(builder.Configuration
        .GetConnectionString(&quot;CatalogDb&quot;)));

builder.Services.AddDbContext&lt;OrderingDbContext&gt;(options =&gt;
    options.UseNpgsql(builder.Configuration
        .GetConnectionString(&quot;OrderingDb&quot;)));
</code></pre>
<p>Your configuration carries multiple connection strings:</p>
<pre><code class="language-json">{
  &quot;ConnectionStrings&quot;: {
    &quot;CatalogDb&quot;: &quot;Host=localhost;Database=myshop_catalog;Username=app;Password=secret&quot;,
    &quot;OrderingDb&quot;: &quot;Host=localhost;Database=myshop_ordering;Username=app;Password=secret&quot;
  }
}
</code></pre>
<p>Migrations are completely independent. Each module runs its own migration at startup without any coordination:</p>
<pre><code class="language-csharp">using var scope = app.Services.CreateScope();

var catalogDb = scope.ServiceProvider.GetRequiredService&lt;CatalogDbContext&gt;();
await catalogDb.Database.MigrateAsync();

var orderingDb = scope.ServiceProvider.GetRequiredService&lt;OrderingDbContext&gt;();
await orderingDb.Database.MigrateAsync();
</code></pre>
<p>This strategy works well when you need physical isolation between modules. If one module has heavy write loads and another is read-heavy, separate databases let you tune each independently. It also gives you independent backup and restore capabilities.</p>
<h2>The Real Trade-offs</h2>
<p>Choosing between schema-per-module and database-per-module comes down to five key areas.</p>
<p><strong>Cross-module transactions.</strong> With schemas, both modules share a database, so you <em>can</em> wrap operations across modules in a single transaction. Treat that as an escape hatch, not a feature - every shared transaction couples the modules a little more. With separate databases, cross-module transactions are impossible by construction. You'll need patterns like the <a href="https://milanjovanovic.tech/blog/outbox-pattern-dual-write-problem"><strong>outbox pattern</strong></a> or <a href="https://milanjovanovic.tech/blog/saga-pattern-modular-monolith"><strong>sagas</strong></a> to maintain consistency across module boundaries. This is a significant complexity jump.</p>
<p><strong>Deployment complexity.</strong> Schema-per-module means one database to provision, monitor, and back up. Database-per-module multiplies that by the number of modules. If you have five modules, you now have five databases to manage. In production, this means more connection pools, more monitoring dashboards, and more backup schedules.</p>
<p><strong>Performance isolation.</strong> Schemas share the same database resources - CPU, memory, I/O. A poorly optimized query in one module can degrade performance for every other module. Separate databases give you true resource isolation. You can allocate more resources to your busiest module without touching the others.</p>
<p><strong>Developer experience.</strong> Schema-per-module is simpler for local development. One PostgreSQL instance, one connection string, and you're running. Database-per-module means each developer needs multiple database instances, which usually means a Docker Compose file with several containers. Not a dealbreaker, but it adds friction.</p>
<p><strong>Migration path to microservices.</strong> If you plan to eventually extract modules into microservices, database-per-module puts you closer to that goal. Each module already owns its data store. With schema-per-module, extracting a module means migrating its schema into a new database and updating all <a href="https://milanjovanovic.tech/blog/modular-monolith-communication-patterns"><strong>module communication</strong></a> to handle network boundaries.</p>
<h2>Head-to-Head Summary</h2>
<p>Here's the whole comparison condensed:</p>
<ul>
<li><strong>Isolation level</strong>: schemas give logical isolation in one database; databases give physical isolation.</li>
<li><strong>Cross-module transactions</strong>: possible (but discouraged) with schemas; impossible with separate databases, which forces saga/outbox patterns.</li>
<li><strong>Deployment overhead</strong>: one database to manage vs one per module.</li>
<li><strong>Performance isolation</strong>: shared CPU, memory, and I/O with schemas; independent resources with databases.</li>
<li><strong>Local dev setup</strong>: one connection string vs a Docker Compose file with several database containers.</li>
<li><strong>Migration to microservices</strong>: schemas require a data extraction step first; databases are already separated.</li>
<li><strong>Backup and restore</strong>: all-or-nothing with one database; per-module with separate databases.</li>
<li><strong>Connection pooling</strong>: a single pool vs one pool per module (watch total connection counts on the server).</li>
</ul>
<h2>Enforcing the Boundary Either Way</h2>
<p>Whichever strategy you pick, isolation only holds if nothing bypasses it.
Use a separate database user per module (with permissions limited to its own schema or database) so a cross-module query fails loudly instead of working silently.
I cover more enforcement techniques in <a href="https://milanjovanovic.tech/blog/how-to-keep-your-data-boundaries-intact-in-a-modular-monolith"><strong>how to keep your data boundaries intact</strong></a>.</p>
<h2>My Recommendation</h2>
<p>Start with schema-per-module. It gives you meaningful data isolation with minimal infrastructure overhead. You get separate namespaces, independent migration histories, and a clear boundary that prevents accidental cross-module queries through EF Core.</p>
<p>Then graduate to database-per-module when you hit one of these triggers:</p>
<ul>
<li>A module needs independent scaling due to different load patterns</li>
<li>You need to restore one module's data without affecting others</li>
<li>A noisy-neighbor problem is degrading performance across modules</li>
<li>You're actively planning to extract a module into a service</li>
</ul>
<p>The transition from schemas to databases is straightforward. You create the new database, migrate the schema's tables into it, and update the connection string. No changes to your <code>DbContext</code> classes or application code beyond configuration.</p>
<p>Picking the &quot;best&quot; strategy upfront is less important than picking one that enforces boundaries at all. A modular monolith with schema isolation beats a monolith where every module reaches into every other module's tables.</p>
<p>Thanks for reading, and stay awesome!</p>
]]></content:encoded>
            <author>milan@milanjovanovic.tech (Milan Jovanović)</author>
        </item>
        <item>
            <title><![CDATA[Saga Pattern in a Modular Monolith]]></title>
            <link>https://milanjovanovic.tech/blog/saga-pattern-modular-monolith</link>
            <guid isPermaLink="false">https://milanjovanovic.tech/blog/saga-pattern-modular-monolith</guid>
            <pubDate>Thu, 13 Aug 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Placing an order touches Ordering, Inventory, Payment, and Shipping, and any step can fail after the previous ones already committed.]]></description>
            <content:encoded><![CDATA[<p>Placing an order starts in the Ordering module.
Fulfilling it needs Inventory, Payment, and Shipping, and any of those steps can fail after the previous ones already committed.
So what happens when the payment is declined and the stock is already reserved?
That's the saga pattern's job, and inside a modular monolith it's simpler than its microservices reputation suggests.</p>
<h2>The Problem With Cross-Module Transactions</h2>
<p>In a <a href="https://milanjovanovic.tech/blog/modular-monolith-architecture-dotnet"><strong>modular monolith</strong></a>, each module owns its data. When a business process spans multiple modules - like placing an order that requires inventory reservation, payment processing, and shipping - you can't wrap everything in a single database transaction.</p>
<p>If your modules use <a href="https://milanjovanovic.tech/blog/modular-monolith-data-isolation"><strong>separate databases</strong></a> or <a href="https://milanjovanovic.tech/blog/schema-per-module-vs-database-per-module"><strong>separate schemas</strong></a>, there's no way to do a distributed ACID transaction without introducing tight coupling.</p>
<p>To be precise: with schema-per-module in one physical database, a cross-module transaction is <em>technically</em> possible.
But using it means one module's transaction now holds locks on another module's tables, and your modules can never be separated without rewriting the process.
Treat module boundaries as transaction boundaries, and the saga pattern follows naturally.</p>
<p>The saga pattern solves this by breaking a long-running process into a sequence of local transactions, each within a single module, coordinated through events or a centralized orchestrator.</p>
<h2>Choreography vs Orchestration</h2>
<p>There are two approaches to implementing sagas.</p>
<p><strong>Choreography</strong> - Each module listens for events and decides what to do next. There's no central coordinator. Module A publishes an event, Module B reacts, publishes its own event, and Module C reacts to that.</p>
<p><strong>Orchestration</strong> - A central saga orchestrator tells each module what to do and when. It maintains the state of the process and sends commands to each participant.</p>
<p>I prefer orchestration for most use cases because the process flow is explicit and visible in one place. With choreography, the business logic is scattered across event handlers in different modules, making it hard to understand and debug.
I've written a deeper comparison in <a href="https://milanjovanovic.tech/blog/orchestration-vs-choreography"><strong>Orchestration vs Choreography</strong></a>.</p>
<h2>Defining a Saga State Machine</h2>
<p>A saga is essentially a state machine. Each step transitions the saga to a new state, and failures trigger compensating actions.</p>
<img src="https://milanjovanovic.tech/blogs/articles/saga-pattern-modular-monolith/order-saga-state.png" alt="State diagram of the order saga moving through Started, InventoryReserved, PaymentProcessed, ShipmentScheduled, and Completed on the happy path, with failure transitions to InventoryCompensated and Failed">
<pre><code class="language-csharp">public class OrderSaga
{
    public Guid Id { get; private set; }
    public Guid OrderId { get; private set; }
    public decimal TotalAmount { get; private set; }
    public OrderSagaState State { get; private set; }
    public DateTime StartedAtUtc { get; private set; }
    public DateTime? CompletedAtUtc { get; private set; }
    public string? FailureReason { get; private set; }

    public static OrderSaga Start(Guid orderId, decimal totalAmount)
    {
        return new OrderSaga
        {
            Id = Guid.NewGuid(),
            OrderId = orderId,
            TotalAmount = totalAmount,
            State = OrderSagaState.Started,
            StartedAtUtc = DateTime.UtcNow
        };
    }

    public void TransitionTo(OrderSagaState newState)
    {
        State = newState;
    }

    public void Fail(string reason)
    {
        FailureReason = reason;
        State = OrderSagaState.Failed;
    }

    public void Complete()
    {
        State = OrderSagaState.Completed;
        CompletedAtUtc = DateTime.UtcNow;
    }
}

public enum OrderSagaState
{
    Started,
    InventoryReserved,
    PaymentProcessed,
    ShipmentScheduled,
    Completed,
    InventoryCompensated,
    Failed
}
</code></pre>
<h2>Building the Saga Orchestrator</h2>
<p>The orchestrator processes events and drives the saga forward. Each event handler checks the current state and issues the next command.</p>
<p>The saga is triggered by the same <code>OrderPlacedIntegrationEvent</code> contract used for <a href="https://milanjovanovic.tech/blog/event-driven-communication-modules"><strong>event-driven communication</strong></a>, and every later step publishes its own thin event.
<code>IIntegrationEvent</code> is the marker from the <a href="https://milanjovanovic.tech/blog/shared-kernel-pattern-modular-monolith"><strong>shared kernel</strong></a>: an <code>EventId</code> plus an <code>OccurredOnUtc</code> timestamp.</p>
<pre><code class="language-csharp">// Ordering.Contracts
public sealed record OrderPlacedIntegrationEvent(
    Guid EventId,
    DateTime OccurredOnUtc,
    Guid OrderId,
    Guid CustomerId,
    decimal TotalAmount) : IIntegrationEvent;

public sealed record OrderLine(Guid ProductId, int Quantity);

// Inventory.Contracts
public sealed record StockReservedEvent(
    Guid EventId, DateTime OccurredOnUtc, Guid OrderId) : IIntegrationEvent;

public sealed record StockReservationFailedEvent(
    Guid EventId, DateTime OccurredOnUtc,
    Guid OrderId, string Reason) : IIntegrationEvent;

// Payment.Contracts
public sealed record PaymentProcessedEvent(
    Guid EventId, DateTime OccurredOnUtc, Guid OrderId) : IIntegrationEvent;

public sealed record PaymentFailedEvent(
    Guid EventId, DateTime OccurredOnUtc,
    Guid OrderId, string Reason) : IIntegrationEvent;

// Shipping.Contracts
public sealed record ShipmentScheduledEvent(
    Guid EventId, DateTime OccurredOnUtc, Guid OrderId) : IIntegrationEvent;
</code></pre>
<p>The orchestrator reacts to each of them:</p>
<pre><code class="language-csharp">public class OrderSagaOrchestrator
{
    private readonly IOrderingModule _ordering;
    private readonly IInventoryModule _inventory;
    private readonly IPaymentModule _payment;
    private readonly IShippingModule _shipping;
    private readonly ISagaRepository _sagaRepository;

    public OrderSagaOrchestrator(
        IOrderingModule ordering,
        IInventoryModule inventory,
        IPaymentModule payment,
        IShippingModule shipping,
        ISagaRepository sagaRepository)
    {
        _ordering = ordering;
        _inventory = inventory;
        _payment = payment;
        _shipping = shipping;
        _sagaRepository = sagaRepository;
    }

    public async Task HandleAsync(OrderPlacedIntegrationEvent @event)
    {
        var saga = OrderSaga.Start(@event.OrderId, @event.TotalAmount);
        await _sagaRepository.SaveAsync(saga);

        var lines = await _ordering.GetOrderLinesAsync(@event.OrderId);
        await _inventory.ReserveStockAsync(@event.OrderId, lines);
    }

    public async Task HandleAsync(StockReservedEvent @event)
    {
        var saga = await _sagaRepository.GetByOrderIdAsync(@event.OrderId);
        saga.TransitionTo(OrderSagaState.InventoryReserved);
        await _sagaRepository.SaveAsync(saga);

        await _payment.ProcessPaymentAsync(saga.OrderId, saga.TotalAmount);
    }

    public async Task HandleAsync(PaymentProcessedEvent @event)
    {
        var saga = await _sagaRepository.GetByOrderIdAsync(@event.OrderId);
        saga.TransitionTo(OrderSagaState.PaymentProcessed);
        await _sagaRepository.SaveAsync(saga);

        await _shipping.ScheduleShipmentAsync(saga.OrderId);
    }

    public async Task HandleAsync(ShipmentScheduledEvent @event)
    {
        var saga = await _sagaRepository.GetByOrderIdAsync(@event.OrderId);
        saga.TransitionTo(OrderSagaState.ShipmentScheduled);
        saga.Complete();

        await _sagaRepository.SaveAsync(saga);
    }
}
</code></pre>
<p>Two details in this class carry more weight than they look.</p>
<p>The saga keeps <code>TotalAmount</code> in its <strong>own state</strong>, captured when the process started.
The Inventory module has no idea what the order costs, so <code>StockReservedEvent</code> can't be the source of that number; without it, the payment step has nothing correct to charge.</p>
<p>And each handler persists the state transition <strong>before</strong> issuing the next command.
Crash between the two and you get a saga parked in a known state that a timeout can pick up later, instead of a payment charge the saga doesn't remember requesting.</p>
<p>Each module exposes a public API - an interface - that the orchestrator calls. The modules don't know about the saga. They simply execute commands and publish events.</p>
<pre><code class="language-csharp">public interface IOrderingModule
{
    Task&lt;IReadOnlyList&lt;OrderLine&gt;&gt; GetOrderLinesAsync(Guid orderId);
    Task CancelOrderAsync(Guid orderId, string reason);
}

public interface IInventoryModule
{
    Task ReserveStockAsync(Guid orderId, IReadOnlyList&lt;OrderLine&gt; lines);
    Task ReleaseStockAsync(Guid orderId);
}

public interface IPaymentModule
{
    Task ProcessPaymentAsync(Guid orderId, decimal amount);
}

public interface IShippingModule
{
    Task ScheduleShipmentAsync(Guid orderId);
}
</code></pre>
<h2>Implementing Compensating Actions</h2>
<p>When a step fails, you need to undo previous steps. These are compensating actions - the reverse of the original operation.</p>
<pre><code class="language-csharp">public async Task HandleAsync(PaymentFailedEvent @event)
{
    var saga = await _sagaRepository.GetByOrderIdAsync(@event.OrderId);

    // Compensate the inventory reservation
    await _inventory.ReleaseStockAsync(@event.OrderId);
    saga.TransitionTo(OrderSagaState.InventoryCompensated);

    saga.Fail(@event.Reason);
    await _sagaRepository.SaveAsync(saga);

    await _ordering.CancelOrderAsync(@event.OrderId, @event.Reason);
}

public async Task HandleAsync(StockReservationFailedEvent @event)
{
    var saga = await _sagaRepository.GetByOrderIdAsync(@event.OrderId);

    saga.Fail(@event.Reason);
    await _sagaRepository.SaveAsync(saga);

    await _ordering.CancelOrderAsync(@event.OrderId, @event.Reason);
}
</code></pre>
<p>Compensation must be idempotent. If the compensating action fails and gets retried, it should produce the same result. Use the <a href="https://milanjovanovic.tech/blog/outbox-pattern-dual-write-problem"><strong>outbox pattern</strong></a> to guarantee event delivery even during failures.</p>
<p>Note that compensation is not a rollback.
The payment happened; the refund is a new business operation with its own audit trail.
Some actions can't be perfectly compensated (you can't unsend an email), so design the step order to put hard-to-compensate actions last.</p>
<h2>Wiring Events to the Orchestrator</h2>
<p>Use your module communication infrastructure to route <a href="https://milanjovanovic.tech/blog/event-driven-communication-modules"><strong>integration events</strong></a> to the orchestrator.
I'm using the <code>IIntegrationEventHandler&lt;T&gt;</code> abstraction from the <a href="https://milanjovanovic.tech/blog/shared-kernel-pattern-modular-monolith"><strong>shared kernel</strong></a>:</p>
<pre><code class="language-csharp">public interface IIntegrationEventHandler&lt;in TEvent&gt;
    where TEvent : IIntegrationEvent
{
    Task HandleAsync(TEvent @event, CancellationToken ct = default);
}
</code></pre>
<p>A small router implements it for every saga event and delegates to the orchestrator:</p>
<pre><code class="language-csharp">public sealed class OrderSagaEventRouter :
    IIntegrationEventHandler&lt;OrderPlacedIntegrationEvent&gt;,
    IIntegrationEventHandler&lt;StockReservedEvent&gt;,
    IIntegrationEventHandler&lt;StockReservationFailedEvent&gt;,
    IIntegrationEventHandler&lt;PaymentProcessedEvent&gt;,
    IIntegrationEventHandler&lt;PaymentFailedEvent&gt;,
    IIntegrationEventHandler&lt;ShipmentScheduledEvent&gt;
{
    private readonly OrderSagaOrchestrator _orchestrator;

    public OrderSagaEventRouter(OrderSagaOrchestrator orchestrator)
    {
        _orchestrator = orchestrator;
    }

    public Task HandleAsync(
        OrderPlacedIntegrationEvent @event, CancellationToken ct = default) =&gt;
        _orchestrator.HandleAsync(@event);

    public Task HandleAsync(
        StockReservedEvent @event, CancellationToken ct = default) =&gt;
        _orchestrator.HandleAsync(@event);

    public Task HandleAsync(
        StockReservationFailedEvent @event, CancellationToken ct = default) =&gt;
        _orchestrator.HandleAsync(@event);

    public Task HandleAsync(
        PaymentProcessedEvent @event, CancellationToken ct = default) =&gt;
        _orchestrator.HandleAsync(@event);

    public Task HandleAsync(
        PaymentFailedEvent @event, CancellationToken ct = default) =&gt;
        _orchestrator.HandleAsync(@event);

    public Task HandleAsync(
        ShipmentScheduledEvent @event, CancellationToken ct = default) =&gt;
        _orchestrator.HandleAsync(@event);
}
</code></pre>
<p>Register the router once per event type it handles:</p>
<pre><code class="language-csharp">public static class OrderSagaModule
{
    public static IServiceCollection AddOrderSaga(
        this IServiceCollection services)
    {
        services.AddScoped&lt;OrderSagaOrchestrator&gt;();
        services.AddScoped&lt;ISagaRepository, SagaRepository&gt;();

        services.AddScoped&lt;
            IIntegrationEventHandler&lt;OrderPlacedIntegrationEvent&gt;,
            OrderSagaEventRouter&gt;();
        services.AddScoped&lt;
            IIntegrationEventHandler&lt;StockReservedEvent&gt;,
            OrderSagaEventRouter&gt;();
        services.AddScoped&lt;
            IIntegrationEventHandler&lt;StockReservationFailedEvent&gt;,
            OrderSagaEventRouter&gt;();
        services.AddScoped&lt;
            IIntegrationEventHandler&lt;PaymentProcessedEvent&gt;,
            OrderSagaEventRouter&gt;();
        services.AddScoped&lt;
            IIntegrationEventHandler&lt;PaymentFailedEvent&gt;,
            OrderSagaEventRouter&gt;();
        services.AddScoped&lt;
            IIntegrationEventHandler&lt;ShipmentScheduledEvent&gt;,
            OrderSagaEventRouter&gt;();

        return services;
    }
}
</code></pre>
<p>Miss a registration and that event is silently ignored, which is exactly how sagas get stuck halfway.
An architecture test that asserts every saga event has a registered handler is cheap insurance.</p>
<h2>Persisting Saga State</h2>
<p>The saga state must be persisted so it survives application restarts. A simple table works:</p>
<pre><code class="language-csharp">public class SagaDbContext(DbContextOptions&lt;SagaDbContext&gt; options)
    : DbContext(options)
{
    public DbSet&lt;OrderSaga&gt; OrderSagas =&gt; Set&lt;OrderSaga&gt;();
}

public interface ISagaRepository
{
    Task&lt;OrderSaga&gt; GetByOrderIdAsync(Guid orderId);
    Task SaveAsync(OrderSaga saga);
}

public class SagaRepository : ISagaRepository
{
    private readonly SagaDbContext _dbContext;

    public SagaRepository(SagaDbContext dbContext)
    {
        _dbContext = dbContext;
    }

    public async Task&lt;OrderSaga&gt; GetByOrderIdAsync(Guid orderId)
    {
        return await _dbContext.OrderSagas
            .FirstOrDefaultAsync(s =&gt; s.OrderId == orderId)
            ?? throw new InvalidOperationException(
                $&quot;Saga not found for order {orderId}.&quot;);
    }

    public async Task SaveAsync(OrderSaga saga)
    {
        var entry = _dbContext.Entry(saga);
        if (entry.State == EntityState.Detached)
        {
            _dbContext.OrderSagas.Add(saga);
        }

        await _dbContext.SaveChangesAsync();
    }
}
</code></pre>
<p>One production concern the simple repository hides: <strong>concurrency</strong>.
If two events for the same saga arrive close together, both handlers load the same row and the last write wins.
Add a concurrency token (<code>xmin</code> in PostgreSQL, <code>rowversion</code> in SQL Server) to the saga entity and retry on <code>DbUpdateConcurrencyException</code>.</p>
<h2>When to Use Sagas in a Modular Monolith</h2>
<p>Not every cross-module operation needs a saga. Use sagas when:</p>
<ul>
<li>The process spans three or more modules</li>
<li>Steps can fail independently and need compensation</li>
<li>You need visibility into the process state for debugging or monitoring</li>
<li>The process is long-running (seconds to days)</li>
</ul>
<p>For simple two-module interactions, direct <a href="https://milanjovanovic.tech/blog/modular-monolith-communication-patterns"><strong>module communication</strong></a> with error handling is often sufficient.</p>
<p>If you'd rather not hand-roll the state machine, <a href="https://milanjovanovic.tech/blog/implementing-the-saga-pattern-with-masstransit"><strong>MassTransit's saga support</strong></a> gives you persistence, concurrency handling, and timeouts out of the box, and it works with an in-memory transport inside a monolith.
For sagas across separate services, see the <a href="https://milanjovanovic.tech/blog/saga-pattern-dotnet"><strong>saga pattern in .NET</strong></a> guide.</p>
<h2>Takeaway</h2>
<ol>
<li>Sagas coordinate long-running business processes that span multiple modules without distributed transactions.</li>
<li>Orchestration keeps the process flow explicit in a single class, making it easier to understand and debug.</li>
<li>Each saga step is a local transaction within one module, maintaining <a href="https://milanjovanovic.tech/blog/schema-per-module-vs-database-per-module"><strong>data isolation</strong></a>.</li>
<li>Compensating actions undo previous steps when a later step fails - they must be idempotent.</li>
<li>Persist saga state so the process survives application restarts and can be monitored.</li>
<li>Use the <a href="https://milanjovanovic.tech/blog/outbox-pattern-dual-write-problem"><strong>outbox pattern</strong></a> alongside sagas for reliable event delivery.</li>
</ol>
<p>Thanks for reading, and stay awesome!</p>
<hr>
]]></content:encoded>
            <author>milan@milanjovanovic.tech (Milan Jovanović)</author>
        </item>
        <item>
            <title><![CDATA[Why the Outbox Pattern Solves the Dual-Write Problem]]></title>
            <link>https://milanjovanovic.tech/blog/outbox-pattern-dual-write-problem</link>
            <guid isPermaLink="false">https://milanjovanovic.tech/blog/outbox-pattern-dual-write-problem</guid>
            <pubDate>Thu, 13 Aug 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[The dual-write problem silently corrupts distributed systems. The Outbox pattern eliminates it by turning two unreliable writes into one atomic operation.]]></description>
            <content:encoded><![CDATA[<p>Every system that writes to a database and publishes to a message broker has the same crack running through it.
Most teams discover it in production, as a ghost order or a shipment that never happened.
This is the dual-write problem, and the Outbox pattern closes it with nothing more exotic than a database transaction.</p>
<h2>The Dual-Write Problem</h2>
<p>Picture this. A customer places an order. Your service saves the order to the database, then publishes an <code>OrderPlaced</code> event so the shipping module can start fulfillment.</p>
<p>Simple enough. Until it isn't.</p>
<p>The database write succeeds. The message broker call fails. Now you have an order sitting in the database, but the shipping module never hears about it. The customer waits. Nobody ships anything. You have a <strong>ghost order</strong>.</p>
<p>This is the dual-write problem. Any time your application writes to two separate systems - a database and a message broker - without a shared transaction, you have a consistency gap. And there are three ways it can bite you:</p>
<ol>
<li><strong>DB succeeds, broker fails.</strong> Data is persisted, but downstream systems never get the event. Orders go unfulfilled. Notifications never send. State drifts silently.</li>
<li><strong>Broker succeeds, DB fails.</strong> Other services react to an event for data that was never saved. The shipping module tries to fulfill an order that doesn't exist.</li>
<li><strong>Distributed transaction bottleneck.</strong> You wrap both writes in a two-phase commit. It works - until the broker goes slow, and suddenly every write in your system is blocked.</li>
</ol>
<p>This is not a theoretical concern. It shows up in production under load, during network blips, and especially during deployments. The insidious part is that it doesn't fail loudly. It fails silently, producing inconsistent state that you only notice hours later when a customer complains.</p>
<h2>Why Other Solutions Fall Short</h2>
<p>The first instinct is usually to add a retry.</p>
<p>If publishing to the broker fails, just try again. But retries don't solve the fundamental problem. Between the DB commit and the successful publish, your process might crash. The message is lost. You'd need to scan the database for &quot;unpublished&quot; records - and now you're reinventing the Outbox pattern anyway.</p>
<p>The second instinct is distributed transactions. Two-phase commit (2PC) can coordinate the database and the broker so both commit or both roll back. In theory, it sounds perfect. In practice, it has serious drawbacks:</p>
<ul>
<li>Most message brokers don't support 2PC at all (RabbitMQ, Kafka).</li>
<li>It adds significant latency to every write.</li>
<li>A coordinator failure can leave participants in a blocked state.</li>
<li>It creates tight coupling between your database and your messaging infrastructure.</li>
</ul>
<p>There's a third approach you'll sometimes see: publish first, save second. The reasoning goes, &quot;if the DB save fails, at least we can compensate.&quot; But this is strictly worse. You've now published an event for data that doesn't exist. Every downstream consumer processes garbage. Compensation logic adds enormous complexity and rarely covers every edge case.</p>
<p>None of these approaches solve the core issue. You're trying to make two independent systems behave atomically, and that's fighting physics.</p>
<h2>How the Outbox Pattern Works</h2>
<p>The key insight behind the Outbox pattern is deceptively simple: <strong>stop writing to two systems</strong>.</p>
<p>Instead of saving your business data to the database and then publishing an event to the broker, you save both the business data and the event to the <strong>same database</strong>, in the <strong>same transaction</strong>.</p>
<p>That's it. That's the whole trick.</p>
<p>You add an outbox table to your database. When you save an order, you also insert a row into the outbox table describing the event you want to publish. Both writes happen in a single database transaction.</p>
<p>A separate background process - the outbox processor - polls that table, picks up unprocessed messages, publishes them to the message broker, and marks them as processed.</p>
<p>The flow looks like this:</p>
<img src="https://milanjovanovic.tech/blogs/articles/outbox-pattern-dual-write-problem/outbox-flow.png" alt="Sequence diagram showing the application inserting the order and outbox message in one committed transaction, then a separate outbox processor later reading unprocessed messages, publishing them to the message broker, and marking them processed">
<p>The order insert and the outbox insert commit together. The database guarantees that. Either both rows are committed or neither is. The dual-write problem vanishes because there is no dual write anymore - there's only one write target.</p>
<p>The publishing step is decoupled and asynchronous. It introduces a small delay, but it introduces something far more valuable: <strong>reliability</strong>.</p>
<h2>The Guarantee</h2>
<p>Why does this actually work? Because a single database transaction is atomic by definition. Your relational database has decades of battle-tested ACID guarantees. By placing the outbox message inside the same transaction as the business data, you're leveraging guarantees that already exist rather than trying to invent new ones across system boundaries.</p>
<p>If the transaction commits, both the order and the outbox message are persisted. The processor will eventually pick it up and publish. If the transaction rolls back, neither exists. No ghost orders. No phantom events.</p>
<p>There is a trade-off, though.</p>
<p>The outbox processor might publish a message and then crash before marking it as processed. On the next run, it publishes the same message again. This means the Outbox pattern provides <strong>at-least-once delivery</strong>, not exactly-once. Your consumers must be idempotent - they need to handle receiving the same event twice without producing duplicate side effects.</p>
<p>This is a well-understood trade-off, and idempotency is far easier to implement than distributed transaction coordination.
The <a href="https://milanjovanovic.tech/blog/idempotent-consumer-handling-duplicate-messages"><strong>idempotent consumer pattern</strong></a> shows how to handle duplicate messages cleanly.</p>
<h2>A Minimal Example</h2>
<p>In .NET with EF Core, the Outbox pattern requires two things: a place to store outbox messages and a mechanism to capture <a href="https://milanjovanovic.tech/blog/domain-events-vs-integration-events"><strong>domain events</strong></a> before they leave the transaction.</p>
<p>The outbox entity is straightforward:</p>
<pre><code class="language-csharp">public sealed class OutboxMessage
{
    public Guid Id { get; set; }
    public string Type { get; set; } = string.Empty;
    public string Content { get; set; } = string.Empty;
    public DateTime OccurredOnUtc { get; set; }
    public DateTime? ProcessedOnUtc { get; set; }
}
</code></pre>
<p>An <a href="https://milanjovanovic.tech/blog/how-to-use-ef-core-interceptors"><strong>EF Core interceptor</strong></a> captures domain events and converts them into outbox rows before the transaction commits.
<code>AggregateRoot</code> here is the base class whose <code>DomainEvents</code> collection entities raise events into; the <a href="https://milanjovanovic.tech/blog/shared-kernel-pattern-modular-monolith"><strong>shared kernel</strong></a> article shows the full base type.</p>
<pre><code class="language-csharp">public sealed class InsertOutboxMessagesInterceptor : SaveChangesInterceptor
{
    public override ValueTask&lt;InterceptionResult&lt;int&gt;&gt; SavingChangesAsync(
        DbContextEventData eventData,
        InterceptionResult&lt;int&gt; result,
        CancellationToken cancellationToken = default)
    {
        if (eventData.Context is not null)
        {
            InsertOutboxMessages(eventData.Context);
        }

        return base.SavingChangesAsync(eventData, result, cancellationToken);
    }

    private static void InsertOutboxMessages(DbContext context)
    {
        var outboxMessages = context.ChangeTracker
            .Entries&lt;AggregateRoot&gt;()
            .SelectMany(entry =&gt;
            {
                var events = entry.Entity.DomainEvents.ToList();
                entry.Entity.ClearDomainEvents();
                return events;
            })
            .Select(domainEvent =&gt; new OutboxMessage
            {
                Id = Guid.NewGuid(),
                Type = domainEvent.GetType().FullName!,
                Content = JsonSerializer.Serialize(domainEvent, domainEvent.GetType()),
                OccurredOnUtc = DateTime.UtcNow
            })
            .ToList();

        context.Set&lt;OutboxMessage&gt;().AddRange(outboxMessages);
    }
}
</code></pre>
<p>When <code>SaveChangesAsync</code> is called, this interceptor collects every domain event from modified aggregates, serializes them into JSON, and adds them to the same <code>DbContext</code>. They all commit together. No second system involved.</p>
<p>One detail worth calling out: the <code>Type</code> column stores the <strong>full</strong> type name.
The processor has to resolve the CLR type to deserialize the JSON later, and a bare class name won't round-trip across assemblies.</p>
<p>Wire the interceptor into the module's <code>DbContext</code> registration:</p>
<pre><code class="language-csharp">services.AddSingleton&lt;InsertOutboxMessagesInterceptor&gt;();

services.AddDbContext&lt;OrderingDbContext&gt;((sp, options) =&gt;
    options
        .UseNpgsql(config.GetConnectionString(&quot;Database&quot;))
        .AddInterceptors(
            sp.GetRequiredService&lt;InsertOutboxMessagesInterceptor&gt;()));
</code></pre>
<p>The background processor that publishes these messages is a separate concern. The critical part - the part that solves the dual-write problem - is entirely in the code above.</p>
<p>For the full end-to-end implementation, including the background worker, see <a href="https://milanjovanovic.tech/blog/implementing-the-outbox-pattern"><strong>implementing the Outbox pattern</strong></a>.
And if you ever need serious throughput, I've written about <a href="https://milanjovanovic.tech/blog/scaling-the-outbox-pattern"><strong>scaling the Outbox to 2 billion messages per day</strong></a>.</p>
<h2>When You Need It (and When You Don't)</h2>
<p>The Outbox pattern is the right choice when losing an event has business consequences.</p>
<p><strong>Reach for it when:</strong></p>
<ul>
<li>You need reliable event publishing after database writes</li>
<li>Modules in a <a href="https://milanjovanovic.tech/blog/modular-monolith-architecture-dotnet"><strong>modular monolith</strong></a> need to <a href="https://milanjovanovic.tech/blog/modular-monolith-communication-patterns"><strong>communicate through events</strong></a></li>
<li>You're publishing events to external brokers in a microservices architecture</li>
<li>Consistency between your data and your events is non-negotiable</li>
</ul>
<p><strong>Skip it when:</strong></p>
<ul>
<li>The events are purely informational - analytics pings, debug logs, metrics. If losing a few is acceptable, the added complexity isn't worth it.</li>
<li>Fire-and-forget notifications where the occasional miss is tolerable.</li>
<li>Your database supports Change Data Capture (CDC) and you'd rather capture changes at the log level. CDC solves a similar problem from a different angle and avoids the outbox table entirely.</li>
</ul>
<p>The pattern adds a table, a background processor, and the idempotency requirement on consumers. That's real complexity. But for any system where &quot;the event must go out if the data was saved,&quot; there's no simpler reliable solution.</p>
<h2>One Write Instead of Two</h2>
<p>The dual-write problem is one of those issues that's easy to overlook and painful to debug after the fact. It doesn't throw exceptions. It produces subtle data inconsistencies that erode trust in your system over time.</p>
<p>The Outbox pattern solves it by reducing two writes to one. Your database transaction becomes the single source of truth for both business data and events. Everything else - the publishing, the delivery, the processing - flows from that one atomic commit.</p>
<p>It's a small architectural decision with outsized impact on system reliability.</p>
<p>Thanks for reading, and stay awesome!</p>
<hr>
]]></content:encoded>
            <author>milan@milanjovanovic.tech (Milan Jovanović)</author>
        </item>
        <item>
            <title><![CDATA[Defining Module Boundaries With Bounded Contexts]]></title>
            <link>https://milanjovanovic.tech/blog/module-boundaries-bounded-contexts</link>
            <guid isPermaLink="false">https://milanjovanovic.tech/blog/module-boundaries-bounded-contexts</guid>
            <pubDate>Thu, 13 Aug 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Draw module boundaries wrong and you fight your own architecture on every feature. Bounded contexts give you a systematic way to draw them: map business…]]></description>
            <content:encoded><![CDATA[<p>The first question every team building a modular monolith asks is: what are the modules?
Not the tech stack, not the folder structure, the actual boundaries.
Bounded contexts from Domain-Driven Design are the most reliable way to answer it, and they come with concrete rules you can apply to your own domain.
Here is the full process, from context maps to the change test.</p>
<h2>Why Boundaries Matter</h2>
<p>Draw module boundaries wrong, and you'll spend your time fighting the architecture instead of building features. Modules that are too fine-grained create an explosion of inter-module communication. Modules that are too coarse become monoliths within a monolith.</p>
<p>A <a href="https://milanjovanovic.tech/blog/bounded-context-ddd-explained"><strong>bounded context</strong></a> from <strong>Domain-Driven Design</strong> defines a clear boundary where a particular domain model applies. It's the best tool we have for deciding what goes in each module of a <a href="https://milanjovanovic.tech/blog/modular-monolith-architecture-dotnet"><strong>Modular Monolith</strong></a>.</p>
<h2>Context Maps</h2>
<p>Start by mapping the subdomains of your business:</p>
<img src="https://milanjovanovic.tech/blogs/articles/module-boundaries-bounded-contexts/context-map.png" alt="Context map of four bounded contexts - Ordering, Catalog, Shipping, and Inventory - with Ordering depending on Catalog and Inventory, and Shipping depending on Inventory">
<p>Each box is a bounded context, and each becomes a module.</p>
<h2>Rules for Drawing Boundaries</h2>
<h3>Rule 1: Each Module Owns Its Language</h3>
<p>In the Ordering module, &quot;Product&quot; means an item in the order with a quantity and price. In the Catalog module, &quot;Product&quot; means an item with descriptions, images, and categories. Same word, different meaning.</p>
<pre><code class="language-csharp">// Ordering Module
public class OrderProduct
{
    public Guid ProductId { get; set; }
    public string Name { get; set; }
    public int Quantity { get; set; }
    public decimal Price { get; set; }
}

// Catalog Module
public class CatalogProduct
{
    public Guid Id { get; set; }
    public string Name { get; set; }
    public string Description { get; set; }
    public List&lt;string&gt; Images { get; set; }
    public Guid CategoryId { get; set; }
}
</code></pre>
<p>Each module has its own model of the same real-world concept. This is <strong>ubiquitous language</strong> in action.</p>
<p>Duplicating the <code>Product</code> concept feels wrong at first. It isn't. The Ordering module's <code>OrderProduct</code> is a snapshot of name and price at the time of ordering - you <em>want</em> it frozen even if the catalog changes later. What looks like duplication is actually two different concepts that happen to share a name.</p>
<h3>Rule 2: Minimize Cross-Module Communication</h3>
<p>If two concepts constantly need each other's data, they probably belong in the same module:</p>
<pre><code>Bad - too chatty between modules
  OrdersModule.PlaceOrder → InventoryModule.CheckStock
  OrdersModule.PlaceOrder → PricingModule.CalculatePrice
  OrdersModule.PlaceOrder → CustomerModule.GetCustomer
  OrdersModule.PlaceOrder → TaxModule.CalculateTax

Better - Pricing is part of Ordering
  OrdersModule.PlaceOrder → InventoryModule.CheckStock
  (Pricing, tax, and customer validation happen within OrdersModule)
</code></pre>
<p>If every order operation calls the pricing module, merge pricing into ordering.</p>
<h3>Rule 3: Each Module Owns Its Data</h3>
<p>No shared databases between modules. Each module has its own tables (or schema):</p>
<pre><code class="language-csharp">// Ordering Module
public class OrderingDbContext : DbContext
{
    public DbSet&lt;Order&gt; Orders { get; set; }
    public DbSet&lt;LineItem&gt; LineItems { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.HasDefaultSchema(&quot;ordering&quot;);
    }
}

// Catalog Module
public class CatalogDbContext : DbContext
{
    public DbSet&lt;Product&gt; Products { get; set; }
    public DbSet&lt;Category&gt; Categories { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.HasDefaultSchema(&quot;catalog&quot;);
    }
}
</code></pre>
<p>See <a href="https://milanjovanovic.tech/blog/modular-monolith-data-isolation"><strong>Modular Monolith Data Isolation</strong></a> for implementation details.</p>
<h3>Rule 4: Communicate Through Contracts</h3>
<p>Modules don't reference each other's internals. They communicate through <a href="https://milanjovanovic.tech/blog/event-driven-communication-modules"><strong>integration events</strong></a> or public APIs:</p>
<pre><code class="language-csharp">// Shared contract
public sealed record OrderPlacedIntegrationEvent(
    Guid EventId,
    DateTime OccurredOnUtc,
    Guid OrderId,
    Guid CustomerId,
    decimal TotalAmount) : IIntegrationEvent;

// Ordering Module publishes
await _eventBus.PublishAsync(
    new OrderPlacedIntegrationEvent(
        Guid.NewGuid(),
        DateTime.UtcNow,
        order.Id,
        order.CustomerId,
        order.TotalAmount));

// Shipping Module subscribes
public sealed class OrderPlacedHandler(ShippingDbContext db)
    : IIntegrationEventHandler&lt;OrderPlacedIntegrationEvent&gt;
{
    public async Task HandleAsync(
        OrderPlacedIntegrationEvent @event,
        CancellationToken cancellationToken = default)
    {
        var shipment = Shipment.CreateFor(@event.OrderId);

        db.Shipments.Add(shipment);
        await db.SaveChangesAsync(cancellationToken);
    }
}
</code></pre>
<p>The <code>IIntegrationEvent</code> and <code>IIntegrationEventHandler&lt;TEvent&gt;</code> abstractions live in the <a href="https://milanjovanovic.tech/blog/shared-kernel-pattern-modular-monolith"><strong>shared kernel</strong></a>, so every module speaks the same contract language.</p>
<h2>Common Mistakes</h2>
<h3>Mistake 1: Entity-Based Modules</h3>
<pre><code>Bad - modules based on entities
Modules/
  OrderModule/
  CustomerModule/
  ProductModule/
  PaymentModule/
  InvoiceModule/
</code></pre>
<p>This creates fine-grained modules that constantly communicate. &quot;Place an order&quot; touches 5 modules.</p>
<h3>Mistake 2: One Giant Module</h3>
<pre><code>Bad - everything in one module
Modules/
  ECommerceModule/  ← 200 entities, 150 handlers
</code></pre>
<p>If a module has more than 15-20 entities, it's probably doing too much.</p>
<h3>Mistake 3: Technical Modules</h3>
<pre><code>Bad - modules based on technical concerns
Modules/
  ApiModule/
  BusinessLogicModule/
  DatabaseModule/
  MessagingModule/
</code></pre>
<p>This is just layers with extra steps. Modules should be business-oriented.</p>
<h2>Practical Process</h2>
<h3>Step 1: List Business Capabilities</h3>
<ul>
<li>Place an order</li>
<li>Manage product catalog</li>
<li>Handle payments</li>
<li>Ship orders</li>
<li>Manage customer accounts</li>
<li>Generate invoices</li>
<li>Handle refunds</li>
</ul>
<h3>Step 2: Group by Cohesion</h3>
<p>Which capabilities change together?</p>
<pre><code>Ordering: Place order, Cancel order, Order status
Catalog: Product management, Categories, Search
Payments: Process payment, Refunds, Payment methods
Shipping: Create shipment, Track delivery, Returns
Identity: User accounts, Authentication, Roles
</code></pre>
<h3>Step 3: Validate With the &quot;Change Test&quot;</h3>
<p>Ask: &quot;If I change feature X, which module is affected?&quot;</p>
<ul>
<li>&quot;Change the order discount logic&quot; → Ordering only</li>
<li>&quot;Add a new product attribute&quot; → Catalog only</li>
<li>&quot;Change how shipping cost is calculated&quot; → Shipping only</li>
</ul>
<p>If a change touches multiple modules, your boundaries might be wrong.</p>
<h2>Boundaries Are Not Forever</h2>
<p>You will get some boundaries wrong. That's expected - you know the least about your domain at the start of the project.</p>
<p>The good news: fixing a boundary inside a monolith is a refactoring, not a migration.
Merging two chatty modules means moving files and combining two DbContexts.
Splitting an overgrown module is harder, but still a single-codebase exercise.
I walk through a real example in <a href="https://milanjovanovic.tech/blog/refactoring-overgrown-bounded-contexts-in-modular-monoliths"><strong>refactoring overgrown bounded contexts</strong></a>.</p>
<p>Compare that with microservices, where a wrong boundary is baked into network contracts, separate databases, and independent deployment pipelines.
This is the strongest argument for validating boundaries in a modular monolith before <a href="https://milanjovanovic.tech/blog/when-to-extract-module-to-microservice"><strong>extracting anything to a microservice</strong></a>.</p>
<h2>Module Structure</h2>
<pre><code>src/
  Modules/
    Ordering/
      Ordering.Application/    ← Use cases, handlers
      Ordering.Domain/         ← Entities, value objects
      Ordering.Infrastructure/ ← EF Core, external services
      Ordering.Contracts/      ← Public API, integration events
    Catalog/
      Catalog.Application/
      Catalog.Domain/
      Catalog.Infrastructure/
      Catalog.Contracts/
</code></pre>
<p>The <code>Contracts</code> project is the only one other modules can reference.</p>
<h2>Getting the Boundaries Right</h2>
<p>Drawing module boundaries with bounded contexts:</p>
<ol>
<li><strong>Map your business capabilities</strong> - not entities, not technical layers</li>
<li><strong>Each module owns its language</strong> - same word, different meaning across modules</li>
<li><strong>Minimize cross-module communication</strong> - chatty modules should merge</li>
<li><strong>Each module owns its data</strong> - separate schemas, no shared tables</li>
<li><strong>Communicate through contracts</strong> - events and public APIs only</li>
<li><strong>Validate with the change test</strong> - a change should affect one module</li>
</ol>
<p>Get boundaries right, and the rest of the Modular Monolith falls into place.</p>
<p>Thanks for reading, and stay awesome!</p>
<hr>
]]></content:encoded>
            <author>milan@milanjovanovic.tech (Milan Jovanović)</author>
        </item>
        <item>
            <title><![CDATA[Event-Driven Communication Between Modules in .NET]]></title>
            <link>https://milanjovanovic.tech/blog/event-driven-communication-modules</link>
            <guid isPermaLink="false">https://milanjovanovic.tech/blog/event-driven-communication-modules</guid>
            <pubDate>Thu, 13 Aug 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[The Ordering module publishes an event, and Shipping and Notifications react without the publisher knowing they exist.]]></description>
            <content:encoded><![CDATA[<p>The Ordering module saves an order, and the Shipping module needs to know about it.
The obvious solution (Ordering calls Shipping directly) couples the two modules forever.
The event-driven alternative inverts the relationship: Ordering announces what happened, and any module that cares reacts on its own terms.</p>
<h2>Why Event-Driven?</h2>
<p>In a <a href="https://milanjovanovic.tech/blog/modular-monolith-architecture-dotnet"><strong>Modular Monolith</strong></a>, modules must stay decoupled. Direct method calls between modules create tight coupling - exactly what you're trying to avoid.</p>
<p>Event-driven communication solves this: one module publishes an event, and other modules react independently. The publisher doesn't know (or care) who's listening.</p>
<h2>Domain Events vs Integration Events</h2>
<p>There are two types of events in a <a href="https://milanjovanovic.tech/blog/modular-monolith-communication-patterns"><strong>Modular Monolith</strong></a>:</p>
<ul>
<li><strong>Domain events</strong> stay within a module and are handled in the same transaction. Example: <code>OrderPlaced</code>, consumed by another handler inside the Ordering module.</li>
<li><strong>Integration events</strong> cross module boundaries and are processed in separate transactions. Example: <code>OrderPlacedIntegrationEvent</code>, consumed by the Shipping and Notification modules.</li>
</ul>
<p><a href="https://milanjovanovic.tech/blog/domain-events-vs-integration-events"><strong>Domain events</strong></a> are internal to a module. Integration events cross module boundaries.
A common flow: a domain event handler inside the module maps the domain event to an integration event and publishes it.
That way, module internals (entity IDs, domain types) never leak into the shared contract.</p>
<h2>Setting Up Integration Events</h2>
<p>Define a shared contract that modules can reference:</p>
<pre><code class="language-csharp">// Shared contracts assembly
public interface IIntegrationEvent : INotification
{
    Guid EventId { get; }
    DateTime OccurredOnUtc { get; }
}

public sealed record OrderPlacedIntegrationEvent(
    Guid EventId,
    DateTime OccurredOnUtc,
    Guid OrderId,
    Guid CustomerId,
    decimal TotalAmount) : IIntegrationEvent;

public sealed record PaymentCompletedIntegrationEvent(
    Guid EventId,
    DateTime OccurredOnUtc,
    Guid OrderId,
    decimal Amount) : IIntegrationEvent;
</code></pre>
<p>These live in a contracts project that both modules can reference - no dependency on internal module code.</p>
<p>One deliberate choice to call out: this <code>IIntegrationEvent</code> marker extends MediatR's <code>INotification</code>, because I'm about to use MediatR as the in-process event bus.
MediatR throws if you publish an object that doesn't implement <code>INotification</code>, so this is not optional.
The cost is a reference to the small MediatR.Contracts package from the contracts project.
The <a href="https://milanjovanovic.tech/blog/shared-kernel-pattern-modular-monolith"><strong>shared kernel</strong></a> article shows the stricter, dependency-free variant of the same contract, with its own <code>IIntegrationEventHandler&lt;T&gt;</code> abstraction instead of MediatR's handler interface.</p>
<h2>In-Process Event Bus</h2>
<p>For a Modular Monolith running in a single process, you can use MediatR notifications as an event bus.
You can also build a <a href="https://milanjovanovic.tech/blog/lightweight-in-memory-message-bus-using-dotnet-channels"><strong>lightweight message bus with .NET Channels</strong></a> if you want true fire-and-forget semantics.
The abstraction is what matters:</p>
<pre><code class="language-csharp">public interface IEventBus
{
    Task PublishAsync&lt;T&gt;(T integrationEvent, CancellationToken ct = default)
        where T : IIntegrationEvent;
}

public class InProcessEventBus : IEventBus
{
    private readonly IPublisher _publisher;

    public InProcessEventBus(IPublisher publisher)
    {
        _publisher = publisher;
    }

    public async Task PublishAsync&lt;T&gt;(
        T integrationEvent, CancellationToken ct) where T : IIntegrationEvent
    {
        await _publisher.Publish(integrationEvent, ct);
    }
}
</code></pre>
<p>Register it:</p>
<pre><code class="language-csharp">services.AddScoped&lt;IEventBus, InProcessEventBus&gt;();
</code></pre>
<h2>Publishing Events</h2>
<p>The Ordering module publishes an event after placing an order:</p>
<pre><code class="language-csharp">// Ordering module
public sealed class PlaceOrderHandler
    : IRequestHandler&lt;PlaceOrderCommand, Result&lt;Guid&gt;&gt;
{
    private readonly OrderingDbContext _db;
    private readonly IEventBus _eventBus;

    public PlaceOrderHandler(
        OrderingDbContext db, IEventBus eventBus)
    {
        _db = db;
        _eventBus = eventBus;
    }

    public async Task&lt;Result&lt;Guid&gt;&gt; Handle(
        PlaceOrderCommand command, CancellationToken ct)
    {
        var order = Order.Create(command.CustomerId, command.Items);

        _db.Orders.Add(order);
        await _db.SaveChangesAsync(ct);

        // Publish integration event
        await _eventBus.PublishAsync(
            new OrderPlacedIntegrationEvent(
                Guid.NewGuid(),
                DateTime.UtcNow,
                order.Id,
                order.CustomerId,
                order.TotalAmount),
            ct);

        return Result.Success(order.Id);
    }
}
</code></pre>
<h2>Consuming Events</h2>
<p>Other modules subscribe to events independently:</p>
<pre><code class="language-csharp">// Shipping module
public sealed class OrderPlacedHandler
    : INotificationHandler&lt;OrderPlacedIntegrationEvent&gt;
{
    private readonly ShippingDbContext _db;

    public OrderPlacedHandler(ShippingDbContext db) =&gt; _db = db;

    public async Task Handle(
        OrderPlacedIntegrationEvent notification, CancellationToken ct)
    {
        var shipment = Shipment.CreateFor(
            notification.OrderId,
            notification.CustomerId);

        _db.Shipments.Add(shipment);
        await _db.SaveChangesAsync(ct);
    }
}

// Notification module
public sealed class SendOrderConfirmationHandler
    : INotificationHandler&lt;OrderPlacedIntegrationEvent&gt;
{
    private readonly IEmailService _emailService;

    public SendOrderConfirmationHandler(IEmailService emailService) =&gt;
        _emailService = emailService;

    public async Task Handle(
        OrderPlacedIntegrationEvent notification, CancellationToken ct)
    {
        await _emailService.SendOrderConfirmationAsync(
            notification.CustomerId,
            notification.OrderId,
            ct);
    }
}
</code></pre>
<p>The Ordering module doesn't know about Shipping or Notifications. Each module independently decides how to react.</p>
<p>One gotcha you should know: MediatR's <code>Publish</code> awaits every handler <strong>synchronously, in the same request scope</strong>.
If the Shipping handler takes two seconds, the user placing the order waits those two seconds.
If the Notification handler throws, the exception propagates back to the publisher.
&quot;Event-driven&quot; here means decoupled in code, not decoupled at runtime.</p>
<p>That's exactly the problem the outbox solves.</p>
<h2>The Outbox Pattern for Reliability</h2>
<p>What if the event consumer fails? With in-process events, you risk inconsistency - the order is saved but the shipment isn't created.</p>
<p>The <a href="https://milanjovanovic.tech/blog/outbox-pattern-dual-write-problem"><strong>Outbox Pattern</strong></a> solves this.
The outbox message is a plain entity in the Ordering module's schema, with a few extra fields for retry bookkeeping that we'll use later:</p>
<pre><code class="language-csharp">public sealed class OutboxMessage
{
    public Guid Id { get; set; }
    public string Type { get; set; } = string.Empty;
    public string Content { get; set; } = string.Empty;
    public DateTime OccurredOnUtc { get; set; }
    public DateTime? ProcessedOnUtc { get; set; }
    public int RetryCount { get; set; }
    public string? Error { get; set; }
    public DateTime? FailedOnUtc { get; set; }
}
</code></pre>
<p>The handler saves the order and the outbox message in the same transaction:</p>
<pre><code class="language-csharp">public sealed class PlaceOrderHandler
    : IRequestHandler&lt;PlaceOrderCommand, Result&lt;Guid&gt;&gt;
{
    private readonly OrderingDbContext _db;

    public PlaceOrderHandler(OrderingDbContext db) =&gt; _db = db;

    public async Task&lt;Result&lt;Guid&gt;&gt; Handle(
        PlaceOrderCommand command, CancellationToken ct)
    {
        var order = Order.Create(command.CustomerId, command.Items);

        var orderPlaced = new OrderPlacedIntegrationEvent(
            Guid.NewGuid(), DateTime.UtcNow,
            order.Id, order.CustomerId, order.TotalAmount);

        // Save order AND outbox message in the same transaction
        _db.Orders.Add(order);
        _db.OutboxMessages.Add(new OutboxMessage
        {
            Id = Guid.NewGuid(),
            Type = orderPlaced.GetType().FullName!,
            Content = JsonSerializer.Serialize(orderPlaced),
            OccurredOnUtc = orderPlaced.OccurredOnUtc
        });

        await _db.SaveChangesAsync(ct);

        return Result.Success(order.Id);
    }
}
</code></pre>
<p>Note that <code>Type</code> stores the <strong>full</strong> type name.
The processor needs it to resolve the CLR type when deserializing, and a bare class name won't resolve across assemblies.</p>
<p>A background job processes outbox messages and publishes them:</p>
<pre><code class="language-csharp">public class OutboxProcessor : BackgroundService
{
    private readonly IServiceScopeFactory _scopeFactory;
    private readonly ILogger&lt;OutboxProcessor&gt; _logger;

    public OutboxProcessor(
        IServiceScopeFactory scopeFactory,
        ILogger&lt;OutboxProcessor&gt; logger)
    {
        _scopeFactory = scopeFactory;
        _logger = logger;
    }

    protected override async Task ExecuteAsync(CancellationToken ct)
    {
        while (!ct.IsCancellationRequested)
        {
            using var scope = _scopeFactory.CreateScope();
            var db = scope.ServiceProvider
                .GetRequiredService&lt;OrderingDbContext&gt;();
            var eventBus = scope.ServiceProvider
                .GetRequiredService&lt;IEventBus&gt;();

            var messages = await db.OutboxMessages
                .Where(m =&gt; m.ProcessedOnUtc == null)
                .OrderBy(m =&gt; m.OccurredOnUtc)
                .Take(20)
                .ToListAsync(ct);

            foreach (var message in messages)
            {
                var @event = DeserializeEvent(
                    message.Type, message.Content);

                await eventBus.PublishAsync(@event, ct);

                message.ProcessedOnUtc = DateTime.UtcNow;
            }

            await db.SaveChangesAsync(ct);
            await Task.Delay(TimeSpan.FromSeconds(5), ct);
        }
    }

    private static IIntegrationEvent DeserializeEvent(
        string type, string content)
    {
        var eventType =
            typeof(OrderPlacedIntegrationEvent).Assembly.GetType(type)
            ?? throw new InvalidOperationException(
                $&quot;Unknown event type: {type}&quot;);

        return (IIntegrationEvent)JsonSerializer
            .Deserialize(content, eventType)!;
    }
}
</code></pre>
<p>Register it as a hosted service:</p>
<pre><code class="language-csharp">services.AddHostedService&lt;OutboxProcessor&gt;();
</code></pre>
<p>Two details in this class do a lot of work.</p>
<p>First, the processor is a <strong>singleton</strong> (every <code>BackgroundService</code> is), while the <code>DbContext</code> and event bus are scoped.
Constructor-injecting them would throw at startup with scope validation enabled, which is why the processor creates a scope per iteration through <code>IServiceScopeFactory</code>.
This is the same rule I covered in <strong>DI lifetimes</strong>.</p>
<p>Second, <code>DeserializeEvent</code> resolves the type from the contracts assembly.
<code>Type.GetType</code> with a bare type name only searches the calling assembly and the core library, so it would return <code>null</code> here and fail at runtime.</p>
<h2>Event Ordering</h2>
<p>With a single outbox processor reading messages ordered by <code>OccurredOnUtc</code>, events are published in the order they were saved.
That's one of the underrated benefits of the in-process setup: you get ordering for free, without partitions or sequence numbers.</p>
<p>Ordering only becomes a real problem when you scale out to multiple processors or move to a message broker.
At that point you need partition keys or per-aggregate sequencing - I cover the options in <strong>message ordering in distributed systems</strong>.</p>
<h2>Error Handling</h2>
<p>When a consumer fails:</p>
<pre><code class="language-csharp">foreach (var message in messages)
{
    try
    {
        var @event = DeserializeEvent(message.Type, message.Content);
        await eventBus.PublishAsync(@event, ct);
        message.ProcessedOnUtc = DateTime.UtcNow;
    }
    catch (Exception ex)
    {
        message.RetryCount++;
        message.Error = ex.Message;

        if (message.RetryCount &gt;= 3)
        {
            message.ProcessedOnUtc = DateTime.UtcNow;
            message.FailedOnUtc = DateTime.UtcNow;
            _logger.LogError(ex,
                &quot;Outbox message {Id} failed after {Retries} retries&quot;,
                message.Id, message.RetryCount);
        }
    }
}
</code></pre>
<p>Failed messages after max retries go to a dead letter state for manual investigation.</p>
<p>There's a flip side to retries: a consumer can receive the same event twice.
If the processor publishes a message and crashes before marking it processed, the next run publishes it again.
Consumers must be <strong>idempotent</strong> - the <a href="https://milanjovanovic.tech/blog/idempotent-consumer-handling-duplicate-messages"><strong>idempotent consumer pattern</strong></a> covers how to handle duplicates safely.
This is exactly why every integration event carries an <code>EventId</code>: it's the natural deduplication key.</p>
<h2>Module Boundaries</h2>
<p>Events enforce boundaries:</p>
<img src="https://milanjovanovic.tech/blogs/articles/event-driven-communication-modules/integration-event-flow.png" alt="Flow diagram of the Orders module saving an order and outbox message in one transaction, a background outbox processor publishing OrderPlacedIntegrationEvent, and the Shipping and Notification modules each reacting independently">
<p>Modules never reference each other's internals. They communicate exclusively through events defined in shared contracts.</p>
<h2>Takeaway</h2>
<p>Event-driven communication in a Modular Monolith:</p>
<ol>
<li><strong>Integration events</strong> cross module boundaries, domain events stay internal</li>
<li><strong>IEventBus</strong> abstracts the publishing mechanism (in-process or message broker)</li>
<li><strong>Outbox Pattern</strong> guarantees events are published even if the consumer fails</li>
<li><strong>Shared contracts</strong> define events without coupling module internals</li>
<li><strong>Background processor</strong> publishes outbox messages with retry logic</li>
</ol>
<p>Start with in-process events. Add the Outbox Pattern when you need reliability. When you eventually <a href="https://milanjovanovic.tech/blog/when-to-extract-module-to-microservice"><strong>extract to microservices</strong></a>, swap the in-process bus for RabbitMQ or Azure Service Bus.</p>
<p>Thanks for reading, and stay awesome!</p>
<hr>
]]></content:encoded>
            <author>milan@milanjovanovic.tech (Milan Jovanović)</author>
        </item>
        <item>
            <title><![CDATA[How to Build a Modular Monolith in .NET Step by Step]]></title>
            <link>https://milanjovanovic.tech/blog/build-modular-monolith-dotnet-step-by-step</link>
            <guid isPermaLink="false">https://milanjovanovic.tech/blog/build-modular-monolith-dotnet-step-by-step</guid>
            <pubDate>Thu, 13 Aug 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Build a working Modular Monolith skeleton in .NET: three modules, each with its own schema and DbContext, cross-module calls that go through Contracts projects…]]></description>
            <content:encoded><![CDATA[<p>Every article about Modular Monoliths tells you to build &quot;well-defined modules with clear boundaries&quot;.
Almost none of them show you the csproj files.
This one does: three modules, contracts-only references, schema-per-module data isolation, and architecture tests that keep the whole thing honest.</p>
<h2>What We're Building</h2>
<p>A Modular Monolith with three modules: <strong>Catalog</strong>, <strong>Ordering</strong>, and <strong>Shipping</strong>. Each module has its own domain, data store, and public API. They communicate through integration events and module interfaces.</p>
<img src="https://milanjovanovic.tech/blogs/articles/build-modular-monolith-dotnet-step-by-step/module-structure.png" alt="Diagram of a modular monolith: a single API host referencing the Catalog, Ordering, and Shipping modules, with Ordering depending only on Catalog.Contracts and all modules sharing an in-process event bus">
<p>If you're not sure this architecture is the right fit, start with <a href="https://milanjovanovic.tech/blog/what-is-a-modular-monolith"><strong>What Is a Modular Monolith?</strong></a> and come back. This article is the hands-on part: by the end, you'll have a working skeleton you can grow into a production system.</p>
<h2>Step 1: Solution Structure</h2>
<pre><code>src/
  Api/                          ← Host application (single deployment)
  Common/
    Common.Application/         ← Shared abstractions
    Common.Infrastructure/      ← Shared infrastructure
  Modules/
    Catalog/
      Catalog.Application/
      Catalog.Domain/
      Catalog.Infrastructure/
      Catalog.Contracts/
    Ordering/
      Ordering.Application/
      Ordering.Domain/
      Ordering.Infrastructure/
      Ordering.Contracts/
    Shipping/
      Shipping.Application/
      Shipping.Domain/
      Shipping.Infrastructure/
      Shipping.Contracts/
</code></pre>
<p>Create the solution:</p>
<pre><code class="language-bash">dotnet new sln -n ModularMonolith

# Host
dotnet new webapi -n Api -o src/Api

# Common
dotnet new classlib -n Common.Application -o src/Common/Common.Application
dotnet new classlib -n Common.Infrastructure -o src/Common/Common.Infrastructure

# Catalog module
dotnet new classlib -n Catalog.Application -o src/Modules/Catalog/Catalog.Application
dotnet new classlib -n Catalog.Domain -o src/Modules/Catalog/Catalog.Domain
dotnet new classlib -n Catalog.Infrastructure -o src/Modules/Catalog/Catalog.Infrastructure
dotnet new classlib -n Catalog.Contracts -o src/Modules/Catalog/Catalog.Contracts
</code></pre>
<p>Repeat for Ordering and Shipping.</p>
<h2>Step 2: Project References</h2>
<p>Each module follows <a href="https://milanjovanovic.tech/blog/clean-architecture-vs-onion-vs-hexagonal"><strong>Clean Architecture</strong></a> internally:</p>
<pre><code class="language-xml">&lt;!-- Catalog.Application.csproj --&gt;
&lt;ItemGroup&gt;
    &lt;ProjectReference Include=&quot;..\Catalog.Domain\Catalog.Domain.csproj&quot; /&gt;
    &lt;ProjectReference Include=&quot;..\..\..\Common\Common.Application\Common.Application.csproj&quot; /&gt;
&lt;/ItemGroup&gt;

&lt;!-- Catalog.Infrastructure.csproj --&gt;
&lt;ItemGroup&gt;
    &lt;ProjectReference Include=&quot;..\Catalog.Application\Catalog.Application.csproj&quot; /&gt;
&lt;/ItemGroup&gt;
</code></pre>
<p>Cross-module references go through <strong>Contracts only</strong>:</p>
<pre><code class="language-xml">&lt;!-- Ordering.Application.csproj --&gt;
&lt;ItemGroup&gt;
    &lt;ProjectReference Include=&quot;..\Ordering.Domain\Ordering.Domain.csproj&quot; /&gt;
    &lt;ProjectReference Include=&quot;..\..\Catalog\Catalog.Contracts\Catalog.Contracts.csproj&quot; /&gt;
&lt;/ItemGroup&gt;
</code></pre>
<p>The host references all Infrastructure projects:</p>
<pre><code class="language-xml">&lt;!-- Api.csproj --&gt;
&lt;ItemGroup&gt;
    &lt;ProjectReference Include=&quot;..\Modules\Catalog\Catalog.Infrastructure\Catalog.Infrastructure.csproj&quot; /&gt;
    &lt;ProjectReference Include=&quot;..\Modules\Ordering\Ordering.Infrastructure\Ordering.Infrastructure.csproj&quot; /&gt;
    &lt;ProjectReference Include=&quot;..\Modules\Shipping\Shipping.Infrastructure\Shipping.Infrastructure.csproj&quot; /&gt;
&lt;/ItemGroup&gt;
</code></pre>
<p>The Contracts project contains only what a module is willing to expose: the module's public interface, integration event records, and simple DTOs.
No entities, no DbContext, no handlers.
If it compiles without referencing anything else in the module, it belongs in Contracts.</p>
<h2>Step 3: Database per Module</h2>
<p>Each module gets its own schema using <a href="https://milanjovanovic.tech/blog/modular-monolith-data-isolation"><strong>separate DbContexts</strong></a>:</p>
<pre><code class="language-csharp">// Catalog.Infrastructure/CatalogDbContext.cs
public class CatalogDbContext(DbContextOptions&lt;CatalogDbContext&gt; options)
    : DbContext(options)
{
    public DbSet&lt;Product&gt; Products { get; set; }
    public DbSet&lt;Category&gt; Categories { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.HasDefaultSchema(&quot;catalog&quot;);
        modelBuilder.ApplyConfigurationsFromAssembly(
            typeof(CatalogDbContext).Assembly);
    }
}
</code></pre>
<pre><code class="language-csharp">// Ordering.Infrastructure/OrderingDbContext.cs
public class OrderingDbContext(DbContextOptions&lt;OrderingDbContext&gt; options)
    : DbContext(options)
{
    public DbSet&lt;Order&gt; Orders { get; set; }
    public DbSet&lt;LineItem&gt; LineItems { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.HasDefaultSchema(&quot;ordering&quot;);
        modelBuilder.ApplyConfigurationsFromAssembly(
            typeof(OrderingDbContext).Assembly);
    }
}
</code></pre>
<p>All schemas live in the same database. Each module only accesses its own schema.</p>
<p>Each <code>DbContext</code> also gets its own migrations history table, so you can evolve modules independently:</p>
<pre><code class="language-bash">dotnet ef migrations add InitialCreate \
  --project src/Modules/Catalog/Catalog.Infrastructure \
  --startup-project src/Api \
  --context CatalogDbContext

dotnet ef migrations add InitialCreate \
  --project src/Modules/Ordering/Ordering.Infrastructure \
  --startup-project src/Api \
  --context OrderingDbContext
</code></pre>
<p>Because the host references every Infrastructure project, you always pass <code>--context</code> to tell EF Core which module you're working with.</p>
<h2>Step 4: Module Registration</h2>
<p>Each module has a registration extension method:</p>
<pre><code class="language-csharp">// Catalog.Infrastructure/CatalogModuleRegistration.cs
public static class CatalogModuleRegistration
{
    public static IServiceCollection AddCatalogModule(
        this IServiceCollection services, IConfiguration config)
    {
        services.AddDbContext&lt;CatalogDbContext&gt;(options =&gt;
            options.UseNpgsql(
                config.GetConnectionString(&quot;Database&quot;),
                o =&gt; o.MigrationsHistoryTable(
                    &quot;__EFMigrationsHistory&quot;, &quot;catalog&quot;)));

        services.AddScoped&lt;ICatalogModule, CatalogModule&gt;();

        services.AddMediatR(cfg =&gt;
            cfg.RegisterServicesFromAssembly(
                typeof(GetProductsQuery).Assembly)); // Catalog.Application

        return services;
    }
}
</code></pre>
<p>Careful with the MediatR assembly: the handlers live in <code>Catalog.Application</code>, not in the Infrastructure project where this registration class sits.
Scanning the wrong assembly is a classic mistake, and the symptom is a runtime &quot;no handler registered&quot; error on the first request.</p>
<p>Wire everything in the host:</p>
<pre><code class="language-csharp">// Api/Program.cs
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCatalogModule(builder.Configuration);
builder.Services.AddOrderingModule(builder.Configuration);
builder.Services.AddShippingModule(builder.Configuration);

var app = builder.Build();

app.MapCatalogEndpoints();
app.MapOrderingEndpoints();
app.MapShippingEndpoints();

app.Run();
</code></pre>
<h2>Step 5: Module Endpoints</h2>
<p>Each module registers its own endpoints:</p>
<pre><code class="language-csharp">// Catalog.Infrastructure/CatalogEndpoints.cs
public static class CatalogEndpoints
{
    public static void MapCatalogEndpoints(this WebApplication app)
    {
        var group = app.MapGroup(&quot;/api/catalog&quot;)
            .WithTags(&quot;Catalog&quot;);

        group.MapGet(&quot;/products&quot;, async (
            ISender sender, CancellationToken ct) =&gt;
        {
            var result = await sender.Send(
                new GetProductsQuery(), ct);
            return Results.Ok(result);
        });

        group.MapGet(&quot;/products/{id:guid}&quot;, async (
            Guid id, ISender sender, CancellationToken ct) =&gt;
        {
            var result = await sender.Send(
                new GetProductByIdQuery(id), ct);
            return result is null
                ? Results.NotFound()
                : Results.Ok(result);
        });

        group.MapPost(&quot;/products&quot;, async (
            CreateProductRequest request,
            ISender sender,
            CancellationToken ct) =&gt;
        {
            var result = await sender.Send(
                new CreateProductCommand(
                    request.Name, request.Price), ct);
            return Results.Created(
                $&quot;/api/catalog/products/{result}&quot;, result);
        });
    }
}
</code></pre>
<p>One gotcha: this class lives in a class library, so <code>Catalog.Infrastructure.csproj</code> needs <code>&lt;FrameworkReference Include=&quot;Microsoft.AspNetCore.App&quot; /&gt;</code> to see <code>WebApplication</code> and <code>Results</code>.</p>
<h2>Step 6: Event Bus</h2>
<p>Create a simple in-process event bus for integration events:</p>
<pre><code class="language-csharp">// Common.Application/IIntegrationEvent.cs
public interface IIntegrationEvent : INotification
{
    Guid EventId { get; }
    DateTime OccurredOnUtc { get; }
}

// Common.Application/IEventBus.cs
public interface IEventBus
{
    Task PublishAsync&lt;T&gt;(T @event, CancellationToken ct = default)
        where T : IIntegrationEvent;
}

// Common.Infrastructure/InProcessEventBus.cs
public class InProcessEventBus : IEventBus
{
    private readonly IPublisher _publisher;

    public InProcessEventBus(IPublisher publisher) =&gt;
        _publisher = publisher;

    public async Task PublishAsync&lt;T&gt;(
        T @event, CancellationToken ct) where T : IIntegrationEvent
    {
        await _publisher.Publish(@event, ct);
    }
}
</code></pre>
<p>The <code>IIntegrationEvent</code> marker extends MediatR's <code>INotification</code>.
That's what lets consumer modules subscribe with plain notification handlers; MediatR refuses to publish objects that don't implement <code>INotification</code>.
If you'd rather keep your contracts free of MediatR entirely, the <a href="https://milanjovanovic.tech/blog/shared-kernel-pattern-modular-monolith"><strong>shared kernel</strong></a> article shows a dependency-free marker with its own <code>IIntegrationEventHandler&lt;T&gt;</code> abstraction.</p>
<p>Register the event bus:</p>
<pre><code class="language-csharp">services.AddScoped&lt;IEventBus, InProcessEventBus&gt;();
</code></pre>
<p>One caveat: MediatR's <code>Publish</code> runs every handler <strong>in the same process and scope, synchronously</strong>.
A slow or failing consumer affects the publisher.
That's fine for a first version, but for anything critical you'll want the <a href="https://milanjovanovic.tech/blog/outbox-pattern-dual-write-problem"><strong>Outbox pattern</strong></a> so events are persisted with the business data and published by a background worker.</p>
<h2>Step 7: Cross-Module Communication</h2>
<p>The ordering module uses the catalog module's <a href="https://milanjovanovic.tech/blog/internal-vs-public-apis-in-modular-monoliths"><strong>public interface</strong></a>.
The contract lives in <code>Catalog.Contracts</code>, so it's the only thing other modules ever see:</p>
<pre><code class="language-csharp">// Catalog.Contracts/ICatalogModule.cs
public interface ICatalogModule
{
    Task&lt;IReadOnlyList&lt;ProductResponse&gt;&gt; GetProductsAsync(
        IReadOnlyCollection&lt;Guid&gt; productIds,
        CancellationToken ct = default);
}

public sealed record ProductResponse(Guid Id, string Name, decimal Price);
</code></pre>
<p>The implementation stays internal to the Catalog module (this is the <code>CatalogModule</code> class we registered in Step 4):</p>
<pre><code class="language-csharp">// Catalog.Infrastructure/CatalogModule.cs
internal sealed class CatalogModule : ICatalogModule
{
    private readonly CatalogDbContext _db;

    public CatalogModule(CatalogDbContext db) =&gt; _db = db;

    public async Task&lt;IReadOnlyList&lt;ProductResponse&gt;&gt; GetProductsAsync(
        IReadOnlyCollection&lt;Guid&gt; productIds,
        CancellationToken ct = default)
    {
        return await _db.Products
            .Where(p =&gt; productIds.Contains(p.Id))
            .Select(p =&gt; new ProductResponse(p.Id, p.Name, p.Price))
            .ToListAsync(ct);
    }
}
</code></pre>
<p>The integration event that Ordering publishes lives in <code>Ordering.Contracts</code>:</p>
<pre><code class="language-csharp">// Ordering.Contracts/OrderPlacedIntegrationEvent.cs
public sealed record OrderPlacedIntegrationEvent(
    Guid EventId,
    DateTime OccurredOnUtc,
    Guid OrderId,
    Guid CustomerId,
    decimal TotalAmount) : IIntegrationEvent;
</code></pre>
<p><code>Ordering.Contracts</code> references <code>Common.Application</code> to see the <code>IIntegrationEvent</code> marker; that's the one shared dependency contracts projects are allowed.</p>
<p>Now the ordering module can place an order:</p>
<pre><code class="language-csharp">// Ordering.Application/PlaceOrderHandler.cs
public sealed class PlaceOrderHandler
    : IRequestHandler&lt;PlaceOrderCommand, Result&lt;Guid&gt;&gt;
{
    private readonly ICatalogModule _catalog;
    private readonly OrderingDbContext _db;
    private readonly IEventBus _eventBus;

    public PlaceOrderHandler(
        ICatalogModule catalog,
        OrderingDbContext db,
        IEventBus eventBus)
    {
        _catalog = catalog;
        _db = db;
        _eventBus = eventBus;
    }

    public async Task&lt;Result&lt;Guid&gt;&gt; Handle(
        PlaceOrderCommand command, CancellationToken ct)
    {
        // Get product info from Catalog module
        var products = await _catalog.GetProductsAsync(
            command.Items.Select(i =&gt; i.ProductId).ToList(), ct);

        if (products.Count != command.Items.Count)
        {
            return Result.Failure&lt;Guid&gt;(new Error(
                &quot;Ordering.ProductsNotFound&quot;,
                &quot;Some products were not found.&quot;));
        }

        // Create order
        var order = Order.Create(command.CustomerId);
        foreach (var item in command.Items)
        {
            var product = products.First(p =&gt; p.Id == item.ProductId);
            order.AddLineItem(item.ProductId, item.Quantity, product.Price);
        }

        _db.Orders.Add(order);
        await _db.SaveChangesAsync(ct);

        // Publish integration event
        await _eventBus.PublishAsync(
            new OrderPlacedIntegrationEvent(
                Guid.NewGuid(), DateTime.UtcNow,
                order.Id, order.CustomerId, order.TotalAmount), ct);

        return Result.Success(order.Id);
    }
}
</code></pre>
<p>One simplification to be upfront about: I'm injecting <code>OrderingDbContext</code> straight into the handler to keep the example short, but with the Step 2 project references the Application project can't actually see the Infrastructure project.
In a real module, either hide persistence behind an abstraction defined in <code>Ordering.Application</code>, or merge Application and Infrastructure into a single project per module (a perfectly valid simplification that many teams choose).</p>
<h2>Step 8: Enforce Module Boundaries</h2>
<p>Use architecture tests to prevent modules from referencing each other's internals:</p>
<pre><code class="language-csharp">[Fact]
public void OrderingModule_ShouldNotReference_CatalogInternals()
{
    var result = Types
        .InAssembly(typeof(OrderingDbContext).Assembly)
        .Should()
        .NotHaveDependencyOn(&quot;Catalog.Domain&quot;)
        .And()
        .NotHaveDependencyOn(&quot;Catalog.Application&quot;)
        .And()
        .NotHaveDependencyOn(&quot;Catalog.Infrastructure&quot;)
        .GetResult();

    result.IsSuccessful.Should().BeTrue();
}
</code></pre>
<p>See <strong>Architecture Testing</strong> for more patterns.</p>
<p>Project references already prevent most violations at compile time (a module can't call what it can't see).
The architecture tests catch the sneakier failures: someone adding a project reference &quot;just this once&quot;, or reflection-based access that the compiler can't see.</p>
<h2>Common Pitfalls</h2>
<p>A few things that bite teams on their first Modular Monolith:</p>
<ul>
<li><strong>A bloated Common project.</strong> <code>Common.Application</code> should hold abstractions (<code>IEventBus</code>, base types, the result pattern), not business logic. If two modules need the same business rule, that's a boundary problem, not a code-sharing problem.</li>
<li><strong>Shared entities.</strong> The Ordering module must not reference <code>Catalog.Domain.Product</code>. It gets a <code>ProductResponse</code> DTO through the contract, or it stores its own copy of the data it needs (product name and price at the time of ordering).</li>
<li><strong>Cross-schema queries.</strong> One <code>DbContext</code> per module means EF Core won't let you join <code>ordering.orders</code> to <code>catalog.products</code>. Someone will try to do it with raw SQL. Don't. That query is a hidden coupling that will break the module boundary silently.</li>
<li><strong>Skipping Contracts because &quot;it's all one process anyway&quot;.</strong> The Contracts project feels like ceremony until you extract a module. Then it becomes the API surface of your new service, for free.</li>
</ul>
<h2>When NOT to Use This Structure</h2>
<p>For a small CRUD application with one team and one bounded context, four projects per module is overkill.
A single project with feature folders will serve you better.</p>
<p>The Modular Monolith earns its structure when you have multiple distinct business capabilities, a team in the roughly 2-15 developer range, or a real chance you'll need to <a href="https://milanjovanovic.tech/blog/when-to-extract-module-to-microservice"><strong>extract a module later</strong></a>.</p>
<p>If you want to see the complete approach applied to a production-grade system, my <a href="https://milanjovanovic.tech/modular-monolith-architecture"><strong>Modular Monolith Architecture</strong></a> course walks through every one of these steps in depth.</p>
<h2>Takeaway</h2>
<p>Building a Modular Monolith step by step:</p>
<ol>
<li><strong>Solution structure</strong> - each module has Domain, Application, Infrastructure, and Contracts</li>
<li><strong>Contracts project</strong> - the only cross-module dependency allowed</li>
<li><strong>Database per module</strong> - separate schemas, separate DbContexts</li>
<li><strong>Module registration</strong> - each module wires its own DI and endpoints</li>
<li><strong>Event bus</strong> - integration events for decoupled cross-module communication</li>
<li><strong>Architecture tests</strong> - enforce <a href="https://milanjovanovic.tech/blog/module-boundaries-bounded-contexts"><strong>module boundaries</strong></a> at build time</li>
</ol>
<p>Start with a Modular Monolith. Extract to microservices only when you need to scale independently.</p>
<p>Thanks for reading, and stay awesome!</p>
<hr>
]]></content:encoded>
            <author>milan@milanjovanovic.tech (Milan Jovanović)</author>
        </item>
        <item>
            <title><![CDATA[When to Use Clean Architecture (And When Not To)]]></title>
            <link>https://milanjovanovic.tech/blog/when-to-use-clean-architecture</link>
            <guid isPermaLink="false">https://milanjovanovic.tech/blog/when-to-use-clean-architecture</guid>
            <pubDate>Tue, 11 Aug 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[A five-endpoint CRUD API with four projects, domain events, and a CQRS pipeline is over-engineering, not discipline.]]></description>
            <content:encoded><![CDATA[<p>Clean Architecture is the default recommendation in the .NET space, and that is exactly the problem.
Defaults get applied without asking whether the project has the problems the architecture solves.
Sometimes four projects and a dependency diagram is the right call, and sometimes it is a five-endpoint API wearing a suit of armor.
Here is a decision framework for telling the two apart.</p>
<h2>Clean Architecture Is Not Always the Answer</h2>
<p>Teams apply <a href="https://milanjovanovic.tech/blog/clean-architecture-and-the-benefits-of-structured-software-design">Clean Architecture</a> to every project, regardless of complexity. A five-endpoint CRUD API with four separate projects, domain events, a CQRS pipeline, and architecture tests.</p>
<p>That's over-engineering.</p>
<p>Clean Architecture solves specific problems. When those problems don't exist, the architecture adds cost without benefit.</p>
<h2>When Clean Architecture Shines</h2>
<h3>Complex Business Logic</h3>
<p>If your domain has real business rules - not just CRUD operations - Clean Architecture protects that logic from infrastructure concerns.</p>
<p><strong>Signals you need it:</strong></p>
<ul>
<li>Business rules that span multiple entities</li>
<li>Domain invariants that must always hold</li>
<li>Complex calculations or state machines</li>
<li>Rules that change independently from infrastructure</li>
</ul>
<pre><code class="language-csharp">// This is domain complexity worth protecting
public class LoanApplication : AggregateRoot
{
    public ApplicationStatus Status { get; private set; }
    public decimal Amount { get; private set; }

    public Result Approve(CreditScore score, DebtToIncomeRatio ratio)
    {
        if (Status != ApplicationStatus.UnderReview)
            return Result.Failure(LoanErrors.NotUnderReview);

        if (score.Value &lt; 650)
            return Result.Failure(LoanErrors.CreditScoreTooLow);

        if (ratio.Value &gt; 0.43m)
            return Result.Failure(LoanErrors.DebtRatioTooHigh);

        Status = ApplicationStatus.Approved;
        RaiseDomainEvent(new LoanApprovedDomainEvent(Id, Amount));

        return Result.Success();
    }
}
</code></pre>
<h3>Long-Lived Projects</h3>
<p>Projects expected to last years benefit from clean boundaries. Requirements change, frameworks evolve, team members rotate. Clean Architecture makes the codebase resilient to these changes.</p>
<p>If the project has a 6-month lifespan and will be replaced, the investment doesn't pay off.</p>
<h3>Multiple Teams</h3>
<p>When multiple teams work on the same codebase, clear boundaries prevent them from stepping on each other. The Domain layer is the stable core everyone agrees on.</p>
<h3>High Testability Requirements</h3>
<p>If comprehensive testing is a hard requirement (regulated industries, financial systems, healthcare), Clean Architecture makes the domain layer trivially testable - no mocks for databases or HTTP needed.</p>
<p>I've written more about <a href="https://milanjovanovic.tech/blog/why-clean-architecture-is-great-for-complex-projects"><strong>why Clean Architecture is great for complex projects</strong></a> if you recognize your project in these signals.</p>
<h2>When to Skip Clean Architecture</h2>
<h3>Simple CRUD Applications</h3>
<p>If your application is mostly reading and writing data with minimal business logic:</p>
<pre><code class="language-csharp">// This doesn't need Clean Architecture
app.MapPost(&quot;/api/todos&quot;, async (TodoRequest request, AppDbContext db) =&gt;
{
    var todo = new Todo { Title = request.Title, IsComplete = false };
    db.Todos.Add(todo);
    await db.SaveChangesAsync();
    return Results.Created($&quot;/api/todos/{todo.Id}&quot;, todo);
});
</code></pre>
<p>For CRUD, a single project with Minimal APIs and EF Core is faster to build, easier to understand, and perfectly maintainable.</p>
<h3>Prototypes and MVPs</h3>
<p>Speed matters more than architecture when you're validating an idea. Build the simplest thing that works. Refactor into Clean Architecture later if the project survives.</p>
<h3>Small Microservices</h3>
<p>A microservice with a narrow responsibility (send emails, resize images, generate PDFs) doesn't need four projects and a domain model. Keep it simple.</p>
<h3>Internal Tools</h3>
<p>Admin dashboards, migration scripts, and one-off data tools don't justify the ceremony. Ship fast, iterate, replace.</p>
<h2>The Decision Framework</h2>
<img src="https://milanjovanovic.tech/blogs/articles/when-to-use-clean-architecture/decision-framework.png" alt="A decision tree: a complex, long-lived, multi-team, high-testability project points to Clean Architecture, a CRUD, MVP, or small-service project points to keeping it simple, and mixed answers point to starting simple and migrating later">
<p>Ask these questions.</p>
<p>Questions where &quot;yes&quot; points to Clean Architecture:</p>
<ul>
<li>Does the domain have complex business rules?</li>
<li>Will the project live for 2+ years?</li>
<li>Do multiple teams contribute to the same codebase?</li>
<li>Is testability a hard requirement (regulated industry, financial, healthcare)?</li>
</ul>
<p>Questions where &quot;yes&quot; points to a simpler structure:</p>
<ul>
<li>Is it mostly CRUD?</li>
<li>Is it a prototype or MVP?</li>
<li>Is it a small, focused microservice?</li>
<li>Is it an internal tool you'd rather replace than maintain?</li>
</ul>
<p>If you answer &quot;yes&quot; to most of the first group, Clean Architecture is a good fit.
If you answer &quot;yes&quot; to most of the second group, keep it simple.
Mixed answers usually mean: start simple, and let the migration path below carry you.</p>
<h2>Alternatives to Clean Architecture</h2>
<h3>Vertical Slice Architecture</h3>
<p><a href="https://milanjovanovic.tech/blog/vertical-slice-architecture">Vertical Slice Architecture</a> organizes code by feature instead of by layer. Each feature contains everything it needs - from request to response.</p>
<p><strong>Best for:</strong> Medium-complexity applications where features are independent and don't share much domain logic.
I compare the two approaches head-to-head in <a href="https://milanjovanovic.tech/blog/vertical-slice-vs-clean-architecture"><strong>Vertical Slice Architecture vs Clean Architecture</strong></a>.</p>
<h3>Simple Layered Architecture</h3>
<p>The classic three-layer approach: Presentation → Business → Data. Less ceremony than Clean Architecture, still provides some separation.</p>
<p><strong>Best for:</strong> Applications with moderate complexity that don't need the full rigor of Clean Architecture.</p>
<h3>Single Project</h3>
<p>Everything in one project, organized by feature folders.</p>
<p><strong>Best for:</strong> Small applications, prototypes, microservices, and internal tools.</p>
<h2>The Migration Path</h2>
<p>You don't have to start with Clean Architecture. Start simple and evolve:</p>
<ol>
<li><strong>Start:</strong> Single project with feature folders</li>
<li><strong>Grow:</strong> Extract a Domain project when business logic appears</li>
<li><strong>Mature:</strong> Add Application and Infrastructure projects when needed</li>
<li><strong>Scale:</strong> Add architecture tests to enforce boundaries</li>
</ol>
<p>This is cheaper than starting with four empty projects and hoping you'll need them.</p>
<h2>Common Objections</h2>
<p><strong>&quot;But what if we need Clean Architecture later?&quot;</strong>
You'll know. When business logic starts leaking into controllers or database code infects your domain, it's time. Refactoring from a well-organized simple project to Clean Architecture is straightforward.</p>
<p><strong>&quot;Clean Architecture is industry standard.&quot;</strong>
It's one approach among many. <a href="https://milanjovanovic.tech/blog/clean-architecture-vs-onion-vs-hexagonal">Hexagonal Architecture</a>, Vertical Slices, and even well-structured monoliths are all valid. Choose based on your specific needs.</p>
<p><strong>&quot;My team expects Clean Architecture.&quot;</strong>
Make the decision based on the project, not on habits. If the team knows Clean Architecture well and the project benefits from it, great. If it's adding overhead, question it.</p>
<h2>The Bottom Line</h2>
<p>Clean Architecture is a tool, not a dogma. Use it when your domain is complex, the project is long-lived, and testability matters. Skip it when simplicity serves you better.</p>
<p>The best architecture is the one that makes your team productive and your software maintainable - whatever that looks like for your specific project.</p>
<p>Thanks for reading, and stay awesome!</p>
<hr>
]]></content:encoded>
            <author>milan@milanjovanovic.tech (Milan Jovanović)</author>
        </item>
        <item>
            <title><![CDATA[Where Do Transactions Belong in Clean Architecture?]]></title>
            <link>https://milanjovanovic.tech/blog/transactions-clean-architecture</link>
            <guid isPermaLink="false">https://milanjovanovic.tech/blog/transactions-clean-architecture</guid>
            <pubDate>Tue, 11 Aug 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[The use case defines what must succeed or fail together, so the transaction boundary belongs to the application layer.]]></description>
            <content:encoded><![CDATA[<p>&quot;Where do I put <code>BeginTransaction</code>?&quot; comes up in every Clean Architecture codebase, and the answers people improvise are all over the place.
In the controller. In the repository. In a middleware that wraps every request.</p>
<p>All three put the boundary in the wrong place, because they let a technical concern decide a business question.
What must succeed or fail together is defined by the <strong>use case</strong>.
So the transaction starts where the use case lives: the application layer.</p>
<h2>The Principle: Atomicity Is a Use Case Property</h2>
<p>Take a classic use case: transferring inventory between warehouses.
Deduct from one location, add to another, record the movement.
Partial success is corruption, so those writes are atomic <strong>because the business says so</strong>, not because EF Core has a transaction API.</p>
<p>That reasoning gives each layer its role:</p>
<img src="https://milanjovanovic.tech/blogs/articles/transactions-clean-architecture/transaction-boundary-ownership.png" alt="The application layer owns the transaction boundary, reasoning about which domain aggregates must stay consistent and delegating the transaction mechanics to the infrastructure layer">
<ul>
<li><strong>Domain</strong>: defines aggregates, and an aggregate is itself a consistency boundary. Changes within one aggregate must always be atomic. The domain implies what must be consistent, but never touches persistence.</li>
<li><strong>Application</strong>: the use case knows which aggregates it modifies together, so it owns the transaction boundary.</li>
<li><strong>Infrastructure</strong>: implements the mechanics with EF Core, and the <a href="https://milanjovanovic.tech/blog/working-with-transactions-in-ef-core"><strong>details of working with transactions</strong></a> stay here.</li>
</ul>
<p>Repositories are the notably wrong place.
A repository sees one aggregate type at a time, and it cannot know whether the current operation is standalone or part of a larger unit.
Controllers are equally wrong for the mirrored reason: they know about HTTP, not about business atomicity.</p>
<p>With the principle set, there are three ways to implement it.
I will rank them as we go.</p>
<h2>Option 1: One SaveChanges Per Use Case (the Default)</h2>
<p>The fact that makes most explicit transactions unnecessary: <strong>EF Core already wraps every <code>SaveChanges</code> call in a database transaction</strong>.
All tracked changes flushed by that one call commit or roll back together.</p>
<p>So the simplest correct pattern is: a use case mutates any number of tracked entities, and calls <code>SaveChanges</code> exactly once at the end.</p>
<pre><code class="language-csharp">public sealed record TransferInventoryCommand(
    Guid SourceLocationId,
    Guid DestinationLocationId,
    string Sku,
    int Quantity) : IRequest;

public sealed class TransferInventoryCommandHandler(
    IInventoryRepository inventoryRepository,
    IUnitOfWork unitOfWork)
    : IRequestHandler&lt;TransferInventoryCommand&gt;
{
    public async Task Handle(TransferInventoryCommand command, CancellationToken ct)
    {
        InventoryItem source = await inventoryRepository
            .GetAsync(command.SourceLocationId, command.Sku, ct)
            ?? throw new SourceInventoryNotFoundException(command.Sku);

        InventoryItem destination = await inventoryRepository
            .GetAsync(command.DestinationLocationId, command.Sku, ct)
            ?? throw new DestinationInventoryNotFoundException(command.Sku);

        source.Remove(command.Quantity);
        destination.Add(command.Quantity);

        await unitOfWork.SaveChangesAsync(ct);
    }
}
</code></pre>
<p><code>IUnitOfWork</code> is a small application-layer interface, implemented by the <code>DbContext</code> in infrastructure:</p>
<pre><code class="language-csharp">public interface IUnitOfWork
{
    Task&lt;int&gt; SaveChangesAsync(CancellationToken ct = default);
}

// Infrastructure
public sealed class AppDbContext : DbContext, IUnitOfWork
{
    // DbSets and configuration
}
</code></pre>
<p>The <code>DbContext</code> was always a <strong>unit of work</strong>; the interface just lets the application layer own the abstraction, keeping the dependency arrow pointing inward.</p>
<p>This is my ranking's number one, and it should cover 90 percent of your use cases.
It has a discipline attached: <strong>repositories never call SaveChanges</strong>.
The moment a repository saves inside itself, the use case loses the ability to compose multiple changes into one atomic commit.</p>
<p>Domain events dispatched before <code>SaveChanges</code> (the standard interceptor approach) ride in the same transaction, so side effects like <a href="https://milanjovanovic.tech/blog/implementing-the-outbox-pattern"><strong>outbox messages</strong></a> commit atomically with the state change.
That combination solves the dual-write problem without any explicit transaction code.</p>
<h2>Option 2: An Explicit Transaction Abstraction (When One SaveChanges Is Not Enough)</h2>
<p>Some use cases genuinely need more than one flush:</p>
<ul>
<li>You need a generated ID from an insert before a second write can proceed.</li>
<li>You mix EF Core changes with Dapper or raw SQL that must commit together.</li>
<li>You call an infrastructure service that writes to the same database outside the <code>DbContext</code>.</li>
</ul>
<p>Then the application layer needs an explicit boundary, still behind its own abstraction:</p>
<pre><code class="language-csharp">public interface ITransactionManager
{
    Task ExecuteInTransactionAsync(
        Func&lt;CancellationToken, Task&gt; action,
        CancellationToken ct = default);
}
</code></pre>
<p>Infrastructure implements it with EF Core, including the execution strategy so it composes with <strong>connection resiliency</strong>:</p>
<pre><code class="language-csharp">public sealed class TransactionManager(AppDbContext dbContext) : ITransactionManager
{
    public async Task ExecuteInTransactionAsync(
        Func&lt;CancellationToken, Task&gt; action,
        CancellationToken ct = default)
    {
        IExecutionStrategy strategy = dbContext.Database.CreateExecutionStrategy();

        await strategy.ExecuteAsync(async token =&gt;
        {
            await using IDbContextTransaction transaction =
                await dbContext.Database.BeginTransactionAsync(token);

            await action(token);

            await transaction.CommitAsync(token);
        }, ct);
    }
}
</code></pre>
<p>And the use case wraps only the part that must be atomic:</p>
<pre><code class="language-csharp">await transactionManager.ExecuteInTransactionAsync(async token =&gt;
{
    await orderRepository.AddAsync(order, token);
    await unitOfWork.SaveChangesAsync(token);

    await auditWriter.WriteAsync(order.Id, token);
}, ct);
</code></pre>
<p>One catch: <code>BeginTransactionAsync</code> opens the transaction on the <code>DbContext</code>'s connection, and it does not flow to other writers automatically.
For the audit writer to actually participate, its Dapper or ADO.NET code must run on that same connection and transaction, which infrastructure can obtain from <code>dbContext.Database.GetDbConnection()</code> and <code>dbContext.Database.CurrentTransaction.GetDbTransaction()</code>.
A writer on its own connection commits independently, and the atomicity you think you have is not there.</p>
<p>This is rank two: exactly as much transaction as the use case needs, no more.
Use it for the minority of use cases that need it, and keep Option 1 everywhere else.
Mixing the two in one codebase is normal.</p>
<h2>Option 3: A Transaction Pipeline Behavior (Blanket Coverage)</h2>
<p>The third approach wraps <strong>every command</strong> in a transaction via a <a href="https://milanjovanovic.tech/blog/mediatr-pipeline-behaviors"><strong>MediatR pipeline behavior</strong></a>:</p>
<pre><code class="language-csharp">public interface ITransactionalCommand { }

public sealed class TransactionBehavior&lt;TRequest, TResponse&gt;(
    AppDbContext dbContext)
    : IPipelineBehavior&lt;TRequest, TResponse&gt;
    where TRequest : ITransactionalCommand
{
    public async Task&lt;TResponse&gt; Handle(
        TRequest request,
        RequestHandlerDelegate&lt;TResponse&gt; next,
        CancellationToken ct)
    {
        IExecutionStrategy strategy = dbContext.Database.CreateExecutionStrategy();

        return await strategy.ExecuteAsync(async token =&gt;
        {
            await using IDbContextTransaction transaction =
                await dbContext.Database.BeginTransactionAsync(token);

            TResponse response = await next();

            await transaction.CommitAsync(token);

            return response;
        }, ct);
    }
}
</code></pre>
<p>The behavior depends on <code>AppDbContext</code> directly, so it lives in infrastructure; have it use <code>ITransactionManager</code> instead if you want it in the application layer.</p>
<p>I rank this third, and I use it rarely.
The upsides are real: handlers stay free of transaction code, and no one can forget the boundary.
But the costs are structural:</p>
<ul>
<li>Most handlers do not need it, because one <code>SaveChanges</code> already gave them atomicity. The blanket transaction just holds a connection and locks for longer than necessary.</li>
<li>It invites multi-<code>SaveChanges</code> handlers to proliferate, because &quot;the behavior will catch it&quot;. Implicit safety breeds sloppy boundaries.</li>
<li>Handlers that call external services now do so inside an open database transaction, stretching lock duration across network calls.</li>
</ul>
<p>If you adopt it, constrain it with a marker interface (like <code>ITransactionalCommand</code> above) so only opt-in commands pay the cost, and never wrap queries.</p>
<h2>What About TransactionScope and Request-Level Middleware?</h2>
<p>Two approaches I recommend against as defaults.</p>
<p><code>TransactionScope</code> with ambient flow looks convenient, but it is easy to hold wrong.
The scope does not flow across <code>await</code> unless you create it with <code>TransactionScopeAsyncFlowOption.Enabled</code>, and forgetting that means work after the first <code>await</code> silently runs outside the transaction.
A second connection can escalate it to a distributed transaction, and the boundary hides in ambient state where nobody can see it.
Explicit boundaries are easier to reason about and easier to test.</p>
<p>A transaction-per-request middleware puts the boundary at the HTTP layer, which is the controller mistake at scale: every read-only GET pays for transaction overhead, and the boundary no longer matches any business definition of atomicity.
The use case is the boundary; the request is just transport.</p>
<p>I walk through this decision, including how the outbox extends atomicity to messaging, in <a href="https://milanjovanovic.tech/pragmatic-clean-architecture"><strong>Pragmatic Clean Architecture</strong></a>.</p>
<h2>The Ranking</h2>
<p>Transactions answer a business question (what must happen together), so the boundary belongs to the layer that models business operations: the application layer.</p>
<p>Ranked:</p>
<ol>
<li><strong>One SaveChanges per use case.</strong> EF Core's implicit transaction covers it. This is the default, and repositories must not save behind the use case's back.</li>
<li><strong>An explicit ITransactionManager</strong> for the few use cases that need multiple flushes or mixed writers, wrapping only what must be atomic.</li>
<li><strong>A pipeline behavior</strong> when you want blanket enforcement, constrained by a marker interface and kept away from queries.</li>
</ol>
<p>If you find <code>BeginTransaction</code> in a controller or a repository today, move the boundary to the handler.
The code gets shorter, and atomicity finally matches what the business actually meant.</p>
]]></content:encoded>
            <author>milan@milanjovanovic.tech (Milan Jovanović)</author>
        </item>
        <item>
            <title><![CDATA[How to Organize Use Cases in Clean Architecture]]></title>
            <link>https://milanjovanovic.tech/blog/organize-use-cases-clean-architecture</link>
            <guid isPermaLink="false">https://milanjovanovic.tech/blog/organize-use-cases-clean-architecture</guid>
            <pubDate>Tue, 11 Aug 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[A folder structure that works at 5 use cases falls apart at 200. Group by feature, keep one use case per folder, and name with verb-noun domain language, and…]]></description>
            <content:encoded><![CDATA[<p>The fastest way to size up an unfamiliar codebase is to open the Application layer and read the folder names.
In a well-organized project, they read like a list of things the system can do.
In a poorly organized one, you get <code>Handlers</code>, <code>Dtos</code>, and <code>Validators</code>, and no idea what the application is for.
Here is how to structure use cases so the codebase stays navigable at 200 of them, not just at 20.</p>
<h2>Use Cases Are the Application Layer</h2>
<p>In <a href="https://milanjovanovic.tech/blog/clean-architecture-and-the-benefits-of-structured-software-design">Clean Architecture</a>, the <a href="https://milanjovanovic.tech/blog/application-layer-clean-architecture">Application layer</a> contains use cases - the actions your system can perform. Each use case represents a single thing the user can do.</p>
<p>But as your project grows from 5 use cases to 50 to 200, organization becomes critical.</p>
<h2>Strategy 1: Group by Feature (Recommended)</h2>
<p>Organize use cases by business feature or aggregate:</p>
<pre><code>Application/
  Orders/
    PlaceOrder/
      PlaceOrderCommand.cs
      PlaceOrderCommandHandler.cs
      PlaceOrderValidator.cs
    CancelOrder/
      CancelOrderCommand.cs
      CancelOrderCommandHandler.cs
    GetOrderById/
      GetOrderByIdQuery.cs
      GetOrderByIdQueryHandler.cs
      OrderResponse.cs
    GetOrders/
      GetOrdersQuery.cs
      GetOrdersQueryHandler.cs
      OrderSummaryResponse.cs
  Customers/
    RegisterCustomer/
      RegisterCustomerCommand.cs
      RegisterCustomerCommandHandler.cs
    GetCustomerProfile/
      GetCustomerProfileQuery.cs
      GetCustomerProfileQueryHandler.cs
</code></pre>
<p>Each use case gets its own folder. Everything related to placing an order - the command, handler, validator, DTOs - lives in one place.</p>
<p><strong>Benefits:</strong></p>
<ul>
<li>Find any use case instantly</li>
<li>Related code is co-located</li>
<li>Fewer merge conflicts (teams work on different features)</li>
<li>Mirrors <a href="https://milanjovanovic.tech/blog/feature-folders-dotnet">feature folders</a> in Vertical Slice Architecture</li>
</ul>
<p>This is <a href="https://milanjovanovic.tech/blog/screaming-architecture"><strong>Screaming Architecture</strong></a> in practice: the folder names shout <code>Orders</code> and <code>Customers</code>, not <code>Handlers</code> and <code>DTOs</code>.</p>
<h2>Strategy 2: Group by CQRS</h2>
<p>When using <a href="https://milanjovanovic.tech/blog/cqrs-pattern-the-way-it-should-have-been-from-the-start">CQRS</a>, separate commands from queries:</p>
<pre><code>Application/
  Commands/
    Orders/
      PlaceOrder/
        PlaceOrderCommand.cs
        PlaceOrderCommandHandler.cs
      CancelOrder/
        CancelOrderCommand.cs
        CancelOrderCommandHandler.cs
  Queries/
    Orders/
      GetOrderById/
        GetOrderByIdQuery.cs
        GetOrderByIdQueryHandler.cs
      GetOrders/
        GetOrdersQuery.cs
        GetOrdersQueryHandler.cs
</code></pre>
<p>This makes it clear which operations modify state and which are read-only.</p>
<p>The downside shows up as the project grows: everything about Orders is now split across two top-level trees.
To understand the Orders feature, you jump between <code>Commands/Orders</code> and <code>Queries/Orders</code> constantly.
I'd only pick this layout if your read and write sides are owned by different teams or deployed separately.
The <code>Command</code>/<code>Query</code> suffix already tells you which side you're on.</p>
<h2>Strategy 3: Group by Technical Type (Avoid This)</h2>
<p>For completeness, here's the layout you should <em>not</em> use:</p>
<pre><code>Application/
  Commands/
    PlaceOrderCommand.cs
    CancelOrderCommand.cs
    RegisterCustomerCommand.cs
  Handlers/
    PlaceOrderCommandHandler.cs
    CancelOrderCommandHandler.cs
    RegisterCustomerCommandHandler.cs
  Validators/
    PlaceOrderValidator.cs
  Dtos/
    OrderResponse.cs
    CustomerResponse.cs
</code></pre>
<p>It looks tidy at 10 use cases.
At 100, adding one feature means touching four distant folders, and <code>Handlers/</code> is a 100-file wall where nothing is related to its neighbors.</p>
<p>Folders should group things that change together.
A command and its handler change together.
Two unrelated handlers don't.</p>
<h2>One Use Case Per Folder</h2>
<p>Each use case should have:</p>
<ul>
<li><strong>A request</strong> (command or query)</li>
<li><strong>A handler</strong></li>
<li><strong>Optionally: a validator, DTOs, and mapping</strong></li>
</ul>
<pre><code class="language-csharp">// PlaceOrderCommand.cs
public sealed record PlaceOrderCommand(
    Guid CustomerId,
    List&lt;OrderItemRequest&gt; Items) : ICommand&lt;Guid&gt;;

// PlaceOrderCommandHandler.cs
public sealed class PlaceOrderCommandHandler
    : ICommandHandler&lt;PlaceOrderCommand, Guid&gt;
{
    private readonly IOrderRepository _orderRepository;
    private readonly IUnitOfWork _unitOfWork;

    public PlaceOrderCommandHandler(
        IOrderRepository orderRepository,
        IUnitOfWork unitOfWork)
    {
        _orderRepository = orderRepository;
        _unitOfWork = unitOfWork;
    }

    public async Task&lt;Result&lt;Guid&gt;&gt; Handle(
        PlaceOrderCommand command, CancellationToken ct)
    {
        var order = Order.Create(command.CustomerId, command.Items);

        _orderRepository.Add(order);
        await _unitOfWork.SaveChangesAsync(ct);

        return order.Id;
    }
}

// PlaceOrderValidator.cs
public sealed class PlaceOrderValidator
    : AbstractValidator&lt;PlaceOrderCommand&gt;
{
    public PlaceOrderValidator()
    {
        RuleFor(x =&gt; x.CustomerId).NotEmpty();
        RuleFor(x =&gt; x.Items).NotEmpty();
        RuleForEach(x =&gt; x.Items).ChildRules(item =&gt;
        {
            item.RuleFor(x =&gt; x.ProductId).NotEmpty();
            item.RuleFor(x =&gt; x.Quantity).GreaterThan(0);
        });
    }
}
</code></pre>
<p>Keep handlers small. If a handler grows beyond 30-40 lines, the business logic probably belongs in a domain service or the domain entity itself.</p>
<h3>Single-File Use Cases</h3>
<p>Some teams take co-location one step further and put the whole use case in one file with a static wrapper class:</p>
<pre><code class="language-csharp">// PlaceOrder.cs
public static class PlaceOrder
{
    public sealed record Command(
        Guid CustomerId,
        List&lt;OrderItemRequest&gt; Items) : ICommand&lt;Guid&gt;;

    public sealed class Validator : AbstractValidator&lt;Command&gt;
    {
        public Validator()
        {
            RuleFor(x =&gt; x.CustomerId).NotEmpty();
            RuleFor(x =&gt; x.Items).NotEmpty();
        }
    }

    internal sealed class Handler : ICommandHandler&lt;Command, Guid&gt;
    {
        // ...
    }
}
</code></pre>
<p>You reference it as <code>PlaceOrder.Command</code>, which reads nicely at call sites.
The tradeoff is longer files and slightly unusual navigation.
I use separate files by default and reach for this style in smaller projects or <a href="https://milanjovanovic.tech/blog/vertical-slice-architecture"><strong>vertical slice</strong></a> codebases.
Pick one convention per project and stick with it.</p>
<h2>Naming Conventions</h2>
<p>Use verb-noun naming that describes what the use case does:</p>
<ul>
<li><code>PlaceOrderCommand</code>, not <code>OrderCommand</code> - the verb says what happens</li>
<li><code>CancelOrderCommand</code>, not <code>UpdateOrderStatusCommand</code> - name the business operation, not the database effect</li>
<li><code>GetOrderByIdQuery</code>, not <code>OrderQuery</code> - be specific about what's fetched</li>
<li><code>RegisterCustomerCommand</code>, not <code>CreateCustomerCommand</code> - match how the business talks</li>
</ul>
<p><code>PlaceOrder</code> is domain language. <code>CreateOrder</code> is CRUD language. Use the Ubiquitous Language from your domain.</p>
<h2>Shared DTOs and Contracts</h2>
<p>When multiple use cases share the same response:</p>
<pre><code>Application/
  Orders/
    PlaceOrder/...
    CancelOrder/...
    GetOrderById/...
    Shared/
      OrderResponse.cs
      OrderSummaryResponse.cs
      OrderItemResponse.cs
</code></pre>
<p>Or at the feature root level:</p>
<pre><code>Application/
  Orders/
    OrderResponse.cs          ← shared by multiple queries
    PlaceOrder/...
    GetOrderById/...
</code></pre>
<p>Keep shared DTOs minimal. If two queries need slightly different data, create separate response types rather than one bloated class.</p>
<h2>Interfaces and Abstractions</h2>
<p>Define repository and service interfaces close to the features that use them:</p>
<pre><code>Application/
  Orders/
    IOrderRepository.cs
    PlaceOrder/...
    CancelOrder/...
  Customers/
    ICustomerRepository.cs
    RegisterCustomer/...
</code></pre>
<p>Or in a centralized <code>Abstractions</code> folder if they're used across features:</p>
<pre><code>Application/
  Abstractions/
    IUnitOfWork.cs
    ICurrentUserService.cs
    IDateTimeProvider.cs
  Orders/
    IOrderRepository.cs
    PlaceOrder/...
</code></pre>
<h2>Behaviors (Cross-Cutting Concerns)</h2>
<p><a href="https://milanjovanovic.tech/blog/mediatr-pipeline-behaviors">Cross-cutting concerns</a> like validation, logging, and caching are pipeline behaviors that wrap use case handlers:</p>
<pre><code>Application/
  Behaviors/
    ValidationBehavior.cs
    LoggingBehavior.cs
    CachingBehavior.cs
  Orders/
    PlaceOrder/...
</code></pre>
<p>These apply to all use cases automatically - no need to duplicate logic in each handler.</p>
<h2>Enforcing the Conventions</h2>
<p>Conventions decay without enforcement.
A few <a href="https://milanjovanovic.tech/blog/enforcing-software-architecture-with-architecture-tests"><strong>architecture tests</strong></a> keep the structure honest:</p>
<pre><code class="language-csharp">[Fact]
public void CommandHandlers_Should_Have_CommandHandler_Suffix()
{
    var result = Types
        .InAssembly(ApplicationAssembly)
        .That()
        .ImplementInterface(typeof(ICommandHandler&lt;,&gt;))
        .Should()
        .HaveNameEndingWith(&quot;CommandHandler&quot;)
        .GetResult();

    result.IsSuccessful.Should().BeTrue();
}

[Fact]
public void Handlers_Should_Be_Sealed()
{
    var result = Types
        .InAssembly(ApplicationAssembly)
        .That()
        .ImplementInterface(typeof(ICommandHandler&lt;,&gt;))
        .Should()
        .BeSealed()
        .GetResult();

    result.IsSuccessful.Should().BeTrue();
}
</code></pre>
<p><code>ApplicationAssembly</code> is a shared static field (<code>typeof(PlaceOrderCommand).Assembly</code>) so every test scans the same project.
These tests also assume the separate-file convention.
If you use the single-file style with nested <code>Handler</code> classes, adjust the naming rule to match.</p>
<p>Cheap to write, and they catch drift in code review before it becomes precedent.</p>
<h2>What Goes Wrong</h2>
<p><strong>Fat handlers.</strong> A handler with 100+ lines is doing too much. Extract business logic to domain entities or domain services. The handler should only orchestrate.</p>
<p><strong>Shared commands.</strong> One command used for both creating and updating. Create separate <code>PlaceOrderCommand</code> and <code>UpdateOrderCommand</code> - they have different validation and different business rules.</p>
<p><strong>Anemic use cases.</strong> A use case that just calls <code>repository.Add(entity)</code> with no business logic. This is fine for simple CRUD, but if there are invariants to enforce, they belong in the domain.</p>
<h2>A Structure That Scales</h2>
<p>Organize use cases by feature, one per folder, with clear verb-noun names:</p>
<ol>
<li>Group by feature or aggregate</li>
<li>One use case per folder (command/query + handler + validator)</li>
<li>Use domain language for naming</li>
<li>Keep handlers thin - orchestration only</li>
<li>Share DTOs sparingly</li>
</ol>
<p>The structure should make it obvious what your application does just by looking at the folder names.</p>
<p>Thanks for reading, and stay awesome!</p>
<hr>
]]></content:encoded>
            <author>milan@milanjovanovic.tech (Milan Jovanović)</author>
        </item>
        <item>
            <title><![CDATA[MediatR Pipeline Behaviors: A Practical Guide to Cross-Cutting Concerns]]></title>
            <link>https://milanjovanovic.tech/blog/mediatr-pipeline-behaviors</link>
            <guid isPermaLink="false">https://milanjovanovic.tech/blog/mediatr-pipeline-behaviors</guid>
            <pubDate>Tue, 11 Aug 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[MediatR pipeline behaviors let you handle logging, validation, caching, and transactions without touching your handlers.]]></description>
            <content:encoded><![CDATA[<p>Your command handlers should read like business logic and nothing else.
In practice, they tend to accumulate logging, validation, caching, and transaction plumbing until the actual use case is buried.
MediatR pipeline behaviors pull that plumbing out into reusable classes that wrap every handler automatically, and they're one of the main reasons to use MediatR at all.</p>
<h2>Why Pipeline Behaviors</h2>
<p>Every <a href="https://milanjovanovic.tech/blog/clean-architecture-and-the-benefits-of-structured-software-design">Clean Architecture</a> project eventually runs into the same problem. You need logging in every handler. Validation before every command. Caching for expensive queries. And you don't want to copy-paste that logic into dozens of classes.</p>
<p>MediatR's <code>IPipelineBehavior&lt;TRequest, TResponse&gt;</code> solves this by wrapping your handlers with reusable middleware. Think of it like ASP.NET Core middleware, but for your <a href="https://milanjovanovic.tech/blog/cqrs-pattern-the-way-it-should-have-been-from-the-start">CQRS</a> pipeline. Each behavior gets a chance to run code before and after the handler, or short-circuit the pipeline entirely.</p>
<p>I'll walk you through four behaviors I use in almost every project.</p>
<p>One thing before we start: <a href="https://milanjovanovic.tech/blog/mediatr-and-masstransit-going-commercial-what-this-means-for-you"><strong>MediatR is now a commercial product</strong></a> for larger companies.
Everything in this article still applies, and the same pattern works with hand-rolled handler interfaces and Scrutor decorators if you'd rather not take the dependency.
I show that decorator-based approach in <a href="https://milanjovanovic.tech/blog/balancing-cross-cutting-concerns-in-clean-architecture"><strong>balancing cross-cutting concerns in Clean Architecture</strong></a>.</p>
<h2>How IPipelineBehavior Works</h2>
<p>A pipeline behavior implements <code>IPipelineBehavior&lt;TRequest, TResponse&gt;</code>. It receives the request, a <code>next</code> delegate that calls the next behavior (or the handler itself), and a cancellation token.</p>
<pre><code class="language-csharp">public class MyBehavior&lt;TRequest, TResponse&gt;
    : IPipelineBehavior&lt;TRequest, TResponse&gt;
    where TRequest : IRequest&lt;TResponse&gt;
{
    public async Task&lt;TResponse&gt; Handle(
        TRequest request,
        RequestHandlerDelegate&lt;TResponse&gt; next,
        CancellationToken cancellationToken)
    {
        // Run code BEFORE the handler.

        var response = await next();

        // Run code AFTER the handler.

        return response;
    }
}
</code></pre>
<p>The <code>next</code> delegate is the key. Calling it passes control down the pipeline. You can inspect the request before calling <code>next</code>, inspect the response after, or skip <code>next</code> entirely to short-circuit.</p>
<h2>Logging Behavior</h2>
<p>The first behavior I add to any project is logging with elapsed time. It gives you visibility into every request flowing through the system without a single log statement in your handlers.</p>
<pre><code class="language-csharp">public class LoggingBehavior&lt;TRequest, TResponse&gt;
    : IPipelineBehavior&lt;TRequest, TResponse&gt;
    where TRequest : IRequest&lt;TResponse&gt;
{
    private readonly ILogger&lt;LoggingBehavior&lt;TRequest, TResponse&gt;&gt; _logger;

    public LoggingBehavior(
        ILogger&lt;LoggingBehavior&lt;TRequest, TResponse&gt;&gt; logger)
    {
        _logger = logger;
    }

    public async Task&lt;TResponse&gt; Handle(
        TRequest request,
        RequestHandlerDelegate&lt;TResponse&gt; next,
        CancellationToken cancellationToken)
    {
        var requestName = typeof(TRequest).Name;

        _logger.LogInformation(&quot;Handling {Request}&quot;, requestName);

        var stopwatch = Stopwatch.StartNew();

        var response = await next();

        stopwatch.Stop();

        _logger.LogInformation(
            &quot;Handled {Request} in {ElapsedMs}ms&quot;,
            requestName,
            stopwatch.ElapsedMilliseconds);

        return response;
    }
}
</code></pre>
<p>Every command and query gets timed automatically. If something runs slow, you'll see it in your logs immediately.</p>
<h2>Validation Behavior</h2>
<p>Validation is a perfect candidate for a pipeline behavior. You run all FluentValidation validators for the incoming request, and if anything fails, you short-circuit the pipeline before the handler ever executes.</p>
<pre><code class="language-csharp">public class ValidationBehavior&lt;TRequest, TResponse&gt;
    : IPipelineBehavior&lt;TRequest, TResponse&gt;
    where TRequest : IRequest&lt;TResponse&gt;
{
    private readonly IEnumerable&lt;IValidator&lt;TRequest&gt;&gt; _validators;

    public ValidationBehavior(
        IEnumerable&lt;IValidator&lt;TRequest&gt;&gt; validators)
    {
        _validators = validators;
    }

    public async Task&lt;TResponse&gt; Handle(
        TRequest request,
        RequestHandlerDelegate&lt;TResponse&gt; next,
        CancellationToken cancellationToken)
    {
        if (!_validators.Any())
        {
            return await next();
        }

        var context = new ValidationContext&lt;TRequest&gt;(request);

        var failures = _validators
            .Select(v =&gt; v.Validate(context))
            .SelectMany(result =&gt; result.Errors)
            .Where(failure =&gt; failure is not null)
            .ToArray();

        if (failures.Length != 0)
        {
            throw new ValidationException(failures);
        }

        return await next();
    }
}
</code></pre>
<p>Notice the behavior never calls <code>next()</code> when validation fails. The handler stays clean - it only deals with business logic, never input validation. You can pair this with the <a href="https://milanjovanovic.tech/blog/functional-error-handling-in-dotnet-with-the-result-pattern">Result pattern</a> if you prefer returning errors over throwing exceptions.</p>
<h2>Caching Behavior</h2>
<p>For query caching, I use a marker interface called <code>ICacheable</code>. Only queries that implement it get cached. Everything else passes straight through.</p>
<pre><code class="language-csharp">public interface ICacheable
{
    string CacheKey { get; }
    TimeSpan CacheDuration { get; }
}

public class CachingBehavior&lt;TRequest, TResponse&gt;
    : IPipelineBehavior&lt;TRequest, TResponse&gt;
    where TRequest : IRequest&lt;TResponse&gt;, ICacheable
{
    private readonly IDistributedCache _cache;
    private readonly ILogger&lt;CachingBehavior&lt;TRequest, TResponse&gt;&gt; _logger;

    public CachingBehavior(
        IDistributedCache cache,
        ILogger&lt;CachingBehavior&lt;TRequest, TResponse&gt;&gt; logger)
    {
        _cache = cache;
        _logger = logger;
    }

    public async Task&lt;TResponse&gt; Handle(
        TRequest request,
        RequestHandlerDelegate&lt;TResponse&gt; next,
        CancellationToken cancellationToken)
    {
        var cached = await _cache.GetStringAsync(
            request.CacheKey, cancellationToken);

        if (cached is not null)
        {
            _logger.LogInformation(
                &quot;Cache hit for {CacheKey}&quot;, request.CacheKey);

            return JsonSerializer.Deserialize&lt;TResponse&gt;(cached)!;
        }

        var response = await next();

        await _cache.SetStringAsync(
            request.CacheKey,
            JsonSerializer.Serialize(response),
            new DistributedCacheEntryOptions
            {
                AbsoluteExpirationRelativeToNow = request.CacheDuration
            },
            cancellationToken);

        return response;
    }
}
</code></pre>
<p>Apply it to a query by implementing <code>ICacheable</code>:</p>
<pre><code class="language-csharp">public record GetProductByIdQuery(Guid ProductId)
    : IRequest&lt;ProductResponse&gt;, ICacheable
{
    public string CacheKey =&gt; $&quot;product:{ProductId}&quot;;
    public TimeSpan CacheDuration =&gt; TimeSpan.FromMinutes(5);
}
</code></pre>
<p>The handler has no idea caching exists. It just returns data, and the behavior handles the rest.</p>
<h2>Transaction Behavior</h2>
<p>Commands that modify data often need to run inside a database transaction. Instead of wrapping every handler in a <code>BeginTransaction</code>/<code>CommitAsync</code> block, you can use a behavior with a marker interface.</p>
<pre><code class="language-csharp">public interface ITransactional;

public class TransactionBehavior&lt;TRequest, TResponse&gt;
    : IPipelineBehavior&lt;TRequest, TResponse&gt;
    where TRequest : IRequest&lt;TResponse&gt;, ITransactional
{
    private readonly ApplicationDbContext _dbContext;
    private readonly ILogger&lt;TransactionBehavior&lt;TRequest, TResponse&gt;&gt; _logger;

    public TransactionBehavior(
        ApplicationDbContext dbContext,
        ILogger&lt;TransactionBehavior&lt;TRequest, TResponse&gt;&gt; logger)
    {
        _dbContext = dbContext;
        _logger = logger;
    }

    public async Task&lt;TResponse&gt; Handle(
        TRequest request,
        RequestHandlerDelegate&lt;TResponse&gt; next,
        CancellationToken cancellationToken)
    {
        await using var transaction = await _dbContext.Database
            .BeginTransactionAsync(cancellationToken);

        try
        {
            var response = await next();

            await _dbContext.SaveChangesAsync(cancellationToken);
            await transaction.CommitAsync(cancellationToken);

            _logger.LogInformation(
                &quot;Transaction committed for {Request}&quot;,
                typeof(TRequest).Name);

            return response;
        }
        catch
        {
            await transaction.RollbackAsync(cancellationToken);
            throw;
        }
    }
}
</code></pre>
<p>Mark any command that needs a transaction:</p>
<pre><code class="language-csharp">public record PlaceOrderCommand(Guid CustomerId, List&lt;OrderItem&gt; Items)
    : IRequest&lt;Guid&gt;, ITransactional;
</code></pre>
<p>If the handler throws, the transaction rolls back automatically. No try-catch clutter in your business logic.</p>
<h2>Registering Behaviors</h2>
<p>MediatR's <code>AddOpenBehavior</code> method handles registration. You call it inside <code>AddMediatR</code>, and MediatR figures out the generic type arguments at runtime.</p>
<pre><code class="language-csharp">builder.Services.AddMediatR(cfg =&gt;
{
    cfg.RegisterServicesFromAssembly(typeof(PlaceOrderCommand).Assembly);

    cfg.AddOpenBehavior(typeof(LoggingBehavior&lt;,&gt;));
    cfg.AddOpenBehavior(typeof(ValidationBehavior&lt;,&gt;));
    cfg.AddOpenBehavior(typeof(CachingBehavior&lt;,&gt;));
    cfg.AddOpenBehavior(typeof(TransactionBehavior&lt;,&gt;));
});
</code></pre>
<p>Don't forget to register your FluentValidation validators too:</p>
<pre><code class="language-csharp">builder.Services.AddValidatorsFromAssembly(
    typeof(PlaceOrderCommand).Assembly);
</code></pre>
<h2>Execution Order</h2>
<p>The order you call <code>AddOpenBehavior</code> determines the execution order. Behaviors execute from the outside in, like layers of an onion.</p>
<p>With the registration above, a request flows like this:</p>
<img src="https://milanjovanovic.tech/blogs/articles/mediatr-pipeline-behaviors/pipeline-execution-order.png" alt="A request passes through LoggingBehavior, ValidationBehavior, CachingBehavior, and TransactionBehavior before reaching the handler, then the response flows back out through each behavior in reverse order">
<ol>
<li><strong>LoggingBehavior</strong> - starts the stopwatch, logs the request name</li>
<li><strong>ValidationBehavior</strong> - runs validators, short-circuits if invalid</li>
<li><strong>CachingBehavior</strong> - returns cached data if available (for cacheable queries)</li>
<li><strong>TransactionBehavior</strong> - begins a transaction (for transactional commands)</li>
<li><strong>Handler</strong> - runs the actual business logic</li>
</ol>
<p>The response then flows back up through each behavior in reverse order. This means the logging behavior captures the total time including validation, caching, and transaction overhead.</p>
<p>Think carefully about ordering. Logging should always be outermost so it captures everything. Validation should run before caching - there's no point caching an invalid request.</p>
<h2>Takeaway</h2>
<p>Pipeline behaviors keep your handlers focused on business logic. Logging, validation, caching, and transactions all live in their own classes, registered once, and applied automatically across every request that matches the constraints.</p>
<p>The pattern scales well. Even with eight or nine behaviors in the pipeline, handlers stay just as clean as they were with zero. If you're building a <a href="https://milanjovanovic.tech/blog/clean-architecture-and-the-benefits-of-structured-software-design">Clean Architecture</a> application with MediatR, pipeline behaviors are the single best tool for managing cross-cutting concerns.</p>
<p>Thanks for reading, and stay awesome!</p>
<hr>
]]></content:encoded>
            <author>milan@milanjovanovic.tech (Milan Jovanović)</author>
        </item>
        <item>
            <title><![CDATA[Mapping Between Layers in Clean Architecture]]></title>
            <link>https://milanjovanovic.tech/blog/mapping-between-layers-clean-architecture</link>
            <guid isPermaLink="false">https://milanjovanovic.tech/blog/mapping-between-layers-clean-architecture</guid>
            <pubDate>Tue, 11 Aug 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Request to command, command to entity, entity to response: a single operation can pass through three mappings, and most of them are ceremony.]]></description>
            <content:encoded><![CDATA[<p>Ask five developers how to map between layers in Clean Architecture and you'll get five different answers, usually involving three different libraries.
The truth is simpler: most mapping code shouldn't exist at all.
Here are the five approaches I see in .NET projects, when each one makes sense, and the two I'd actually use.</p>
<h2>Why Mapping Between Layers Exists</h2>
<p>In <a href="https://milanjovanovic.tech/blog/clean-architecture-and-the-benefits-of-structured-software-design">Clean Architecture</a>, each layer has its own models:</p>
<ul>
<li><strong><a href="https://milanjovanovic.tech/blog/domain-layer-clean-architecture">Domain layer</a></strong> - Entities and Value Objects (business rules)</li>
<li><strong><a href="https://milanjovanovic.tech/blog/application-layer-clean-architecture">Application layer</a></strong> - Commands, Queries, DTOs (use case contracts)</li>
<li><strong>Infrastructure layer</strong> - Persistence models, configuration entities</li>
<li><strong>Presentation layer</strong> - API request/response models</li>
</ul>
<p>These models look similar but serve different purposes. You need mapping to convert between them.</p>
<p>An HTTP request arrives as a <code>PlaceOrderRequest</code>. You map it to a <code>PlaceOrderCommand</code>. The handler creates an <code>Order</code> entity. You return an <code>OrderResponse</code> to the client.</p>
<p>That's three mappings in one request flow.</p>
<h2>When Is Mapping Worth It?</h2>
<p><strong>Map when:</strong></p>
<ul>
<li>The API contract differs from the internal model (different field names, flattened structures)</li>
<li>You want to hide internal implementation details from API consumers</li>
<li>Domain entities expose behaviors you don't want serialized</li>
<li>You need to return computed or aggregated data that doesn't exist on the entity</li>
</ul>
<p><strong>Don't map when:</strong></p>
<ul>
<li>The source and target are structurally identical</li>
<li>Adding a mapping layer provides zero value beyond &quot;architecture says so&quot;</li>
</ul>
<p>Pragmatism matters. A 1:1 mapping between <code>PlaceOrderRequest</code> and <code>PlaceOrderCommand</code> with identical fields is ceremony, not architecture.</p>
<h2>Approach 1: Manual Mapping</h2>
<p>The simplest option. Extension methods or static factory methods:</p>
<pre><code class="language-csharp">public static class OrderMappings
{
    public static PlaceOrderCommand ToCommand(this PlaceOrderRequest request)
    {
        return new PlaceOrderCommand(
            request.CustomerId,
            request.Items.Select(i =&gt; new OrderItemDto(
                i.ProductId,
                i.Quantity)).ToList());
    }

    public static OrderResponse ToResponse(this Order order)
    {
        return new OrderResponse(
            order.Id,
            order.Customer.Name,
            order.TotalAmount.Amount,
            order.Status.Name,
            order.CreatedAt);
    }
}
</code></pre>
<p>Usage:</p>
<pre><code class="language-csharp">app.MapPost(&quot;/api/orders&quot;, async (
    PlaceOrderRequest request,
    ICommandHandler&lt;PlaceOrderCommand, Guid&gt; handler,
    CancellationToken cancellationToken) =&gt;
{
    var command = request.ToCommand();
    var result = await handler.Handle(command, cancellationToken);
    return result.IsSuccess
        ? Results.Created($&quot;/api/orders/{result.Value}&quot;, result.Value)
        : result.ToProblemDetails();
});
</code></pre>
<p><strong>Pros:</strong> Simple, explicit, easy to debug, no magic.
<strong>Cons:</strong> Repetitive for large models, manual maintenance when models change.</p>
<h2>Approach 2: Projection in Queries (CQRS)</h2>
<p>For read operations, skip mapping entirely. Project directly from the database into your response model:</p>
<pre><code class="language-csharp">public class GetOrderByIdQueryHandler : IQueryHandler&lt;GetOrderByIdQuery, OrderResponse&gt;
{
    private readonly IApplicationDbContext _dbContext;

    public GetOrderByIdQueryHandler(IApplicationDbContext dbContext)
    {
        _dbContext = dbContext;
    }

    public async Task&lt;Result&lt;OrderResponse&gt;&gt; Handle(
        GetOrderByIdQuery query,
        CancellationToken cancellationToken)
    {
        var order = await _dbContext.Orders
            .Where(o =&gt; o.Id == query.OrderId)
            .Select(o =&gt; new OrderResponse(
                o.Id,
                o.Customer.Name,
                o.TotalAmount.Amount,
                o.Status.Name,
                o.CreatedAt))
            .FirstOrDefaultAsync(cancellationToken);

        if (order is null)
        {
            return Result.Failure&lt;OrderResponse&gt;(OrderErrors.NotFound(query.OrderId));
        }

        return order;
    }
}
</code></pre>
<p>This is the <a href="https://milanjovanovic.tech/blog/cqrs-pattern-the-way-it-should-have-been-from-the-start">CQRS pattern</a> in action. No domain entity loaded, no mapping needed. EF Core generates an efficient SQL query that returns only the columns you need.</p>
<p>The handler depends on an <code>IApplicationDbContext</code> abstraction rather than the concrete <code>DbContext</code>, so the Application layer never references Infrastructure directly.</p>
<p>This is my preferred approach for queries. It's fast, clean, and eliminating the mapping layer eliminates a whole category of bugs.</p>
<h2>Approach 3: Constructor Mapping on DTOs</h2>
<p>Let the DTO/response model accept the entity in its constructor:</p>
<pre><code class="language-csharp">public sealed record OrderResponse
{
    public OrderResponse(Order order)
    {
        Id = order.Id;
        CustomerName = order.Customer.Name;
        TotalAmount = order.TotalAmount.Amount;
        Status = order.Status.Name;
        CreatedAt = order.CreatedAt;
    }

    public Guid Id { get; init; }
    public string CustomerName { get; init; }
    public decimal TotalAmount { get; init; }
    public string Status { get; init; }
    public DateTime CreatedAt { get; init; }
}
</code></pre>
<p><strong>Pros:</strong> Self-contained, mapping logic lives near the data.
<strong>Cons:</strong> Creates a dependency from the response model to the domain entity. If your response models are in the Presentation layer, this couples Presentation to Domain - which some teams want to avoid.</p>
<h2>Approach 4: AutoMapper</h2>
<p><a href="https://automapper.org/">AutoMapper</a> handles mapping by convention:</p>
<pre><code class="language-csharp">public class OrderProfile : Profile
{
    public OrderProfile()
    {
        CreateMap&lt;Order, OrderResponse&gt;()
            .ForMember(dest =&gt; dest.CustomerName, opt =&gt; opt.MapFrom(src =&gt; src.Customer.Name))
            .ForMember(dest =&gt; dest.TotalAmount, opt =&gt; opt.MapFrom(src =&gt; src.TotalAmount.Amount))
            .ForMember(dest =&gt; dest.Status, opt =&gt; opt.MapFrom(src =&gt; src.Status.Name));
    }
}
</code></pre>
<pre><code class="language-csharp">var response = _mapper.Map&lt;OrderResponse&gt;(order);
</code></pre>
<p><strong>Pros:</strong> Reduces boilerplate for large models, convention-based for simple mappings.
<strong>Cons:</strong> Magic behavior, runtime errors instead of compile-time errors, hard to debug, performance overhead, easy to misconfigure.</p>
<p>I generally avoid AutoMapper. The magic it provides isn't worth the debugging cost. When a mapping breaks, you're reading AutoMapper source code instead of a simple <code>ToResponse()</code> method.</p>
<p>There's also a licensing angle now: AutoMapper <a href="https://milanjovanovic.tech/blog/mediatr-and-masstransit-going-commercial-what-this-means-for-you">went commercial</a> alongside MediatR. If you were on the fence, that's one more reason to prefer boring, dependency-free mapping code.</p>
<h2>Approach 5: Mapster</h2>
<p>Mapster is a faster alternative to AutoMapper with code generation support:</p>
<pre><code class="language-csharp">var response = order.Adapt&lt;OrderResponse&gt;();
</code></pre>
<p>Or with code generation for compile-time safety:</p>
<pre><code class="language-csharp">[AdaptFrom(typeof(Order))]
public sealed record OrderResponse(
    Guid Id,
    string CustomerName,
    decimal TotalAmount,
    string Status,
    DateTime CreatedAt);
</code></pre>
<p><strong>Pros:</strong> Better performance than AutoMapper, code generation option.
<strong>Cons:</strong> Still involves magic, still another dependency.</p>
<h2>My Recommendation</h2>
<ol>
<li><strong>Use projections for queries</strong> - don't load entities just to map them to DTOs</li>
<li><strong>Use manual mapping for commands</strong> - extension methods are simple and explicit</li>
<li><strong>Skip mapping when models are identical</strong> - don't create a <code>PlaceOrderCommand</code> that's identical to <code>PlaceOrderRequest</code></li>
<li><strong>Avoid AutoMapper</strong> - the convenience doesn't justify the debugging cost</li>
</ol>
<p>Here's the practical flow:</p>
<img src="https://milanjovanovic.tech/blogs/articles/mapping-between-layers-clean-architecture/request-mapping-flow.png" alt="The write side maps a request to a command to a domain entity and returns a Guid, while the read side runs a query as an EF Core projection that produces the response directly with no entity loaded">
<h2>How Many Models Do You Actually Need?</h2>
<p>Not every layer needs its own model. A pragmatic approach:</p>
<p><strong>On the write side (commands):</strong></p>
<ul>
<li>Presentation: a request model</li>
<li>Application: a command (which can reuse the request type when the shapes match)</li>
<li>Domain: the entity</li>
</ul>
<p><strong>On the read side (queries):</strong></p>
<ul>
<li>Application: a response DTO</li>
<li>Infrastructure: nothing - the EF Core projection produces the DTO directly</li>
<li>Domain: not involved; no entity is loaded</li>
</ul>
<p>For simple CRUD, the request model might <em>be</em> the command. The response DTO might <em>be</em> the projection result. Two models instead of five.</p>
<p>Going too far in the other direction - a fresh DTO and mapper at every boundary - is what I call mapping mania in my <a href="https://milanjovanovic.tech/blog/clean-architecture-anti-patterns"><strong>Clean Architecture anti-patterns</strong></a> article.</p>
<h2>Takeaway</h2>
<p>Mapping between layers is a means, not an end. Map when it provides value - contract independence, data transformation, security (hiding internal fields). Skip it when it's just ceremony.</p>
<p>Projections eliminate the most common mapping. Manual extension methods handle the rest. You rarely need a mapping framework.</p>
<p>Thanks for reading, and stay awesome!</p>
<hr>
]]></content:encoded>
            <author>milan@milanjovanovic.tech (Milan Jovanović)</author>
        </item>
        <item>
            <title><![CDATA[Logging Strategy in Clean Architecture]]></title>
            <link>https://milanjovanovic.tech/blog/logging-strategy-clean-architecture</link>
            <guid isPermaLink="false">https://milanjovanovic.tech/blog/logging-strategy-clean-architecture</guid>
            <pubDate>Tue, 11 Aug 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[One pipeline behavior can log every use case with timing and failures, so handlers stay clean and the domain never sees an ILogger.]]></description>
            <content:encoded><![CDATA[<p>Ask five developers where logging belongs in Clean Architecture and you'll get five answers, from <code>ILogger</code> in every entity to nothing outside middleware.
The honest answer is that each layer has a different job, so each layer logs differently.
This article maps it out: one pipeline behavior that covers every use case, free logging in infrastructure, and a domain layer that stays clean without going dark.</p>
<h2>The Logging Dilemma</h2>
<p>You want observability - <a href="https://milanjovanovic.tech/blog/structured-logging-in-asp-net-core-with-serilog">structured logging</a>, trace IDs, request timings. But you also want clean domain logic without <code>ILogger</code> scattered everywhere.</p>
<p>In <a href="https://milanjovanovic.tech/blog/clean-architecture-and-the-benefits-of-structured-software-design">Clean Architecture</a>, logging has clear boundaries.</p>
<h2>Where to Log (And Where Not To)</h2>
<ul>
<li><strong>Domain</strong>: don't log. Raise domain events instead, and log in their handlers.</li>
<li><strong>Application</strong>: log minimally, through a <code>LoggingBehavior</code> pipeline behavior rather than inside handlers.</li>
<li><strong>Infrastructure</strong>: log freely. Inject <code>ILogger</code> directly and record every external interaction.</li>
<li><strong>Presentation</strong>: log via middleware - HTTP request/response logging and the global exception handler.</li>
</ul>
<p>The <a href="https://milanjovanovic.tech/blog/domain-layer-clean-architecture">Domain layer</a> should never reference <code>ILogger</code>. Domain entities don't need to know about logging infrastructure.</p>
<img src="https://milanjovanovic.tech/blogs/articles/logging-strategy-clean-architecture/logging-by-layer.png" alt="Logging responsibility per Clean Architecture layer: middleware and the global handler at Presentation, a LoggingBehavior pipeline at Application, direct ILogger use at Infrastructure, and no logging in the Domain which raises events instead">
<h2>Pipeline Behavior: Log Every Use Case</h2>
<p>A single <a href="https://milanjovanovic.tech/blog/mediatr-pipeline-behaviors">MediatR pipeline behavior</a> can log every command and query automatically:</p>
<pre><code class="language-csharp">public class LoggingBehavior&lt;TRequest, TResponse&gt;
    : IPipelineBehavior&lt;TRequest, TResponse&gt;
    where TRequest : IRequest&lt;TResponse&gt;
{
    private readonly ILogger&lt;LoggingBehavior&lt;TRequest, TResponse&gt;&gt; _logger;

    public LoggingBehavior(
        ILogger&lt;LoggingBehavior&lt;TRequest, TResponse&gt;&gt; logger)
    {
        _logger = logger;
    }

    public async Task&lt;TResponse&gt; Handle(
        TRequest request,
        RequestHandlerDelegate&lt;TResponse&gt; next,
        CancellationToken ct)
    {
        var requestName = typeof(TRequest).Name;

        _logger.LogInformation(
            &quot;Handling {RequestName} {@Request}&quot;,
            requestName,
            request);

        var stopwatch = Stopwatch.StartNew();
        var response = await next();
        stopwatch.Stop();

        if (stopwatch.ElapsedMilliseconds &gt; 500)
        {
            _logger.LogWarning(
                &quot;Long-running request: {RequestName} took {ElapsedMs}ms&quot;,
                requestName,
                stopwatch.ElapsedMilliseconds);
        }

        if (response is Result { IsFailure: true } result)
        {
            _logger.LogError(
                &quot;Handled {RequestName} with error {ErrorCode} in {ElapsedMs}ms&quot;,
                requestName,
                result.Error.Code,
                stopwatch.ElapsedMilliseconds);
        }
        else
        {
            _logger.LogInformation(
                &quot;Handled {RequestName} in {ElapsedMs}ms&quot;,
                requestName,
                stopwatch.ElapsedMilliseconds);
        }

        return response;
    }
}
</code></pre>
<p>Register it in the Application layer's DI:</p>
<pre><code class="language-csharp">services.AddMediatR(config =&gt;
{
    config.RegisterServicesFromAssembly(assembly);
    config.AddOpenBehavior(typeof(LoggingBehavior&lt;,&gt;));
    config.AddOpenBehavior(typeof(ValidationBehavior&lt;,&gt;));
});
</code></pre>
<p>Now every command and query is logged (entry, exit, duration, and failed results) without touching a single handler.
Note that the behavior doesn't catch exceptions.
Those propagate to the global exception handler, so each failure gets logged exactly once.</p>
<h2>Structured Properties</h2>
<p>Enrich your logs with contextual properties using <a href="https://milanjovanovic.tech/blog/structured-logging-in-asp-net-core-with-serilog">structured logging</a>:</p>
<pre><code class="language-csharp">_logger.LogInformation(
    &quot;Handling {RequestName} with {UserId} {@Request}&quot;,
    requestName,
    currentUser.UserId,
    request);
</code></pre>
<p>Structured properties make logs searchable. You can find all requests from a specific user or all executions of <code>PlaceOrderCommand</code>.</p>
<p><strong>Be careful with <code>{@Request}</code>.</strong> This destructures the entire request object. If a command contains sensitive data (passwords, tokens), you'll log it.</p>
<p>Note that <code>[JsonIgnore]</code> won't help here - Serilog uses its own destructuring, not System.Text.Json.
Use the <a href="https://github.com/destructurama/attributed">Destructurama.Attributed</a> package to mask properties:</p>
<pre><code class="language-csharp">public sealed record LoginCommand(
    string Email,
    [property: NotLogged] string Password) : ICommand&lt;TokenResponse&gt;;
</code></pre>
<p>Or configure a destructuring policy in your Serilog setup.
The safest default: log the request <em>name</em>, not the request <em>body</em>, and opt in to logging specific properties.
I cover this and other pitfalls in <a href="https://milanjovanovic.tech/blog/5-serilog-best-practices-for-better-structured-logging"><strong>5 Serilog best practices</strong></a>.</p>
<h2>Infrastructure Layer Logging</h2>
<p>The <a href="https://milanjovanovic.tech/blog/infrastructure-layer-clean-architecture">Infrastructure layer</a> is where you log external interactions:</p>
<pre><code class="language-csharp">public class EmailService : IEmailService
{
    private readonly ILogger&lt;EmailService&gt; _logger;
    private readonly SmtpClient _client;

    public EmailService(ILogger&lt;EmailService&gt; logger, SmtpClient client)
    {
        _logger = logger;
        _client = client;
    }

    public async Task SendAsync(
        string to, string subject, string body, CancellationToken ct)
    {
        _logger.LogInformation(
            &quot;Sending email to {Recipient} with subject {Subject}&quot;,
            to,
            subject);

        try
        {
            await _client.SendMailAsync(
                new MailMessage(&quot;noreply@app.com&quot;, to, subject, body), ct);

            _logger.LogInformation(
                &quot;Email sent to {Recipient}&quot;, to);
        }
        catch (SmtpException ex)
        {
            _logger.LogError(ex,
                &quot;Failed to send email to {Recipient}: {Error}&quot;,
                to,
                ex.Message);
            throw;
        }
    }
}
</code></pre>
<p>Log before and after external calls. Log failures with full exception details. This is your debugging lifeline when third-party services fail.</p>
<h2>Request/Response Logging Middleware</h2>
<p>Log HTTP requests at the API layer with middleware:</p>
<pre><code class="language-csharp">public class RequestLoggingMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger&lt;RequestLoggingMiddleware&gt; _logger;

    public RequestLoggingMiddleware(
        RequestDelegate next,
        ILogger&lt;RequestLoggingMiddleware&gt; logger)
    {
        _next = next;
        _logger = logger;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        var stopwatch = Stopwatch.StartNew();

        try
        {
            await _next(context);
        }
        finally
        {
            stopwatch.Stop();

            _logger.LogInformation(
                &quot;HTTP {Method} {Path} responded {StatusCode} in {ElapsedMs}ms&quot;,
                context.Request.Method,
                context.Request.Path,
                context.Response.StatusCode,
                stopwatch.ElapsedMilliseconds);
        }
    }
}
</code></pre>
<p>Or use Serilog's built-in request logging:</p>
<pre><code class="language-csharp">app.UseSerilogRequestLogging(options =&gt;
{
    options.EnrichDiagnosticContext = (diagnosticContext, httpContext) =&gt;
    {
        diagnosticContext.Set(&quot;UserId&quot;,
            httpContext.User.FindFirstValue(ClaimTypes.NameIdentifier));
    };
});
</code></pre>
<h2>Domain Events Instead of Logging</h2>
<p>Instead of injecting <code>ILogger</code> into domain entities, raise <a href="https://milanjovanovic.tech/blog/how-to-use-domain-events-to-build-loosely-coupled-systems">domain events</a>:</p>
<pre><code class="language-csharp">public class Order : AggregateRoot
{
    public OrderStatus Status { get; private set; }
    public string? CancellationReason { get; private set; }

    public void Cancel(string reason)
    {
        if (Status == OrderStatus.Shipped)
            throw new DomainException(&quot;Cannot cancel a shipped order.&quot;);

        Status = OrderStatus.Cancelled;
        CancellationReason = reason;

        // Don't log here - raise an event
        RaiseDomainEvent(new OrderCancelledDomainEvent(Id, reason));
    }
}
</code></pre>
<p>Handle the event in the Application or Infrastructure layer where logging is appropriate:</p>
<pre><code class="language-csharp">public class OrderCancelledEventHandler
    : INotificationHandler&lt;OrderCancelledDomainEvent&gt;
{
    private readonly ILogger&lt;OrderCancelledEventHandler&gt; _logger;

    public OrderCancelledEventHandler(
        ILogger&lt;OrderCancelledEventHandler&gt; logger)
    {
        _logger = logger;
    }

    public Task Handle(
        OrderCancelledDomainEvent notification, CancellationToken ct)
    {
        _logger.LogInformation(
            &quot;Order {OrderId} cancelled. Reason: {Reason}&quot;,
            notification.OrderId,
            notification.Reason);

        return Task.CompletedTask;
    }
}
</code></pre>
<p>The domain stays clean. The logging happens in the right layer.</p>
<h2>Error Logging</h2>
<p>Log exceptions in the <a href="https://milanjovanovic.tech/blog/global-error-handling-in-aspnetcore-from-middleware-to-modern-handlers">global error handler</a>:</p>
<pre><code class="language-csharp">public class GlobalExceptionHandler : IExceptionHandler
{
    private readonly ILogger&lt;GlobalExceptionHandler&gt; _logger;

    public GlobalExceptionHandler(ILogger&lt;GlobalExceptionHandler&gt; logger)
    {
        _logger = logger;
    }

    public async ValueTask&lt;bool&gt; TryHandleAsync(
        HttpContext httpContext,
        Exception exception,
        CancellationToken ct)
    {
        _logger.LogError(
            exception,
            &quot;Unhandled exception: {Message}&quot;,
            exception.Message);

        httpContext.Response.StatusCode = 500;
        await httpContext.Response.WriteAsJsonAsync(new ProblemDetails
        {
            Status = 500,
            Title = &quot;Internal Server Error&quot;
        }, ct);

        return true;
    }
}
</code></pre>
<p>Log the full exception with stack trace. Return a generic message to the client.</p>
<h2>Log Levels</h2>
<p>Use the right level for the right situation:</p>
<ul>
<li><code>Trace</code> - detailed diagnostic info (never in production)</li>
<li><code>Debug</code> - development-time debugging</li>
<li><code>Information</code> - normal operations, use case execution</li>
<li><code>Warning</code> - slow requests, degraded performance</li>
<li><code>Error</code> - unhandled exceptions, failed operations</li>
<li><code>Critical</code> - application startup failures, data corruption</li>
</ul>
<h2>Logging Rules by Layer</h2>
<p>Logging in Clean Architecture follows the dependency rule:</p>
<ol>
<li><strong>Domain layer</strong> - No logging. Raise domain events instead.</li>
<li><strong>Application layer</strong> - Pipeline behaviors for automatic use case logging.</li>
<li><strong>Infrastructure layer</strong> - Direct <code>ILogger</code> injection for external integrations.</li>
<li><strong>Presentation layer</strong> - Middleware for HTTP request/response logging.</li>
<li><strong>Global error handler</strong> - Catches and logs all unhandled exceptions.</li>
</ol>
<p>Use structured logging everywhere. Enrich with correlation IDs and user context. Let the architecture guide where logging belongs.</p>
<p>Thanks for reading, and stay awesome!</p>
<hr>
]]></content:encoded>
            <author>milan@milanjovanovic.tech (Milan Jovanović)</author>
        </item>
        <item>
            <title><![CDATA[The Infrastructure Layer in Clean Architecture]]></title>
            <link>https://milanjovanovic.tech/blog/infrastructure-layer-clean-architecture</link>
            <guid isPermaLink="false">https://milanjovanovic.tech/blog/infrastructure-layer-clean-architecture</guid>
            <pubDate>Tue, 11 Aug 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Databases, message brokers, email providers, caches: everything that talks to the outside world lives in the Infrastructure layer.]]></description>
            <content:encoded><![CDATA[<p>The Infrastructure layer gets the least attention in Clean Architecture discussions, yet it's where most of the actual code ends up.
Databases, message brokers, email providers, caching: all of it lives here, behind interfaces the inner layers define.
Here is how to structure the Infrastructure layer in .NET so the frameworks stay out of your domain.</p>
<h2>What Is the Infrastructure Layer?</h2>
<p>The Infrastructure layer is the outermost layer in <a href="https://milanjovanovic.tech/blog/clean-architecture-and-the-benefits-of-structured-software-design">Clean Architecture</a>. It provides implementations for the abstractions defined in the Domain and <a href="https://milanjovanovic.tech/blog/application-layer-clean-architecture">Application layers</a>.</p>
<p>Everything that talks to the outside world lives here:</p>
<ul>
<li><strong>Database access</strong> - EF Core <code>DbContext</code>, Dapper queries, repository implementations</li>
<li><strong>External API clients</strong> - payment gateways, email services, third-party APIs</li>
<li><strong>Message brokers</strong> - RabbitMQ, Azure Service Bus, Kafka</li>
<li><strong>File storage</strong> - local disk, Azure Blob Storage, S3</li>
<li><strong>Caching</strong> - Redis, in-memory cache</li>
<li><strong>Authentication providers</strong> - Identity, OAuth, JWT token services</li>
</ul>
<p>The Infrastructure layer depends on the Domain and Application layers. It implements their interfaces. The inner layers never reference Infrastructure directly.</p>
<img src="https://milanjovanovic.tech/blogs/articles/infrastructure-layer-clean-architecture/dependency-direction.png" alt="Dependency arrows pointing inward: Infrastructure implements the interfaces of Application, which depends on Domain, so the inner layers never reference Infrastructure">
<h2>Implementing Repository Interfaces</h2>
<p>The Domain layer defines repository interfaces (some teams keep them in the Application layer instead; either placement works). Infrastructure provides the implementation:</p>
<pre><code class="language-csharp">// Domain layer - the interface
public interface IOrderRepository
{
    Task&lt;Order?&gt; GetByIdAsync(Guid id, CancellationToken cancellationToken = default);
    void Add(Order order);
}
</code></pre>
<pre><code class="language-csharp">// Infrastructure layer - the implementation
public sealed class OrderRepository : IOrderRepository
{
    private readonly AppDbContext _dbContext;

    public OrderRepository(AppDbContext dbContext)
    {
        _dbContext = dbContext;
    }

    public async Task&lt;Order?&gt; GetByIdAsync(
        Guid id,
        CancellationToken cancellationToken = default)
    {
        return await _dbContext.Orders
            .Include(o =&gt; o.LineItems)
            .FirstOrDefaultAsync(o =&gt; o.Id == id, cancellationToken);
    }

    public void Add(Order order)
    {
        _dbContext.Orders.Add(order);
    }
}
</code></pre>
<p>The repository pattern keeps EF Core confined to the Infrastructure layer. Your domain logic never sees a <code>DbContext</code>.</p>
<h2>The DbContext</h2>
<p>Your <code>DbContext</code> lives in Infrastructure and handles all EF Core configuration:</p>
<pre><code class="language-csharp">public sealed class AppDbContext : DbContext, IUnitOfWork
{
    public AppDbContext(DbContextOptions&lt;AppDbContext&gt; options) : base(options) { }

    public DbSet&lt;Customer&gt; Customers =&gt; Set&lt;Customer&gt;();
    public DbSet&lt;Order&gt; Orders =&gt; Set&lt;Order&gt;();

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.ApplyConfigurationsFromAssembly(
            typeof(AppDbContext).Assembly);
    }
}
</code></pre>
<p>Entity configurations use <code>IEntityTypeConfiguration&lt;T&gt;</code>:</p>
<pre><code class="language-csharp">public sealed class OrderConfiguration : IEntityTypeConfiguration&lt;Order&gt;
{
    public void Configure(EntityTypeBuilder&lt;Order&gt; builder)
    {
        builder.HasKey(o =&gt; o.Id);

        builder.Property(o =&gt; o.Status)
            .HasConversion(
                status =&gt; status.Name,
                name =&gt; OrderStatus.FromName(name));

        builder.HasMany(o =&gt; o.LineItems)
            .WithOne()
            .HasForeignKey(li =&gt; li.OrderId)
            .OnDelete(DeleteBehavior.Cascade);

        builder.ComplexProperty(o =&gt; o.TotalAmount, money =&gt;
        {
            money.Property(m =&gt; m.Amount).HasColumnName(&quot;total_amount&quot;);
            money.Property(m =&gt; m.Currency).HasColumnName(&quot;total_currency&quot;);
        });
    }
}
</code></pre>
<p><code>OrderStatus</code> is a smart enum from the <a href="https://milanjovanovic.tech/blog/domain-layer-clean-architecture">Domain layer</a>, a class rather than a plain <code>enum</code>, so we tell EF Core explicitly how to convert it: store the <code>Name</code>, rehydrate with the <code>FromName</code> lookup on the <code>Enumeration&lt;TEnum&gt;</code> base class.</p>
<h2>Implementing External Service Interfaces</h2>
<p>The Application layer defines interfaces for external services. Infrastructure implements them:</p>
<pre><code class="language-csharp">// Application layer
public interface IEmailService
{
    Task SendOrderConfirmationAsync(
        string recipientEmail,
        Guid orderId,
        CancellationToken cancellationToken = default);
}
</code></pre>
<pre><code class="language-csharp">// Infrastructure layer
public sealed class EmailSettings
{
    public string BaseUrl { get; init; } = string.Empty;
    public string OrderConfirmationTemplateId { get; init; } = string.Empty;
}

public sealed class EmailService : IEmailService
{
    private readonly IHttpClientFactory _httpClientFactory;
    private readonly EmailSettings _settings;

    public EmailService(
        IHttpClientFactory httpClientFactory,
        IOptions&lt;EmailSettings&gt; settings)
    {
        _httpClientFactory = httpClientFactory;
        _settings = settings.Value;
    }

    public async Task SendOrderConfirmationAsync(
        string recipientEmail,
        Guid orderId,
        CancellationToken cancellationToken = default)
    {
        var client = _httpClientFactory.CreateClient(&quot;EmailApi&quot;);

        var request = new SendEmailRequest
        {
            To = recipientEmail,
            Subject = &quot;Order Confirmed&quot;,
            TemplateId = _settings.OrderConfirmationTemplateId,
            Data = new { OrderId = orderId }
        };

        await client.PostAsJsonAsync(&quot;/v1/emails&quot;, request, cancellationToken);
    }
}
</code></pre>
<h2>Implementing Caching</h2>
<p>Application layer defines the caching abstraction:</p>
<pre><code class="language-csharp">// Application layer
public interface ICacheService
{
    Task&lt;T?&gt; GetAsync&lt;T&gt;(string key, CancellationToken cancellationToken = default);
    Task SetAsync&lt;T&gt;(string key, T value, TimeSpan? expiration = null, CancellationToken cancellationToken = default);
    Task RemoveAsync(string key, CancellationToken cancellationToken = default);
}
</code></pre>
<pre><code class="language-csharp">// Infrastructure layer
public sealed class RedisCacheService : ICacheService
{
    private readonly IDistributedCache _cache;

    public RedisCacheService(IDistributedCache cache)
    {
        _cache = cache;
    }

    public async Task&lt;T?&gt; GetAsync&lt;T&gt;(
        string key,
        CancellationToken cancellationToken = default)
    {
        var bytes = await _cache.GetAsync(key, cancellationToken);

        return bytes is null
            ? default
            : JsonSerializer.Deserialize&lt;T&gt;(bytes);
    }

    public async Task SetAsync&lt;T&gt;(
        string key,
        T value,
        TimeSpan? expiration = null,
        CancellationToken cancellationToken = default)
    {
        var bytes = JsonSerializer.SerializeToUtf8Bytes(value);

        var options = new DistributedCacheEntryOptions
        {
            AbsoluteExpirationRelativeToNow = expiration ?? TimeSpan.FromMinutes(5)
        };

        await _cache.SetAsync(key, bytes, options, cancellationToken);
    }

    public async Task RemoveAsync(
        string key,
        CancellationToken cancellationToken = default)
    {
        await _cache.RemoveAsync(key, cancellationToken);
    }
}
</code></pre>
<h2>DI Registration Module</h2>
<p>Keep your Infrastructure DI registrations organized in a single method:</p>
<pre><code class="language-csharp">public static class DependencyInjection
{
    public static IServiceCollection AddInfrastructure(
        this IServiceCollection services,
        IConfiguration configuration)
    {
        services.AddDbContext&lt;AppDbContext&gt;(options =&gt;
            options.UseNpgsql(configuration.GetConnectionString(&quot;Database&quot;)));

        services.AddScoped&lt;IUnitOfWork&gt;(sp =&gt;
            sp.GetRequiredService&lt;AppDbContext&gt;());

        services.AddScoped&lt;IOrderRepository, OrderRepository&gt;();
        services.AddScoped&lt;ICustomerRepository, CustomerRepository&gt;();

        services.Configure&lt;EmailSettings&gt;(configuration.GetSection(&quot;Email&quot;));

        services.AddHttpClient(&quot;EmailApi&quot;, client =&gt;
            client.BaseAddress = new Uri(configuration[&quot;Email:BaseUrl&quot;]!));

        services.AddScoped&lt;IEmailService, EmailService&gt;();
        services.AddScoped&lt;ICacheService, RedisCacheService&gt;();

        services.AddStackExchangeRedisCache(options =&gt;
            options.Configuration = configuration.GetConnectionString(&quot;Redis&quot;));

        return services;
    }
}
</code></pre>
<p>Then in <code>Program.cs</code>:</p>
<pre><code class="language-csharp">builder.Services.AddInfrastructure(builder.Configuration);
</code></pre>
<p>This keeps the <strong>Presentation layer</strong> from knowing about Infrastructure internals.</p>
<h2>Folder Structure</h2>
<pre><code>Infrastructure/
  Data/
    AppDbContext.cs
    Configurations/
      OrderConfiguration.cs
      CustomerConfiguration.cs
    Repositories/
      OrderRepository.cs
      CustomerRepository.cs
    Interceptors/
      PublishDomainEventsInterceptor.cs
  Services/
    EmailService.cs
    PaymentService.cs
  Caching/
    RedisCacheService.cs
  Messaging/
    EventBus.cs
    Consumers/
      OrderCompletedConsumer.cs
  DependencyInjection.cs
</code></pre>
<h2>Common Mistakes</h2>
<p><strong>1. Leaking Infrastructure into the Domain.</strong> If your entity has <code>[Column]</code> or <code>[Table]</code> attributes, you've coupled the Domain to EF Core. Use <code>IEntityTypeConfiguration&lt;T&gt;</code> instead.</p>
<p><strong>2. Not using <code>IEntityTypeConfiguration&lt;T&gt;</code>.</strong> Putting all configuration in <code>OnModelCreating</code> becomes unmanageable. One config class per entity.</p>
<p><strong>3. Referencing Infrastructure from Application.</strong> The Application layer should only use interfaces. If you see <code>using Infrastructure;</code> in an Application class, something is wrong.</p>
<p><strong>4. Missing the Unit of Work.</strong> Don't call <code>SaveChanges</code> in every repository method. Use a dedicated Unit of Work that commits after the use case completes.</p>
<p><strong>5. Fat Infrastructure classes.</strong> If your email service also handles templates, formatting, and retry logic, split it. Keep each implementation focused.</p>
<h2>Takeaway</h2>
<p>The Infrastructure layer is where all external concerns live. It implements the interfaces defined by inner layers and keeps framework dependencies from leaking inward.</p>
<p>Structure it by concern (Data, Services, Caching, Messaging), register everything cleanly, and test it with <a href="https://milanjovanovic.tech/blog/testcontainers-integration-testing-using-docker-in-dotnet">integration tests</a> that verify actual external interactions.</p>
<p>Thanks for reading, and stay awesome!</p>
<hr>
]]></content:encoded>
            <author>milan@milanjovanovic.tech (Milan Jovanović)</author>
        </item>
        <item>
            <title><![CDATA[Exception Handling Strategy in Clean Architecture]]></title>
            <link>https://milanjovanovic.tech/blog/exception-handling-clean-architecture</link>
            <guid isPermaLink="false">https://milanjovanovic.tech/blog/exception-handling-clean-architecture</guid>
            <pubDate>Tue, 11 Aug 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Expected failures are not exceptional, so stop throwing them. A clear strategy gives each layer one job: the domain throws on invariant violations, the…]]></description>
            <content:encoded><![CDATA[<p>Every .NET codebase eventually develops an exception handling strategy, usually by accident.
One handler throws, another returns null, and the API layer plays catch-and-guess with whatever bubbles up.
Clean Architecture gives you a better option: each layer gets one clear job in the error handling story.
Here is the full strategy, from domain invariants to Problem Details responses.</p>
<h2>The Problem</h2>
<p>Exceptions fly around your application - validation errors, not-found scenarios, business rule violations, infrastructure failures. Without a clear strategy, exception handling becomes inconsistent:</p>
<ul>
<li>Some handlers throw exceptions, others return nulls</li>
<li>The API layer catches <code>NullReferenceException</code> and guesses what went wrong</li>
<li>Business errors are mixed with infrastructure failures</li>
<li>Error responses are inconsistent across endpoints</li>
</ul>
<h2>A Layered Exception Strategy</h2>
<p>Each layer in <a href="https://milanjovanovic.tech/blog/clean-architecture-and-the-benefits-of-structured-software-design">Clean Architecture</a> handles errors differently:</p>
<ul>
<li><strong>Domain</strong>: throw domain exceptions for invariant violations</li>
<li><strong>Application</strong>: return <code>Result&lt;T&gt;</code> for expected business errors</li>
<li><strong>Infrastructure</strong>: let infrastructure exceptions propagate</li>
<li><strong>Presentation</strong>: map results and exceptions to HTTP responses</li>
</ul>
<h2>Domain Layer: Guard Invariants</h2>
<p>The <a href="https://milanjovanovic.tech/blog/domain-layer-clean-architecture">Domain layer</a> throws exceptions when invariants are violated - things that should never happen if the system is working correctly:</p>
<pre><code class="language-csharp">public class Order
{
    private Order(Guid customerId, List&lt;LineItem&gt; lineItems)
    {
        if (customerId == Guid.Empty)
            throw new DomainException(&quot;Customer ID cannot be empty.&quot;);

        if (lineItems.Count == 0)
            throw new DomainException(&quot;Order must have at least one line item.&quot;);

        Id = Guid.NewGuid();
        CustomerId = customerId;
        LineItems = lineItems;
        Status = OrderStatus.Draft;
    }

    public Guid Id { get; private set; }
    public Guid CustomerId { get; private set; }
    public List&lt;LineItem&gt; LineItems { get; private set; }
    public OrderStatus Status { get; private set; }
    public string? CancellationReason { get; private set; }

    public static Order Create(Guid customerId, List&lt;LineItem&gt; lineItems) =&gt;
        new(customerId, lineItems);

    public void Cancel(string reason)
    {
        if (Status == OrderStatus.Shipped)
            throw new DomainException(&quot;Cannot cancel a shipped order.&quot;);

        Status = OrderStatus.Cancelled;
        CancellationReason = reason;
    }
}
</code></pre>
<p>Domain exceptions signal bugs or invalid state transitions. They're not for expected scenarios like &quot;customer not found.&quot;</p>
<pre><code class="language-csharp">public class DomainException : Exception
{
    public DomainException(string message) : base(message) { }
}
</code></pre>
<h2>Application Layer: Result Pattern</h2>
<p>The <a href="https://milanjovanovic.tech/blog/application-layer-clean-architecture">Application layer</a> uses the <a href="https://milanjovanovic.tech/blog/functional-error-handling-in-dotnet-with-the-result-pattern">Result pattern</a> for expected business failures:</p>
<pre><code class="language-csharp">public sealed class PlaceOrderCommandHandler
    : ICommandHandler&lt;PlaceOrderCommand, Guid&gt;
{
    private readonly IOrderRepository _orderRepository;
    private readonly ICustomerRepository _customerRepository;
    private readonly IUnitOfWork _unitOfWork;

    public PlaceOrderCommandHandler(
        IOrderRepository orderRepository,
        ICustomerRepository customerRepository,
        IUnitOfWork unitOfWork)
    {
        _orderRepository = orderRepository;
        _customerRepository = customerRepository;
        _unitOfWork = unitOfWork;
    }

    public async Task&lt;Result&lt;Guid&gt;&gt; Handle(
        PlaceOrderCommand command, CancellationToken ct)
    {
        var customer = await _customerRepository.GetByIdAsync(
            command.CustomerId, ct);

        if (customer is null)
        {
            return Result.Failure&lt;Guid&gt;(
                CustomerErrors.NotFound(command.CustomerId));
        }

        if (!customer.IsActive)
        {
            return Result.Failure&lt;Guid&gt;(
                CustomerErrors.Inactive(command.CustomerId));
        }

        var order = Order.Create(command.CustomerId, command.Items);

        _orderRepository.Add(order);
        await _unitOfWork.SaveChangesAsync(ct);

        return order.Id;
    }
}
</code></pre>
<p>Define typed errors:</p>
<pre><code class="language-csharp">public static class CustomerErrors
{
    public static Error NotFound(Guid id) =&gt; new(
        &quot;Customer.NotFound&quot;,
        $&quot;Customer with ID '{id}' was not found.&quot;,
        ErrorType.NotFound);

    public static Error Inactive(Guid id) =&gt; new(
        &quot;Customer.Inactive&quot;,
        $&quot;Customer with ID '{id}' is inactive.&quot;,
        ErrorType.Conflict);
}

public record Error(string Code, string Description, ErrorType Type);

public enum ErrorType
{
    Validation,
    NotFound,
    Conflict,
    Forbidden,
    Failure
}
</code></pre>
<p>The handler never throws for expected scenarios. <code>Customer not found</code> is not exceptional - it's an expected outcome.</p>
<h2>Validation: Before the Handler</h2>
<p>Use a validation pipeline behavior to reject invalid requests before the handler runs:</p>
<pre><code class="language-csharp">public class ValidationBehavior&lt;TRequest, TResponse&gt;
    : IPipelineBehavior&lt;TRequest, TResponse&gt;
    where TRequest : IRequest&lt;TResponse&gt;
    where TResponse : Result
{
    private readonly IEnumerable&lt;IValidator&lt;TRequest&gt;&gt; _validators;

    public ValidationBehavior(IEnumerable&lt;IValidator&lt;TRequest&gt;&gt; validators)
    {
        _validators = validators;
    }

    public async Task&lt;TResponse&gt; Handle(
        TRequest request,
        RequestHandlerDelegate&lt;TResponse&gt; next,
        CancellationToken ct)
    {
        var failures = _validators
            .Select(v =&gt; v.Validate(request))
            .SelectMany(result =&gt; result.Errors)
            .Where(f =&gt; f is not null)
            .ToList();

        if (failures.Count != 0)
        {
            return CreateValidationResult&lt;TResponse&gt;(failures);
        }

        return await next();
    }
}
</code></pre>
<p>Validation errors are returned as <code>Result.Failure</code> with <code>ErrorType.Validation</code> - not thrown as exceptions.</p>
<p>The <code>CreateValidationResult</code> helper needs a small amount of reflection to construct either <code>Result</code> or <code>Result&lt;T&gt;</code> depending on the request type.
I walk through the complete implementation in <a href="https://milanjovanovic.tech/blog/cqrs-validation-with-mediatr-pipeline-and-fluentvalidation"><strong>CQRS validation with MediatR pipeline and FluentValidation</strong></a>.</p>
<h2>Presentation Layer: Map to HTTP</h2>
<p>The API layer maps <code>Result&lt;T&gt;</code> to HTTP responses:</p>
<pre><code class="language-csharp">app.MapPost(&quot;/api/orders&quot;, async (
    PlaceOrderRequest request,
    ISender sender,
    CancellationToken ct) =&gt;
{
    var command = new PlaceOrderCommand(request.CustomerId, request.Items);
    var result = await sender.Send(command, ct);

    return result.IsSuccess
        ? Results.Created($&quot;/api/orders/{result.Value}&quot;, result.Value)
        : result.ToProblemDetails();
});
</code></pre>
<pre><code class="language-csharp">public static IResult ToProblemDetails(this Result result)
{
    if (result.IsSuccess)
    {
        throw new InvalidOperationException(
            &quot;Can't convert a success result to a problem response.&quot;);
    }

    return Results.Problem(
        statusCode: result.Error.Type switch
        {
            ErrorType.Validation =&gt; StatusCodes.Status400BadRequest,
            ErrorType.NotFound =&gt; StatusCodes.Status404NotFound,
            ErrorType.Conflict =&gt; StatusCodes.Status409Conflict,
            ErrorType.Forbidden =&gt; StatusCodes.Status403Forbidden,
            _ =&gt; StatusCodes.Status500InternalServerError
        },
        title: result.Error.Code,
        detail: result.Error.Description);
}
</code></pre>
<h2>Global Exception Handler: Safety Net</h2>
<p>For unexpected exceptions (bugs, infrastructure failures), use <a href="https://milanjovanovic.tech/blog/global-error-handling-in-aspnetcore-from-middleware-to-modern-handlers">global error handling</a>:</p>
<pre><code class="language-csharp">app.UseExceptionHandler(errorApp =&gt;
{
    errorApp.Run(async context =&gt;
    {
        var exception = context.Features
            .Get&lt;IExceptionHandlerFeature&gt;()?.Error;

        var (statusCode, title) = exception switch
        {
            DomainException =&gt; (400, &quot;Domain Rule Violation&quot;),
            _ =&gt; (500, &quot;Internal Server Error&quot;)
        };

        context.Response.StatusCode = statusCode;
        context.Response.ContentType = &quot;application/problem+json&quot;;

        await context.Response.WriteAsJsonAsync(new ProblemDetails
        {
            Status = statusCode,
            Title = title,
            Detail = statusCode == 500
                ? &quot;An unexpected error occurred.&quot;
                : exception?.Message
        });
    });
});
</code></pre>
<p>Or with .NET 8's <code>IExceptionHandler</code>:</p>
<pre><code class="language-csharp">public class GlobalExceptionHandler : IExceptionHandler
{
    private readonly ILogger&lt;GlobalExceptionHandler&gt; _logger;

    public GlobalExceptionHandler(ILogger&lt;GlobalExceptionHandler&gt; logger)
    {
        _logger = logger;
    }

    public async ValueTask&lt;bool&gt; TryHandleAsync(
        HttpContext httpContext,
        Exception exception,
        CancellationToken ct)
    {
        _logger.LogError(exception, &quot;Unhandled exception: {Message}&quot;, exception.Message);

        httpContext.Response.StatusCode = exception switch
        {
            DomainException =&gt; StatusCodes.Status400BadRequest,
            _ =&gt; StatusCodes.Status500InternalServerError
        };

        await httpContext.Response.WriteAsJsonAsync(new ProblemDetails
        {
            Status = httpContext.Response.StatusCode,
            Title = &quot;An error occurred&quot;,
            Detail = httpContext.Response.StatusCode == 500
                ? &quot;An unexpected error occurred.&quot;
                : exception.Message
        }, ct);

        return true;
    }
}
</code></pre>
<p><strong>Never expose internal exception details in production.</strong> Log the full exception, return a generic message to the client.</p>
<p>Either way, keep the response shape consistent with <a href="https://milanjovanovic.tech/blog/problem-details-for-aspnetcore-apis"><strong>Problem Details</strong></a> so expected failures and unexpected errors look the same to API consumers.</p>
<h2>The Decision Flow</h2>
<p>When an error occurs, how do you decide what to do?</p>
<img src="https://milanjovanovic.tech/blogs/articles/exception-handling-clean-architecture/error-decision-flow.png" alt="Decision flow routing each error kind: invalid input, not found, and business rules become Result failures mapped to Problem Details, while domain invariant violations and infrastructure failures flow to the global exception handler">
<ol>
<li><strong>Invalid input</strong> → Validation behavior returns <code>Result.Failure(ValidationError)</code></li>
<li><strong>Entity not found</strong> → Handler returns <code>Result.Failure(NotFoundError)</code></li>
<li><strong>Business rule violation (expected)</strong> → Handler returns <code>Result.Failure(ConflictError)</code></li>
<li><strong>Domain invariant violated</strong> → Domain throws <code>DomainException</code> → global handler catches</li>
<li><strong>Infrastructure failure</strong> → Exception propagates → global handler catches and logs</li>
</ol>
<p>Rules of thumb:</p>
<ul>
<li><strong>Expected failures</strong> → <code>Result&lt;T&gt;</code></li>
<li><strong>Programming errors</strong> → Exceptions</li>
<li><strong>Infrastructure issues</strong> → Exceptions</li>
</ul>
<h2>Takeaway</h2>
<p>A consistent exception handling strategy in Clean Architecture:</p>
<ol>
<li>Domain layer throws exceptions for invariant violations</li>
<li>Application layer returns <code>Result&lt;T&gt;</code> for expected business errors</li>
<li>Validation happens before the handler via pipeline behaviors</li>
<li>The presentation layer maps results to HTTP responses</li>
<li>A global exception handler catches unexpected failures</li>
</ol>
<p>Stop mixing exceptions and return values randomly. Pick a clear strategy and apply it consistently.</p>
<p>Thanks for reading, and stay awesome!</p>
<hr>
]]></content:encoded>
            <author>milan@milanjovanovic.tech (Milan Jovanović)</author>
        </item>
        <item>
            <title><![CDATA[The Domain Layer in Clean Architecture]]></title>
            <link>https://milanjovanovic.tech/blog/domain-layer-clean-architecture</link>
            <guid isPermaLink="false">https://milanjovanovic.tech/blog/domain-layer-clean-architecture</guid>
            <pubDate>Tue, 11 Aug 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Every business rule in your system should have exactly one home: the Domain layer. That means entities that guard their invariants, value objects instead of…]]></description>
            <content:encoded><![CDATA[<p>Every Clean Architecture diagram puts the Domain layer at the center, but most of them never tell you what actually goes inside it.
That's a problem, because the Domain layer is the one part of the system you can't afford to get wrong.
Here is what belongs there, what doesn't, and how to keep it free of every framework concern.</p>
<h2>What Is the Domain Layer?</h2>
<p>The Domain layer is the innermost layer in <a href="https://milanjovanovic.tech/blog/clean-architecture-and-the-benefits-of-structured-software-design">Clean Architecture</a>. It sits at the center of the dependency graph, and nothing depends on anything outside of it.</p>
<p>This is where your <strong>business rules</strong> live. Not HTTP concerns. Not database details. Just the rules that make your business what it is.</p>
<p>The Domain layer contains:</p>
<ul>
<li><strong>Entities</strong> - objects with identity and lifecycle</li>
<li><strong>Value Objects</strong> - immutable objects defined by their attributes</li>
<li><strong>Domain Events</strong> - notifications that something meaningful happened</li>
<li><strong>Enumerations</strong> - smart enums representing domain concepts</li>
<li><strong>Domain Services</strong> - business logic that doesn't belong to a single entity</li>
<li><strong>Repository Interfaces</strong> - abstractions for data access</li>
<li><strong>Custom Exceptions</strong> - domain-specific error types</li>
</ul>
<h2>Entities</h2>
<p>Entities are objects defined by their <strong>identity</strong>, not their attributes. Two customers with the same name are still different customers.</p>
<pre><code class="language-csharp">public abstract class Entity : IEquatable&lt;Entity&gt;
{
    private readonly List&lt;IDomainEvent&gt; _domainEvents = new();

    protected Entity(Guid id)
    {
        Id = id;
    }

    public Guid Id { get; private init; }

    public IReadOnlyList&lt;IDomainEvent&gt; DomainEvents =&gt; _domainEvents.AsReadOnly();

    public void ClearDomainEvents() =&gt; _domainEvents.Clear();

    protected void RaiseDomainEvent(IDomainEvent domainEvent) =&gt;
        _domainEvents.Add(domainEvent);

    public bool Equals(Entity? other)
    {
        return other is not null &amp;&amp; Id == other.Id;
    }

    public override bool Equals(object? obj)
    {
        return obj is Entity entity &amp;&amp; Equals(entity);
    }

    public override int GetHashCode() =&gt; Id.GetHashCode();
}
</code></pre>
<p>The base class also collects domain events (we'll define <code>IDomainEvent</code> in a moment), so entities can record what happened and let an outer layer publish it after saving.</p>
<p>And a concrete entity:</p>
<pre><code class="language-csharp">public sealed class Customer : Entity
{
    private Customer(Guid id, string name, Email email) : base(id)
    {
        Name = name;
        Email = email;
    }

    public string Name { get; private set; }
    public Email Email { get; private set; }

    public static Customer Create(string name, Email email)
    {
        var customer = new Customer(Guid.NewGuid(), name, email);

        customer.RaiseDomainEvent(new CustomerCreatedDomainEvent(customer.Id));

        return customer;
    }

    public void UpdateName(string name)
    {
        if (string.IsNullOrWhiteSpace(name))
        {
            throw new DomainException(&quot;Customer name cannot be empty.&quot;);
        }

        Name = name;
    }
}
</code></pre>
<p>Key principles:</p>
<ul>
<li><strong>Private constructor</strong> - creation goes through a factory method that enforces invariants</li>
<li><strong>Private setters</strong> - state changes go through methods that validate the transition</li>
<li><strong>Domain events</strong> - the entity signals when something important happens</li>
</ul>
<p>This is what makes the domain model the best place to <a href="https://milanjovanovic.tech/blog/what-invariants-are-and-why-a-domain-model-is-the-best-place-to-enforce-them"><strong>enforce invariants</strong></a>: there's no way to construct or mutate the entity into an invalid state.</p>
<h2>Value Objects</h2>
<p><a href="https://milanjovanovic.tech/blog/value-objects-in-dotnet-ddd-fundamentals">Value Objects</a> represent concepts with no identity. Two <code>Email(&quot;test@example.com&quot;)</code> instances are equal.</p>
<pre><code class="language-csharp">public sealed class Email : ValueObject
{
    private Email(string value) =&gt; Value = value;

    public string Value { get; }

    public static Result&lt;Email&gt; Create(string email)
    {
        if (string.IsNullOrWhiteSpace(email) || !email.Contains('@'))
        {
            return Result.Failure&lt;Email&gt;(DomainErrors.Email.InvalidFormat);
        }

        return new Email(email.Trim().ToLowerInvariant());
    }

    protected override IEnumerable&lt;object&gt; GetAtomicValues()
    {
        yield return Value;
    }
}
</code></pre>
<p>Use Value Objects to replace primitives wherever a concept has business rules attached to it.</p>
<h2>Domain Events</h2>
<p><a href="https://milanjovanovic.tech/blog/domain-events-vs-integration-events">Domain events</a> represent something that happened in the domain. They're raised by entities and handled by the <a href="https://milanjovanovic.tech/blog/application-layer-clean-architecture">Application layer</a>.</p>
<pre><code class="language-csharp">public interface IDomainEvent
{
    Guid Id { get; }
    DateTime OccurredOnUtc { get; }
}

public sealed record CustomerCreatedDomainEvent(Guid CustomerId) : IDomainEvent
{
    public Guid Id { get; } = Guid.NewGuid();
    public DateTime OccurredOnUtc { get; } = DateTime.UtcNow;
}
</code></pre>
<p>Domain events live in the Domain layer because they describe domain facts. But the <em>handlers</em> live in the Application layer.</p>
<h2>Smart Enumerations</h2>
<p>Instead of plain <code>enum</code> types, use smart enums for domain concepts that carry behavior:</p>
<pre><code class="language-csharp">public abstract class OrderStatus : Enumeration&lt;OrderStatus&gt;
{
    public static readonly OrderStatus Draft = new DraftStatus();
    public static readonly OrderStatus Confirmed = new ConfirmedStatus();
    public static readonly OrderStatus Shipped = new ShippedStatus();
    public static readonly OrderStatus Delivered = new DeliveredStatus();
    public static readonly OrderStatus Cancelled = new CancelledStatus();

    private OrderStatus(int id, string name) : base(id, name) { }

    public abstract bool CanTransitionTo(OrderStatus next);

    private sealed class DraftStatus : OrderStatus
    {
        public DraftStatus() : base(1, &quot;Draft&quot;) { }

        public override bool CanTransitionTo(OrderStatus next) =&gt;
            next == Confirmed || next == Cancelled;
    }

    private sealed class ConfirmedStatus : OrderStatus
    {
        public ConfirmedStatus() : base(2, &quot;Confirmed&quot;) { }

        public override bool CanTransitionTo(OrderStatus next) =&gt;
            next == Shipped || next == Cancelled;
    }

    // ... other statuses
}
</code></pre>
<p>The state transition rules are embedded in the enum itself. No service needed to check if an order can be cancelled.</p>
<p>The <code>Enumeration&lt;TEnum&gt;</code> base class handles equality and provides static lookups like <code>FromValue</code> and <code>FromName</code>, which is what the Infrastructure layer uses to persist the enum as a string.</p>
<img src="https://milanjovanovic.tech/blogs/articles/domain-layer-clean-architecture/order-status-state-machine.png" alt="State machine for OrderStatus showing the allowed transitions: Draft moves to Confirmed or Cancelled, Confirmed moves to Shipped or Cancelled, Shipped moves to Delivered, and Delivered and Cancelled are terminal states">
<h2>Repository Interfaces</h2>
<p>Repository interfaces can live in the Domain layer, right next to the aggregates they load and save. That's the convention I'm showing here. Their <strong>implementations</strong> always go in Infrastructure.</p>
<pre><code class="language-csharp">public interface ICustomerRepository
{
    Task&lt;Customer?&gt; GetByIdAsync(Guid id, CancellationToken cancellationToken = default);
    Task&lt;Customer?&gt; GetByEmailAsync(Email email, CancellationToken cancellationToken = default);
    void Add(Customer customer);
    void Update(Customer customer);
}
</code></pre>
<p>This follows the <a href="https://milanjovanovic.tech/blog/dependency-rule-clean-architecture">Dependency Rule</a>: the Domain layer defines what it needs, and outer layers provide it.</p>
<p>Note: some developers place repository interfaces in the Application layer instead. Both approaches are valid - the key is that implementations go in Infrastructure either way.</p>
<h2>Domain Services</h2>
<p>When business logic doesn't naturally belong to a single entity, use a Domain Service:</p>
<pre><code class="language-csharp">public sealed class PricingService
{
    public Money CalculateTotal(
        IReadOnlyCollection&lt;OrderLineItem&gt; items,
        DiscountCode? discountCode)
    {
        var subtotal = items.Aggregate(
            Money.Zero(Currency.Usd),
            (sum, item) =&gt; sum + item.TotalPrice);

        if (discountCode is not null)
        {
            subtotal = discountCode.Apply(subtotal);
        }

        return subtotal;
    }
}
</code></pre>
<p>Domain Services are stateless. They operate on entities and value objects passed to them.</p>
<h2>Domain Errors</h2>
<p>Define domain-specific errors in the Domain layer:</p>
<pre><code class="language-csharp">public static class DomainErrors
{
    public static class Email
    {
        public static readonly Error InvalidFormat = new(
            &quot;Email.InvalidFormat&quot;,
            &quot;The email address is not in a valid format.&quot;);
    }

    public static class Customer
    {
        public static readonly Error NotFound = new(
            &quot;Customer.NotFound&quot;,
            &quot;The customer was not found.&quot;);

        public static readonly Error EmailNotUnique = new(
            &quot;Customer.EmailNotUnique&quot;,
            &quot;The email address is already in use.&quot;);
    }

    public static class Order
    {
        public static readonly Error AlreadyCancelled = new(
            &quot;Order.AlreadyCancelled&quot;,
            &quot;The order has already been cancelled.&quot;);
    }
}
</code></pre>
<p>These errors are used with the <a href="https://milanjovanovic.tech/blog/functional-error-handling-in-dotnet-with-the-result-pattern">Result pattern</a> for explicit error handling without exceptions.</p>
<h2>Folder Structure</h2>
<pre><code>Domain/
  Customers/
    Customer.cs
    CustomerCreatedDomainEvent.cs
    ICustomerRepository.cs
  Orders/
    Order.cs
    OrderLineItem.cs
    OrderStatus.cs
    OrderCompletedDomainEvent.cs
    IOrderRepository.cs
  Shared/
    Entity.cs
    AggregateRoot.cs
    ValueObject.cs
    IDomainEvent.cs
    Result.cs
    Error.cs
    DomainException.cs
</code></pre>
<p>Organize by aggregate or concept, not by pattern type. Don't create folders like <code>Entities/</code>, <code>ValueObjects/</code>, <code>Events/</code>.</p>
<h2>What Does NOT Belong in the Domain Layer</h2>
<ul>
<li><strong>DTOs</strong> - those belong in the Application layer</li>
<li><strong>Database concerns</strong> - no <code>DbContext</code>, no <code>[Table]</code> attributes, no migration code</li>
<li><strong>Logging</strong> - the domain doesn't know about <code>ILogger</code></li>
<li><strong>External service calls</strong> - no HTTP, no message queue access</li>
<li><strong>Validation with FluentValidation</strong> - command/query validation belongs in Application</li>
</ul>
<p>The Domain layer has <strong>zero</strong> external package references. It references only .NET base class libraries.</p>
<h2>Enforcing the Rules</h2>
<p>Use <a href="https://milanjovanovic.tech/blog/enforcing-software-architecture-with-architecture-tests">architecture tests</a> to keep the Domain layer pure:</p>
<pre><code class="language-csharp">[Fact]
public void Domain_Should_Not_Have_Dependencies_On_Other_Layers()
{
    var result = Types
        .InAssembly(typeof(Customer).Assembly)
        .ShouldNot()
        .HaveDependencyOnAny(&quot;Application&quot;, &quot;Infrastructure&quot;, &quot;Presentation&quot;)
        .GetResult();

    result.IsSuccessful.Should().BeTrue();
}
</code></pre>
<h2>Keep It Pure</h2>
<p>The Domain layer is the most stable and valuable part of your Clean Architecture solution. It contains the business rules that make your application unique - everything else is infrastructure.</p>
<p>Keep it pure, keep it focused, and the rest of your architecture will thank you.</p>
<p>Thanks for reading, and stay awesome!</p>
<hr>
]]></content:encoded>
            <author>milan@milanjovanovic.tech (Milan Jovanović)</author>
        </item>
        <item>
            <title><![CDATA[Domain Events vs Integration Events in .NET]]></title>
            <link>https://milanjovanovic.tech/blog/domain-events-vs-integration-events</link>
            <guid isPermaLink="false">https://milanjovanovic.tech/blog/domain-events-vs-integration-events</guid>
            <pubDate>Tue, 11 Aug 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[A domain event is handled in-process, inside the same transaction. An integration event crosses service boundaries through a message broker and needs the…]]></description>
            <content:encoded><![CDATA[<p>An order gets placed.
Inventory has to reserve stock right away, and a separate Notification service has to send a confirmation email eventually.
Same trigger, two very different events: one stays inside the bounded context and joins the transaction, the other crosses a message broker and arrives later.
Blur that line and you get lost events, accidental coupling, and confirmation emails for orders that never committed.</p>
<h2>Two Types of Events, Two Different Jobs</h2>
<p>Events are central to building loosely coupled systems.
But not all events are created equal.</p>
<p>In a well-designed .NET application, you'll typically work with two kinds:</p>
<ul>
<li><strong>Domain events</strong> - something happened <em>within</em> a bounded context</li>
<li><strong>Integration events</strong> - something happened that <em>other systems need to know about</em></li>
</ul>
<p>Confusing the two leads to tight coupling, inconsistent data, and architectural headaches.</p>
<p>Let's clear this up.</p>
<h2>What Are Domain Events?</h2>
<p>A <strong>domain event</strong> represents something meaningful that happened in your domain.
It's raised by an <a href="https://milanjovanovic.tech/blog/refactoring-from-an-anemic-domain-model-to-a-rich-domain-model">aggregate</a> and handled <em>within the same bounded context</em>.</p>
<pre><code class="language-csharp">public interface IDomainEvent : INotification;

public sealed record OrderPlacedDomainEvent(
    Guid OrderId,
    Guid CustomerId,
    decimal TotalAmount) : IDomainEvent;
</code></pre>
<p><code>IDomainEvent</code> extends MediatR's <code>INotification</code>, so we can dispatch events with <code>IMediator</code> and handle them with <code>INotificationHandler&lt;T&gt;</code>.
That couples the domain to MediatR's contracts.
If you want a fully pure domain, you can adapt events with a generic wrapper instead; I cover that tradeoff in the <a href="https://milanjovanovic.tech/blog/clean-architecture-solution-template-dotnet"><strong>Clean Architecture solution template</strong></a>.</p>
<p>When an order is placed, the <code>Order</code> aggregate raises this event:</p>
<pre><code class="language-csharp">public class Order : AggregateRoot
{
    public static Order Create(Customer customer, List&lt;LineItem&gt; items)
    {
        var order = new Order(customer.Id, items);

        order.RaiseDomainEvent(new OrderPlacedDomainEvent(
            order.Id,
            order.CustomerId,
            order.TotalAmount));

        return order;
    }
}
</code></pre>
<p>Domain events are typically:</p>
<ul>
<li><strong>In-process</strong> - they execute within the same application, same transaction</li>
<li><strong>Synchronous</strong> (usually) - handlers run before <code>SaveChanges</code> completes or right after</li>
<li><strong>Private</strong> - they stay inside the bounded context that raised them</li>
<li><strong>Consistent</strong> - they maintain strong consistency with the operation that triggered them</li>
</ul>
<h3>What Domain Event Handlers Do</h3>
<p>Domain event handlers react to what happened and execute side effects <em>within the same context</em>:</p>
<pre><code class="language-csharp">public class OrderPlacedDomainEventHandler : INotificationHandler&lt;OrderPlacedDomainEvent&gt;
{
    private readonly IInventoryService _inventoryService;

    public OrderPlacedDomainEventHandler(IInventoryService inventoryService)
    {
        _inventoryService = inventoryService;
    }

    public async Task Handle(
        OrderPlacedDomainEvent notification,
        CancellationToken cancellationToken)
    {
        await _inventoryService.ReserveStockAsync(notification.OrderId);
    }
}
</code></pre>
<p>Common uses for domain event handlers:</p>
<ul>
<li>Updating related data within the same aggregate or module</li>
<li>Enforcing business rules that span multiple entities</li>
<li>Preparing data for projections or read models</li>
</ul>
<p>For a full implementation guide, see my article on <a href="https://milanjovanovic.tech/blog/how-to-use-domain-events-to-build-loosely-coupled-systems">domain events in .NET</a>.</p>
<h2>What Are Integration Events?</h2>
<p>An <strong>integration event</strong> represents something that happened that <em>other bounded contexts or external systems</em> need to react to.</p>
<pre><code class="language-csharp">public interface IIntegrationEvent;

public sealed record OrderPlacedIntegrationEvent(
    Guid OrderId,
    Guid CustomerId,
    decimal TotalAmount,
    DateTime OccurredAt) : IIntegrationEvent;
</code></pre>
<p>Integration events are:</p>
<ul>
<li><strong>Published to a message broker</strong> (RabbitMQ, Azure Service Bus, Amazon SQS)</li>
<li><strong>Asynchronous</strong> - consumers process them at their own pace</li>
<li><strong>Public</strong> - they cross bounded context boundaries</li>
<li><strong>Eventually consistent</strong> - there's a delay between publishing and consuming</li>
</ul>
<h3>Publishing Integration Events</h3>
<p>You typically publish integration events <em>after</em> the domain transaction succeeds.
This is where the <a href="https://milanjovanovic.tech/blog/implementing-the-outbox-pattern">Outbox pattern</a> becomes critical - you need to guarantee that the event gets published even if the application crashes after saving.</p>
<pre><code class="language-csharp">public class PublishOrderPlacedIntegrationEventHandler
    : INotificationHandler&lt;OrderPlacedDomainEvent&gt;
{
    private readonly IOutboxWriter _outbox;

    public PublishOrderPlacedIntegrationEventHandler(IOutboxWriter outbox)
    {
        _outbox = outbox;
    }

    public async Task Handle(
        OrderPlacedDomainEvent notification,
        CancellationToken cancellationToken)
    {
        // Convert domain event to integration event
        var integrationEvent = new OrderPlacedIntegrationEvent(
            notification.OrderId,
            notification.CustomerId,
            notification.TotalAmount,
            DateTime.UtcNow);

        // Write to outbox (same transaction as domain changes)
        await _outbox.WriteAsync(integrationEvent, cancellationToken);
    }
}
</code></pre>
<p>MediatR runs every handler registered for a notification, so this handler executes alongside the inventory handler from earlier.
One reacts inside the bounded context, the other hands the event off to the outside world.</p>
<p>A background process picks up outbox messages and publishes them to the message broker.</p>
<h2>Key Differences</h2>
<p>Here's how the two compare, aspect by aspect:</p>
<ul>
<li><strong>Scope</strong>: domain events stay within a bounded context; integration events cross bounded context boundaries.</li>
<li><strong>Transport</strong>: domain events travel in-memory (MediatR or a custom dispatcher); integration events go through a message broker (RabbitMQ, Azure Service Bus, SQS).</li>
<li><strong>Consistency</strong>: domain events can participate in the same transaction; integration events are eventually consistent.</li>
<li><strong>Failure handling</strong>: domain event side effects roll back with the transaction; integration events need retries and idempotent consumers.</li>
<li><strong>Schema</strong>: a domain event is internal and can change freely; an integration event is a public contract that needs versioning.</li>
<li><strong>Timing</strong>: domain events are handled immediately; integration events are processed with a delay, at the consumer's pace.</li>
</ul>
<h2>The Flow: Domain Event → Integration Event</h2>
<p>In a well-architected system, the flow looks like this:</p>
<ol>
<li>An aggregate performs a business operation</li>
<li>The aggregate raises a <strong>domain event</strong></li>
<li>A domain event handler processes the event <em>within the same transaction</em></li>
<li>If other bounded contexts need to know, the handler writes an <strong>integration event</strong> to the <a href="https://milanjovanovic.tech/blog/implementing-the-outbox-pattern">outbox</a></li>
<li>A background worker publishes the integration event to the message broker</li>
<li>Consumers in other bounded contexts receive and process the event</li>
</ol>
<img src="https://milanjovanovic.tech/blogs/articles/domain-events-vs-integration-events/domain-to-integration-flow.png" alt="The flow from a domain event to an integration event: an aggregate raises a domain event handled in-process within the same transaction, that handler writes an integration event to the outbox, a background worker publishes it to the message broker, and a consumer in another bounded context processes it">
<p>This two-step approach gives you the best of both worlds:</p>
<ul>
<li>Strong consistency for domain-level side effects</li>
<li>Reliable asynchronous delivery for cross-boundary communication</li>
</ul>
<h2>Publishing Domain Events With EF Core</h2>
<p>A common pattern is to dispatch domain events when <code>SaveChanges</code> is called.
You can use an <a href="https://milanjovanovic.tech/blog/how-to-use-ef-core-interceptors">EF Core interceptor</a> for this:</p>
<pre><code class="language-csharp">public class PublishDomainEventsInterceptor : SaveChangesInterceptor
{
    private readonly IMediator _mediator;

    public PublishDomainEventsInterceptor(IMediator mediator)
    {
        _mediator = mediator;
    }

    public override async ValueTask&lt;InterceptionResult&lt;int&gt;&gt; SavingChangesAsync(
        DbContextEventData eventData,
        InterceptionResult&lt;int&gt; result,
        CancellationToken cancellationToken = default)
    {
        var dbContext = eventData.Context;

        if (dbContext is null)
        {
            return result;
        }

        var aggregates = dbContext.ChangeTracker
            .Entries&lt;AggregateRoot&gt;()
            .Select(entry =&gt; entry.Entity)
            .Where(aggregate =&gt; aggregate.DomainEvents.Count &gt; 0)
            .ToList();

        var domainEvents = aggregates
            .SelectMany(aggregate =&gt; aggregate.DomainEvents)
            .ToList();

        foreach (var aggregate in aggregates)
        {
            aggregate.ClearDomainEvents();
        }

        foreach (var domainEvent in domainEvents)
        {
            await _mediator.Publish(domainEvent, cancellationToken);
        }

        return result;
    }
}
</code></pre>
<p>One important subtlety: this interceptor uses <code>SavingChangesAsync</code>, which runs <strong>before</strong> the save completes.
Handlers execute inside the same transaction, so anything they add through the change tracker (like an outbox message) commits atomically with the domain changes.
That's exactly what the outbox handler above needs.
If your handlers only have side effects independent of the transaction, you can dispatch from <code>SavedChangesAsync</code> after the commit instead.
Just don't write to the outbox from there; dispatching after the commit reintroduces the exact dual-write problem the outbox is supposed to solve.</p>
<p>I wrote a dedicated guide on <a href="https://milanjovanovic.tech/blog/building-a-custom-domain-events-dispatcher-in-dotnet">building a custom domain events dispatcher</a> if you want the full implementation.</p>
<h2>Integration Event Contracts</h2>
<p>Since integration events cross boundaries, their schema is a <strong>public contract</strong>.
Treat them like an API:</p>
<ul>
<li><strong>Version them</strong> - don't break consumers when you change the event</li>
<li><strong>Keep them minimal</strong> - only include what consumers need</li>
<li><strong>Use primitive types</strong> - avoid domain-specific types that consumers would need to reference</li>
<li><strong>Document them</strong> - consumers need to know what to expect</li>
</ul>
<pre><code class="language-csharp">// ✅ Good: minimal, self-contained, uses primitives
public sealed record OrderPlacedIntegrationEvent(
    Guid OrderId,
    Guid CustomerId,
    decimal TotalAmount,
    DateTime OccurredAt);

// ❌ Bad: leaks domain details, forces consumers to reference your domain assembly
public sealed record OrderPlacedIntegrationEvent(
    Order Order,
    Customer Customer,
    List&lt;LineItem&gt; LineItems);
</code></pre>
<h2>Consuming Integration Events</h2>
<p>Consumers in other bounded contexts subscribe to integration events through the message broker.
Two critical patterns apply:</p>
<ol>
<li><strong><a href="https://milanjovanovic.tech/blog/idempotent-consumer-handling-duplicate-messages">Idempotent Consumer</a></strong> - handle the same message multiple times safely</li>
<li><strong><a href="https://milanjovanovic.tech/blog/implementing-the-inbox-pattern-for-reliable-message-consumption">Inbox pattern</a></strong> - deduplicate incoming messages before processing</li>
</ol>
<p>Here's what that looks like in a MassTransit consumer:</p>
<pre><code class="language-csharp">public class OrderPlacedIntegrationEventConsumer
    : IConsumer&lt;OrderPlacedIntegrationEvent&gt;
{
    private readonly IInboxStore _inbox;
    private readonly IEmailService _emailService;

    public OrderPlacedIntegrationEventConsumer(
        IInboxStore inbox,
        IEmailService emailService)
    {
        _inbox = inbox;
        _emailService = emailService;
    }

    public async Task Consume(ConsumeContext&lt;OrderPlacedIntegrationEvent&gt; context)
    {
        if (await _inbox.HasBeenProcessedAsync(context.MessageId))
        {
            return; // Already processed - skip
        }

        var message = context.Message;

        // Handle the event
        await _emailService.SendOrderConfirmationAsync(
            message.CustomerId,
            message.OrderId);

        await _inbox.MarkAsProcessedAsync(context.MessageId);
    }
}
</code></pre>
<h2>When to Use Domain Events</h2>
<p>Use domain events when:</p>
<ul>
<li>Side effects must be consistent with the main operation</li>
<li>The handler lives in the same bounded context</li>
<li>You need immediate execution (same request lifecycle)</li>
<li>Example: reserving inventory when an order is placed</li>
</ul>
<h2>When to Use Integration Events</h2>
<p>Use integration events when:</p>
<ul>
<li>Another bounded context or service needs to react</li>
<li>Eventual consistency is acceptable</li>
<li>You need reliable delivery across process boundaries</li>
<li>Example: sending an email confirmation from the Notification service</li>
</ul>
<h2>Common Mistakes</h2>
<p><strong>1. Publishing integration events synchronously.</strong> Integration events should go through a message broker, not through direct HTTP calls. Direct calls create temporal coupling and fragile systems.</p>
<p><strong>2. Putting too much data in integration events.</strong> Only include what consumers need. If a consumer needs more details, it can query the originating service.</p>
<p><strong>3. Using domain events across bounded contexts.</strong> Domain events are internal. If another context needs the information, create an integration event with a separate schema.</p>
<p><strong>4. Skipping the Outbox pattern.</strong> Without the <a href="https://milanjovanovic.tech/blog/implementing-the-outbox-pattern">Outbox</a>, you risk losing events if the app crashes between saving to the database and publishing to the broker.</p>
<h2>The Golden Rule</h2>
<p>Domain events keep your bounded context consistent. Integration events keep your system loosely coupled.</p>
<p>The golden rule: <strong>domain events stay inside, integration events go outside.</strong></p>
<p>Use domain events for immediate side effects within the same transaction.
Use integration events for asynchronous communication across boundaries.
And always use the Outbox pattern to guarantee delivery.</p>
<p>Thanks for reading, and stay awesome!</p>
<hr>
]]></content:encoded>
            <author>milan@milanjovanovic.tech (Milan Jovanović)</author>
        </item>
        <item>
            <title><![CDATA[Dependency Rule in Clean Architecture Explained]]></title>
            <link>https://milanjovanovic.tech/blog/dependency-rule-clean-architecture</link>
            <guid isPermaLink="false">https://milanjovanovic.tech/blog/dependency-rule-clean-architecture</guid>
            <pubDate>Tue, 11 Aug 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Source code dependencies must only point inward. That one sentence decides your project references, where your interfaces live, and why the DI container sits…]]></description>
            <content:encoded><![CDATA[<p>Strip away the diagrams and the folder structures, and Clean Architecture reduces to a single rule: source code dependencies point inward.
Everything else (the layers, the interfaces, the DI wiring) exists to serve that rule.
Here is what it means in practice, the part about control flow that confuses everyone at first, and how to make the compiler enforce it for you.</p>
<h2>What Is the Dependency Rule?</h2>
<p>Robert C. Martin defines it simply:</p>
<blockquote>
<p>Source code dependencies must only point inward.</p>
</blockquote>
<p>In <a href="https://milanjovanovic.tech/blog/clean-architecture-and-the-benefits-of-structured-software-design">Clean Architecture</a>, the application is organized in concentric circles:</p>
<ol>
<li><strong>Domain</strong> (innermost) - entities, value objects, domain events</li>
<li><strong>Application</strong> - use cases, commands, queries, interfaces</li>
<li><strong>Infrastructure</strong> (outermost) - database access, external services, frameworks</li>
</ol>
<p>The Dependency Rule says: <strong>nothing in an inner circle can know anything about an outer circle.</strong></p>
<ul>
<li>Domain doesn't reference Application</li>
<li>Application doesn't reference Infrastructure</li>
<li>Infrastructure references Application and Domain</li>
</ul>
<p>Arrows always point inward. Never outward.</p>
<h2>Why the Dependency Rule Matters</h2>
<p>Without it, your domain logic gets polluted with infrastructure concerns:</p>
<pre><code class="language-csharp">// WRONG - Domain depends on Infrastructure
public class Order
{
    public void Confirm()
    {
        // Domain class knows about EF Core
        using var context = new AppDbContext();
        context.Orders.Update(this);
        context.SaveChanges();

        // Domain class knows about email service
        var emailService = new SendGridEmailService();
        emailService.Send(this.CustomerEmail, &quot;Order confirmed!&quot;);
    }
}
</code></pre>
<p>This code can't be tested without a database and an email service. It can't be reused in a different context. And changing your email provider means changing your domain.</p>
<p>With the Dependency Rule:</p>
<pre><code class="language-csharp">// RIGHT - Domain knows nothing about infrastructure
public class Order
{
    public void Confirm()
    {
        // Pure business logic
        Status = OrderStatus.Confirmed;
        ConfirmedAt = DateTime.UtcNow;
        RaiseDomainEvent(new OrderConfirmedDomainEvent(Id));
    }
}
</code></pre>
<p>The domain is pure. How to persist the order and send the email is handled by outer layers.</p>
<h2>How to Enforce It in .NET</h2>
<h3>Project References</h3>
<p>Set up your project references to match the Dependency Rule:</p>
<pre><code class="language-xml">&lt;!-- Domain - references NOTHING --&gt;
&lt;Project Sdk=&quot;Microsoft.NET.Sdk&quot;&gt;
  &lt;PropertyGroup&gt;
    &lt;TargetFramework&gt;net10.0&lt;/TargetFramework&gt;
  &lt;/PropertyGroup&gt;
&lt;/Project&gt;

&lt;!-- Application - references Domain only --&gt;
&lt;Project Sdk=&quot;Microsoft.NET.Sdk&quot;&gt;
  &lt;ItemGroup&gt;
    &lt;ProjectReference Include=&quot;..\Domain\Domain.csproj&quot; /&gt;
  &lt;/ItemGroup&gt;
&lt;/Project&gt;

&lt;!-- Infrastructure - references Application (and Domain transitively) --&gt;
&lt;Project Sdk=&quot;Microsoft.NET.Sdk&quot;&gt;
  &lt;ItemGroup&gt;
    &lt;ProjectReference Include=&quot;..\Application\Application.csproj&quot; /&gt;
  &lt;/ItemGroup&gt;
&lt;/Project&gt;

&lt;!-- API - references Infrastructure and Application --&gt;
&lt;Project Sdk=&quot;Microsoft.NET.Sdk.Web&quot;&gt;
  &lt;ItemGroup&gt;
    &lt;ProjectReference Include=&quot;..\Application\Application.csproj&quot; /&gt;
    &lt;ProjectReference Include=&quot;..\Infrastructure\Infrastructure.csproj&quot; /&gt;
  &lt;/ItemGroup&gt;
&lt;/Project&gt;
</code></pre>
<p>The compiler enforces this. If Domain tries to reference an Infrastructure class, it won't compile.</p>
<h3>Dependency Inversion</h3>
<p>The Application layer defines interfaces. The Infrastructure layer implements them:</p>
<pre><code class="language-csharp">// Application layer - defines what it needs
public interface IOrderRepository
{
    Task&lt;Order?&gt; GetByIdAsync(Guid id, CancellationToken ct);
    void Add(Order order);
}

public interface IEmailService
{
    Task SendOrderConfirmationAsync(string email, Guid orderId, CancellationToken ct);
}
</code></pre>
<pre><code class="language-csharp">// Infrastructure layer - implements using specific technology
public class OrderRepository : IOrderRepository
{
    private readonly AppDbContext _dbContext;

    public OrderRepository(AppDbContext dbContext)
    {
        _dbContext = dbContext;
    }

    public async Task&lt;Order?&gt; GetByIdAsync(Guid id, CancellationToken ct)
    {
        return await _dbContext.Orders
            .Include(o =&gt; o.LineItems)
            .FirstOrDefaultAsync(o =&gt; o.Id == id, ct);
    }

    public void Add(Order order) =&gt; _dbContext.Orders.Add(order);
}
</code></pre>
<p>The Application layer calls <code>IOrderRepository</code>. It doesn't know (or care) that Entity Framework Core is behind it.</p>
<p>This is the <strong>Dependency Inversion Principle</strong> at work - high-level modules (Application) don't depend on low-level modules (Infrastructure). Both depend on abstractions.</p>
<h2>Flow of Control vs Source Dependencies</h2>
<p>Here's the part that confuses most people when they first meet the Dependency Rule.</p>
<p>At runtime, control flows <strong>outward</strong>: the <a href="https://milanjovanovic.tech/blog/application-layer-clean-architecture">Application layer</a> calls the database, sends emails, publishes messages.
So how can dependencies point inward?</p>
<p>Because a <em>source dependency</em> is about what your code references at compile time, not who calls whom at runtime.</p>
<p>Walk through a single request:</p>
<ol>
<li>The API controller (outer) calls a command handler (inner). Control flows inward, dependency points inward. No conflict.</li>
<li>The handler calls <code>IOrderRepository.GetByIdAsync(...)</code>. Control is about to flow outward into EF Core, but the handler only references an interface <em>it owns</em>.</li>
<li>The DI container resolved <code>IOrderRepository</code> to <code>OrderRepository</code> from Infrastructure at startup. The call lands in the outer layer without the inner layer ever naming it.</li>
</ol>
<p>The interface is the trick.
It lets control flow outward while the compile-time arrow keeps pointing inward.
When you see a diagram of Clean Architecture, the arrows show source dependencies, not call direction.</p>
<img src="https://milanjovanovic.tech/blogs/articles/dependency-rule-clean-architecture/control-vs-dependency.png" alt="The command handler references the IOrderRepository interface it owns in the application layer, the Infrastructure OrderRepository implements that interface, and at runtime control flows outward from the handler to the implementation while both source dependencies point inward to the interface">
<h2>The Rule Applies to NuGet Packages Too</h2>
<p>Project references are only half the story.
A Domain project with zero project references but a <code>Microsoft.EntityFrameworkCore</code> package reference violates the Dependency Rule just the same.</p>
<p>My guidelines:</p>
<ul>
<li><strong>Domain</strong>: no packages. Entities, value objects, and domain events are plain C#.</li>
<li><strong>Application</strong>: contracts-only packages at most (MediatR contracts, FluentValidation). No EF Core, no HTTP clients, no cloud SDKs.</li>
<li><strong>Infrastructure</strong>: this is where the package weight belongs - EF Core, message brokers, external SDKs.</li>
</ul>
<p>The gray zone is logging.
<code>Microsoft.Extensions.Logging.Abstractions</code> in the Application layer is a pragmatic exception most teams accept: it's an abstraction package with no implementation baggage.</p>
<h2>Where Does the DI Container Fit?</h2>
<p>Someone has to know about <em>all</em> the layers to wire them together.
That's the <strong>composition root</strong>: the API project's <code>Program.cs</code>.</p>
<p>The outermost layer referencing everything isn't a violation.
It's the design: the most volatile, framework-heavy project depends on everything, and nothing depends on it.</p>
<pre><code class="language-csharp">builder.Services
    .AddApplication()       // handlers, validators, behaviors
    .AddInfrastructure(builder.Configuration); // EF Core, email, auth
</code></pre>
<p>If you find yourself wanting to resolve services inside the Domain layer (service locator style), that's the Dependency Rule being violated at runtime even though the compiler is happy.
Domain objects receive what they need as method parameters; they don't ask a container.</p>
<h2>What Breaks the Dependency Rule</h2>
<h3>1. Framework Attributes in Domain</h3>
<pre><code class="language-csharp">// WRONG - Domain depends on EF Core
public class Order
{
    [Key]
    public Guid Id { get; set; }

    [Required]
    [MaxLength(100)]
    public string CustomerName { get; set; }
}
</code></pre>
<p>Fix: Use EF Core's Fluent API configuration in the Infrastructure layer:</p>
<pre><code class="language-csharp">// Infrastructure layer
public class OrderConfiguration : IEntityTypeConfiguration&lt;Order&gt;
{
    public void Configure(EntityTypeBuilder&lt;Order&gt; builder)
    {
        builder.HasKey(o =&gt; o.Id);
        builder.Property(o =&gt; o.CustomerName).IsRequired().HasMaxLength(100);
    }
}
</code></pre>
<h3>2. Using Concrete Services in Application</h3>
<pre><code class="language-csharp">// WRONG - Application depends on Infrastructure
public class PlaceOrderCommandHandler
{
    private readonly AppDbContext _dbContext;  // Infrastructure class
}
</code></pre>
<p>Fix: Depend on abstractions defined in the Application layer:</p>
<pre><code class="language-csharp">// RIGHT - Application depends on its own interfaces
public class PlaceOrderCommandHandler
{
    private readonly IOrderRepository _repository;
    private readonly IUnitOfWork _unitOfWork;
}
</code></pre>
<h3>3. Leaking Infrastructure Types</h3>
<pre><code class="language-csharp">// WRONG - Application returns infrastructure types
public class GetOrderQueryHandler
{
    public async Task&lt;DbSet&lt;Order&gt;&gt; Handle(GetOrderQuery query)
    {
        return _dbContext.Orders;  // Leaking DbSet&lt;T&gt;
    }
}
</code></pre>
<p>Fix: Map to DTOs or domain objects:</p>
<pre><code class="language-csharp">public async Task&lt;OrderResponse?&gt; Handle(GetOrderQuery query)
{
    return await _dbContext.Orders
        .Where(o =&gt; o.Id == query.OrderId)
        .Select(o =&gt; new OrderResponse(
            o.Id,
            o.Customer.Name,
            o.TotalAmount.Amount,
            o.Status.Name,
            o.CreatedAt))
        .FirstOrDefaultAsync();
}
</code></pre>
<h2>Architecture Tests</h2>
<p>Enforce the Dependency Rule automatically with architecture tests:</p>
<pre><code class="language-csharp">[Fact]
public void Domain_Should_Not_Reference_Application()
{
    var result = Types
        .InAssembly(typeof(Order).Assembly)
        .ShouldNot()
        .HaveDependencyOn(typeof(PlaceOrderCommand).Assembly.GetName().Name)
        .GetResult();

    result.IsSuccessful.Should().BeTrue();
}

[Fact]
public void Domain_Should_Not_Reference_Infrastructure()
{
    var result = Types
        .InAssembly(typeof(Order).Assembly)
        .ShouldNot()
        .HaveDependencyOn(typeof(AppDbContext).Assembly.GetName().Name)
        .GetResult();

    result.IsSuccessful.Should().BeTrue();
}

[Fact]
public void Application_Should_Not_Reference_Infrastructure()
{
    var result = Types
        .InAssembly(typeof(PlaceOrderCommand).Assembly)
        .ShouldNot()
        .HaveDependencyOn(typeof(AppDbContext).Assembly.GetName().Name)
        .GetResult();

    result.IsSuccessful.Should().BeTrue();
}
</code></pre>
<p>Run these in CI. If someone adds a wrong dependency, the build fails.</p>
<p>I wrote more about this approach in <a href="https://milanjovanovic.tech/blog/enforcing-software-architecture-with-architecture-tests"><strong>enforcing software architecture with architecture tests</strong></a>, including rules that go beyond layer references (naming conventions, sealed handlers, interface placement).</p>
<p>Violating the Dependency Rule is also the most common of the <a href="https://milanjovanovic.tech/blog/clean-architecture-anti-patterns"><strong>Clean Architecture anti-patterns</strong></a> I see in real codebases - usually starting with a single EF Core attribute on a domain entity.</p>
<h2>Three Layers of Defense</h2>
<p>The Dependency Rule is the foundation of Clean Architecture. It keeps your domain and application logic independent of frameworks, databases, and external services.</p>
<p>Enforce it through:</p>
<ol>
<li><strong>Project references</strong> - the compiler prevents invalid dependencies</li>
<li><strong>Dependency Inversion</strong> - interfaces in Application, implementations in Infrastructure</li>
<li><strong>Architecture tests</strong> - automated verification in CI</li>
</ol>
<p>Get the Dependency Rule right, and everything else in Clean Architecture falls into place.</p>
<p>Thanks for reading, and stay awesome!</p>
<hr>
]]></content:encoded>
            <author>milan@milanjovanovic.tech (Milan Jovanović)</author>
        </item>
        <item>
            <title><![CDATA[Clean Architecture vs Onion Architecture vs Hexagonal Architecture]]></title>
            <link>https://milanjovanovic.tech/blog/clean-architecture-vs-onion-vs-hexagonal</link>
            <guid isPermaLink="false">https://milanjovanovic.tech/blog/clean-architecture-vs-onion-vs-hexagonal</guid>
            <pubDate>Tue, 11 Aug 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Clean Architecture, Onion Architecture, and Hexagonal Architecture all solve the same fundamental problem: decoupling business logic from infrastructure.]]></description>
            <content:encoded><![CDATA[<p>Clean Architecture, Onion Architecture, and Hexagonal Architecture come up in every discussion about structuring .NET applications.
They're often treated as competing options, but all three defend the same idea: business logic at the center, infrastructure at the edges.
In a .NET solution, they even produce nearly identical project structures.
Here's what actually differs, what's just naming, and how to pick one without losing a week to the debate.</p>
<h2>Three Names, One Goal</h2>
<p>If you've been reading about software architecture, you've probably encountered three similar-looking approaches:</p>
<ul>
<li><strong>Clean Architecture</strong> (Robert C. Martin, 2012)</li>
<li><strong>Onion Architecture</strong> (Jeffrey Palermo, 2008)</li>
<li><strong>Hexagonal Architecture</strong> / Ports and Adapters (Alistair Cockburn, 2005)</li>
</ul>
<p>They all aim at the same thing: <strong>keep your business logic independent of frameworks, databases, and external concerns.</strong></p>
<p>The core insight behind all three is the <a href="https://milanjovanovic.tech/blog/clean-architecture-and-the-benefits-of-structured-software-design">Dependency Rule</a> - dependencies point inward. Infrastructure depends on your domain, never the other way around.</p>
<p>So what's actually different?</p>
<h2>Hexagonal Architecture (Ports and Adapters)</h2>
<p>Hexagonal Architecture was the first of the three, introduced by Alistair Cockburn in 2005.</p>
<p>The key concept is simple: your application has a core (the hexagon) surrounded by <strong>ports</strong> and <strong>adapters</strong>.</p>
<ul>
<li><strong>Ports</strong> are interfaces that define how the application talks to the outside world (and vice versa)</li>
<li><strong>Adapters</strong> are implementations that connect those ports to real infrastructure</li>
</ul>
<p>There are two types of ports:</p>
<ul>
<li><strong>Driving ports</strong> (primary) - how the outside world talks to your application (e.g., an HTTP controller calling a use case)</li>
<li><strong>Driven ports</strong> (secondary) - how your application talks to external systems (e.g., a repository interface for database access)</li>
</ul>
<p>The hexagon itself contains your business logic. It doesn't know about HTTP, databases, or message queues.</p>
<p><strong>Strengths:</strong></p>
<ul>
<li>The ports/adapters mental model is intuitive</li>
<li>Makes testability explicit - you test by plugging in fake adapters</li>
<li>Symmetric - treats all external concerns equally (UI, database, messaging)</li>
</ul>
<p><strong>In .NET terms:</strong> Your domain and application logic sit in one project. Interfaces (ports) define the boundaries. Infrastructure implementations (adapters) connect to real systems.</p>
<h2>Onion Architecture</h2>
<p>Jeffrey Palermo introduced Onion Architecture in 2008, building on the hexagonal idea with a more explicit layer structure.</p>
<p>The architecture is organized in concentric rings:</p>
<ol>
<li><strong>Domain Model</strong> (center) - entities, value objects, domain logic</li>
<li><strong>Domain Services</strong> - business operations that span multiple entities</li>
<li><strong>Application Services</strong> - use case orchestration, DTOs</li>
<li><strong>Infrastructure</strong> (outer ring) - database, file system, external services</li>
</ol>
<p>The rules are:</p>
<ul>
<li>Inner layers define interfaces</li>
<li>Outer layers provide implementations</li>
<li>Dependencies always point inward</li>
</ul>
<p><strong>Strengths:</strong></p>
<ul>
<li>Clearer layer separation than hexagonal</li>
<li>Explicit placement of domain services</li>
<li>The &quot;onion rings&quot; visualization is easy to communicate</li>
</ul>
<p><strong>In .NET terms:</strong> You typically create separate projects for each ring - <code>Domain</code>, <code>Application</code>, <code>Infrastructure</code>, and <code>Presentation</code>.</p>
<h2>Clean Architecture</h2>
<p>Robert C. Martin formalized <a href="https://milanjovanovic.tech/blog/clean-architecture-and-the-benefits-of-structured-software-design">Clean Architecture</a> in 2012, combining ideas from hexagonal and onion architectures.</p>
<p>The layers are:</p>
<ol>
<li><strong>Entities</strong> (Enterprise Business Rules) - core business objects</li>
<li><strong>Use Cases</strong> (Application Business Rules) - application-specific logic</li>
<li><strong>Interface Adapters</strong> - controllers, presenters, gateways</li>
<li><strong>Frameworks &amp; Drivers</strong> - web framework, database, external tools</li>
</ol>
<p>Clean Architecture adds explicit concepts like:</p>
<ul>
<li><strong>Use Cases</strong> as first-class citizens</li>
<li><strong>Input/Output boundaries</strong> between layers</li>
<li><strong>The Dependency Rule</strong> stated explicitly</li>
</ul>
<p><strong>Strengths:</strong></p>
<ul>
<li>Most prescriptive of the three - clear guidance on where things go</li>
<li>Use cases are front and center</li>
<li>Strong community adoption in .NET (thanks to common templates and <a href="https://milanjovanovic.tech/blog/clean-architecture-folder-structure">structured guides</a>)</li>
</ul>
<p><strong>In .NET terms:</strong> The typical project structure is <code>Domain</code>, <code>Application</code>, <code>Infrastructure</code>, <code>Presentation</code> - which happens to match the Onion Architecture layout closely.</p>
<h2>Side-by-Side Comparison</h2>
<p>Here's how the three approaches stack up:</p>
<p><strong>Hexagonal Architecture (2005)</strong></p>
<ul>
<li>Core concept: ports and adapters around an application core</li>
<li>Layer names: Application, Ports, Adapters</li>
<li>Primary focus: treating all external systems symmetrically</li>
<li>Testing approach: swap real adapters for fakes</li>
<li>Prescriptiveness: low - it's a mental model more than a structure</li>
<li>.NET adoption: moderate; the terminology shows up more than the literal structure</li>
</ul>
<p><strong>Onion Architecture (2008)</strong></p>
<ul>
<li>Core concept: concentric rings with the domain model at the center</li>
<li>Layer names: Domain Model, Domain Services, Application Services, Infrastructure</li>
<li>Primary focus: layer discipline and interface ownership by inner rings</li>
<li>Testing approach: mock the interfaces defined by inner layers</li>
<li>Prescriptiveness: medium</li>
<li>.NET adoption: moderate; historically popular in the .NET community where it originated</li>
</ul>
<p><strong>Clean Architecture (2012)</strong></p>
<ul>
<li>Core concept: the Dependency Rule plus use cases as first-class citizens</li>
<li>Layer names: Entities, Use Cases, Interface Adapters, Frameworks &amp; Drivers</li>
<li>Primary focus: isolating application-specific business rules</li>
<li>Testing approach: test use cases independently of infrastructure</li>
<li>Prescriptiveness: high - the most opinionated of the three</li>
<li>.NET adoption: very high, with widely used templates and guides</li>
</ul>
<h2>What They Have in Common</h2>
<p>All three share these core principles:</p>
<ol>
<li><strong>Business logic at the center</strong> - domain code has zero dependencies on infrastructure</li>
<li><strong>Dependency inversion</strong> - outer layers depend on inner layers, never the reverse</li>
<li><strong>Testability by design</strong> - you can test business logic without databases, HTTP, or external services</li>
<li><strong>Framework independence</strong> - swapping a web framework or database shouldn't require rewriting business rules</li>
<li><strong>Interfaces as boundaries</strong> - abstractions define how layers communicate</li>
</ol>
<p>In practice, a .NET solution following any of these three architectures looks remarkably similar. The same four rings show up under different names, with dependencies always pointing inward:</p>
<img src="https://milanjovanovic.tech/blogs/articles/clean-architecture-vs-onion-vs-hexagonal/three-names-one-structure.png" alt="The four concentric rings shared by Clean, Onion, and Hexagonal architectures, each labeled with the equivalent term from all three: the core is Entities, Domain Model, or Core; the outer ring is Frameworks and Drivers, Infrastructure, or Adapters">
<p>The naming differs. The structure is nearly identical.</p>
<h2>Which Should You Choose?</h2>
<p>Honestly? <strong>It doesn't matter as much as you think.</strong></p>
<p>All three solve the same problem. If you understand the core principles - dependency inversion, business logic at the center, infrastructure at the edges - you can build clean systems with any of them.</p>
<p>That said, here's a practical decision guide:</p>
<p><strong>Choose Clean Architecture when:</strong></p>
<ul>
<li>You want clear, prescriptive guidance</li>
<li>Your team benefits from a well-known, widely documented approach</li>
<li>You're building in .NET (the ecosystem has strong support with templates and <a href="https://milanjovanovic.tech/pragmatic-clean-architecture">courses</a>)</li>
</ul>
<p><strong>Choose Hexagonal when:</strong></p>
<ul>
<li>You want maximum flexibility in how you structure things</li>
<li>You work in a polyglot environment where the ports/adapters terminology is common</li>
<li>You're focused on adapter swappability (testing, multi-channel systems)</li>
</ul>
<p><strong>Choose Onion when:</strong></p>
<ul>
<li>You want layer discipline without the prescriptiveness of Clean Architecture</li>
<li>You prefer the rings mental model for communicating with your team</li>
</ul>
<p><strong>Or don't choose at all:</strong>
If your project is simple, a <a href="https://milanjovanovic.tech/blog/vertical-slice-architecture">Vertical Slice Architecture</a> might be more practical. Not every project needs concentric rings.
I've written a separate guide on <a href="https://milanjovanovic.tech/blog/when-to-use-clean-architecture"><strong>when to use Clean Architecture</strong></a> if you're on the fence.</p>
<h2>Common Mistakes</h2>
<p><strong>1. Treating the choice as binary.</strong> You can mix ideas. Use Clean Architecture's use case structure with Hexagonal's ports and adapters terminology. The principles are compatible.</p>
<p><strong>2. Over-engineering small projects.</strong> A CRUD API with five endpoints doesn't need four projects. Start simple and <a href="https://milanjovanovic.tech/blog/why-clean-architecture-is-great-for-complex-projects">add architecture when complexity demands it</a>.</p>
<p><strong>3. Arguing about names instead of principles.</strong> The Dependency Rule matters more than whether you call it a &quot;port&quot; or an &quot;interface&quot;. Focus on the direction of dependencies, not the labels.</p>
<p><strong>4. Forgetting that architecture is a means, not an end.</strong> The goal is maintainable, testable software. If your architecture makes the codebase harder to work with, you've missed the point.</p>
<h2>Principles Over Labels</h2>
<p>Clean Architecture, Onion Architecture, and Hexagonal Architecture are variations of the same idea: protect your business logic by making infrastructure depend on your domain, not the other way around.</p>
<p>They differ in naming, structure, and emphasis - but the principles are identical.</p>
<p>Pick the one your team understands best, and focus on applying the principles consistently.</p>
<p>Thanks for reading, and stay awesome!</p>
<hr>
]]></content:encoded>
            <author>milan@milanjovanovic.tech (Milan Jovanović)</author>
        </item>
        <item>
            <title><![CDATA[Clean Architecture Solution Template for .NET]]></title>
            <link>https://milanjovanovic.tech/blog/clean-architecture-solution-template-dotnet</link>
            <guid isPermaLink="false">https://milanjovanovic.tech/blog/clean-architecture-solution-template-dotnet</guid>
            <pubDate>Tue, 11 Aug 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Setting up a Clean Architecture project from scratch takes time. Here is a complete .NET solution template with the right project structure, references, and…]]></description>
            <content:encoded><![CDATA[<p>Every new Clean Architecture solution starts with the same hour of ceremony: create the projects, wire the references, add MediatR and FluentValidation, set up DI.
None of that work is interesting, and all of it is easy to get subtly wrong.
This article walks through the complete template: project structure, references, base primitives, CQRS abstractions, and the architecture tests that keep it honest.
At the end, you can package it as a <code>dotnet new</code> template and never do the setup by hand again.</p>
<h2>Why a Solution Template?</h2>
<p>Every time you start a new <a href="https://milanjovanovic.tech/blog/clean-architecture-and-the-benefits-of-structured-software-design"><strong>Clean Architecture</strong></a> project, you repeat the same steps - creating projects, adding references, configuring DI, setting up MediatR, adding FluentValidation.
A solution template saves hours and ensures consistency.</p>
<p>Here's the complete setup I use for production projects.</p>
<h2>The Solution Structure</h2>
<pre><code>src/
  MyApp.Domain/
  MyApp.Application/
  MyApp.Infrastructure/
  MyApp.Persistence/
  MyApp.Api/
tests/
  MyApp.Domain.UnitTests/
  MyApp.Application.UnitTests/
  MyApp.Infrastructure.IntegrationTests/
  MyApp.Api.FunctionalTests/
  MyApp.ArchitectureTests/
</code></pre>
<p>Five source projects.
Four layers plus a separate Persistence project (you can merge Infrastructure and Persistence if you prefer - I only split them when the infrastructure surface grows).</p>
<h2>Project References (The Dependency Rule)</h2>
<p>The <a href="https://milanjovanovic.tech/blog/dependency-rule-clean-architecture"><strong>dependency rule</strong></a> is the non-negotiable constraint. Every project reference points inward, toward Domain:</p>
<img src="https://milanjovanovic.tech/blogs/articles/clean-architecture-solution-template-dotnet/project-dependency-graph.png" alt="Project reference graph for the solution: Api references Application, Infrastructure, and Persistence; Infrastructure and Persistence reference Application; Application references Domain; Domain references nothing">
<p>Domain depends on nothing.
Application depends only on Domain.
Infrastructure and Persistence depend on Application.
The API project wires everything together.</p>
<h2>Domain Project</h2>
<p>The <a href="https://milanjovanovic.tech/blog/domain-layer-clean-architecture"><strong>Domain layer</strong></a> contains entities, value objects, domain events, and domain exceptions.
No NuGet packages:</p>
<pre><code class="language-xml">&lt;!-- MyApp.Domain.csproj --&gt;
&lt;Project Sdk=&quot;Microsoft.NET.Sdk&quot;&gt;
  &lt;PropertyGroup&gt;
    &lt;TargetFramework&gt;net10.0&lt;/TargetFramework&gt;
  &lt;/PropertyGroup&gt;
&lt;/Project&gt;
</code></pre>
<pre><code>Domain/
  Entities/
    Order.cs
    Customer.cs
    Product.cs
  ValueObjects/
    Money.cs
    Address.cs
    Email.cs
  Events/
    OrderPlacedDomainEvent.cs
    OrderCancelledDomainEvent.cs
  Exceptions/
    DomainException.cs
  Repositories/
    IOrderRepository.cs
    ICustomerRepository.cs
  Primitives/
    Entity.cs
    AggregateRoot.cs
    IDomainEvent.cs
    IUnitOfWork.cs
    Result.cs
    Error.cs
</code></pre>
<p>Base classes:</p>
<pre><code class="language-csharp">public abstract class Entity
{
    public Guid Id { get; protected init; }
}

public abstract class AggregateRoot : Entity
{
    private readonly List&lt;IDomainEvent&gt; _domainEvents = [];

    public IReadOnlyCollection&lt;IDomainEvent&gt; DomainEvents =&gt; _domainEvents;

    protected void RaiseDomainEvent(IDomainEvent domainEvent) =&gt;
        _domainEvents.Add(domainEvent);

    public void ClearDomainEvents() =&gt; _domainEvents.Clear();
}

public interface IDomainEvent;
</code></pre>
<p>Notice that <code>IDomainEvent</code> is a plain marker interface.
A lot of templates declare it as <code>IDomainEvent : MediatR.INotification</code>, which quietly gives your Domain project a MediatR dependency and contradicts the &quot;no packages&quot; rule.</p>
<p>You have two honest options:</p>
<ol>
<li><strong>Keep the domain pure</strong> (what I show above) and adapt domain events to MediatR notifications in the Application layer with a generic wrapper.</li>
<li><strong>Accept the tradeoff</strong> and reference <code>MediatR.Contracts</code> from Domain. It's a contracts-only package, but it still couples your domain to a library. And with <a href="https://milanjovanovic.tech/blog/mediatr-and-masstransit-going-commercial-what-this-means-for-you"><strong>MediatR going commercial</strong></a>, coupling your innermost layer to it deserves a second thought.</li>
</ol>
<p>Either is workable.
Just make the choice deliberately instead of inheriting it from a template.</p>
<h2>Application Project</h2>
<p>The <a href="https://milanjovanovic.tech/blog/application-layer-clean-architecture"><strong>Application layer</strong></a> contains use cases, validation, <a href="https://milanjovanovic.tech/blog/cqrs-pattern-the-way-it-should-have-been-from-the-start"><strong>CQRS</strong></a> abstractions, and behaviors.</p>
<pre><code class="language-xml">&lt;!-- MyApp.Application.csproj --&gt;
&lt;Project Sdk=&quot;Microsoft.NET.Sdk&quot;&gt;
  &lt;PropertyGroup&gt;
    &lt;TargetFramework&gt;net10.0&lt;/TargetFramework&gt;
  &lt;/PropertyGroup&gt;
  &lt;ItemGroup&gt;
    &lt;PackageReference Include=&quot;FluentValidation.DependencyInjectionExtensions&quot; /&gt;
    &lt;PackageReference Include=&quot;MediatR&quot; /&gt;
  &lt;/ItemGroup&gt;
  &lt;ItemGroup&gt;
    &lt;ProjectReference Include=&quot;..\MyApp.Domain\MyApp.Domain.csproj&quot; /&gt;
  &lt;/ItemGroup&gt;
&lt;/Project&gt;
</code></pre>
<pre><code>Application/
  Abstractions/
    Messaging/
      ICommand.cs
      ICommandHandler.cs
      IQuery.cs
      IQueryHandler.cs
    ICurrentUserService.cs
    IDateTimeProvider.cs
    IEmailService.cs
  Behaviors/
    ValidationBehavior.cs
    LoggingBehavior.cs
  Orders/
    PlaceOrder/
      PlaceOrderCommand.cs
      PlaceOrderCommandHandler.cs
      PlaceOrderValidator.cs
    GetOrderById/
      GetOrderByIdQuery.cs
      GetOrderByIdQueryHandler.cs
      OrderResponse.cs
  DependencyInjection.cs
</code></pre>
<p>CQRS abstractions:</p>
<pre><code class="language-csharp">public interface ICommand : IRequest&lt;Result&gt;;
public interface ICommand&lt;TResponse&gt; : IRequest&lt;Result&lt;TResponse&gt;&gt;;

public interface ICommandHandler&lt;TCommand&gt;
    : IRequestHandler&lt;TCommand, Result&gt;
    where TCommand : ICommand;

public interface ICommandHandler&lt;TCommand, TResponse&gt;
    : IRequestHandler&lt;TCommand, Result&lt;TResponse&gt;&gt;
    where TCommand : ICommand&lt;TResponse&gt;;

public interface IQuery&lt;TResponse&gt; : IRequest&lt;Result&lt;TResponse&gt;&gt;;

public interface IQueryHandler&lt;TQuery, TResponse&gt;
    : IRequestHandler&lt;TQuery, Result&lt;TResponse&gt;&gt;
    where TQuery : IQuery&lt;TResponse&gt;;
</code></pre>
<p>These thin interfaces buy you two things: every handler returns a <code>Result</code>, and pipeline behaviors can target commands or queries specifically (validate commands, cache queries).</p>
<p>DI registration:</p>
<pre><code class="language-csharp">// Application/DependencyInjection.cs
public static class DependencyInjection
{
    public static IServiceCollection AddApplication(
        this IServiceCollection services)
    {
        var assembly = typeof(DependencyInjection).Assembly;

        services.AddMediatR(config =&gt;
        {
            config.RegisterServicesFromAssembly(assembly);
            config.AddOpenBehavior(typeof(ValidationBehavior&lt;,&gt;));
            config.AddOpenBehavior(typeof(LoggingBehavior&lt;,&gt;));
        });

        services.AddValidatorsFromAssembly(assembly);

        return services;
    }
}
</code></pre>
<h2>Persistence Project</h2>
<p>Handles <strong>EF Core</strong> configuration:</p>
<pre><code class="language-xml">&lt;!-- MyApp.Persistence.csproj --&gt;
&lt;Project Sdk=&quot;Microsoft.NET.Sdk&quot;&gt;
  &lt;PropertyGroup&gt;
    &lt;TargetFramework&gt;net10.0&lt;/TargetFramework&gt;
  &lt;/PropertyGroup&gt;
  &lt;ItemGroup&gt;
    &lt;PackageReference Include=&quot;Microsoft.EntityFrameworkCore&quot; /&gt;
    &lt;PackageReference Include=&quot;Npgsql.EntityFrameworkCore.PostgreSQL&quot; /&gt;
  &lt;/ItemGroup&gt;
  &lt;ItemGroup&gt;
    &lt;ProjectReference Include=&quot;..\MyApp.Application\MyApp.Application.csproj&quot; /&gt;
  &lt;/ItemGroup&gt;
&lt;/Project&gt;
</code></pre>
<pre><code>Persistence/
  ApplicationDbContext.cs
  Configurations/
    OrderConfiguration.cs
    CustomerConfiguration.cs
  Repositories/
    OrderRepository.cs
    CustomerRepository.cs
  DependencyInjection.cs
</code></pre>
<pre><code class="language-csharp">// Persistence/DependencyInjection.cs
public static class DependencyInjection
{
    public static IServiceCollection AddPersistence(
        this IServiceCollection services,
        IConfiguration configuration)
    {
        services.AddDbContext&lt;ApplicationDbContext&gt;(options =&gt;
            options.UseNpgsql(
                configuration.GetConnectionString(&quot;Database&quot;)));

        services.AddScoped&lt;IUnitOfWork&gt;(sp =&gt;
            sp.GetRequiredService&lt;ApplicationDbContext&gt;());

        services.AddScoped&lt;IOrderRepository, OrderRepository&gt;();
        services.AddScoped&lt;ICustomerRepository, CustomerRepository&gt;();

        return services;
    }
}
</code></pre>
<h2>Infrastructure Project</h2>
<p>The <a href="https://milanjovanovic.tech/blog/infrastructure-layer-clean-architecture"><strong>Infrastructure layer</strong></a> handles external concerns - email, caching, authentication, file storage:</p>
<pre><code>Infrastructure/
  Authentication/
    CurrentUserService.cs
    JwtConfiguration.cs
  Email/
    EmailService.cs
  Caching/
    CacheService.cs
  Time/
    DateTimeProvider.cs
  DependencyInjection.cs
</code></pre>
<pre><code class="language-csharp">// Infrastructure/DependencyInjection.cs
public static class DependencyInjection
{
    public static IServiceCollection AddInfrastructure(
        this IServiceCollection services,
        IConfiguration configuration)
    {
        services.AddScoped&lt;ICurrentUserService, CurrentUserService&gt;();
        services.AddSingleton&lt;IDateTimeProvider, DateTimeProvider&gt;();
        services.AddTransient&lt;IEmailService, EmailService&gt;();

        services.AddJwtAuthentication(configuration);

        return services;
    }
}
</code></pre>
<h2>API Project</h2>
<p>The entry point. Wires everything together:</p>
<pre><code class="language-csharp">// Program.cs
var builder = WebApplication.CreateBuilder(args);

builder.Services
    .AddApplication()
    .AddPersistence(builder.Configuration)
    .AddInfrastructure(builder.Configuration);

builder.Services.AddOpenApi();
builder.Services.AddExceptionHandler&lt;GlobalExceptionHandler&gt;();
builder.Services.AddProblemDetails();

var app = builder.Build();

app.UseExceptionHandler();

if (app.Environment.IsDevelopment())
{
    app.MapOpenApi();
}

app.UseAuthentication();
app.UseAuthorization();

app.MapOrderEndpoints();
app.MapCustomerEndpoints();

app.Run();
</code></pre>
<p>A few deliberate choices here:</p>
<ul>
<li><code>AddOpenApi()</code>/<code>MapOpenApi()</code> is the built-in OpenAPI support in .NET 9+; no Swashbuckle package needed.</li>
<li>The exception handler is registered as a service (<code>IExceptionHandler</code>) and <code>UseExceptionHandler()</code> sits at the top of the pipeline, so it catches failures from everything after it.</li>
<li>Each layer contributes exactly one <code>AddX()</code> extension method. <code>Program.cs</code> stays readable at a glance.</li>
</ul>
<h2>The Architecture Tests Project</h2>
<p>The template isn't complete without tests that keep it honest.
The <code>MyApp.ArchitectureTests</code> project enforces the dependency rule in CI:</p>
<pre><code class="language-csharp">[Fact]
public void Domain_Should_Not_Reference_Other_Projects()
{
    var result = Types
        .InAssembly(typeof(Entity).Assembly)
        .ShouldNot()
        .HaveDependencyOnAny(&quot;MyApp.Application&quot;, &quot;MyApp.Infrastructure&quot;,
            &quot;MyApp.Persistence&quot;, &quot;MyApp.Api&quot;)
        .GetResult();

    result.IsSuccessful.Should().BeTrue();
}
</code></pre>
<p>Without this, the template's project references are just a suggestion.
I shared my five go-to rules in <a href="https://milanjovanovic.tech/blog/5-architecture-tests-you-should-add-to-your-dotnet-projects"><strong>5 architecture tests you should add to your .NET projects</strong></a>.</p>
<h2>Should You Use an Existing Template?</h2>
<p>There are excellent public templates (Jason Taylor's Clean Architecture template and Ardalis' Clean Architecture solution are the best known).
They're great for learning the patterns.</p>
<p>But I'd still encourage you to build your own, for two reasons:</p>
<ol>
<li><strong>Public templates encode someone else's defaults.</strong> Identity setup, mapping libraries, front-end scaffolding - you'll spend the first day deleting things.</li>
<li><strong>The best template is a project you already shipped.</strong> Strip out the business logic, keep the skeleton, and you have a starting point that matches how your team actually works.</li>
</ol>
<p>My own version of this setup is the foundation of <a href="https://milanjovanovic.tech/pragmatic-clean-architecture"><strong>Pragmatic Clean Architecture</strong></a>, where I walk through every one of these decisions in depth.</p>
<h2>Creating the Template</h2>
<p>To turn the solution into a <code>dotnet new</code> template, create a <code>.template.config/template.json</code> file in the solution root:</p>
<pre><code class="language-json">{
  &quot;$schema&quot;: &quot;http://json.schemastore.org/template&quot;,
  &quot;author&quot;: &quot;Your Name&quot;,
  &quot;classifications&quot;: [&quot;Web&quot;, &quot;Clean Architecture&quot;],
  &quot;identity&quot;: &quot;CleanArchitecture.Template&quot;,
  &quot;name&quot;: &quot;Clean Architecture Solution&quot;,
  &quot;shortName&quot;: &quot;cleanarch&quot;,
  &quot;sourceName&quot;: &quot;MyApp&quot;,
  &quot;tags&quot;: {
    &quot;language&quot;: &quot;C#&quot;,
    &quot;type&quot;: &quot;solution&quot;
  }
}
</code></pre>
<p>The <code>sourceName</code> means every occurrence of &quot;MyApp&quot; in filenames and file content gets replaced with whatever name you pass via <code>-n</code>.</p>
<p>Then install the template and create new solutions from it:</p>
<pre><code class="language-bash">dotnet new install ./path/to/template
dotnet new cleanarch -n MyApp
</code></pre>
<h2>Takeaway</h2>
<p>A Clean Architecture solution template for .NET needs:</p>
<ol>
<li><strong>Five projects</strong> - Domain, Application, Infrastructure, Persistence, Api</li>
<li><strong>Strict dependency rule</strong> - references only flow inward</li>
<li><strong>CQRS abstractions</strong> - ICommand, IQuery, handlers, pipeline behaviors</li>
<li><strong>DI extension methods</strong> - one <code>AddX()</code> per project</li>
<li><strong>Base primitives</strong> - Entity, AggregateRoot, IDomainEvent, Result</li>
<li><strong>Architecture tests</strong> - the dependency rule enforced in CI, not just in a diagram</li>
</ol>
<p>Set it up once and reuse it across every project.</p>
<p>Thanks for reading, and stay awesome!</p>
<hr>
]]></content:encoded>
            <author>milan@milanjovanovic.tech (Milan Jovanović)</author>
        </item>
        <item>
            <title><![CDATA[Clean Architecture With Minimal APIs in .NET]]></title>
            <link>https://milanjovanovic.tech/blog/clean-architecture-minimal-apis</link>
            <guid isPermaLink="false">https://milanjovanovic.tech/blog/clean-architecture-minimal-apis</guid>
            <pubDate>Tue, 11 Aug 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Minimal API endpoints and Clean Architecture are a natural fit: the endpoint receives HTTP, dispatches a command, and maps the result back.]]></description>
            <content:encoded><![CDATA[<p>Minimal APIs strip endpoint definitions down to plain functions.
Clean Architecture gives all the logic those functions shouldn't contain a proper home.
The combination produces an API layer so thin it's almost boring, which is exactly what you want.
Here's how to structure it, from endpoint organization to validation filters and Problem Details.</p>
<h2>Why Minimal APIs With Clean Architecture?</h2>
<p><a href="https://milanjovanovic.tech/blog/minimal-apis-dotnet">Minimal APIs</a> give you a lightweight, low-ceremony way to define HTTP endpoints in ASP.NET Core.</p>
<p><a href="https://milanjovanovic.tech/blog/clean-architecture-and-the-benefits-of-structured-software-design">Clean Architecture</a> gives you a structured way to organize business logic independently from infrastructure.</p>
<p>Together, they create a Presentation layer that's thin, readable, and easy to maintain. Your Minimal API endpoints become simple adapters that translate HTTP requests into commands/queries and return HTTP responses.</p>
<h2>The Architecture</h2>
<p>An HTTP request flows through the Minimal API endpoint into an Application layer command or query, and the result flows back out as an HTTP response:</p>
<img src="https://milanjovanovic.tech/blogs/articles/clean-architecture-minimal-apis/request-flow-through-layers.png" alt="An HTTP request enters a Minimal API endpoint in the Presentation layer, becomes a command or query handled in the Application layer, which uses the Domain entities and Infrastructure implementations, then returns a result that maps back to an HTTP response">
<p>The Api project is a thin coordination layer. It has two jobs:</p>
<ol>
<li>Map HTTP requests to Application layer commands and queries</li>
<li>Map Application layer results back to HTTP responses</li>
</ol>
<h2>Defining an Endpoint</h2>
<p>Here's a Minimal API endpoint that calls a command handler:</p>
<pre><code class="language-csharp">app.MapPost(&quot;/api/orders&quot;, async (
    PlaceOrderRequest request,
    ICommandHandler&lt;PlaceOrderCommand, Guid&gt; handler,
    CancellationToken cancellationToken) =&gt;
{
    var command = new PlaceOrderCommand(request.CustomerId, request.Items);

    var result = await handler.Handle(command, cancellationToken);

    return result.IsSuccess
        ? Results.Created($&quot;/api/orders/{result.Value}&quot;, new { id = result.Value })
        : result.ToProblemDetails();
});
</code></pre>
<p>The endpoint:</p>
<ol>
<li>Receives the HTTP request</li>
<li>Creates a command from the request body</li>
<li>Passes it to the handler (from the <a href="https://milanjovanovic.tech/blog/application-layer-clean-architecture">Application layer</a>)</li>
<li>Returns an appropriate HTTP response</li>
</ol>
<p>No business logic. No database calls. Just request/response translation.</p>
<p>One note on wiring: the endpoint resolves <code>ICommandHandler&lt;PlaceOrderCommand, Guid&gt;</code> straight from DI.
That works when your handlers are registered against these interfaces (a single <a href="https://github.com/khellang/Scrutor">Scrutor</a> assembly scan handles it).
If you're using MediatR, inject <code>ISender</code> and call <code>Send</code> instead; the shape of the endpoint stays the same.</p>
<h2>Organizing Endpoints</h2>
<p>Minimal APIs can get messy if you dump everything into <code>Program.cs</code>. Organize them using static classes:</p>
<pre><code class="language-csharp">public static class OrderEndpoints
{
    public static void MapOrderEndpoints(this IEndpointRouteBuilder app)
    {
        var group = app.MapGroup(&quot;/api/orders&quot;)
            .WithTags(&quot;Orders&quot;);

        group.MapPost(&quot;&quot;, PlaceOrder);
        group.MapGet(&quot;{orderId:guid}&quot;, GetOrderById);
        group.MapPut(&quot;{orderId:guid}/cancel&quot;, CancelOrder);
        group.MapGet(&quot;&quot;, GetOrders);
    }

    private static async Task&lt;IResult&gt; PlaceOrder(
        PlaceOrderRequest request,
        ICommandHandler&lt;PlaceOrderCommand, Guid&gt; handler,
        CancellationToken cancellationToken)
    {
        var command = new PlaceOrderCommand(request.CustomerId, request.Items);

        var result = await handler.Handle(command, cancellationToken);

        return result.IsSuccess
            ? Results.Created($&quot;/api/orders/{result.Value}&quot;, new { id = result.Value })
            : result.ToProblemDetails();
    }

    private static async Task&lt;IResult&gt; GetOrderById(
        Guid orderId,
        IQueryHandler&lt;GetOrderByIdQuery, OrderResponse&gt; handler,
        CancellationToken cancellationToken)
    {
        var query = new GetOrderByIdQuery(orderId);

        var result = await handler.Handle(query, cancellationToken);

        return result.IsSuccess
            ? Results.Ok(result.Value)
            : Results.NotFound();
    }

    private static async Task&lt;IResult&gt; CancelOrder(
        Guid orderId,
        ICommandHandler&lt;CancelOrderCommand&gt; handler,
        CancellationToken cancellationToken)
    {
        var command = new CancelOrderCommand(orderId);

        var result = await handler.Handle(command, cancellationToken);

        return result.IsSuccess
            ? Results.NoContent()
            : result.ToProblemDetails();
    }

    private static async Task&lt;IResult&gt; GetOrders(
        [AsParameters] GetOrdersRequest request,
        IQueryHandler&lt;GetOrdersQuery, PagedList&lt;OrderSummary&gt;&gt; handler,
        CancellationToken cancellationToken)
    {
        var query = new GetOrdersQuery(request.Page, request.PageSize);

        var result = await handler.Handle(query, cancellationToken);

        return result.IsSuccess
            ? Results.Ok(result.Value)
            : result.ToProblemDetails();
    }
}
</code></pre>
<p>Then register them in <code>Program.cs</code>:</p>
<pre><code class="language-csharp">app.MapOrderEndpoints();
app.MapCustomerEndpoints();
app.MapProductEndpoints();
</code></pre>
<p>Each feature gets its own endpoint file. This follows the <a href="https://milanjovanovic.tech/blog/screaming-architecture">Screaming Architecture</a> principle.</p>
<p>I shared more variations of this approach in <a href="https://milanjovanovic.tech/blog/how-to-structure-minimal-apis"><strong>how to structure Minimal APIs</strong></a>.
And if you want to skip the manual <code>Map*</code> calls in <code>Program.cs</code>, you can <a href="https://milanjovanovic.tech/blog/automatically-register-minimal-apis-in-aspnetcore"><strong>register Minimal API endpoints automatically</strong></a> with a bit of reflection.</p>
<h2>Using Carter for Endpoint Organization</h2>
<p><a href="https://github.com/CarterCommunity/Carter">Carter</a> provides a module system for organizing Minimal API endpoints:</p>
<pre><code class="language-csharp">public class OrderModule : ICarterModule
{
    public void AddRoutes(IEndpointRouteBuilder app)
    {
        var group = app.MapGroup(&quot;/api/orders&quot;).WithTags(&quot;Orders&quot;);

        group.MapPost(&quot;&quot;, PlaceOrder);
        group.MapGet(&quot;{orderId:guid}&quot;, GetOrderById);
    }

    private static async Task&lt;IResult&gt; PlaceOrder(
        PlaceOrderRequest request,
        ICommandHandler&lt;PlaceOrderCommand, Guid&gt; handler,
        CancellationToken cancellationToken)
    {
        var command = new PlaceOrderCommand(request.CustomerId, request.Items);
        var result = await handler.Handle(command, cancellationToken);

        return result.IsSuccess
            ? Results.Created($&quot;/api/orders/{result.Value}&quot;, new { id = result.Value })
            : result.ToProblemDetails();
    }

    private static async Task&lt;IResult&gt; GetOrderById(
        Guid orderId,
        IQueryHandler&lt;GetOrderByIdQuery, OrderResponse&gt; handler,
        CancellationToken cancellationToken)
    {
        var query = new GetOrderByIdQuery(orderId);
        var result = await handler.Handle(query, cancellationToken);

        return result.IsSuccess ? Results.Ok(result.Value) : Results.NotFound();
    }
}
</code></pre>
<p>Carter auto-discovers modules and registers them:</p>
<pre><code class="language-csharp">builder.Services.AddCarter();
// ...
app.MapCarter();
</code></pre>
<h2>Adding Endpoint Filters</h2>
<p>Endpoint Filters are the Minimal API equivalent of action filters. Use them for cross-cutting concerns:</p>
<pre><code class="language-csharp">public class ValidationFilter&lt;TRequest&gt; : IEndpointFilter
{
    public async ValueTask&lt;object?&gt; InvokeAsync(
        EndpointFilterInvocationContext context,
        EndpointFilterDelegate next)
    {
        var request = context.Arguments.OfType&lt;TRequest&gt;().FirstOrDefault();

        if (request is null)
        {
            return Results.BadRequest(&quot;Invalid request.&quot;);
        }

        var validator = context.HttpContext
            .RequestServices
            .GetService&lt;IValidator&lt;TRequest&gt;&gt;();

        if (validator is not null)
        {
            var validationResult = await validator.ValidateAsync(request);
            if (!validationResult.IsValid)
            {
                return Results.ValidationProblem(validationResult.ToDictionary());
            }
        }

        return await next(context);
    }
}
</code></pre>
<p>Apply it to endpoints:</p>
<pre><code class="language-csharp">group.MapPost(&quot;&quot;, PlaceOrder)
    .AddEndpointFilter&lt;ValidationFilter&lt;PlaceOrderRequest&gt;&gt;();
</code></pre>
<h2>Mapping Results to HTTP Responses</h2>
<p>Create an extension method that maps your <a href="https://milanjovanovic.tech/blog/functional-error-handling-in-dotnet-with-the-result-pattern">Result pattern</a> to Problem Details responses:</p>
<pre><code class="language-csharp">public static class ResultExtensions
{
    public static IResult ToProblemDetails(this Result result)
    {
        if (result.IsSuccess)
        {
            throw new InvalidOperationException(&quot;Cannot convert a success result to problem details.&quot;);
        }

        return Results.Problem(
            statusCode: GetStatusCode(result.Error.Type),
            title: GetTitle(result.Error.Type),
            extensions: new Dictionary&lt;string, object?&gt;
            {
                { &quot;errors&quot;, new[] { result.Error } }
            });
    }

    private static int GetStatusCode(ErrorType errorType) =&gt; errorType switch
    {
        ErrorType.Validation =&gt; StatusCodes.Status400BadRequest,
        ErrorType.NotFound =&gt; StatusCodes.Status404NotFound,
        ErrorType.Conflict =&gt; StatusCodes.Status409Conflict,
        ErrorType.Forbidden =&gt; StatusCodes.Status403Forbidden,
        _ =&gt; StatusCodes.Status500InternalServerError
    };

    private static string GetTitle(ErrorType errorType) =&gt; errorType switch
    {
        ErrorType.Validation =&gt; &quot;Bad Request&quot;,
        ErrorType.NotFound =&gt; &quot;Not Found&quot;,
        ErrorType.Conflict =&gt; &quot;Conflict&quot;,
        ErrorType.Forbidden =&gt; &quot;Forbidden&quot;,
        _ =&gt; &quot;Server Error&quot;
    };
}
</code></pre>
<p>This gives you consistent <a href="https://milanjovanovic.tech/blog/problem-details-for-aspnetcore-apis">Problem Details</a> responses across all endpoints.</p>
<h2>Minimal APIs vs Controllers</h2>
<p>How do the two approaches compare?</p>
<ul>
<li><strong>Ceremony</strong>: Minimal APIs are just functions. Controllers need a class, a base type, and routing attributes.</li>
<li><strong>Performance</strong>: Minimal APIs are slightly faster because they skip parts of the MVC pipeline (model binding infrastructure, filters, view support).</li>
<li><strong>Testability</strong>: identical. In both cases you test the handler in the Application layer, not the HTTP adapter.</li>
<li><strong>Organization</strong>: extension methods or Carter modules for Minimal APIs; controller classes for MVC.</li>
<li><strong>OpenAPI</strong>: both work with the built-in OpenAPI document generation in .NET 9+. Minimal APIs describe metadata fluently (<code>WithTags</code>, <code>Produces</code>), controllers use attributes.</li>
<li><strong>Model binding</strong>: Minimal APIs bind from route, query, and body by convention with <code>[AsParameters]</code> for grouping. Controllers use <code>[FromBody]</code>, <code>[FromQuery]</code>, and friends.</li>
</ul>
<p>In a Clean Architecture setup, <strong>both approaches are thin adapters.</strong> The real logic lives in the Application layer. The choice between Minimal APIs and controllers is mostly a style preference.</p>
<p>I prefer Minimal APIs for new projects because they're more concise and align well with the <a href="https://milanjovanovic.tech/blog/repr-pattern-aspnetcore">REPR pattern</a>.</p>
<h2>Takeaway</h2>
<p>Minimal APIs and Clean Architecture complement each other. The API layer becomes a thin adapter: receive HTTP, create command/query, call handler, return HTTP response.</p>
<p>Organize endpoints by feature using extension methods or Carter modules. Use endpoint filters for cross-cutting concerns. Map results to Problem Details consistently.</p>
<p>Keep your endpoints thin, and let the Application layer do the real work.</p>
<p>Thanks for reading, and stay awesome!</p>
<hr>
]]></content:encoded>
            <author>milan@milanjovanovic.tech (Milan Jovanović)</author>
        </item>
        <item>
            <title><![CDATA[Where Does Caching Belong in Clean Architecture?]]></title>
            <link>https://milanjovanovic.tech/blog/caching-clean-architecture</link>
            <guid isPermaLink="false">https://milanjovanovic.tech/blog/caching-clean-architecture</guid>
            <pubDate>Tue, 11 Aug 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Caching is infrastructure, but the decision to cache is application-level. Get that split wrong and cache concerns leak into your use cases, or worse, into…]]></description>
            <content:encoded><![CDATA[<p>Someone on your team adds <code>IMemoryCache</code> to a query handler.
A week later a <code>DbContext</code>-shaped cache call shows up in a domain service, and a month later you cannot answer &quot;what happens if we flush Redis&quot; without reading half the codebase.</p>
<p>Caching has a way of leaking everywhere, because it feels harmless at every individual call site.
Clean Architecture gives you a precise answer for where it goes.
The answer has two halves, and both matter.</p>
<h2>The Split: Mechanism vs. Decision</h2>
<p>Here is the principle that resolves every &quot;where does this go&quot; debate about caching:</p>
<ul>
<li><strong>The mechanism is infrastructure.</strong> Redis clients, <code>IMemoryCache</code>, serialization, TTL bookkeeping, key formatting. All of it is a technical detail, replaceable without touching business behavior. It lives in the infrastructure layer.</li>
<li><strong>The decision is application-level.</strong> <em>What</em> is worth caching, <em>how stale</em> it may be, and <em>when</em> it must be invalidated are things only the use case knows. Product catalog: 5 minutes stale is fine. Account balance: absolutely not. That knowledge belongs to the application layer.</li>
</ul>
<p>And one hard rule: <strong>the domain layer never knows caching exists.</strong>
No entity, value object, or domain service should reference a cache, and no invariant may depend on cached state.
Caching is a performance optimization, and the domain models business rules, not performance.</p>
<p>So the application layer expresses caching <em>intent</em> through an abstraction it owns, and infrastructure provides the implementation.
The abstraction is small:</p>
<pre><code class="language-csharp">public interface ICacheService
{
    Task&lt;T?&gt; GetAsync&lt;T&gt;(string key, CancellationToken ct = default);

    Task SetAsync&lt;T&gt;(
        string key,
        T value,
        TimeSpan? expiration = null,
        CancellationToken ct = default);

    Task RemoveAsync(string key, CancellationToken ct = default);
}
</code></pre>
<p>The interface is defined in the application layer.
Infrastructure implements it with <strong>Redis</strong>, <code>HybridCache</code>, or an in-memory store, and the dependency arrow points inward, exactly as the <a href="https://milanjovanovic.tech/blog/dependency-rule-clean-architecture"><strong>dependency rule</strong></a> requires.</p>
<p>With the split established, there are two clean places to apply it.</p>
<h2>Approach 1: The Caching Decorator</h2>
<p>The <a href="https://milanjovanovic.tech/blog/decorator-pattern-in-asp-net-core"><strong>decorator pattern</strong></a> is the classic answer, and it is still the cleanest when you want caching to be invisible to consumers.</p>
<p>Say you have a repository interface in the application layer:</p>
<pre><code class="language-csharp">public interface IProductRepository
{
    Task&lt;Product?&gt; GetByIdAsync(Guid id, CancellationToken ct = default);

    Task UpdateAsync(Product product, CancellationToken ct = default);
}
</code></pre>
<p>The caching decorator wraps the real implementation:</p>
<pre><code class="language-csharp">public sealed class CachedProductRepository(
    IProductRepository inner,
    ICacheService cache) : IProductRepository
{
    private static readonly TimeSpan Expiration = TimeSpan.FromMinutes(5);

    public async Task&lt;Product?&gt; GetByIdAsync(Guid id, CancellationToken ct = default)
    {
        string key = $&quot;products:{id}&quot;;

        Product? cached = await cache.GetAsync&lt;Product&gt;(key, ct);
        if (cached is not null)
        {
            return cached;
        }

        Product? product = await inner.GetByIdAsync(id, ct);

        if (product is not null)
        {
            await cache.SetAsync(key, product, Expiration, ct);
        }

        return product;
    }

    public async Task UpdateAsync(Product product, CancellationToken ct = default)
    {
        await inner.UpdateAsync(product, ct);

        await cache.RemoveAsync($&quot;products:{product.Id}&quot;, ct);
    }
}
</code></pre>
<p>Notice <code>UpdateAsync</code>: the decorator is also the natural home for <strong>write-through invalidation</strong>, because it sees every mutation that goes through the interface.</p>
<img src="https://milanjovanovic.tech/blogs/articles/caching-clean-architecture/caching-decorator-flow.png" alt="Request flow through a caching decorator: on a cache hit it returns the cached value, and on a miss it calls the inner repository, reads the database, stores the result with a TTL, then returns it">
<p>Registration with <a href="https://milanjovanovic.tech/blog/improving-aspnetcore-dependency-injection-with-scrutor"><strong>Scrutor</strong></a> is one <code>Decorate</code> call:</p>
<pre><code class="language-csharp">builder.Services.AddScoped&lt;IProductRepository, ProductRepository&gt;();
builder.Services.Decorate&lt;IProductRepository, CachedProductRepository&gt;();
</code></pre>
<p>The strengths of this approach: use case handlers do not change at all, caching is centralized per aggregate, and removing it is a one-line rollback.
The weakness: the TTL decision now lives in infrastructure, one step removed from the use cases that actually know the staleness requirements.
For entity-by-id caching that is usually fine, because the policy is uniform.</p>
<p>One caveat: the decorator caches a domain entity, which costs nothing with an in-memory store but means serialization in a distributed cache.
Encapsulated entities with private setters often do not round-trip through JSON without extra serializer configuration, so verify that before pointing this at Redis.</p>
<h2>Approach 2: The Use Case Declares Its Caching</h2>
<p>For query results (the read side of CQRS), I prefer the use case itself to declare its caching policy.
The query says what it needs; a pipeline behavior does the work.</p>
<p>Define a marker interface in the application layer:</p>
<pre><code class="language-csharp">public interface ICachedQuery
{
    string CacheKey { get; }

    TimeSpan? Expiration { get; }
}
</code></pre>
<p>A query opts in by implementing it:</p>
<pre><code class="language-csharp">public sealed record GetProductCatalogQuery(int Page, int PageSize)
    : IRequest&lt;ProductCatalogResponse&gt;, ICachedQuery
{
    public string CacheKey =&gt; $&quot;catalog:page-{Page}:size-{PageSize}&quot;;

    public TimeSpan? Expiration =&gt; TimeSpan.FromMinutes(5);
}
</code></pre>
<p>And a single <a href="https://milanjovanovic.tech/blog/mediatr-pipeline-behaviors"><strong>pipeline behavior</strong></a> handles every cached query in the system:</p>
<pre><code class="language-csharp">public sealed class QueryCachingBehavior&lt;TRequest, TResponse&gt;(
    ICacheService cache,
    ILogger&lt;QueryCachingBehavior&lt;TRequest, TResponse&gt;&gt; logger)
    : IPipelineBehavior&lt;TRequest, TResponse&gt;
    where TRequest : ICachedQuery
{
    public async Task&lt;TResponse&gt; Handle(
        TRequest request,
        RequestHandlerDelegate&lt;TResponse&gt; next,
        CancellationToken ct)
    {
        TResponse? cached = await cache.GetAsync&lt;TResponse&gt;(request.CacheKey, ct);

        if (cached is not null)
        {
            logger.LogDebug(&quot;Cache hit: {CacheKey}&quot;, request.CacheKey);

            return cached;
        }

        TResponse response = await next();

        await cache.SetAsync(request.CacheKey, response, request.Expiration, ct);

        return response;
    }
}
</code></pre>
<p>Registered once:</p>
<pre><code class="language-csharp">builder.Services.AddMediatR(config =&gt;
{
    config.RegisterServicesFromAssembly(typeof(ICachedQuery).Assembly);
    config.AddOpenBehavior(typeof(QueryCachingBehavior&lt;,&gt;));
});
</code></pre>
<p>This is my default for query caching, for one reason: <strong>the staleness contract sits on the query definition</strong>, right where the person changing the use case will see it.
The decision is application-level, visibly, while the mechanism stays behind <code>ICacheService</code> in infrastructure.
Exactly the split we wanted.</p>
<h2>Invalidation Is Application Logic Too</h2>
<p>The part most articles skip: who evicts?</p>
<p>Knowing that <code>UpdateProductCommand</code> invalidates <code>products:{id}</code> and every <code>catalog:*</code> page is pure use-case knowledge.
So invalidation belongs in the application layer, and the cleanest trigger is the same event flow you already have.</p>
<p>If your aggregates raise domain events, a cache-eviction handler is a natural subscriber:</p>
<pre><code class="language-csharp">public sealed class ProductUpdatedCacheEvictionHandler(ICacheService cache)
    : INotificationHandler&lt;ProductUpdatedDomainEvent&gt;
{
    public async Task Handle(ProductUpdatedDomainEvent domainEvent, CancellationToken ct)
    {
        await cache.RemoveAsync($&quot;products:{domainEvent.ProductId}&quot;, ct);
        await cache.RemoveByPrefixAsync(&quot;catalog:&quot;, ct);
    }
}
</code></pre>
<p><code>RemoveByPrefixAsync</code> is an extra method you add to <code>ICacheService</code> for view-level eviction; the Redis implementation backs it with key scans or key tagging.</p>
<p>One handler owns the mapping from &quot;product changed&quot; to &quot;these views are stale&quot;.
When invalidation logic is scattered across command handlers, one forgotten eviction ships a stale-data bug; centralizing it per event keeps the mapping auditable.
For the eviction techniques themselves (prefix removal, versioned keys, TTL fallbacks), see <strong>cache invalidation strategies</strong>.</p>
<p>Two guardrails to keep this honest:</p>
<ul>
<li><strong>Always set a TTL, even with explicit invalidation.</strong> Expiration is your safety net when an eviction path is missed.</li>
<li><strong>On the query side, cache DTOs and read models, not entities.</strong> Responses and read models are stable, flat, and safe to serialize. Entity caching belongs in the repository decorator, centralized and paired with its invalidation, not scattered across handlers.</li>
</ul>
<p>I cover this decision, including where caching fits among the other cross-cutting concerns, in <a href="https://milanjovanovic.tech/pragmatic-clean-architecture"><strong>Pragmatic Clean Architecture</strong></a>.</p>
<h2>Choosing Between the Two</h2>
<p>Practical guidance:</p>
<ul>
<li><strong>Entity-by-id lookups behind a repository</strong>: decorator. Uniform policy, invisible to handlers, easy rollback.</li>
<li><strong>Query results and composed read models</strong>: pipeline behavior with <code>ICachedQuery</code>. Policy lives with the use case that owns the staleness requirement.</li>
<li><strong>Both in one codebase</strong>: completely fine, and common. They share the same <code>ICacheService</code>, so infrastructure stays singular.</li>
</ul>
<p>Whichever you pick, run the flush test: if flushing the cache changes any business outcome (not latency, outcome), caching has leaked past infrastructure and needs to be pushed back out.</p>
<h2>The Two-Part Answer</h2>
<p>The mechanism (Redis, memory, serialization, TTLs) is infrastructure behind an application-owned <code>ICacheService</code>.
The decision (what to cache, how stale, when to evict) is application knowledge, expressed either as a decorator policy per repository or as a declaration on the query itself.</p>
<p>The domain never finds out.
And when someone asks &quot;what happens if we flush Redis&quot;, the answer should be one sentence: everything gets slower for a minute, and nothing else changes.</p>
]]></content:encoded>
            <author>milan@milanjovanovic.tech (Milan Jovanović)</author>
        </item>
        <item>
            <title><![CDATA[Background Jobs in Clean Architecture]]></title>
            <link>https://milanjovanovic.tech/blog/background-jobs-clean-architecture</link>
            <guid isPermaLink="false">https://milanjovanovic.tech/blog/background-jobs-clean-architecture</guid>
            <pubDate>Tue, 11 Aug 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Where do background jobs fit in Clean Architecture? Treat them as entry points, exactly like controllers.]]></description>
            <content:encoded><![CDATA[<p>Sooner or later, every codebase grows a background job with 300 lines of business logic inside <code>ExecuteAsync</code>.
Order expiration, email digests, data cleanup, all living in a class the domain layer has never heard of.</p>
<p>Clean Architecture already has an answer for this, and it is simpler than people expect.
A background job is just another <strong>entry point</strong>.
It belongs in the same ring as your controllers, and it should be just as thin.</p>
<h2>Jobs Are Entry Points, Not a New Layer</h2>
<p>Think about what a controller does in <a href="https://milanjovanovic.tech/blog/clean-architecture-dotnet"><strong>Clean Architecture</strong></a>: it accepts input from the outside world (HTTP), translates it into a use case invocation, and returns the result.
It contains no business rules.</p>
<p>A background job is the same thing with a different trigger.
Instead of an HTTP request, the trigger is a schedule, a timer, or a queue message.
Everything after the trigger should be identical: invoke a use case in the application layer.</p>
<p>That gives you the placement rule:</p>
<ul>
<li><strong>The job class</strong> (the <code>BackgroundService</code>, the Quartz <code>IJob</code>, the Hangfire method) lives in infrastructure or presentation, wherever your other entry points live.</li>
<li><strong>The work itself</strong> is an application-layer use case: a command handler or an application service.</li>
<li><strong>The domain rules</strong> the work enforces stay in the domain layer.</li>
</ul>
<img src="https://milanjovanovic.tech/blogs/articles/background-jobs-clean-architecture/job-as-entry-point.png" alt="A background job and an HTTP controller are two triggers for the same use case: both create a scope and send a command to a handler in the application layer, which invokes the domain rules">
<p>The payoff is the same one controllers get.
The use case is testable without any scheduler, reusable from other entry points (an admin endpoint can trigger the same cleanup), and wrapped by the same cross-cutting behaviors: validation, logging, transactions.</p>
<p>If the logic is in the job class, none of that is true.
You cannot invoke it from anywhere else, and testing it means fighting the scheduling machinery, which is exactly the pain I described in <strong>testing background services</strong>.</p>
<h2>The Mechanical Detail: Scoped Services in a Singleton</h2>
<p>Before the examples, the one wiring detail that trips up everyone.</p>
<p>A <code>BackgroundService</code> is registered as a singleton and lives for the entire process.
Your application services, your <code>DbContext</code>, and your MediatR handlers are <strong>scoped</strong>.
Inject a scoped service into the singleton constructor and you get the infamous &quot;Cannot consume scoped service from singleton&quot; error, or worse, a single <code>DbContext</code> instance shared across the whole application lifetime.</p>
<p>The fix is always the same: inject <code>IServiceScopeFactory</code> and create a scope per execution.
I covered the underlying rules in <a href="https://milanjovanovic.tech/blog/using-scoped-services-from-singletons-in-aspnetcore"><strong>using scoped services from singletons</strong></a>, and you will see the pattern in both examples below.</p>
<h2>Option 1: BackgroundService Triggering a Use Case</h2>
<p>Start with the use case itself, in the application layer.
It knows nothing about scheduling:</p>
<pre><code class="language-csharp">public sealed record ExpireStaleOrdersCommand : IRequest&lt;int&gt;;

public sealed class ExpireStaleOrdersCommandHandler(
    IOrderRepository orderRepository,
    IUnitOfWork unitOfWork,
    TimeProvider timeProvider)
    : IRequestHandler&lt;ExpireStaleOrdersCommand, int&gt;
{
    public async Task&lt;int&gt; Handle(
        ExpireStaleOrdersCommand command,
        CancellationToken ct)
    {
        DateTime cutoff = timeProvider.GetUtcNow().UtcDateTime.AddHours(-24);

        IReadOnlyList&lt;Order&gt; staleOrders =
            await orderRepository.GetPendingOlderThanAsync(cutoff, ct);

        foreach (Order order in staleOrders)
        {
            order.Expire();
        }

        await unitOfWork.SaveChangesAsync(ct);

        return staleOrders.Count;
    }
}
</code></pre>
<p>The <code>Expire</code> method is a domain behavior on the <code>Order</code> entity.
The handler orchestrates; the entity enforces the rules.</p>
<p>Now the job, in infrastructure.
It is a scheduling shell around the command:</p>
<pre><code class="language-csharp">public sealed class ExpireStaleOrdersJob(
    IServiceScopeFactory scopeFactory,
    ILogger&lt;ExpireStaleOrdersJob&gt; logger) : BackgroundService
{
    private static readonly TimeSpan Interval = TimeSpan.FromMinutes(15);

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        using var timer = new PeriodicTimer(Interval);

        while (await timer.WaitForNextTickAsync(stoppingToken))
        {
            try
            {
                using IServiceScope scope = scopeFactory.CreateScope();

                ISender sender = scope.ServiceProvider
                    .GetRequiredService&lt;ISender&gt;();

                int expired = await sender.Send(
                    new ExpireStaleOrdersCommand(), stoppingToken);

                logger.LogInformation(&quot;Expired {Count} stale orders&quot;, expired);
            }
            catch (Exception ex) when (ex is not OperationCanceledException)
            {
                logger.LogError(ex, &quot;Order expiration run failed&quot;);
            }
        }
    }
}
</code></pre>
<p>Register it in <code>Program.cs</code>:</p>
<pre><code class="language-csharp">builder.Services.AddHostedService&lt;ExpireStaleOrdersJob&gt;();
</code></pre>
<p>Three details worth noticing:</p>
<ul>
<li>The scope is created <strong>per tick</strong>, so each run gets a fresh <code>DbContext</code> and properly scoped dependencies.</li>
<li>The <code>try/catch</code> swallows failures per run instead of letting one exception kill the loop forever. An exception that escapes <code>ExecuteAsync</code> stops the job for good, and since .NET 6 the default <code>BackgroundServiceExceptionBehavior</code> takes the entire host down with it.</li>
<li>Cancellation flows through, so shutdown is clean.</li>
</ul>
<p>The shell never changes as business logic evolves.
Every future change happens in the handler, where it is unit-testable.</p>
<h2>Option 2: Quartz Job Triggering the Same Use Case</h2>
<p><code>PeriodicTimer</code> loops are fine for simple intervals.
For cron schedules, persistence, and no-overlap guarantees, I reach for <a href="https://milanjovanovic.tech/blog/scheduling-background-jobs-with-quartz-net"><strong>Quartz</strong></a>:</p>
<pre><code class="language-bash">dotnet add package Quartz.Extensions.Hosting
</code></pre>
<p>The Quartz job is even thinner, because the Microsoft DI integration resolves every job from a fresh scope per execution:</p>
<pre><code class="language-csharp">[DisallowConcurrentExecution]
public sealed class ExpireStaleOrdersQuartzJob(ISender sender) : IJob
{
    public async Task Execute(IJobExecutionContext context)
    {
        await sender.Send(
            new ExpireStaleOrdersCommand(),
            context.CancellationToken);
    }
}
</code></pre>
<p>And the wiring:</p>
<pre><code class="language-csharp">builder.Services.AddQuartz(options =&gt;
{
    var jobKey = JobKey.Create(nameof(ExpireStaleOrdersQuartzJob));

    options.AddJob&lt;ExpireStaleOrdersQuartzJob&gt;(jobKey)
        .AddTrigger(trigger =&gt; trigger
            .ForJob(jobKey)
            .WithCronSchedule(&quot;0 0/15 * * * ?&quot;));
});

builder.Services.AddQuartzHostedService(options =&gt;
{
    options.WaitForJobsToComplete = true;
});
</code></pre>
<p>Because Quartz resolves the job from a scope, <code>ISender</code> injects directly.
No <code>IServiceScopeFactory</code> ceremony.
<code>DisallowConcurrentExecution</code> prevents overlapping runs when one execution outlasts its schedule, and in a clustered Quartz setup that guarantee holds across application instances, something the timer loop cannot offer.</p>
<p>The important part: <strong>the handler did not change</strong>.
Swapping the scheduling technology (timer loop, Quartz, Hangfire, a queue consumer) touches only the outer shell.
That is the dependency rule doing its job.</p>
<h2>Where the Boundaries Earn Their Keep</h2>
<p>This structure pays off in three specific ways.</p>
<p><strong>Testing.</strong>
The handler is a plain class with injected interfaces.
Unit test it like any other use case, the way I showed in <a href="https://milanjovanovic.tech/blog/unit-testing-clean-architecture-use-cases"><strong>unit testing Clean Architecture use cases</strong></a>.
Zero scheduler involvement, and <code>TimeProvider</code> makes the cutoff logic deterministic.</p>
<p><strong>Reuse.</strong>
A support engineer needs to force-run the cleanup?
Add an admin endpoint that sends the same command.
The job and the endpoint are two triggers for one use case.</p>
<p><strong>Cross-cutting behaviors.</strong>
If your pipeline has validation, logging, and transaction behaviors, jobs get them for free by going through <code>ISender</code>.
Job-embedded logic bypasses all of it.</p>
<p>One boundary question comes up often: what about the <strong>outbox processor</strong>, the job that publishes pending integration events?
That one is legitimately pure infrastructure.
It moves messages, it invokes no business use case, so it can live entirely in the infrastructure layer without an application-layer command.
The rule is not &quot;every job sends a command&quot;; it is &quot;business logic never lives in the job&quot;.
The <a href="https://milanjovanovic.tech/blog/implementing-the-outbox-pattern"><strong>outbox pattern</strong></a> processor has no business logic, so it is fine as-is.</p>
<p>I go deep on structuring entry points, use cases, and the dependency rule in <a href="https://milanjovanovic.tech/pragmatic-clean-architecture"><strong>Pragmatic Clean Architecture</strong></a>, including how jobs and messaging consumers fit the same shape.</p>
<h2>Takeaway</h2>
<p>Background jobs do not need a special place in Clean Architecture, because they are not special.
They are entry points: a schedule instead of an HTTP request, a scoped execution instead of a request scope, and a use case invocation at the center either way.</p>
<p>Keep the job class thin enough that it never needs a test beyond &quot;it sends the command&quot;.
Create a scope per execution, catch per-run exceptions so the loop survives, and let Quartz handle cron and overlap when the schedule gets serious.</p>
<p>If your job class has business rules in it today, extract them into a command handler.
The job will shrink to ten lines, and the logic will finally be testable.</p>
]]></content:encoded>
            <author>milan@milanjovanovic.tech (Milan Jovanović)</author>
        </item>
        <item>
            <title><![CDATA[Authentication and Authorization in Clean Architecture]]></title>
            <link>https://milanjovanovic.tech/blog/authentication-authorization-clean-architecture</link>
            <guid isPermaLink="false">https://milanjovanovic.tech/blog/authentication-authorization-clean-architecture</guid>
            <pubDate>Tue, 11 Aug 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Authentication is infrastructure, authorization is a business rule, and confusing the two is how ASP.NET Core leaks into your domain.]]></description>
            <content:encoded><![CDATA[<p>Every application needs to answer two questions: who is calling, and are they allowed to do this?
In Clean Architecture, those two questions have very different answers.
Authentication is plumbing that belongs at the edge, while authorization is business logic that belongs in the core.
Get the split wrong and you either couple your domain to ASP.NET Core or scatter permission checks across controllers where half your entry points never see them.</p>
<h2>The Question</h2>
<p><a href="https://milanjovanovic.tech/blog/clean-architecture-and-the-benefits-of-structured-software-design"><strong>Clean Architecture</strong></a> has strict rules about dependencies.
So where do authentication and authorization fit?</p>
<ul>
<li><strong>Authentication</strong> (who are you?) is an infrastructure concern</li>
<li><strong>Authorization</strong> (can you do this?) spans multiple layers</li>
</ul>
<p>Let me show you how to implement both without violating layer boundaries.</p>
<h2>Authentication: Infrastructure Layer</h2>
<p>Authentication is how users prove their identity - JWT tokens, cookies, OAuth flows.
This is entirely an <a href="https://milanjovanovic.tech/blog/infrastructure-layer-clean-architecture"><strong>Infrastructure layer</strong></a> concern.</p>
<p>Configure <a href="https://milanjovanovic.tech/blog/jwt-authentication-aspnetcore"><strong>JWT authentication</strong></a> in the Presentation or Infrastructure layer:</p>
<pre><code class="language-csharp">// Infrastructure/Authentication/JwtConfiguration.cs
public static class JwtConfiguration
{
    public static IServiceCollection AddJwtAuthentication(
        this IServiceCollection services,
        IConfiguration configuration)
    {
        services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
            .AddJwtBearer(options =&gt;
            {
                options.TokenValidationParameters = new TokenValidationParameters
                {
                    ValidateIssuer = true,
                    ValidateAudience = true,
                    ValidateLifetime = true,
                    ValidateIssuerSigningKey = true,
                    ValidIssuer = configuration[&quot;Jwt:Issuer&quot;],
                    ValidAudience = configuration[&quot;Jwt:Audience&quot;],
                    IssuerSigningKey = new SymmetricSecurityKey(
                        Encoding.UTF8.GetBytes(configuration[&quot;Jwt:SecretKey&quot;]!))
                };
            });

        return services;
    }
}
</code></pre>
<p>The Application layer doesn't know <em>how</em> users are authenticated.
It only knows <em>who</em> the current user is.</p>
<h2>The Current User Abstraction</h2>
<p>Define an interface in the Application layer:</p>
<pre><code class="language-csharp">// Application/Abstractions/ICurrentUserService.cs
public interface ICurrentUserService
{
    Guid UserId { get; }
    string Email { get; }
    IReadOnlyCollection&lt;string&gt; Roles { get; }
    IReadOnlyCollection&lt;string&gt; Permissions { get; }
    bool IsAuthenticated { get; }
}
</code></pre>
<p>Implement it in Infrastructure:</p>
<pre><code class="language-csharp">// Infrastructure/Authentication/CurrentUserService.cs
public class CurrentUserService : ICurrentUserService
{
    private readonly IHttpContextAccessor _httpContextAccessor;

    public CurrentUserService(IHttpContextAccessor httpContextAccessor)
    {
        _httpContextAccessor = httpContextAccessor;
    }

    public Guid UserId =&gt; Guid.Parse(
        _httpContextAccessor.HttpContext?.User
            .FindFirstValue(ClaimTypes.NameIdentifier) ?? Guid.Empty.ToString());

    public string Email =&gt;
        _httpContextAccessor.HttpContext?.User
            .FindFirstValue(ClaimTypes.Email) ?? string.Empty;

    public IReadOnlyCollection&lt;string&gt; Roles =&gt;
        _httpContextAccessor.HttpContext?.User
            .FindAll(ClaimTypes.Role)
            .Select(c =&gt; c.Value)
            .ToList() ?? [];

    public IReadOnlyCollection&lt;string&gt; Permissions =&gt;
        _httpContextAccessor.HttpContext?.User
            .FindAll(&quot;permission&quot;)
            .Select(c =&gt; c.Value)
            .ToList() ?? [];

    public bool IsAuthenticated =&gt;
        _httpContextAccessor.HttpContext?.User.Identity?.IsAuthenticated ?? false;
}
</code></pre>
<p>Register it:</p>
<pre><code class="language-csharp">services.AddHttpContextAccessor();
services.AddScoped&lt;ICurrentUserService, CurrentUserService&gt;();
</code></pre>
<p>Now your handlers can access the current user without depending on ASP.NET Core:</p>
<pre><code class="language-csharp">public sealed class GetMyOrdersQueryHandler
    : IQueryHandler&lt;GetMyOrdersQuery, List&lt;OrderResponse&gt;&gt;
{
    private readonly ICurrentUserService _currentUser;
    private readonly IOrderRepository _orderRepository;

    public GetMyOrdersQueryHandler(
        ICurrentUserService currentUser,
        IOrderRepository orderRepository)
    {
        _currentUser = currentUser;
        _orderRepository = orderRepository;
    }

    public async Task&lt;Result&lt;List&lt;OrderResponse&gt;&gt;&gt; Handle(
        GetMyOrdersQuery query, CancellationToken ct)
    {
        var orders = await _orderRepository.GetByCustomerIdAsync(
            _currentUser.UserId, ct);

        return orders.Select(o =&gt; o.ToResponse()).ToList();
    }
}
</code></pre>
<p>I go deeper on this abstraction (and a few variations of it) in <a href="https://milanjovanovic.tech/blog/getting-the-current-user-in-clean-architecture"><strong>getting the current user in Clean Architecture</strong></a>.</p>
<p>One gotcha to watch for: if you populate roles or permissions from an external identity provider, the claims may not arrive under the claim types you expect.
<a href="https://milanjovanovic.tech/blog/master-claims-transformation-for-flexible-aspnetcore-authorization"><strong>Claims transformation</strong></a> is the right place to normalize them before your <code>CurrentUserService</code> reads them.</p>
<p>Another JWT-specific gotcha: <code>CurrentUserService</code> reads <code>ClaimTypes.NameIdentifier</code>, which only exists because ASP.NET Core remaps the token's <code>sub</code> claim by default.
If you disable that mapping with <code>options.MapInboundClaims = false</code>, read the <code>sub</code> claim directly instead.
Otherwise <code>UserId</code> silently falls back to <code>Guid.Empty</code>.</p>
<h2>Authorization: Application Layer</h2>
<p>Authorization logic lives in the Application layer because it's a business rule: &quot;Only managers can approve orders over $10,000.&quot;</p>
<h3>Option 1: In the Handler</h3>
<p>For simple authorization checks:</p>
<pre><code class="language-csharp">public sealed class ApproveOrderCommandHandler
    : ICommandHandler&lt;ApproveOrderCommand&gt;
{
    private readonly ICurrentUserService _currentUser;
    private readonly IOrderRepository _orderRepository;
    private readonly IUnitOfWork _unitOfWork;

    public ApproveOrderCommandHandler(
        ICurrentUserService currentUser,
        IOrderRepository orderRepository,
        IUnitOfWork unitOfWork)
    {
        _currentUser = currentUser;
        _orderRepository = orderRepository;
        _unitOfWork = unitOfWork;
    }

    public async Task&lt;Result&gt; Handle(
        ApproveOrderCommand command, CancellationToken ct)
    {
        if (!_currentUser.Roles.Contains(&quot;Manager&quot;))
        {
            return Result.Failure(AuthorizationErrors.InsufficientRole(&quot;Manager&quot;));
        }

        var order = await _orderRepository.GetByIdAsync(command.OrderId, ct);

        if (order is null)
        {
            return Result.Failure(OrderErrors.NotFound(command.OrderId));
        }

        order.Approve(_currentUser.UserId);
        await _unitOfWork.SaveChangesAsync(ct);

        return Result.Success();
    }
}
</code></pre>
<h3>Option 2: Authorization Pipeline Behavior</h3>
<p>For declarative, reusable <strong>permission-based authorization</strong>, decorate the command with the required permission:</p>
<pre><code class="language-csharp">// Application/Authorization/AuthorizeAttribute.cs
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
public class AuthorizeAttribute : Attribute
{
    public string? Permission { get; set; }
    public string? Role { get; set; }
}

// Decorate the command
[Authorize(Permission = &quot;orders:approve&quot;)]
public sealed record ApproveOrderCommand(Guid OrderId) : ICommand;
</code></pre>
<p>Then create a pipeline behavior that enforces it:</p>
<pre><code class="language-csharp">public class AuthorizationBehavior&lt;TRequest, TResponse&gt;
    : IPipelineBehavior&lt;TRequest, TResponse&gt;
    where TRequest : IRequest&lt;TResponse&gt;
{
    private readonly ICurrentUserService _currentUser;

    public AuthorizationBehavior(ICurrentUserService currentUser)
    {
        _currentUser = currentUser;
    }

    public async Task&lt;TResponse&gt; Handle(
        TRequest request,
        RequestHandlerDelegate&lt;TResponse&gt; next,
        CancellationToken ct)
    {
        var authorizeAttributes = request
            .GetType()
            .GetCustomAttributes&lt;AuthorizeAttribute&gt;()
            .ToList();

        if (authorizeAttributes.Count == 0)
        {
            return await next();
        }

        foreach (var attribute in authorizeAttributes)
        {
            if (attribute.Permission is not null &amp;&amp;
                !_currentUser.Permissions.Contains(attribute.Permission))
            {
                throw new ForbiddenAccessException(attribute.Permission);
            }

            if (attribute.Role is not null &amp;&amp;
                !_currentUser.Roles.Contains(attribute.Role))
            {
                throw new ForbiddenAccessException(attribute.Role);
            }
        }

        return await next();
    }
}
</code></pre>
<p>The <code>ForbiddenAccessException</code> is a simple custom exception defined in the Application layer:</p>
<pre><code class="language-csharp">public sealed class ForbiddenAccessException : Exception
{
    public ForbiddenAccessException(string requirement)
        : base($&quot;Access denied. Missing requirement: {requirement}&quot;)
    {
    }
}
</code></pre>
<p>Your global exception handler in the Presentation layer translates it to a <code>403 Forbidden</code> response.
If you prefer to avoid exceptions for flow control, you can constrain <code>TResponse</code> to your <code>Result</code> type and return a failure instead; the tradeoff is some reflection to construct <code>Result&lt;T&gt;</code> failures generically.</p>
<p>This approach separates <em>what</em> permissions are needed (attribute on the command) from <em>how</em> they're enforced (pipeline behavior).</p>
<h3>Option 3: Resource-Based Authorization</h3>
<p>When authorization depends on the resource itself:</p>
<pre><code class="language-csharp">public sealed class UpdateProjectCommandHandler
    : ICommandHandler&lt;UpdateProjectCommand&gt;
{
    private readonly ICurrentUserService _currentUser;
    private readonly IProjectRepository _projectRepository;
    private readonly IUnitOfWork _unitOfWork;

    public UpdateProjectCommandHandler(
        ICurrentUserService currentUser,
        IProjectRepository projectRepository,
        IUnitOfWork unitOfWork)
    {
        _currentUser = currentUser;
        _projectRepository = projectRepository;
        _unitOfWork = unitOfWork;
    }

    public async Task&lt;Result&gt; Handle(
        UpdateProjectCommand command, CancellationToken ct)
    {
        var project = await _projectRepository.GetByIdAsync(
            command.ProjectId, ct);

        if (project is null)
        {
            return Result.Failure(ProjectErrors.NotFound(command.ProjectId));
        }

        if (!project.IsOwner(_currentUser.UserId) &amp;&amp;
            !project.IsMember(_currentUser.UserId))
        {
            return Result.Failure(AuthorizationErrors.Forbidden());
        }

        project.Update(command.Name, command.Description);
        await _unitOfWork.SaveChangesAsync(ct);

        return Result.Success();
    }
}
</code></pre>
<p>The authorization rule (<code>IsOwner</code> or <code>IsMember</code>) is part of the domain model.</p>
<h2>What About the [Authorize] Attribute?</h2>
<p>You should still use ASP.NET Core's <code>[Authorize]</code> attribute (or <code>RequireAuthorization()</code> on Minimal API endpoints).
Just be clear about its job.</p>
<p>Endpoint-level authorization is a <strong>coarse gate</strong>: is the caller authenticated, does the token carry the right scope?
It protects the HTTP entry point, and only the HTTP entry point.</p>
<p>The permission checks in your Application layer are the real enforcement.
They run no matter how the use case is invoked: HTTP endpoint, background job, message consumer, or a test.
If your only authorization lives in controller attributes, every non-HTTP entry point bypasses it.</p>
<p>Use both, at different granularities:</p>
<ul>
<li><strong>Presentation</strong>: <code>[Authorize]</code> for &quot;must be authenticated&quot; and scope checks</li>
<li><strong>Application</strong>: permission and role checks per use case</li>
<li><strong>Domain</strong>: ownership and membership rules on the aggregate</li>
</ul>
<h2>The Background Job Gotcha</h2>
<p>Here's the failure mode that bites almost everyone eventually.</p>
<p><code>CurrentUserService</code> reads from <code>IHttpContextAccessor</code>.
In a background job or a message consumer, there is no HTTP context, so <code>HttpContext</code> is <code>null</code> and <code>UserId</code> silently becomes <code>Guid.Empty</code>.
Your audit trail now says &quot;nobody&quot; approved the order.</p>
<p>Two ways to handle it:</p>
<ol>
<li><strong>Pass the user explicitly.</strong> When a use case is triggered from a message, include the acting user's ID in the message payload and flow it into the command. This is my default: it's explicit and survives serialization boundaries.</li>
<li><strong>Swap the implementation.</strong> Register a different <code>ICurrentUserService</code> for worker processes (for example, one representing a system account). This works well for genuinely system-initiated operations like scheduled cleanups.</li>
</ol>
<p>Whichever you pick, make <code>IsAuthenticated</code> meaningful in both contexts and fail loudly (not with <code>Guid.Empty</code>) when a use case requires a real user.</p>
<h2>The Layer Boundaries</h2>
<p>Here's the summary of where each concern lives:</p>
<img src="https://milanjovanovic.tech/blogs/articles/authentication-authorization-clean-architecture/auth-concerns-by-layer.png" alt="Authentication and authorization concerns split across layers: Presentation holds the Authorize attribute and 401/403 responses, Application holds permission checks and the ICurrentUserService interface, Infrastructure implements JWT validation and CurrentUserService, and Domain holds ownership rules">
<ul>
<li><strong>JWT validation, cookie handling</strong>: Infrastructure</li>
<li><strong><code>ICurrentUserService</code> interface</strong>: Application</li>
<li><strong><code>CurrentUserService</code> implementation</strong>: Infrastructure</li>
<li><strong>Permission checks</strong>: Application (pipeline behavior or handler)</li>
<li><strong>Resource-based ownership rules</strong>: Domain</li>
<li><strong><code>[Authorize]</code> attribute and 401/403 responses</strong>: Presentation</li>
</ul>
<h2>Takeaway</h2>
<p>Authentication is infrastructure.
Authorization is a business rule.</p>
<ol>
<li>Define <code>ICurrentUserService</code> in the Application layer</li>
<li>Implement it in Infrastructure using <code>IHttpContextAccessor</code></li>
<li>Use pipeline behaviors for permission-based authorization</li>
<li>Put resource-based authorization in handlers or domain entities</li>
<li>Keep <code>[Authorize]</code> for coarse endpoint-level checks only</li>
<li>Never let your Application layer depend on ASP.NET Core directly</li>
</ol>
<p>This keeps your business logic testable - mock <code>ICurrentUserService</code> in tests instead of wrestling with HTTP contexts.</p>
<p>Thanks for reading, and stay awesome!</p>
<hr>
]]></content:encoded>
            <author>milan@milanjovanovic.tech (Milan Jovanović)</author>
        </item>
        <item>
            <title><![CDATA[The Application Layer in Clean Architecture]]></title>
            <link>https://milanjovanovic.tech/blog/application-layer-clean-architecture</link>
            <guid isPermaLink="false">https://milanjovanovic.tech/blog/application-layer-clean-architecture</guid>
            <pubDate>Tue, 11 Aug 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[The Application layer is where your use cases live: commands, queries, and the handlers that orchestrate your domain objects.]]></description>
            <content:encoded><![CDATA[<p>The Application layer is where your use cases live: every command, every query, every orchestration step between the outside world and the domain.
It's also where Clean Architecture most often goes wrong, with business logic leaking into handlers that should only coordinate.
Here is what belongs in the Application layer, what doesn't, and how to keep your handlers thin.</p>
<h2>What Is the Application Layer?</h2>
<p>In <a href="https://milanjovanovic.tech/blog/clean-architecture-and-the-benefits-of-structured-software-design"><strong>Clean Architecture</strong></a>, the Application layer sits between the Domain layer (inner) and the Infrastructure layer (outer).</p>
<p>It has three main responsibilities:</p>
<ol>
<li><strong>Define use cases</strong> - each use case is a single application operation (place an order, cancel a subscription, get user details)</li>
<li><strong>Orchestrate domain objects</strong> - it calls domain entities and services to execute business logic</li>
<li><strong>Define abstractions</strong> - it declares interfaces that the Infrastructure layer implements (repositories, email services, payment gateways)</li>
</ol>
<p>The Application layer knows about the Domain layer but knows nothing about databases, HTTP, or external services.</p>
<p>If the <a href="https://milanjovanovic.tech/blog/domain-layer-clean-architecture"><strong>Domain layer</strong></a> answers &quot;what are the business rules?&quot;, the Application layer answers &quot;when do they run, and what happens around them?&quot;.</p>
<h2>What Belongs in the Application Layer</h2>
<p><strong>Use cases (Command/Query handlers):</strong></p>
<pre><code class="language-csharp">public class PlaceOrderCommandHandler : ICommandHandler&lt;PlaceOrderCommand, Guid&gt;
{
    private readonly IOrderRepository _orderRepository;
    private readonly IUnitOfWork _unitOfWork;

    public PlaceOrderCommandHandler(
        IOrderRepository orderRepository,
        IUnitOfWork unitOfWork)
    {
        _orderRepository = orderRepository;
        _unitOfWork = unitOfWork;
    }

    public async Task&lt;Result&lt;Guid&gt;&gt; Handle(
        PlaceOrderCommand command,
        CancellationToken cancellationToken)
    {
        var order = Order.Create(command.CustomerId, command.Items);

        _orderRepository.Add(order);

        await _unitOfWork.SaveChangesAsync(cancellationToken);

        return order.Id;
    }
}
</code></pre>
<p><strong>DTOs and response models:</strong></p>
<pre><code class="language-csharp">public sealed record OrderResponse(
    Guid Id,
    string CustomerName,
    decimal TotalAmount,
    string Status,
    DateTime CreatedAt);
</code></pre>
<p><strong>Interface definitions (ports):</strong></p>
<pre><code class="language-csharp">public interface IOrderRepository
{
    Task&lt;Order?&gt; GetByIdAsync(Guid id, CancellationToken cancellationToken = default);
    void Add(Order order);
}

public interface IEmailService
{
    Task SendOrderConfirmationAsync(
        string recipientEmail,
        Guid orderId,
        CancellationToken cancellationToken = default);
}

public interface IUnitOfWork
{
    Task&lt;int&gt; SaveChangesAsync(CancellationToken cancellationToken = default);
}
</code></pre>
<p><strong>Validators:</strong></p>
<pre><code class="language-csharp">public class PlaceOrderCommandValidator : AbstractValidator&lt;PlaceOrderCommand&gt;
{
    public PlaceOrderCommandValidator()
    {
        RuleFor(x =&gt; x.CustomerId).NotEmpty();
        RuleFor(x =&gt; x.Items).NotEmpty();
        RuleFor(x =&gt; x.Items)
            .Must(items =&gt; items.All(i =&gt; i.Quantity &gt; 0))
            .WithMessage(&quot;All items must have a positive quantity.&quot;);
    }
}
</code></pre>
<h2>What Does NOT Belong</h2>
<ul>
<li><strong>Database access code</strong> - no <code>DbContext</code>, no SQL, no connection strings</li>
<li><strong>HTTP concerns</strong> - no controllers, no <code>HttpContext</code>, no request/response objects</li>
<li><strong>Framework dependencies</strong> - no EF Core, no MassTransit, no Serilog</li>
<li><strong>Third-party service implementations</strong> - only interfaces</li>
</ul>
<p>The Application layer defines <em>what</em> the application does.
Infrastructure defines <em>how</em>.</p>
<h2>Keep Handlers Thin</h2>
<p>The most common mistake I see in application layers is business logic leaking into handlers.</p>
<p>Here's what that looks like:</p>
<pre><code class="language-csharp">// Business logic in the handler - don't do this
public async Task&lt;Result&gt; Handle(CancelOrderCommand command, CancellationToken ct)
{
    var order = await _orderRepository.GetByIdAsync(command.OrderId, ct);

    if (order.Status == OrderStatus.Shipped ||
        order.Status == OrderStatus.Delivered)
    {
        return Result.Failure(OrderErrors.CannotCancel);
    }

    order.Status = OrderStatus.Cancelled;
    order.CancelledAt = DateTime.UtcNow;

    await _unitOfWork.SaveChangesAsync(ct);
    return Result.Success();
}
</code></pre>
<p>The rule &quot;you can't cancel a shipped order&quot; is a business rule.
It belongs in the domain, where every other code path that cancels orders will also enforce it:</p>
<pre><code class="language-csharp">// Orchestration in the handler, rules in the domain
public async Task&lt;Result&gt; Handle(CancelOrderCommand command, CancellationToken ct)
{
    var order = await _orderRepository.GetByIdAsync(command.OrderId, ct);

    if (order is null)
    {
        return Result.Failure(OrderErrors.NotFound(command.OrderId));
    }

    var result = order.Cancel();

    if (result.IsFailure)
    {
        return result;
    }

    await _unitOfWork.SaveChangesAsync(ct);
    return Result.Success();
}
</code></pre>
<p>The handler loads the aggregate, calls one domain method, and saves.
If your handlers read like a table of contents (load, act, save), you got it right.
If they read like a business rules engine, your domain model is <a href="https://milanjovanovic.tech/blog/refactoring-from-an-anemic-domain-model-to-a-rich-domain-model"><strong>anemic</strong></a>.</p>
<h2>Organizing Use Cases</h2>
<p>I recommend organizing use cases by feature, not by type:</p>
<pre><code>Application/
  Orders/
    PlaceOrder/
      PlaceOrderCommand.cs
      PlaceOrderCommandHandler.cs
      PlaceOrderCommandValidator.cs
    CancelOrder/
      CancelOrderCommand.cs
      CancelOrderCommandHandler.cs
    GetOrderById/
      GetOrderByIdQuery.cs
      GetOrderByIdQueryHandler.cs
      OrderResponse.cs
  Customers/
    RegisterCustomer/
      RegisterCustomerCommand.cs
      RegisterCustomerCommandHandler.cs
</code></pre>
<p>This follows the <a href="https://milanjovanovic.tech/blog/screaming-architecture"><strong>Screaming Architecture</strong></a> principle - the folder structure tells you what the application does, not what frameworks it uses.</p>
<p>I cover the alternatives (and when each one breaks down) in <a href="https://milanjovanovic.tech/blog/organize-use-cases-clean-architecture"><strong>how to organize use cases in Clean Architecture</strong></a>, and there's a full worked example in <a href="https://milanjovanovic.tech/blog/building-your-first-use-case-with-clean-architecture"><strong>building your first use case with Clean Architecture</strong></a>.</p>
<h2>CQRS in the Application Layer</h2>
<p>The <a href="https://milanjovanovic.tech/blog/cqrs-pattern-with-mediatr"><strong>CQRS pattern</strong></a> is a natural fit for the Application layer.
Commands change state, queries return data.</p>
<img src="https://milanjovanovic.tech/blogs/articles/application-layer-clean-architecture/cqrs-command-query-flow.png" alt="Two paths through the application layer: a command loads an aggregate, runs domain logic, and saves via the unit of work, while a query bypasses the domain and projects directly to a DTO">
<p>Commands go through the full domain model:</p>
<pre><code class="language-csharp">// Command → Load aggregate → Execute domain logic → Save
var order = await _orderRepository.GetByIdAsync(command.OrderId, cancellationToken);
order.Cancel();
await _unitOfWork.SaveChangesAsync(cancellationToken);
</code></pre>
<p>Queries bypass the domain model entirely:</p>
<pre><code class="language-csharp">// Query → Project directly from database → Return DTO
return await _dbContext.Orders
    .Where(o =&gt; o.Id == query.OrderId)
    .Select(o =&gt; new OrderResponse(
        o.Id,
        o.Customer.Name,
        o.TotalAmount.Amount,
        o.Status.Name,
        o.CreatedAt))
    .FirstOrDefaultAsync(cancellationToken);
</code></pre>
<p>This separation lets you optimize reads independently from writes.
Your write side uses the rich domain model; your read side projects directly into flat DTOs.</p>
<p>There's a pragmatic tension hiding in that query example: projecting &quot;directly from the database&quot; means the query handler needs some database access.
You have three options, from purest to most pragmatic:</p>
<ul>
<li><strong>A query-specific abstraction</strong> (e.g. <code>IOrderReadService</code>) implemented in Infrastructure. Purest, but you write an interface per query group.</li>
<li><strong>An <code>IApplicationDbContext</code> interface</strong> exposing <code>DbSet&lt;T&gt;</code> properties. Convenient, but it leaks EF Core types into the Application layer.</li>
<li><strong>Dapper with an <code>IDbConnectionFactory</code></strong>. Fast and explicit SQL, at the cost of a second data access approach to maintain.</li>
</ul>
<p>All three work in practice.
For most teams, the <code>IApplicationDbContext</code> compromise is fine for queries, as long as commands still go through repositories and the domain model.</p>
<h2>Where Do the Interfaces Live?</h2>
<p>Teams argue about whether repository interfaces belong in the Domain layer or the Application layer.
Both are defensible:</p>
<ul>
<li><strong>Domain layer</strong>: the repository is conceptually part of the aggregate's contract (&quot;an Order can be loaded and saved&quot;). This is the classic DDD position.</li>
<li><strong>Application layer</strong>: the domain stays 100% persistence-free, and repositories are just another port the application needs, like <code>IEmailService</code>.</li>
</ul>
<p>I lean toward the Application layer for everything except cases where a domain service genuinely needs the abstraction.
What matters far more than the choice is consistency: pick one location and enforce it with architecture tests.</p>
<h2>Cross-Cutting Concerns</h2>
<p>The Application layer is the right place to define <a href="https://milanjovanovic.tech/blog/balancing-cross-cutting-concerns-in-clean-architecture"><strong>cross-cutting behaviors</strong></a> like validation, logging, and caching - typically implemented as decorators (here using Scrutor's <code>Decorate</code>) or pipeline behaviors:</p>
<pre><code class="language-csharp">// Each Decorate wraps the previous registration:
// Logging runs first, then Validation, then the handler
builder.Services.Decorate(typeof(ICommandHandler&lt;,&gt;), typeof(ValidationCommandHandler&lt;,&gt;));
builder.Services.Decorate(typeof(ICommandHandler&lt;,&gt;), typeof(LoggingCommandHandler&lt;,&gt;));
</code></pre>
<p>This keeps your handlers focused on business logic while cross-cutting concerns are handled transparently.
If you're using MediatR, <a href="https://milanjovanovic.tech/blog/mediatr-pipeline-behaviors"><strong>pipeline behaviors</strong></a> give you the same result.</p>
<h2>The Application Layer's Dependencies</h2>
<p>The Application layer should only reference:</p>
<ul>
<li>The <strong>Domain layer</strong> (entities, value objects, domain events, domain services)</li>
<li><strong>Abstractions packages</strong> (FluentValidation contracts, MediatR contracts - but not their implementations)</li>
</ul>
<p>It should NOT reference:</p>
<ul>
<li>Infrastructure packages (EF Core, Dapper, MassTransit)</li>
<li>Presentation packages (ASP.NET Core)</li>
</ul>
<p>You can enforce this with <a href="https://milanjovanovic.tech/blog/enforcing-software-architecture-with-architecture-tests"><strong>architecture tests</strong></a>:</p>
<pre><code class="language-csharp">[Fact]
public void Application_Should_Not_Reference_Infrastructure()
{
    var result = Types
        .InAssembly(typeof(PlaceOrderCommand).Assembly)
        .ShouldNot()
        .HaveDependencyOn(&quot;Infrastructure&quot;)
        .GetResult();

    result.IsSuccessful.Should().BeTrue();
}
</code></pre>
<p>Without a test like this, the boundary erodes one &quot;temporary&quot; using directive at a time.</p>
<h2>Takeaway</h2>
<p>The Application layer is where your system's use cases live. It:</p>
<ul>
<li>Defines use cases as commands and queries</li>
<li>Orchestrates domain objects without knowing about infrastructure</li>
<li>Declares interfaces that the outer layers implement</li>
<li>Keeps business logic testable and framework-independent</li>
</ul>
<p>Keep handlers thin, structure the layer by feature, use CQRS to separate reads from writes, and enforce the boundaries with architecture tests.</p>
<p>Thanks for reading, and stay awesome!</p>
<hr>
]]></content:encoded>
            <author>milan@milanjovanovic.tech (Milan Jovanović)</author>
        </item>
        <item>
            <title><![CDATA[Architecture Fitness Functions in .NET With ArchUnitNET]]></title>
            <link>https://milanjovanovic.tech/blog/architecture-fitness-functions-archunitnet</link>
            <guid isPermaLink="false">https://milanjovanovic.tech/blog/architecture-fitness-functions-archunitnet</guid>
            <pubDate>Sat, 25 Jul 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[An architecture rule nobody enforces is a wish with a code-review lottery attached. A fitness function turns it into a build failure that names the offending…]]></description>
            <content:encoded><![CDATA[<p>Every team has architecture rules.
&quot;Application must not reference Infrastructure.&quot;
&quot;Handlers are sealed and end in <code>Handler</code>.&quot;
&quot;Modules talk through contracts, never through each other's internals.&quot;</p>
<p>Almost no team has them <strong>enforced</strong>, which means they are not rules.
They are wishes with a code-review lottery attached, and they lose that lottery on the busy weeks, which are the weeks that matter.</p>
<p>A fitness function is the fix, and it is less work than the rule was to agree on.</p>
<h2>What a Fitness Function Actually Is</h2>
<p>The term comes from evolutionary architecture: an automated check of a <strong>structural</strong> property of your system, run like any other test.</p>
<p>A unit test asserts behavior.
Given this input, the method returns that output.</p>
<p>A fitness function asserts <strong>shape</strong>.
Nothing in the application layer references the infrastructure layer.
No two feature folders depend on each other in a loop.
Only the persistence assembly knows that EF Core exists.</p>
<p>That distinction matters because shape is what erodes.
Behavior breaks loudly and immediately.
A single new <code>using</code> in the wrong file breaks nothing today, ships fine, and shows up two years later as the reason nobody can extract that module.</p>
<p>I have written before about <a href="https://milanjovanovic.tech/blog/enforcing-software-architecture-with-architecture-tests"><strong>enforcing architecture with tests</strong></a>, and the general case for <a href="https://milanjovanovic.tech/blog/shift-left-with-architecture-testing-in-dotnet"><strong>shifting that feedback left</strong></a> holds regardless of tool.
This article is about the heavier of the two .NET libraries, <a href="https://github.com/TNG/ArchUnitNET"><strong>ArchUnitNET</strong></a>, a port of Java's ArchUnit, and what it can express that lighter tools cannot.</p>
<h2>Setting It Up</h2>
<p>Two packages: the core library and the adapter for your test framework.</p>
<pre><code class="language-bash">dotnet add package TngTech.ArchUnitNET
dotnet add package TngTech.ArchUnitNET.xUnitV3
</code></pre>
<p>Loading the architecture is the expensive step, because ArchUnitNET parses the IL of every assembly you hand it.
Do it once for the whole test run:</p>
<pre><code class="language-csharp">using ArchUnitNET.Domain;
using ArchUnitNET.Loader;

public static class ArchitectureFixture
{
    // Loaded once, shared by every rule. Parsing the assemblies is the slow part;
    // evaluating a rule against an already-built Architecture is in-memory work.
    public static readonly Architecture Architecture = new ArchLoader()
        .LoadAssemblies(
            typeof(Shop.Domain.AssemblyReference).Assembly,
            typeof(Shop.Application.AssemblyReference).Assembly,
            typeof(Shop.Infrastructure.AssemblyReference).Assembly,
            typeof(Shop.Api.AssemblyReference).Assembly)
        .Build();
}
</code></pre>
<p>The <code>AssemblyReference</code> marker is just an empty public class in each project's root, so the test project can name an assembly without hardcoding its string name.</p>
<p>Now the first rule:</p>
<pre><code class="language-csharp">using ArchUnitNET.Fluent;
using ArchUnitNET.xUnitV3;
using static ArchUnitNET.Fluent.ArchRuleDefinition;

public class LayeringTests
{
    [Fact]
    public void Application_Should_Not_Depend_On_Infrastructure()
    {
        IArchRule rule = Types()
            .That().ResideInNamespace(&quot;Shop.Application&quot;, useRegularExpressions: false)
            .Should().NotDependOnAny(
                Types().That().ResideInNamespace(&quot;Shop.Infrastructure&quot;, useRegularExpressions: false));

        rule.Check(ArchitectureFixture.Architecture);
    }
}
</code></pre>
<p><code>Check</code> comes from the test-framework adapter package.
It throws when the rule fails, and the message names every offending type and the dependency that violated the rule, which is the entire reason to use a library instead of reflection by hand.</p>
<h2>The Rules Worth Writing First</h2>
<p>Four shapes cover most of what teams actually argue about in reviews.</p>
<p><strong>Dependency direction.</strong> The rule above, once per boundary you care about. In <a href="https://milanjovanovic.tech/blog/clean-architecture-dotnet"><strong>Clean Architecture</strong></a> that is domain depending on nothing, application depending on domain only. In a <a href="https://milanjovanovic.tech/blog/modular-monolith-architecture-dotnet"><strong>modular monolith</strong></a> it is one rule per module pair, allowing the contracts namespace and forbidding everything else.</p>
<p><strong>Library containment.</strong> Keep infrastructure concerns from leaking inward:</p>
<pre><code class="language-csharp">IArchRule domainIsPersistenceIgnorant = Types()
    .That().ResideInNamespace(&quot;Shop.Domain&quot;, useRegularExpressions: false)
    .Should().NotDependOnAny(
        Types().That().ResideInNamespace(&quot;Microsoft.EntityFrameworkCore&quot;, useRegularExpressions: false));
</code></pre>
<p>This one earns its keep faster than the layering rules, because an EF Core attribute on a domain entity looks harmless in a diff.</p>
<p><strong>Conventions.</strong> The things you keep retyping in review comments:</p>
<pre><code class="language-csharp">IArchRule handlerConventions = Classes()
    .That().ImplementInterface(&quot;IRequestHandler&quot;)
    .Should().BeSealed()
    .AndShould().HaveNameEndingWith(&quot;Handler&quot;);
</code></pre>
<p><strong>Cycle freedom.</strong> Covered next, because it is the one you should not try to write yourself.</p>
<p>I listed my own starting set in <a href="https://milanjovanovic.tech/blog/5-architecture-tests-you-should-add-to-your-dotnet-projects"><strong>5 architecture tests every .NET project should have</strong></a>.</p>
<h2>Cycle Freedom Is the Rule You Cannot Hand-Roll</h2>
<p>ArchUnitNET can slice a codebase by a namespace pattern and assert that the slices form no dependency cycles:</p>
<pre><code class="language-csharp">using static ArchUnitNET.Fluent.Slices.SliceRuleDefinition;

IArchRule noSliceCycles = Slices()
    .Matching(&quot;Shop.Features.(*)&quot;)
    .Should().BeFreeOfCycles();
</code></pre>
<p><code>Matching</code> captures one slice per distinct value of <code>(*)</code>, so <code>Shop.Features.Orders</code>, <code>Shop.Features.Billing</code>, and <code>Shop.Features.Shipping</code> each become a node, and the rule fails if the dependency graph between them contains a loop.</p>
<p>This is the rot that turns a <a href="https://milanjovanovic.tech/blog/vertical-slice-architecture-dotnet"><strong>vertical slice architecture</strong></a> into mud, and it is close to invisible in review.
Orders reaches into Billing for a tax calculation.
Months later Billing reaches into Orders for a customer lookup.
Neither pull request looked wrong on its own, and now the two features cannot be understood, tested, or extracted separately.</p>
<p>Detecting that means building a dependency graph and running cycle detection over it.
You can write that, but it is a genuine algorithm with genuine edge cases, and it is the main reason I reach for ArchUnitNET over lighter alternatives like <a href="https://github.com/BenMorris/NetArchTest"><strong>NetArchTest</strong></a>, which has no equivalent.</p>
<h2>When You Need the Verdict, Not the Exception</h2>
<p><code>Check</code> is right inside a test: it throws, the runner prints the message, done.</p>
<p>When the result feeds something other than a test runner (a report, a dashboard, a per-rule verdict on a build page), evaluate the rule and read the results yourself:</p>
<pre><code class="language-csharp">foreach (EvaluationResult result in noSliceCycles.Evaluate(ArchitectureFixture.Architecture))
{
    if (!result.Passed)
    {
        Console.WriteLine(result.Description); // names the object and why it failed
    }
}
</code></pre>
<p>Each <code>EvaluationResult</code> carries <code>Passed</code>, a <code>Description</code>, and the object that was evaluated, so you can group violations by rule, count them over time, or render them next to the code that caused them.</p>
<h2>Three Ways a Fitness Function Lies to You</h2>
<p>All three produce a green build for a codebase that is violating the rule.
They are worth knowing before you start trusting the suite.</p>
<p><strong>An empty rule set always passes.</strong>
<code>Classes().That().ImplementInterface(&quot;IRequestHandler&quot;)</code> matching zero types means &quot;every one of the zero handlers is sealed&quot;, which is true.
Rename the interface, and the convention rule goes green forever without a single handler being checked.
Guard the rules that matter:</p>
<pre><code class="language-csharp">[Fact]
public void Handler_Convention_Rule_Has_Something_To_Check()
{
    IEnumerable&lt;Class&gt; handlers = Classes()
        .That().ImplementInterface(&quot;IRequestHandler&quot;)
        .GetObjects(ArchitectureFixture.Architecture);

    Assert.NotEmpty(handlers);
}
</code></pre>
<p><strong>Namespace strings do not get refactored.</strong>
Rename <code>Shop.Persistence</code> to <code>Shop.Infrastructure.Persistence</code> and every rule written against the old string silently matches nothing.
Prefer assembly-based selection where you can, and when you do use namespaces, keep them in one <code>const</code> per layer so a rename is one edit instead of a search.</p>
<p><strong>A rule can encode an accident.</strong>
Write the rules by reading the current code and you will faithfully enshrine whatever shortcut is in there today.
Write them from the decision you actually made, watch them fail, then fix the code or consciously change the decision.
A fitness function that has never failed has never told you anything.</p>
<h2>Running Them in CI</h2>
<p>They are ordinary tests, so there is nothing special to wire up:</p>
<pre><code class="language-yaml">- name: Architecture tests
  run: dotnet test --filter &quot;FullyQualifiedName~ArchitectureTests&quot;
</code></pre>
<p>No database, no HTTP, no containers.
The whole suite is one assembly parse plus in-memory graph work, so it belongs in the fast job that runs on every pull request, next to your <a href="https://milanjovanovic.tech/blog/unit-testing-best-practices-dotnet"><strong>unit tests</strong></a>.</p>
<p>The change this makes to a team is smaller than it sounds and matters more than it sounds: an architecture violation stops being a review comment somebody has to write, notice, and win an argument about, and becomes a red build with the offending type name in it.</p>
<h2>When Rules Outgrow Code</h2>
<p>Rules in code are the right default.
They live next to the tests, they refactor with the solution, and the full fluent vocabulary is available.</p>
<p>There is a point where that breaks down: when the same handful of rule shapes repeat across many targets, when people who do not build the solution need to author rules, or when the rules are content rather than configuration.
Then it is worth declaring rules as <strong>data</strong> and mapping them onto the fluent API:</p>
<pre><code class="language-json">[
  { &quot;id&quot;: &quot;notifier-abstraction&quot;, &quot;kind&quot;: &quot;interface-must-exist&quot;,
    &quot;interface&quot;: &quot;INotifier&quot; },
  { &quot;id&quot;: &quot;service-not-concrete&quot;, &quot;kind&quot;: &quot;type-must-not-reference&quot;,
    &quot;type&quot;: &quot;OrderService&quot;, &quot;target&quot;: &quot;SmtpNotifier&quot; },
  { &quot;id&quot;: &quot;app-not-infra&quot;, &quot;kind&quot;: &quot;layer-must-not-depend-on&quot;,
    &quot;from&quot;: &quot;Shop.Application&quot;, &quot;to&quot;: &quot;Shop.Infrastructure&quot; }
]
</code></pre>
<pre><code class="language-csharp">public static ArchitectureRule Create(RuleDefinition definition) =&gt; definition.Kind switch
{
    &quot;interface-must-exist&quot;     =&gt; ArchRules.InterfaceMustExist(definition.Id, definition.Interface!),
    &quot;type-must-not-reference&quot;  =&gt; ArchRules.TypeMustNotReference(definition.Id, definition.Type!, definition.Target!),
    &quot;layer-must-not-depend-on&quot; =&gt; ArchRules.LayerMustNotDependOn(definition.Id, definition.From!, definition.To!),
    _ =&gt; throw new InvalidOperationException($&quot;Unknown rule kind '{definition.Kind}'.&quot;)
};
</code></pre>
<p>Because an <code>ArchitectureRule</code> is ultimately just an id, a description, and an <code>IArchRule</code>, anything the data format cannot express drops down to a raw fluent rule in code, like the slice-cycle check above.
Data for the common shapes, the full vocabulary for the exotic ones.</p>
<p>That is not a trade most teams need to make.
Do not build it until the rules have actually multiplied.</p>
<h2>The Property That Makes This Safe</h2>
<p>One implementation detail is worth knowing because it changes what you can point ArchUnitNET at: <strong>it never executes the assembly.</strong></p>
<p><code>ArchLoader</code> reads assemblies with <a href="https://github.com/jbevain/cecil"><strong>Mono.Cecil</strong></a>, a static IL and metadata parser.
The assembly is never <code>Assembly.Load</code>ed into the runtime, so no module initializer runs, no static constructor fires, and nothing the code wants to do at load time happens.</p>
<p>For most teams that is a pleasant footnote: your fitness functions cannot be slowed down or broken by application startup code.</p>
<p>It stops being a footnote when the code is not yours.
<a href="https://katabench.com"><strong>Katabench</strong></a>, my coding platform, has an architecture track where you refactor a small codebase (extract an abstraction, invert a dependency, break a cycle) and the platform grades the result.
Grading a refactoring means answering &quot;is the structure right now?&quot; mechanically, for code a stranger uploaded a second ago.
The submission is compiled with Roslyn, handed to Cecil, and evaluated as a set of fitness functions, all without a single line of it running.
Behavior gets judged separately, inside a sandbox, and both gates have to pass.</p>
<p>Static structure checking and behavioral testing being genuinely independent is not a grading trick.
It is why the shape check costs milliseconds and can run on every save while the slow gate runs later.</p>
<h2>Start With Three</h2>
<p>You do not need a rule catalog to get value out of this.</p>
<ol>
<li>Add one test project and load the architecture once in a static field.</li>
<li>Write the three rules you already state in onboarding: your most important dependency direction, the naming convention you keep repeating in reviews, and a slice-cycle rule over your feature namespaces.</li>
<li>Run them in the fast CI job.</li>
</ol>
<p>Then let them fail.
The first failure is the useful one, because it tells you the gap between the architecture you describe and the one you have.</p>
<p>Architecture that is not executable erodes at exactly the speed your team ships.
If you want to see rules graded against a codebase in real time, the architecture track on <a href="https://katabench.com"><strong>Katabench</strong></a> does it every time you hit Run.</p>
]]></content:encoded>
            <author>milan@milanjovanovic.tech (Milan Jovanović)</author>
        </item>
        <item>
            <title><![CDATA[Content-Addressed Caching in .NET: Cache Keys That Never Go Stale]]></title>
            <link>https://milanjovanovic.tech/blog/content-addressed-cache-dotnet</link>
            <guid isPermaLink="false">https://milanjovanovic.tech/blog/content-addressed-cache-dotnet</guid>
            <pubDate>Sat, 25 Jul 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Cache invalidation is hard because we name cache entries after locations. Name them after a hash of their inputs instead, and the whole class of stale-read…]]></description>
            <content:encoded><![CDATA[<p>Cache invalidation earned its place in the &quot;two hard things&quot; joke for a specific reason: we name cache entries after <strong>locations</strong>.</p>
<p><code>invoice:1187:pdf</code> points at a slot.
The slot keeps its name when the thing it describes changes, so correctness depends on someone remembering to evict, at every site that writes, forever.
Miss one and the cache serves a wrong answer with complete confidence.</p>
<p>There is another way to name things, and it makes that entire class of bug impossible: name the entry after a <strong>hash of its inputs</strong>.</p>
<p>You depend on this several times a day already.
<a href="https://git-scm.com/book/en/v2/Git-Internals-Git-Objects"><strong>Git</strong></a> addresses every blob, tree, and commit by a hash of its content.
<a href="https://docs.docker.com/build/cache/"><strong>Docker</strong></a> rebuilds a layer only when the instruction or the files it copies change.
NuGet, npm, and Cargo pin packages by content hash.
None of those systems has invalidation logic, because none of them needs any.</p>
<p>The same trick works inside an ASP.NET Core application, and it is underused there.</p>
<h2>Location Keys and Content Keys</h2>
<p>Say you render invoice PDFs from a template plus some data.
The obvious key names the thing:</p>
<pre><code class="language-csharp">string key = $&quot;invoice-pdf:{invoiceId}&quot;;
</code></pre>
<p>Now list everything that can change the bytes this key points at:</p>
<ul>
<li>the invoice data (line items, totals, the customer address)</li>
<li>the template body, which the design team edits</li>
<li>the culture used to format currency and dates</li>
<li>the renderer itself, the day you upgrade it or fix a layout bug</li>
</ul>
<p>Four inputs, and the key mentions one of them.
The other three are handled by hope, or by eviction code written at each of the places that can change them.
The template edit is the one that bites: nothing in the invoice changed, so nothing in the invoice's write path fires, and every cached PDF keeps rendering with last quarter's letterhead.</p>
<p>The content-addressed key names the <strong>inputs</strong> instead:</p>
<pre><code class="language-csharp">string key = ContentKey.From(templateId, templateSource, dataJson, culture, RendererVersion);
</code></pre>
<p>Edit the template and <code>templateSource</code> changes, so the key changes, so the lookup misses, so the PDF is rendered again and stored under the new key.
The old entry is not stale.
It is unreachable, because nothing computes its key anymore.</p>
<p>There is no invalidation code in this design.
There is nothing to forget.</p>
<h2>Building a Key You Can Trust</h2>
<p>The key builder is small, and two details in it are load-bearing:</p>
<pre><code class="language-csharp">using System.Security.Cryptography;
using System.Text;

public static class ContentKey
{
    public static string From(params string?[] parts)
    {
        using IncrementalHash hash = IncrementalHash.CreateHash(HashAlgorithmName.SHA256);

        foreach (string? part in parts)
        {
            byte[] bytes = Encoding.UTF8.GetBytes(part ?? string.Empty);

            // Length-prefix every part so the boundary between parts is unambiguous.
            hash.AppendData(BitConverter.GetBytes(bytes.Length));
            hash.AppendData(bytes);
        }

        return Convert.ToHexStringLower(hash.GetHashAndReset());
    }
}
</code></pre>
<p>The length prefix is the detail people skip.
Concatenate the parts with a separator instead, and <code>(&quot;a b&quot;, &quot;c&quot;)</code> and <code>(&quot;a&quot;, &quot;b c&quot;)</code> produce the same bytes, so two different inputs collide onto one entry.
That is the exact failure this pattern exists to prevent, reintroduced in the first four lines.</p>
<p>The second detail is what you pass in.
Build the key next to the thing that defines the computation, and make the list exhaustive:</p>
<pre><code class="language-csharp">public sealed record InvoiceRenderRequest(
    string TemplateId,
    string TemplateSource,
    string DataJson,
    string Culture);

public static class ContentKeys
{
    // Bump on any change to how the renderer turns inputs into bytes.
    private const string RendererVersion = &quot;pdf-v3&quot;;

    public static string ForInvoice(InvoiceRenderRequest request) =&gt; ContentKey.From(
        request.TemplateId,
        request.TemplateSource,   // the template body, not just its name
        request.DataJson,
        request.Culture,
        RendererVersion);
}
</code></pre>
<p><code>RendererVersion</code> is the part that feels wrong and is not.
<strong>The code is an input.</strong>
Ship a renderer that fixes a rounding bug, leave the version alone, and every existing entry keeps serving output produced by the bug you just fixed.
Docker does the same thing when it puts the instruction text into the layer's cache key, not only the files the instruction copies.</p>
<p>Note also that <code>TemplateSource</code> is the template's <strong>content</strong>, not its ID.
A key that references an input by name inherits exactly the staleness problem you are trying to leave behind.</p>
<h2>Reading and Writing</h2>
<p>Everything above is about the key.
The store can be whatever you already use.
With <a href="https://milanjovanovic.tech/blog/hybrid-cache-in-aspnetcore-new-caching-library"><strong>HybridCache</strong></a> it is one call:</p>
<pre><code class="language-csharp">public sealed class InvoiceRenderer(HybridCache cache, IPdfEngine engine)
{
    public async Task&lt;byte[]&gt; RenderAsync(
        InvoiceRenderRequest request,
        CancellationToken cancellationToken = default)
    {
        string key = ContentKeys.ForInvoice(request);

        return await cache.GetOrCreateAsync(
            key,
            (engine, request),
            static (state, token) =&gt; state.engine.RenderAsync(state.request, token),
            new HybridCacheEntryOptions
            {
                // The value can never be wrong, so expiration is a storage decision,
                // not a correctness one. Pick it from your memory and Redis budget.
                Expiration = TimeSpan.FromDays(30),
                LocalCacheExpiration = TimeSpan.FromMinutes(10)
            },
            cancellationToken: cancellationToken);
    }
}
</code></pre>
<p>The comment on <code>Expiration</code> is the whole payoff.
In a location-keyed cache, TTL is a correctness knob: it bounds how long you serve wrong data, so it fights your hit rate directly.
Here it bounds nothing but storage, so you set it from what you can afford to keep, and a longer TTL is strictly better.</p>
<h2>Concurrency Stops Being a Correctness Problem</h2>
<p>Two requests arrive for the same cold key at the same time.
Both miss, both render, both store.</p>
<p>In a location-keyed cache this is a <a href="https://milanjovanovic.tech/blog/caching-in-aspnetcore-improving-application-performance"><strong>cache stampede</strong></a>, and the standard answer is to coalesce the concurrent factory calls so only one runs.
<code>HybridCache</code> does that for you, and so does <a href="https://milanjovanovic.tech/blog/fusioncache-multi-level-caching-dotnet"><strong>FusionCache</strong></a>.</p>
<p>With a content key, the coalescing is a <strong>performance</strong> feature and nothing more.
Both callers derived the key from the same inputs, and the computation is deterministic, so whatever the loser of the write race stored is the same value the winner stored.
The cost of losing the race is one duplicated computation.
The correctness of the entry is never in question.</p>
<p>That difference shows up when you write the store yourself:</p>
<pre><code class="language-csharp">public void Store(string key, byte[] value)
{
    string path = Path.Combine(_directory, key);
    string tmp = $&quot;{path}.{Guid.NewGuid():N}.tmp&quot;;

    // Write to a unique temp file first, then move it into place, so a crash
    // mid-write can never leave a truncated entry behind.
    File.WriteAllBytes(tmp, value);

    try
    {
        File.Move(tmp, path, overwrite: true);
    }
    catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
    {
        // A concurrent writer got there first. It wrote the same value we did,
        // so there is nothing to reconcile and nothing to retry.
        try { File.Delete(tmp); } catch { /* best effort */ }
    }
}
</code></pre>
<p>A directory of files named by key is a perfectly good cache when the values are megabytes rather than kilobytes, and it survives restarts and redeploys for free.
Put a <code>ConcurrentDictionary</code> in front of it and a hot entry costs a dictionary lookup instead of a disk read.</p>
<p>One honest caveat on determinism.
Many renderers embed a creation timestamp, so two runs produce different bytes from the same inputs.
That is fine here, because both outputs are equally valid answers.
What is not fine is a computation that can produce a <strong>wrong</strong> answer for the same inputs, and that is the line where this pattern stops applying.</p>
<h2>You Traded Invalidation for Garbage Collection</h2>
<p>Content addressing never deletes anything, so every edit strands the previous entry.
That is the cost, and it is real: the template team edits a header twenty times in an afternoon and you now hold twenty invoice renderings nobody will ever ask for again.</p>
<p>You have two ways to bound it, and they are not equivalent.</p>
<p><strong>Expiry or size caps.</strong> A TTL, <code>SizeLimit</code> on <code>IMemoryCache</code>, or <code>maxmemory</code> with an LRU policy on Redis. Imprecise but free, and correct by construction, because evicting a live entry only costs a recompute.</p>
<p><strong>A sweep</strong>, when you can enumerate the keys the current inputs can produce:</p>
<pre><code class="language-csharp">/// &lt;summary&gt;Deletes entries whose key no live input can produce.&lt;/summary&gt;
public int Prune(IReadOnlySet&lt;string&gt; liveKeys)
{
    int removed = 0;

    foreach (string file in Directory.EnumerateFiles(_directory)
                 .Where(f =&gt; !liveKeys.Contains(Path.GetFileName(f))))
    {
        try
        {
            File.Delete(file);
            removed++;
        }
        catch (IOException)
        {
            // Another instance is sweeping too. It is idempotent; let it win.
        }
    }

    return removed;
}
</code></pre>
<p>Recompute the key for every live template and configuration at startup, hand the set to <code>Prune</code>, and the cache is bounded to exactly what the current content can produce.
That keeps the spirit of the rest of the pattern: what is valid is <strong>derived</strong> from the live inputs, never tracked in a separate ledger that can drift.</p>
<h2>The One Failure Mode</h2>
<p>Everything good about this pattern comes from one assumption, and there is exactly one way to break it: <strong>leave an input out of the key</strong>.</p>
<p>Do that and the key stops changing when the value should.
The cache serves the old answer indefinitely, and unlike a location-keyed cache, there is no eviction path anywhere to save you.
It is a quiet bug, and the usual shape is an input that did not look like one: an environment variable, a feature flag, a config file the renderer reads on its own, the machine's default culture.</p>
<p>Three habits keep it closed:</p>
<ul>
<li><strong>Over-include.</strong> A key that is too sensitive costs an unnecessary recompute. A key that is too coarse costs a wrong answer. That trade is not close.</li>
<li><strong>Make the input list one line of code.</strong> If the key is assembled in three places, one of them will fall behind. A single <code>ContentKeys.ForX</code> method is the thing you review when the computation changes.</li>
<li><strong>Treat implicit inputs as explicit.</strong> Anything the computation reads that is not a parameter (config, flags, ambient culture) either goes into the key or gets passed in as a parameter so it does.</li>
</ul>
<h2>Where This Fits, and Where It Does Not</h2>
<p>Reach for a content key when all three hold:</p>
<ul>
<li><strong>The computation is deterministic</strong>, in the sense that any output it produces for a given set of inputs is equally valid.</li>
<li><strong>You can enumerate the inputs</strong>, all of them.</li>
<li><strong>Deriving the value is expensive relative to hashing it.</strong> Hashing a few KB is microseconds. Rendering a document, resizing an image, running a model, compiling something: easy call.</li>
</ul>
<p>That covers more than it sounds like: report and document rendering, image transforms, code generation, compiled query plans, expensive pure aggregations over immutable data, and LLM calls at temperature zero.</p>
<p>It does not cover data with genuine freshness semantics.
A stock price, an inventory count, a user's notification badge: those change because the world changed, not because an input to a derivation changed.
Those still want a TTL and a <a href="https://milanjovanovic.tech/blog/caching-in-aspnetcore-improving-application-performance"><strong>deliberate caching strategy</strong></a>, and no amount of hashing helps.</p>
<h2>One From Production</h2>
<p><a href="https://katabench.com"><strong>Katabench</strong></a>, my coding platform, runs on this for a value that has no hand-written answer at all.</p>
<p>Many katas do not ship an expected output.
Instead the platform derives one: it runs the kata's reference solution against the test's input inside a sandbox and keeps whatever comes back.
A full sandboxed execution per test is far too slow to repeat on every submission, so it has to be cached.</p>
<p>The location-keyed version of that cache would be <code>{puzzleId}:{testName}</code>, and it would need an eviction hook on every edit to a reference solution, a test input, or a seed script.
The content-keyed version puts the reference solution's <strong>source code</strong> and the test's full input into the key.
Editing either one produces a different key, a miss, and a fresh derivation, with no eviction code anywhere in the system.</p>
<p>The kind of bug that would keep me up otherwise (grading somebody's submission against an expected answer derived from a reference solution I edited last week) cannot happen, because there is no code path that produces it.</p>
<h2>The Takeaway</h2>
<p>Cache invalidation is hard when the key is a location.
It disappears when the key is a hash of everything the value depends on, because &quot;the inputs changed&quot; and &quot;the key changed&quot; become the same event.</p>
<p>The move worth making is smaller than adopting a pattern.
Next time you cache an expensive derived value, write down every input it actually depends on, including the code that produces it.
If that list is finite and you can hash it, you can have a cache that is never wrong, and the only thing left to manage is disk.</p>
<p>If you want to see the idea running under real load, the sandboxed grading on <a href="https://katabench.com"><strong>Katabench</strong></a> is built on it, and I am always happy to hear what breaks.</p>
]]></content:encoded>
            <author>milan@milanjovanovic.tech (Milan Jovanović)</author>
        </item>
        <item>
            <title><![CDATA[Vertical Slice Architecture in .NET: The Complete Guide]]></title>
            <link>https://milanjovanovic.tech/blog/vertical-slice-architecture-dotnet</link>
            <guid isPermaLink="false">https://milanjovanovic.tech/blog/vertical-slice-architecture-dotnet</guid>
            <pubDate>Fri, 10 Jul 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Vertical Slice Architecture organizes code around behavior. Learn slice boundaries, validation, testing, and how VSA works with CQRS and Clean Architecture.]]></description>
            <content:encoded><![CDATA[<p>Layered codebases tend to develop the same problem.
Shipping one small feature meant touching a controller, a service, a repository, and a handful of interfaces in between.
Vertical Slice Architecture flips that model: you organize code by feature, so everything a slice needs lives in one place.
This page is my complete guide to VSA in .NET, from the core ideas to slice structure, testing, and combining it with other architectures.</p>
<h2>What is Vertical Slice Architecture?</h2>
<p>Vertical Slice Architecture organizes code by <strong>feature</strong> instead of by <strong>layer</strong>.
Each feature (or &quot;slice&quot;) contains everything it needs - the request, the handler, the validation, and the data access - in one place.</p>
<p>Instead of scattering a single feature across Controllers, Services, and Repositories, you keep it together.
This makes each slice self-contained, easier to understand, and simpler to change without affecting unrelated features.</p>
<p>The result is a codebase where adding a new feature means adding a new slice, not modifying five different layers.</p>
<img src="https://milanjovanovic.tech/blogs/articles/vertical-slice-architecture-dotnet/vertical-slices-vs-layers.png" alt="Each feature such as Place Order, Get Order, and Cancel Order is a vertical slice owning its own endpoint, handler, and data access">
<h2>Getting Started</h2>
<p>These articles introduce the core ideas behind Vertical Slice Architecture, explain when it makes sense, and show you how to get started.</p>
<ul>
<li><a href="https://milanjovanovic.tech/blog/vertical-slice-architecture">Vertical Slice Architecture</a></li>
<li><a href="https://milanjovanovic.tech/blog/vertical-slice-architecture-is-easier-than-you-think">Vertical Slice Architecture Is Easier Than You Think</a></li>
<li><a href="https://milanjovanovic.tech/blog/screaming-architecture">Screaming Architecture</a></li>
<li><a href="https://milanjovanovic.tech/blog/vertical-slice-vs-clean-architecture">Vertical Slice Architecture vs Clean Architecture</a></li>
<li><a href="https://milanjovanovic.tech/blog/when-to-choose-vertical-slice-architecture">When to Choose Vertical Slice Architecture</a></li>
</ul>
<h2>Structuring Your Slices</h2>
<p>Once you understand the basics, you'll need patterns for organizing slices as your project grows. These articles cover structure, shared logic, and CQRS integration.</p>
<ul>
<li><a href="https://milanjovanovic.tech/blog/vertical-slice-architecture-structuring-vertical-slices">Vertical Slice Architecture: Structuring Vertical Slices</a></li>
<li><a href="https://milanjovanovic.tech/blog/vertical-slice-architecture-where-does-the-shared-logic-live">Vertical Slice Architecture: Where Does the Shared Logic Live?</a></li>
<li><a href="https://milanjovanovic.tech/blog/cqrs-pattern-the-way-it-should-have-been-from-the-start">CQRS Pattern: The Way It Should Have Been From the Start</a></li>
<li><a href="https://milanjovanovic.tech/blog/repr-pattern-aspnetcore">The REPR Pattern in ASP.NET Core</a></li>
<li><a href="https://milanjovanovic.tech/blog/vertical-slice-project-structure-dotnet">Vertical Slice Architecture Project Structure</a></li>
<li><a href="https://milanjovanovic.tech/blog/feature-folders-dotnet">Feature Folders in .NET</a></li>
<li><a href="https://milanjovanovic.tech/blog/combining-vertical-slices-cqrs">Combining Vertical Slices with CQRS</a></li>
<li><a href="https://milanjovanovic.tech/blog/vertical-slice-architecture-carter-dotnet">Vertical Slice Architecture with Carter in .NET</a></li>
</ul>
<h2>Cross-Cutting Concerns and Validation</h2>
<p>Even self-contained slices share some concerns. These articles cover how to handle validation and cross-cutting logic without breaking slice isolation.</p>
<ul>
<li><a href="https://milanjovanovic.tech/blog/validation-vertical-slice-architecture">Validation in Vertical Slice Architecture</a></li>
<li><a href="https://milanjovanovic.tech/blog/cross-cutting-concerns-in-vertical-slice-architecture">Cross-Cutting Concerns in Vertical Slice Architecture</a></li>
</ul>
<h2>Combining with Other Architectures</h2>
<p>Vertical slices don't exist in isolation. They work well inside Modular Monoliths and alongside Clean Architecture. These articles explore the combinations.</p>
<ul>
<li><a href="https://milanjovanovic.tech/blog/where-vertical-slices-fit-inside-the-modular-monolith-architecture">Where Vertical Slices Fit Inside the Modular Monolith Architecture</a></li>
<li><a href="https://milanjovanovic.tech/blog/what-is-a-modular-monolith">What Is a Modular Monolith?</a></li>
<li><a href="https://milanjovanovic.tech/blog/clean-architecture-the-missing-chapter">Clean Architecture: The Missing Chapter</a></li>
</ul>
<h2>Testing and Quality</h2>
<p>Vertical slices are inherently testable - each slice is a focused unit with clear inputs and outputs.</p>
<ul>
<li><a href="https://milanjovanovic.tech/blog/enforcing-software-architecture-with-architecture-tests">Enforcing Software Architecture with Architecture Tests</a></li>
<li><a href="https://milanjovanovic.tech/blog/shift-left-with-architecture-testing-in-dotnet">Shift Left with Architecture Testing in .NET</a></li>
<li><a href="https://milanjovanovic.tech/blog/5-architecture-tests-you-should-add-to-your-dotnet-projects">5 Architecture Tests You Should Add to Your .NET Projects</a></li>
<li><a href="https://milanjovanovic.tech/blog/testing-vertical-slices-dotnet">Testing Vertical Slices in .NET</a></li>
</ul>
]]></content:encoded>
            <author>milan@milanjovanovic.tech (Milan Jovanović)</author>
            <enclosure url="https://milanjovanovic.tech/article-covers/vertical-slice-architecture-dotnet.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[OpenTelemetry Collectors: The Agent + Gateway Pattern]]></title>
            <link>https://milanjovanovic.tech/blog/opentelemetry-collector-agent-gateway</link>
            <guid isPermaLink="false">https://milanjovanovic.tech/blog/opentelemetry-collector-agent-gateway</guid>
            <pubDate>Fri, 10 Jul 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[One OpenTelemetry Collector is easy. The interesting design shows up the moment your apps run on more than one machine: a small agent collector on every box…]]></description>
            <content:encoded><![CDATA[<p>Every OpenTelemetry tutorial ends the same way: one app, one collector, one Grafana.
I wrote one of those myself in <a href="https://milanjovanovic.tech/blog/monitoring-dotnet-applications-with-opentelemetry-and-grafana"><strong>monitoring .NET applications with OpenTelemetry and Grafana</strong></a>.</p>
<p>The design gets interesting when the system grows past one machine.
<a href="https://katabench.com"><strong>Katabench</strong></a> runs its API and its grading workers on separate boxes, with the monitoring stack on a third, and that split forces the question the tutorials skip: who sends telemetry where?</p>
<p>The answer that works is a two-tier collector deployment: an <strong>agent</strong> on every box, one <strong>gateway</strong> in front of the storage.</p>
<h2>The Topology</h2>
<img src="https://milanjovanovic.tech/blogs/articles/opentelemetry-collector-agent-gateway/agent-gateway-topology.png" alt="The agent and gateway collector topology: the API box and the worker box each run a local agent collector that their app exports OTLP to; both agents forward over the private network to the gateway collector on the monitoring box, which fans traces to Tempo, logs to Loki, and metrics to a Prometheus scrape endpoint, all rendered by Grafana">
<p>Each application box runs a small collector next to the app.
The app exports OTLP to <code>http://otel-collector:4317</code>, a name that resolves inside the box's own compose network, and knows nothing else.</p>
<p>The agents forward to the gateway on the monitoring box over a private <a href="https://milanjovanovic.tech/blog/build-your-own-vpn-with-tailscale"><strong>Tailscale network</strong></a>.
The gateway is the only component that knows the backends exist: traces go to Tempo, logs to Loki over its native OTLP ingest, and metrics are exposed on a scrape endpoint for Prometheus.</p>
<p>Adding a box to the system costs zero monitoring configuration.
Telemetry is push-based, <code>service.instance.id</code> distinguishes instances, and the new box's agent just needs the one gateway address.</p>
<h2>The Agent: A Local Hop That Earns Its Keep</h2>
<p>The agent config is short enough to show almost in full:</p>
<pre><code class="language-yaml">receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317

processors:
  # Hard ceiling on the collector's own memory. Spikes are dropped, not queued.
  memory_limiter:
    check_interval: 1s
    limit_mib: 96
    spike_limit_mib: 32
  batch:

exporters:
  otlp/gateway:
    endpoint: ${env:OTEL_ENDPOINT}   # no default: missing value fails at boot
    tls:
      insecure: true                 # the link is WireGuard-encrypted already
    retry_on_failure:
      enabled: true
    sending_queue:
      enabled: true
      queue_size: 5000

service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [memory_limiter, batch]
      exporters: [otlp/gateway]
    # metrics and logs: same shape
</code></pre>
<p>Four deliberate decisions live in those thirty lines:</p>
<ul>
<li><strong>The app only knows a local address.</strong> No cross-box IP is baked into app config. When the monitoring stack moves, app deployments don't change.</li>
<li><strong>A sending queue plus retry rides out a gateway or network blip</strong> instead of dropping spans. Direct app-to-backend export has no such buffer.</li>
<li><strong><code>memory_limiter</code> runs first in every pipeline</strong>, and the numbers matter on shared boxes. My worker box's RAM is budgeted for sandbox runs, so the agent gets a hard 96 MB ceiling and sheds load rather than competing with the actual workload for memory. An observability sidecar that can trigger the OOM killer is a self-own.</li>
<li><strong><code>${env:OTEL_ENDPOINT}</code> has no default.</strong> A missing value crashes the collector at boot. The alternative is a collector that starts fine and silently swallows telemetry, which you discover three weeks later, mid-incident.</li>
</ul>
<p>One more property worth copying: the agent publishes <strong>no host ports</strong>.
It is reachable only by the app container on the same compose network, so no box exposes an OTLP ingest surface, not even on the private network.</p>
<h2>The Gateway: One Ingest Point, Per-Signal Fan-Out</h2>
<p>The gateway is where the per-signal routing happens:</p>
<pre><code class="language-yaml">exporters:
  prometheus:
    endpoint: 0.0.0.0:8889        # scrape surface for Prometheus
  otlphttp/loki:
    endpoint: http://loki:3100/otlp # Loki ingests OTLP natively since 3.0
  otlp/tempo:
    endpoint: tempo:4317

service:
  pipelines:
    traces:
      receivers: [otlp, faro]
      processors: [batch]
      exporters: [otlp/tempo]
    metrics:
      receivers: [otlp]
      processors: [batch]
      exporters: [prometheus]
    logs:
      receivers: [otlp, faro]
      processors: [batch]
      exporters: [otlphttp/loki]
</code></pre>
<p>That <code>faro</code> receiver is the bonus a central gateway buys you.
The browser SPA reports real-user monitoring (JS errors, Web Vitals, fetch spans) through Grafana Faro into the <strong>same</strong> collector, sharing the traces and logs pipelines.
A browser fetch and the API span it triggered land in one Tempo trace, stitched by the propagated <code>traceparent</code>.
It is the single observability endpoint that must be public, because it is called from users' browsers; everything else stays private.</p>
<h2>When Do You Actually Need This?</h2>
<p>Rules of thumb, from running it:</p>
<ul>
<li><strong>One box, one app:</strong> a single collector is fine. Don't build the two-tier setup for a monolith on one VPS.</li>
<li><strong>Two or more boxes, or any box whose RAM you care about:</strong> add agents. The local buffering alone pays for the extra container the first time your monitoring box restarts during a deploy and no telemetry is lost.</li>
<li><strong>Browser RUM, or multiple teams sending telemetry:</strong> you want the gateway as the single, controlled ingest point regardless of box count.</li>
</ul>
<p>The nice thing about the pattern is that migrating to it is invisible to your applications.
The .NET side of the wiring never changes: the OTLP exporter still points at whatever <code>OTEL_EXPORTER_OTLP_ENDPOINT</code> says, which is exactly how it worked back when the whole stack was one docker-compose file (the setup from <a href="https://milanjovanovic.tech/blog/introduction-to-distributed-tracing-with-opentelemetry-in-dotnet"><strong>introduction to distributed tracing with OpenTelemetry in .NET</strong></a>).
Only the address changed.</p>
]]></content:encoded>
            <author>milan@milanjovanovic.tech (Milan Jovanović)</author>
            <enclosure url="https://milanjovanovic.tech/article-covers/opentelemetry-collector-agent-gateway.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[Modular Monolith Architecture in .NET: The Complete Guide]]></title>
            <link>https://milanjovanovic.tech/blog/modular-monolith-architecture-dotnet</link>
            <guid isPermaLink="false">https://milanjovanovic.tech/blog/modular-monolith-architecture-dotnet</guid>
            <pubDate>Fri, 10 Jul 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[A modular monolith keeps one deployment while enforcing business boundaries inside the codebase.]]></description>
            <content:encoded><![CDATA[<p>Every .NET team eventually hits the same fork: the monolith is turning into a mess, but microservices look like an operational money pit.
The Modular Monolith is the third option, and for most systems it's the right one.
This guide collects everything you need to build one in .NET: defining module boundaries, isolating data, communication patterns, testing strategies, and extracting microservices when a module actually earns it.</p>
<h2>What is a Modular Monolith?</h2>
<p>A Modular Monolith is a software architecture style where a single deployable unit is organized into well-defined, loosely coupled modules.
Each module encapsulates a specific business capability with its own data, logic, and API surface.</p>
<p>Unlike a traditional monolith, the boundaries between modules are explicit and enforced.
Unlike microservices, you get the simplicity of a single deployment, a single database connection, and no distributed system complexity.</p>
<p>The Modular Monolith gives you the <strong>best of both worlds</strong> - strong modularity with operational simplicity.</p>
<img src="https://milanjovanovic.tech/blogs/articles/modular-monolith-architecture-dotnet/modular-monolith-overview.png" alt="Overview diagram of a modular monolith: Catalog, Ordering, and Shipping modules inside one deployable unit, communicating through integration events, each owning its own database schema">
<p>Want to master this architecture? My <a href="https://milanjovanovic.tech/modular-monolith-architecture"><strong>Modular Monolith Architecture</strong></a> course covers the complete approach I use for building production systems.</p>
<h2>Getting Started</h2>
<p>These articles introduce the core concepts and help you understand when a Modular Monolith is the right choice for your project.</p>
<ul>
<li><a href="https://milanjovanovic.tech/blog/what-is-a-modular-monolith">What Is a Modular Monolith?</a></li>
<li><a href="https://milanjovanovic.tech/blog/monolith-to-microservices-how-a-modular-monolith-helps">Monolith to Microservices: How a Modular Monolith Helps</a></li>
<li><a href="https://milanjovanovic.tech/blog/scaling-monoliths-a-practical-guide-for-growing-systems">Scaling Monoliths: A Practical Guide for Growing Systems</a></li>
<li><a href="https://milanjovanovic.tech/blog/modular-monolith-vs-microservices">Modular Monolith vs Microservices: How to Choose</a></li>
<li><a href="https://milanjovanovic.tech/blog/build-modular-monolith-dotnet-step-by-step">How to Build a Modular Monolith in .NET Step by Step</a></li>
</ul>
<h2>Module Boundaries and Data Isolation</h2>
<p>Getting module boundaries right is the most important design decision. These articles cover how to define boundaries, isolate data, and enforce separation.</p>
<ul>
<li><a href="https://milanjovanovic.tech/blog/modular-monolith-data-isolation">Modular Monolith Data Isolation</a></li>
<li><a href="https://milanjovanovic.tech/blog/how-to-keep-your-data-boundaries-intact-in-a-modular-monolith">How to Keep Your Data Boundaries Intact in a Modular Monolith</a></li>
<li><a href="https://milanjovanovic.tech/blog/internal-vs-public-apis-in-modular-monoliths">Internal vs Public APIs in Modular Monoliths</a></li>
<li><a href="https://milanjovanovic.tech/blog/refactoring-overgrown-bounded-contexts-in-modular-monoliths">Refactoring Overgrown Bounded Contexts in Modular Monoliths</a></li>
<li><a href="https://milanjovanovic.tech/blog/module-boundaries-bounded-contexts">Defining Module Boundaries with Bounded Contexts</a></li>
<li><a href="https://milanjovanovic.tech/blog/schema-per-module-vs-database-per-module">Schema-per-Module vs Database-per-Module</a></li>
<li><a href="https://milanjovanovic.tech/blog/shared-kernel-pattern-modular-monolith">The Shared Kernel Pattern in a Modular Monolith</a></li>
</ul>
<h2>Communication Patterns</h2>
<p>Modules need to communicate without creating tight coupling. These articles cover synchronous and asynchronous patterns for inter-module communication.</p>
<ul>
<li><a href="https://milanjovanovic.tech/blog/modular-monolith-communication-patterns">Modular Monolith Communication Patterns</a></li>
<li><a href="https://milanjovanovic.tech/blog/orchestration-vs-choreography">Orchestration vs Choreography</a></li>
<li><a href="https://milanjovanovic.tech/blog/how-to-use-domain-events-to-build-loosely-coupled-systems">How to Use Domain Events to Build Loosely Coupled Systems</a></li>
<li><a href="https://milanjovanovic.tech/blog/building-a-custom-domain-events-dispatcher-in-dotnet">Building a Custom Domain Events Dispatcher in .NET</a></li>
<li><a href="https://milanjovanovic.tech/blog/event-driven-communication-modules">Event-Driven Communication Between Modules</a></li>
<li><a href="https://milanjovanovic.tech/blog/outbox-pattern-dual-write-problem">Why the Outbox Pattern Solves the Dual-Write Problem</a></li>
<li><a href="https://milanjovanovic.tech/blog/saga-pattern-modular-monolith">The Saga Pattern in a Modular Monolith</a></li>
</ul>
<h2>Testing</h2>
<p>Testing a Modular Monolith requires strategies that validate both individual modules and cross-module interactions.</p>
<ul>
<li><a href="https://milanjovanovic.tech/blog/testing-modular-monoliths-system-integration-testing">Testing Modular Monoliths: System Integration Testing</a></li>
<li><a href="https://milanjovanovic.tech/blog/enforcing-software-architecture-with-architecture-tests">Enforcing Software Architecture with Architecture Tests</a></li>
<li><a href="https://milanjovanovic.tech/blog/5-architecture-tests-you-should-add-to-your-dotnet-projects">5 Architecture Tests You Should Add to Your .NET Projects</a></li>
</ul>
<h2>Migrating to Microservices</h2>
<p>A Modular Monolith is often the best starting point before moving to microservices. These articles cover when and how to make that transition.</p>
<ul>
<li><a href="https://milanjovanovic.tech/blog/breaking-it-down-how-to-migrate-your-modular-monolith-to-microservices">Breaking It Down: How to Migrate Your Modular Monolith to Microservices</a></li>
<li><a href="https://milanjovanovic.tech/blog/understanding-microservices-core-concepts-and-benefits">Understanding Microservices: Core Concepts and Benefits</a></li>
<li><a href="https://milanjovanovic.tech/blog/microservices-dotnet-getting-started">Getting Started with Microservices in .NET</a></li>
<li><a href="https://milanjovanovic.tech/blog/when-to-extract-module-to-microservice">When to Extract a Module into a Microservice</a></li>
<li><a href="https://milanjovanovic.tech/blog/strangler-fig-modular-monolith-migration">The Strangler Fig Pattern for Modular Monolith Migration</a></li>
</ul>
<h2>Vertical Slices in a Modular Monolith</h2>
<p>Vertical Slice Architecture is a natural fit inside individual modules. These articles explore how the two approaches complement each other.</p>
<ul>
<li><a href="https://milanjovanovic.tech/blog/where-vertical-slices-fit-inside-the-modular-monolith-architecture">Where Vertical Slices Fit Inside the Modular Monolith Architecture</a></li>
<li><a href="https://milanjovanovic.tech/blog/vertical-slice-architecture">Vertical Slice Architecture</a></li>
</ul>
]]></content:encoded>
            <author>milan@milanjovanovic.tech (Milan Jovanović)</author>
            <enclosure url="https://milanjovanovic.tech/article-covers/modular-monolith-architecture-dotnet.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[How I Use NATS JetStream as a Job Queue in .NET]]></title>
            <link>https://milanjovanovic.tech/blog/nats-jetstream-job-queue-dotnet</link>
            <guid isPermaLink="false">https://milanjovanovic.tech/blog/nats-jetstream-job-queue-dotnet</guid>
            <pubDate>Fri, 10 Jul 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[My first job queue was a Redis list, and it had two ways to silently lose a job: a worker crash after LPOP, and a broker restart.]]></description>
            <content:encoded><![CDATA[<p><a href="https://katabench.com"><strong>Katabench</strong></a> grades code submissions in a worker pool: the API accepts a submission, a worker compiles and executes it in a sandbox, and the user waits for the verdict.
Between the API and the workers sits a queue, and the hard constraint I set for it was simple: a user must always get their result or a retriable error, <strong>never a silent loss</strong>.</p>
<p>My first transport failed that constraint twice, and the fix is a nice case study in what a real job queue gives you.
The whole design fits in one article.</p>
<h2>Where the Redis List Design Loses Jobs</h2>
<p>Version one was the classic minimal queue: a Redis list.
The API does <code>RPUSH</code>, workers poll with <code>LPOP</code>, results go into a per-job key with a TTL.</p>
<p>It has two silent loss modes, and both are structural:</p>
<ul>
<li><strong><code>LPOP</code> removes the job with no acknowledgment.</strong> A worker that crashes mid-job (and mine run untrusted code, so crashing is normal operation) takes the job with it. Nothing redelivers it.</li>
<li><strong>No persistence.</strong> An in-memory Redis restart or redeploy wipes the queue and every in-flight result.</li>
</ul>
<p>You can patch around both (use <code>LMOVE</code> to a processing list, add AOF persistence, build a sweeper for stuck jobs), but at that point you are hand-building acknowledgments and durability.
That is exactly the feature set of a real message broker.</p>
<p>I picked <strong>NATS JetStream</strong> over RabbitMQ for one operational reason: it is a single lightweight binary that is trivial to run on a VPS, and it plugs straight into the Prometheus and Grafana stack I already had.
I covered the fundamentals in <a href="https://milanjovanovic.tech/blog/getting-started-with-nats-jetstream-in-dotnet"><strong>getting started with NATS JetStream in .NET</strong></a>; this is what the production shape looks like.</p>
<h2>The Shape: Two Streams</h2>
<img src="https://milanjovanovic.tech/blogs/articles/nats-jetstream-job-queue-dotnet/job-queue-streams.png" alt="The two-stream job queue: the API publishes grading jobs to a file-backed work-queue stream, a pool of workers competes on one durable pull consumer, and each worker publishes the outcome to a short-retention results stream that the API awaits filtered by job id">
<p>Everything is built on two file-backed streams:</p>
<pre><code class="language-csharp">var js = new NatsJSContext(connection);

// The job queue: a message is removed once a worker acks it.
await js.CreateStreamAsync(new StreamConfig(&quot;GRADING_JOBS&quot;, [&quot;grading.jobs&quot;])
{
    Retention = StreamConfigRetention.Workqueue,
    Storage = StreamConfigStorage.File
});

// Results: short retention, one subject per job id.
await js.CreateStreamAsync(new StreamConfig(&quot;GRADING_RESULTS&quot;, [&quot;grading.results.&gt;&quot;])
{
    Retention = StreamConfigRetention.Limits,
    MaxAge = TimeSpan.FromMinutes(5),
    Storage = StreamConfigStorage.File
});
</code></pre>
<p>The <strong>jobs stream</strong> uses work-queue retention: acking a job deletes it, so the stream behaves like a queue instead of a log.
All workers compete on <strong>one shared durable pull consumer</strong>, which is how you get the competing-consumers pattern; adding a worker is just starting another process, with zero configuration.</p>
<p>The <strong>results stream</strong> is the part most people skip.
The worker publishes each outcome to <code>grading.results.&lt;jobId&gt;</code>, and the API awaits it with an ephemeral ordered consumer filtered to that one subject.
Because the stream retains messages for a few minutes, a result that was published <strong>before</strong> the API started waiting is still delivered.
That closes an entire class of races that a fire-and-forget reply channel has.</p>
<h2>The One Ordering Rule That Matters</h2>
<p>The worker loop looks like this, and the order of the last two lines is the whole reliability story:</p>
<pre><code class="language-csharp">var consumer = await js.CreateOrUpdateConsumerAsync(&quot;GRADING_JOBS&quot;,
    new ConsumerConfig(&quot;workers&quot;)
    {
        AckWait = TimeSpan.FromSeconds(60), // redeliver if no ack in time
        MaxDeliver = 4                      // bounded poison retry
    });

await foreach (var msg in consumer.ConsumeAsync&lt;GradingJob&gt;(cancellationToken: ct))
{
    GradingJobOutcome outcome = await grader.GradeAsync(msg.Data, ct);

    // Publish the result FIRST, then ack the job.
    await js.PublishAsync($&quot;grading.results.{msg.Data.JobId}&quot;, outcome, cancellationToken: ct);
    await msg.AckAsync(cancellationToken: ct);
}
</code></pre>
<p><strong>Publish the result before you ack the job.</strong>
Walk the failure windows and you'll see why:</p>
<ul>
<li>Crash <strong>before</strong> the publish: the job is un-acked, so JetStream redelivers it after <code>AckWait</code> and another worker grades it. Nothing is lost.</li>
<li>Crash <strong>between</strong> publish and ack: the job is redelivered and graded again, and a second identical result is published. Grading is idempotent and keyed by job id, so the duplicate is harmless.</li>
</ul>
<p>Ack first and you reopen the Redis hole: a crash after the ack but before the publish loses the result with no way to recover it.
At-least-once delivery plus idempotent processing beats exactly-once promises every time.</p>
<p><code>MaxDeliver</code> handles the adversarial case.
A submission that reproducibly crashes its worker (I run untrusted code, so this is a when, not an if) is redelivered a bounded number of times and then dropped, and JetStream emits a <code>MAX_DELIVERIES</code> advisory that monitoring alerts on.
Without it, one poison job crash-loops your whole worker pool forever.</p>
<h2>Masking It All Behind a Synchronous API</h2>
<p>From the user's perspective nothing here is asynchronous: the API holds the HTTP request open, awaits the result subject up to a dispatch budget, and returns a retriable <strong>503</strong> on timeout.
Every failure mode above degrades to &quot;try again&quot;, never to a silently missing result.</p>
<p>Two honest limitations, so you can steal this design with eyes open:</p>
<ul>
<li><strong>A single NATS node is a single point of failure.</strong> Jobs survive a restart (file-backed streams), but not a dead disk. Clustered JetStream with RAFT replicas is the fix when you outgrow one box; at my volume, one box is fine.</li>
<li><strong>The results stream is retention-bounded.</strong> If a client should be able to fetch a result hours later, the queue is the wrong home for it; persist results to Postgres and treat the stream purely as transport.</li>
</ul>
<p>And if you want to see the queue from the user's side, every submission on <a href="https://katabench.com"><strong>Katabench</strong></a> rides through it, a few hundred milliseconds at a time.</p>
]]></content:encoded>
            <author>milan@milanjovanovic.tech (Milan Jovanović)</author>
            <enclosure url="https://milanjovanovic.tech/article-covers/nats-jetstream-job-queue-dotnet.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[EF Core Performance: Tips, Tricks, and Best Practices]]></title>
            <link>https://milanjovanovic.tech/blog/ef-core-performance-guide</link>
            <guid isPermaLink="false">https://milanjovanovic.tech/blog/ef-core-performance-guide</guid>
            <pubDate>Fri, 10 Jul 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Most EF Core slowdowns come from query shape, excess tracking, or round trips. Choose projections, split queries, batching, compiled models, and diagnostics.]]></description>
            <content:encoded><![CDATA[<p>Slow <strong>EF Core</strong> queries are rarely EF Core's fault.
Most of the time it's a missing projection, an accidental N+1, or change tracking doing work nobody asked for.</p>
<p>This page maps EF Core performance techniques from quick wins to specialized optimizations.
Start with query optimization; that's where the biggest gains hide.</p>
<h2>Why EF Core Performance Matters</h2>
<p>Entity Framework Core is the most popular ORM in the .NET ecosystem.
It makes data access simple, but that simplicity can hide inefficient queries, excessive database round trips, and memory issues.</p>
<p>Without proper optimization, EF Core can become the bottleneck in your application - especially under load.</p>
<p>This guide collects the most impactful performance techniques, organized from quick wins to advanced strategies.</p>
<h2>Query Optimization</h2>
<p>The biggest performance gains usually come from how you write your queries. These articles cover techniques that reduce database load and improve response times.</p>
<ul>
<li><a href="https://milanjovanovic.tech/blog/how-to-improve-performance-with-ef-core-query-splitting">How to Improve Performance with EF Core Query Splitting</a></li>
<li><a href="https://milanjovanovic.tech/blog/unleash-ef-core-performance-with-compiled-queries">Unleash EF Core Performance with Compiled Queries</a></li>
<li><a href="https://milanjovanovic.tech/blog/how-i-made-my-efcore-query-faster-with-batching">How I Made My EF Core Query Faster with Batching</a></li>
<li><a href="https://milanjovanovic.tech/blog/how-to-use-global-query-filters-in-ef-core">How to Use Global Query Filters in EF Core</a></li>
<li><a href="https://milanjovanovic.tech/blog/dbcontext-is-not-thread-safe-parallelizing-ef-core-queries-the-right-way">DbContext Is Not Thread Safe: Parallelizing EF Core Queries the Right Way</a></li>
<li>Common EF Core Query Performance Mistakes</li>
<li>Solving the N+1 Query Problem in EF Core</li>
<li>Lazy, Eager, and Explicit Loading in EF Core</li>
</ul>
<h2>Choosing Your Data Access</h2>
<p>EF Core is not the only way to talk to your database, and it is not always the fastest. These guides help you pick the right tool and get the most out of a lightweight one.</p>
<ul>
<li><a href="https://milanjovanovic.tech/blog/ef-core-vs-dapper">EF Core vs Dapper: When to Use Each</a></li>
<li><a href="https://milanjovanovic.tech/blog/dapper-dotnet-guide">Dapper in .NET: A Complete Guide</a></li>
</ul>
<h2>Bulk Operations</h2>
<p>When you need to insert or update thousands of rows, the standard <code>SaveChanges()</code> approach won't cut it. These articles show you how to handle bulk data efficiently.</p>
<ul>
<li><a href="https://milanjovanovic.tech/blog/fast-sql-bulk-inserts-with-csharp-and-ef-core">Fast SQL Bulk Inserts with C# and EF Core</a></li>
<li><a href="https://milanjovanovic.tech/blog/what-you-need-to-know-about-ef-core-bulk-updates">What You Need to Know About EF Core Bulk Updates</a></li>
<li><a href="https://milanjovanovic.tech/blog/how-to-use-the-new-bulk-update-feature-in-ef-core-7">How to Use the New Bulk Update Feature in EF Core 7</a></li>
<li><a href="https://milanjovanovic.tech/blog/optimizing-bulk-database-updates-in-dotnet">Optimizing Bulk Database Updates in .NET</a></li>
</ul>
<h2>Concurrency and Locking</h2>
<p>Concurrent access to the same data can cause race conditions and data corruption. EF Core provides built-in mechanisms to handle this safely.</p>
<ul>
<li><a href="https://milanjovanovic.tech/blog/solving-race-conditions-with-ef-core-optimistic-locking">Solving Race Conditions with EF Core Optimistic Locking</a></li>
<li><a href="https://milanjovanovic.tech/blog/a-clever-way-to-implement-pessimistic-locking-in-ef-core">A Clever Way to Implement Pessimistic Locking in EF Core</a></li>
<li><a href="https://milanjovanovic.tech/blog/working-with-transactions-in-ef-core">Working with Transactions in EF Core</a></li>
</ul>
<h2>Advanced Features</h2>
<p>EF Core has a rich feature set beyond basic CRUD. These articles cover interceptors, raw SQL, soft deletes, multi-tenancy, and more.</p>
<ul>
<li><a href="https://milanjovanovic.tech/blog/5-ef-core-features-you-need-to-know">5 EF Core Features You Need to Know</a></li>
<li><a href="https://milanjovanovic.tech/blog/how-to-use-ef-core-interceptors">How to Use EF Core Interceptors</a></li>
<li><a href="https://milanjovanovic.tech/blog/ef-core-raw-sql-queries">EF Core Raw SQL Queries</a></li>
<li><a href="https://milanjovanovic.tech/blog/implementing-soft-delete-with-ef-core">Implementing Soft Delete with EF Core</a></li>
<li><a href="https://milanjovanovic.tech/blog/multi-tenant-applications-with-ef-core">Multi-Tenant Applications with EF Core</a></li>
<li><a href="https://milanjovanovic.tech/blog/using-multiple-ef-core-dbcontext-in-single-application">Using Multiple EF Core DbContext in a Single Application</a></li>
<li><a href="https://milanjovanovic.tech/blog/using-stored-procedures-and-functions-with-ef-core-and-postgresql">Using Stored Procedures and Functions with EF Core and PostgreSQL</a></li>
<li>EF Core Compiled Models for Faster Startup</li>
<li>DbContext Pooling in EF Core</li>
<li>EF Core Connection Resiliency</li>
<li>Understanding the EF Core Change Tracker</li>
</ul>
<h2>What's New in EF Core 10</h2>
<p>EF Core 10 ships with .NET 10 and adds features that used to require workarounds. These articles cover what changed and how to put it to use.</p>
<ul>
<li><a href="https://milanjovanovic.tech/blog/named-query-filters-in-ef-10-multiple-query-filters-per-entity">Named Query Filters in EF 10: Multiple Query Filters Per Entity</a></li>
<li><a href="https://milanjovanovic.tech/blog/whats-new-in-ef-core-10-leftjoin-and-rightjoin-operators-in-linq">What's New in EF Core 10: LeftJoin and RightJoin Operators in LINQ</a></li>
</ul>
<h2>Migrations and Schema Management</h2>
<p>Keeping your database schema in sync with your code is a critical part of any EF Core project.</p>
<ul>
<li><a href="https://milanjovanovic.tech/blog/efcore-migrations-a-detailed-guide">EF Core Migrations: A Detailed Guide</a></li>
<li>EF Core Migrations Best Practices</li>
<li>Zero-Downtime EF Core Migrations</li>
</ul>
]]></content:encoded>
            <author>milan@milanjovanovic.tech (Milan Jovanović)</author>
            <enclosure url="https://milanjovanovic.tech/article-covers/ef-core-performance-guide.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[Why Postgres Ignores Your Index (Sargability, Taught by a Kata)]]></title>
            <link>https://milanjovanovic.tech/blog/sql-index-not-used-sargability</link>
            <guid isPermaLink="false">https://milanjovanovic.tech/blog/sql-index-not-used-sargability</guid>
            <pubDate>Fri, 10 Jul 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Your query is correct, your tests pass, and the index you carefully created is never touched.]]></description>
            <content:encoded><![CDATA[<p>Your query returns the right rows.
Every test is green.
And the index you carefully created is never touched.</p>
<p>This failure mode is invisible precisely because nothing fails: the query is correct at 6 rows and a disaster at 60 million, and the difference never shows up in a unit test.
It is also, in my experience, the single most common database performance bug in application code, and it usually comes from one innocent-looking habit.</p>
<h2>The Habit: Wrapping the Column in a Function</h2>
<p>Say a CI server stores one row per build in a <code>builds</code> table, with an index on <code>result</code>.
You want every build with a given result, case-insensitively, so you write the defensive classic:</p>
<pre><code class="language-sql">SELECT id FROM builds
WHERE lower(result) = lower(@value)
ORDER BY id;
</code></pre>
<p>Correct. And the index on <code>result</code> is now useless.</p>
<p>A B-tree index stores the <strong>raw column values</strong> in sorted order.
Postgres can seek into it for <code>result = 'CANCELLED'</code>, but <code>lower(result)</code> is a different value that exists nowhere in the index, so the only option is to compute <code>lower()</code> for <strong>every row in the table</strong>.
That is a sequential scan, by construction.</p>
<p>The property has an old-fashioned name: <strong>sargability</strong> (from &quot;Search ARGument-able&quot;).
A predicate is sargable when the column stands alone on its side of the comparison, and every function you wrap around the column takes the index off the table.
The same trap has many costumes:</p>
<ul>
<li><code>lower(email) = lower(@email)</code> and friends</li>
<li><code>EXTRACT(YEAR FROM placed_at) = 2026</code>, or in EF Core LINQ, <code>o.PlacedAt.Year == year</code>, which translates to exactly that</li>
<li><code>CAST(id AS text) LIKE @pattern</code></li>
<li>Arithmetic on the column: <code>price * 1.2 &gt; @limit</code></li>
</ul>
<p>The fix is always the same move: <strong>transform the parameter, not the column</strong>.
A year filter becomes a range over the raw column (<code>placed_at &gt;= '2026-01-01' AND placed_at &lt; '2027-01-01'</code>).
And the case-insensitive comparison? If the data is stored in one consistent case, compare the raw column and let the index seek; if you genuinely need case-insensitivity, put an expression index on <code>lower(result)</code> or use <code>citext</code>, and the wrapped form becomes sargable.</p>
<h2>Seeing It: EXPLAIN Is the Only Truth</h2>
<p>You cannot detect this by reading the query, timing it, or counting green tests.
The only tool that tells the truth is the query plan:</p>
<pre><code class="language-sql">EXPLAIN (FORMAT JSON)
SELECT id FROM builds
WHERE lower(result) = lower(@value)
ORDER BY id;
</code></pre>
<p><code>EXPLAIN</code> without <code>ANALYZE</code> only plans, never executes, so it is safe anywhere.
If the plan tree contains a <code>Seq Scan</code> on your table where you expected an index node, you have your answer.</p>
<p>This mattered enough to me that I built it into a product.
<a href="https://katabench.com"><strong>Katabench</strong></a>, my coding platform, has a database track where every kata runs your C# (raw SQL via <a href="https://milanjovanovic.tech/blog/dapper-dotnet-guide"><strong>Dapper</strong></a>, or EF Core) against a <strong>real, throwaway PostgreSQL instance</strong> created for your submission and destroyed afterwards.
No in-memory fakes; the same philosophy as Testcontainers, because the planner is the thing being learned.</p>
<img src="https://milanjovanovic.tech/blogs/articles/sql-index-not-used-sargability/database-kata.png" alt="A Katabench database kata: the task statement explains aggregating in SQL instead of pulling rows into C#, the editor holds a starter Solution.cs, and the test panel shows all five tests passing with per-test time budgets and memory">
<p>Here is the kata built on exactly the <code>lower()</code> trap (&quot;Builds by Result&quot;).
The starter code ships the defensive query, and the interactive Run shows you the SQL your code produced <strong>and the plan it got</strong>, right under the test:</p>
<img src="https://milanjovanovic.tech/blogs/articles/sql-index-not-used-sargability/naive-query-seq-scan.png" alt="Running the naive lower(result) = lower(@value) query on Katabench: the test passes, but the captured SQL below it shows the query plan with a Seq Scan on builds flagged as a full table read">
<p>Look at the bottom-right: the test <strong>passes</strong> (175 ms, well inside budget), and the plan underneath says <code>Seq Scan on builds, full table read</code>.
That pairing is the whole lesson in one screenshot: correct and slow-at-scale are compatible, and only the plan tells you.</p>
<h2>Grading the Plan, Not the Stopwatch</h2>
<p>Here is the design problem that made this kata interesting to build: <strong>you cannot teach this with a timing gate</strong>.</p>
<p>On a small visible fixture, the sequential scan genuinely is the fastest plan (reading six rows beats bouncing through an index), so the naive query would pass any latency threshold you set.
Worse, on a tiny table Postgres will seq-scan even a perfectly sargable query, because the planner is right to.
Timing gates on small data teach nothing, and timing gates on huge data are flaky.</p>
<p>So the kata grades the <strong>plan itself</strong>, against a large hidden dataset (sixty thousand rows, seeded and then <code>ANALYZE</code>d so the planner has honest statistics).
After the timed runs, the harness re-issues your captured statements as <code>EXPLAIN (FORMAT JSON)</code> with the same bound parameters, and asserts rules over the plan tree: no <code>Seq Scan</code> on <code>builds</code>, the plan must use <code>ix_builds_result</code>, and the answer must be a single statement, one round-trip.</p>
<p>Rewrite the query to compare the raw column, and the verdict flips:</p>
<img src="https://milanjovanovic.tech/blogs/articles/sql-index-not-used-sargability/plan-gate-passing.png" alt="The fixed query on Katabench: all four tests pass and all three query-plan rules hold, with the graded plan tree showing a Bitmap Index Scan using ix_builds_result instead of a sequential scan">
<p>Same green tests, but now the graded plan shows <code>Bitmap Index Scan using ix_builds_result</code>, and all three plan rules hold.
The difference between the two screenshots is one <code>lower()</code> call.</p>
<h2>The Takeaway Habit</h2>
<p>You do not need a kata platform to build the reflex; you need one habit:</p>
<p><strong>When you write a WHERE clause, look at which side of the operator the column is on.</strong>
If the column is wrapped in anything (a function, a cast, arithmetic, a property extraction that your ORM turns into a function), the index is out of the game, and only <code>EXPLAIN</code> on realistic data will tell you.</p>
<p>And if you want the reflex drilled into your fingers rather than your bookmarks, the database track on <a href="https://katabench.com"><strong>Katabench</strong></a> will happily fail your query plan until it sticks.</p>
]]></content:encoded>
            <author>milan@milanjovanovic.tech (Milan Jovanović)</author>
            <enclosure url="https://milanjovanovic.tech/article-covers/sql-index-not-used-sargability.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[Distributed Locking With Postgres Advisory Locks in .NET]]></title>
            <link>https://milanjovanovic.tech/blog/postgres-advisory-locks-dotnet</link>
            <guid isPermaLink="false">https://milanjovanovic.tech/blog/postgres-advisory-locks-dotnet</guid>
            <pubDate>Fri, 10 Jul 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[You probably do not need Redis, RedLock, or ZooKeeper for distributed locking. If your instances already share a Postgres database, they share a lock manager…]]></description>
            <content:encoded><![CDATA[<p>I needed a distributed lock for an unglamorous reason: awarding achievements.
On <a href="https://katabench.com"><strong>Katabench</strong></a>, two concurrent submissions from the same user could both observe &quot;streak not yet awarded&quot; and both award it.
A classic check-then-act race, per user, across API instances, so an in-process <strong>SemaphoreSlim lock</strong> cannot fix it once the API scales past one instance.</p>
<p>The reflex answer is Redis and a RedLock library (I surveyed the options in <a href="https://milanjovanovic.tech/blog/distributed-locking-in-dotnet-coordinating-work-across-multiple-instances"><strong>distributed locking in .NET</strong></a>).
But there is a lock manager already in the architecture, battle-tested, transactional, and free: <strong>Postgres</strong>.</p>
<h2>Advisory Locks in One Minute</h2>
<p>Postgres advisory locks are locks over an application-defined 64-bit key.
Postgres never interprets the key; it just guarantees that only one session can hold it at a time:</p>
<pre><code class="language-sql">SELECT pg_advisory_lock(42);    -- blocks until acquired
SELECT pg_advisory_unlock(42);  -- explicit release
</code></pre>
<p>Two properties make the session-level variant the right primitive for a distributed lock:</p>
<ul>
<li><strong>It serializes across every instance</strong> that shares the database. The lock lives in Postgres, not in process memory.</li>
<li><strong>It auto-releases when the holding connection closes.</strong> A crashed pod, a killed deploy, a network partition that drops the connection: in every case, Postgres frees the lock immediately. There is no TTL to tune, no lease to renew, and no stuck key to page you at 3 a.m.</li>
</ul>
<p>That second property is the whole argument.
A TTL-based Redis lease has to pick an expiry: too short and the lock expires mid-critical-section (now you need fencing tokens), too long and a crash wedges the key for the full TTL.
Session advisory locks sidestep the dilemma, because the lock's lifetime <strong>is</strong> the connection's lifetime.</p>
<h2>The Implementation</h2>
<p>The full class is small.
The design decisions are in the details, so let's walk them:</p>
<pre><code class="language-csharp">public sealed class PostgresAdvisoryLock(
    string connectionString,
    ILogger&lt;PostgresAdvisoryLock&gt; logger) : IDistributedLock
{
    // How long a caller waits for a contended lock before giving up.
    private static readonly TimeSpan WaitTimeout = TimeSpan.FromSeconds(15);

    public async Task&lt;IAsyncDisposable&gt; AcquireAsync(
        string key,
        CancellationToken cancellationToken = default)
    {
        long lockKey = HashKey(key);
        NpgsqlConnection? connection = null;
        try
        {
            // A dedicated connection: the lock is held for the handle's lifetime.
            connection = new NpgsqlConnection(connectionString);
            await connection.OpenAsync(cancellationToken);

            using var timeout = CancellationTokenSource
                .CreateLinkedTokenSource(cancellationToken);
            timeout.CancelAfter(WaitTimeout);

            using var command = new NpgsqlCommand(
                &quot;SELECT pg_advisory_lock(@key)&quot;, connection);
            command.Parameters.AddWithValue(&quot;key&quot;, lockKey);
            await command.ExecuteNonQueryAsync(timeout.Token);

            return new Handle(connection, lockKey, logger);
        }
        catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested)
        {
            // Our WaitTimeout fired, not the caller's token: a holder is stuck.
            logger.LogWarning(
                &quot;Lock '{Key}' timed out after {Timeout}s; proceeding without it.&quot;,
                key, WaitTimeout.TotalSeconds);
            await DisposeQuietlyAsync(connection);
            return NullAsyncDisposable.Instance;
        }
        catch (Exception ex) when (ex is NpgsqlException or TimeoutException)
        {
            logger.LogWarning(ex, &quot;Lock '{Key}' unavailable; proceeding without it.&quot;, key);
            await DisposeQuietlyAsync(connection);
            return NullAsyncDisposable.Instance;
        }
    }
}
</code></pre>
<p><strong>The lock lives on a dedicated connection.</strong>
<code>pg_advisory_lock</code> binds the lock to the session that ran it, so the handle owns one connection from acquire to dispose.
That is the cost model to keep in mind: each held lock pins a connection for the duration of the critical section.
Keep critical sections short, or this pattern will walk you straight into an <strong>exhausted connection pool</strong>.</p>
<p><strong>The wait is bounded.</strong>
<code>pg_advisory_lock</code> blocks server-side until the lock is granted, so a stuck holder could hang requests forever.
The linked cancellation token caps the wait at 15 seconds, and Npgsql cancels the server-side wait when it fires.</p>
<p><strong>String keys are hashed with FNV-1a, not <code>GetHashCode</code>.</strong>
Advisory locks take a <code>bigint</code>, and the map from <code>&quot;achievements:user:123&quot;</code> to that bigint must be identical in every process:</p>
<pre><code class="language-csharp">public static long HashKey(string key)
{
    const ulong offsetBasis = 14695981039346656037UL;
    const ulong prime = 1099511628211UL;
    ulong hash = offsetBasis;
    foreach (byte b in Encoding.UTF8.GetBytes(key))
    {
        hash ^= b;
        hash *= prime;
    }

    return unchecked((long)hash);
}
</code></pre>
<p><code>string.GetHashCode()</code> is randomized per process in .NET, so two instances would hash the same key to different lock ids and never contend.
A 64-bit FNV-1a collision, if you ever hit one, only makes two unrelated keys occasionally serialize: a performance blip, never a correctness bug.</p>
<p><strong>Release has a built-in safety net.</strong>
Disposal runs <code>pg_advisory_unlock</code>, but even if that command fails, disposing the connection ends the Postgres session, and the session's death releases every advisory lock it held.
The lock physically cannot leak.</p>
<h2>Failing Open Is a Choice: Make It Consciously</h2>
<p>Notice what the <code>catch</code> blocks do: when the lock cannot be acquired (database unreachable, or a stuck holder times out the wait), the method logs a warning and returns a <strong>no-op handle</strong>, and the caller proceeds without the lock.</p>
<p>That is fail-open, and it is only correct because of what this lock protects: a best-effort feature where the fallback is the pre-lock behavior (a rare duplicate award, resolved by an idempotent write).
Degrading to the old race beats turning submissions into 500s because the lock store hiccuped.</p>
<p>Invert the decision the moment the lock guards something that must never run twice: money movement, external side effects, destructive migrations.
There, a failed acquisition should fail the operation loudly.
The point is that fail-open versus fail-closed is a per-call-site decision about blast radius, not a property of the lock class.</p>
<h2>When Postgres Is the Wrong Lock</h2>
<p>Honest boundaries, so you know when to reach for something else:</p>
<ul>
<li><strong>High lock throughput.</strong> A connection open per acquisition is fine at &quot;per-user, occasionally contended&quot; rates and wrong at thousands of acquisitions per second. That is Redis territory.</li>
<li><strong>No shared database.</strong> If the services that need mutual exclusion do not already share a Postgres instance, adding one just for locks buys you nothing over Redis.</li>
<li><strong>Critical section = exactly one transaction.</strong> Use <code>pg_advisory_xact_lock</code> instead: it releases automatically at commit or rollback, and there is no handle to dispose.</li>
</ul>
<p>But if your instances already share a Postgres database, and your contention profile is &quot;rare and brief&quot;, you get a crash-safe distributed lock with zero new infrastructure and about a hundred lines of code.
That trade is hard to beat.</p>
]]></content:encoded>
            <author>milan@milanjovanovic.tech (Milan Jovanović)</author>
            <enclosure url="https://milanjovanovic.tech/article-covers/postgres-advisory-locks-dotnet.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[Clean Architecture in .NET: The Complete Guide]]></title>
            <link>https://milanjovanovic.tech/blog/clean-architecture-dotnet</link>
            <guid isPermaLink="false">https://milanjovanovic.tech/blog/clean-architecture-dotnet</guid>
            <pubDate>Fri, 10 Jul 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[A practical map of Clean Architecture in .NET: dependency direction, layer responsibilities, use-case organization, cross-cutting concerns, and the tradeoffs…]]></description>
            <content:encoded><![CDATA[<p>Clean Architecture is a widely used way to structure long-lived .NET applications.
It keeps your business logic independent of frameworks, databases, and UI, which pays off as the system grows.
But the ideas span dozens of topics: layers, the dependency rule, CQRS, cross-cutting concerns, testing, domain modeling.
This guide pulls them all together, from the fundamentals to the patterns you'll use in production.</p>
<h2>What is Clean Architecture?</h2>
<p>Clean Architecture is a software design philosophy that separates the elements of a design into ring levels.
The key rule is that <strong>dependencies can only point inward</strong> - outer layers can depend on inner layers, but not vice versa.</p>
<p>In .NET, this typically means organizing your solution into layers like Domain, Application, Infrastructure, and Presentation.
Each layer has a clear responsibility, and the dependency rule ensures your business logic stays independent of frameworks, databases, and UI concerns.</p>
<p>This guide brings together everything you need to master Clean Architecture in .NET.</p>
<p>Want to go deeper? My <a href="https://milanjovanovic.tech/pragmatic-clean-architecture"><strong>Pragmatic Clean Architecture</strong></a> course teaches the complete system I use to ship production-ready applications.</p>
<h2>Getting Started</h2>
<p>These articles cover the foundational concepts. If you're new to Clean Architecture, start here to understand the &quot;why&quot; before diving into implementation details.</p>
<ul>
<li><a href="https://milanjovanovic.tech/blog/clean-architecture-and-the-benefits-of-structured-software-design">Clean Architecture and the Benefits of Structured Software Design</a></li>
<li><a href="https://milanjovanovic.tech/blog/clean-architecture-folder-structure">Clean Architecture Folder Structure</a></li>
<li><a href="https://milanjovanovic.tech/blog/building-your-first-use-case-with-clean-architecture">Building Your First Use Case with Clean Architecture</a></li>
<li><a href="https://milanjovanovic.tech/blog/clean-architecture-solution-template-dotnet">A Clean Architecture Solution Template for .NET</a></li>
<li><a href="https://milanjovanovic.tech/blog/clean-architecture-minimal-apis">Clean Architecture with Minimal APIs</a></li>
</ul>
<h2>Core Concepts</h2>
<p>Once you understand the basics, these articles explore the principles that make Clean Architecture effective in real-world projects - from handling complexity to managing cross-cutting concerns.</p>
<ul>
<li><a href="https://milanjovanovic.tech/blog/why-clean-architecture-is-great-for-complex-projects">Why Clean Architecture Is Great for Complex Projects</a></li>
<li><a href="https://milanjovanovic.tech/blog/clean-architecture-the-missing-chapter">Clean Architecture: The Missing Chapter</a></li>
<li><a href="https://milanjovanovic.tech/blog/balancing-cross-cutting-concerns-in-clean-architecture">Balancing Cross-Cutting Concerns in Clean Architecture</a></li>
<li><a href="https://milanjovanovic.tech/blog/getting-the-current-user-in-clean-architecture">Getting the Current User in Clean Architecture</a></li>
<li><a href="https://milanjovanovic.tech/blog/clean-architecture-anti-patterns">Clean Architecture Anti-Patterns and Common Mistakes</a></li>
<li><a href="https://milanjovanovic.tech/blog/clean-architecture-vs-onion-vs-hexagonal">Clean Architecture vs Onion vs Hexagonal Architecture</a></li>
<li><a href="https://milanjovanovic.tech/blog/when-to-use-clean-architecture">When to Use Clean Architecture (And When Not To)</a></li>
<li><a href="https://milanjovanovic.tech/blog/dependency-rule-clean-architecture">The Dependency Rule in Clean Architecture</a></li>
</ul>
<h2>The Layers</h2>
<p>Clean Architecture is built from four layers, each with a distinct responsibility. These guides go layer by layer, covering what belongs where and how the pieces map onto each other.</p>
<ul>
<li><a href="https://milanjovanovic.tech/blog/domain-layer-clean-architecture">The Domain Layer in Clean Architecture</a></li>
<li><a href="https://milanjovanovic.tech/blog/application-layer-clean-architecture">The Application Layer in Clean Architecture</a></li>
<li><a href="https://milanjovanovic.tech/blog/infrastructure-layer-clean-architecture">The Infrastructure Layer in Clean Architecture</a></li>
<li><a href="https://milanjovanovic.tech/blog/mapping-between-layers-clean-architecture">Mapping Between Layers in Clean Architecture</a></li>
<li><a href="https://milanjovanovic.tech/blog/organize-use-cases-clean-architecture">Organizing Use Cases in Clean Architecture</a></li>
</ul>
<h2>CQRS and MediatR</h2>
<p>The CQRS pattern is a natural complement to Clean Architecture. It separates read and write operations, keeping your use cases focused and testable.</p>
<ul>
<li><a href="https://milanjovanovic.tech/blog/cqrs-pattern-with-mediatr">CQRS Pattern with MediatR</a></li>
<li><a href="https://milanjovanovic.tech/blog/cqrs-pattern-the-way-it-should-have-been-from-the-start">CQRS Pattern: The Way It Should Have Been From the Start</a></li>
<li><a href="https://milanjovanovic.tech/blog/stop-conflating-cqrs-and-mediatr">Stop Conflating CQRS and MediatR</a></li>
<li><a href="https://milanjovanovic.tech/blog/cqrs-validation-with-mediatr-pipeline-and-fluentvalidation">CQRS Validation with MediatR Pipeline and FluentValidation</a></li>
<li><a href="https://milanjovanovic.tech/blog/mediatr-pipeline-behaviors">MediatR Pipeline Behaviors in .NET</a></li>
</ul>
<h2>Cross-Cutting Concerns</h2>
<p>Clean Architecture gives cross-cutting concerns a clear home instead of letting them leak across layers. These guides cover concerns that appear in many production applications.</p>
<ul>
<li><a href="https://milanjovanovic.tech/blog/exception-handling-clean-architecture">Exception Handling in Clean Architecture</a></li>
<li><a href="https://milanjovanovic.tech/blog/logging-strategy-clean-architecture">Logging Strategy in Clean Architecture</a></li>
<li><a href="https://milanjovanovic.tech/blog/authentication-authorization-clean-architecture">Authentication and Authorization in Clean Architecture</a></li>
<li><a href="https://milanjovanovic.tech/blog/caching-clean-architecture">Caching in Clean Architecture</a></li>
<li><a href="https://milanjovanovic.tech/blog/background-jobs-clean-architecture">Background Jobs in Clean Architecture</a></li>
<li><a href="https://milanjovanovic.tech/blog/transactions-clean-architecture">Transactions in Clean Architecture</a></li>
</ul>
<h2>Testing</h2>
<p>A well-structured Clean Architecture solution is inherently testable. These guides cover testing strategies at different levels.</p>
<ul>
<li><a href="https://milanjovanovic.tech/blog/unit-testing-clean-architecture-use-cases">Unit Testing Clean Architecture Use Cases</a></li>
<li><a href="https://milanjovanovic.tech/blog/enforcing-software-architecture-with-architecture-tests">Enforcing Software Architecture with Architecture Tests</a></li>
<li><a href="https://milanjovanovic.tech/blog/5-architecture-tests-you-should-add-to-your-dotnet-projects">5 Architecture Tests You Should Add to Your .NET Projects</a></li>
</ul>
<h2>Domain-Driven Design</h2>
<p>Clean Architecture provides the structure, and DDD provides the modeling techniques. These articles cover the DDD fundamentals you'll use inside the Domain layer.</p>
<ul>
<li><a href="https://milanjovanovic.tech/blog/value-objects-in-dotnet-ddd-fundamentals">Value Objects in .NET: DDD Fundamentals</a></li>
<li><a href="https://milanjovanovic.tech/blog/how-to-use-domain-events-to-build-loosely-coupled-systems">How to Use Domain Events to Build Loosely Coupled Systems</a></li>
<li><a href="https://milanjovanovic.tech/blog/refactoring-from-an-anemic-domain-model-to-a-rich-domain-model">Refactoring From an Anemic Domain Model to a Rich Domain Model</a></li>
<li><a href="https://milanjovanovic.tech/blog/from-transaction-scripts-to-domain-models-a-refactoring-journey">From Transaction Scripts to Domain Models: A Refactoring Journey</a></li>
</ul>
<h2>Related Architectures</h2>
<p>Clean Architecture isn't the only option. These complementary approaches can be used alongside it or as alternatives depending on your project's needs.</p>
<ul>
<li><a href="https://milanjovanovic.tech/blog/vertical-slice-architecture">Vertical Slice Architecture</a></li>
<li><a href="https://milanjovanovic.tech/blog/what-is-a-modular-monolith">What Is a Modular Monolith?</a></li>
<li><a href="https://milanjovanovic.tech/blog/screaming-architecture">Screaming Architecture</a></li>
</ul>
]]></content:encoded>
            <author>milan@milanjovanovic.tech (Milan Jovanović)</author>
            <enclosure url="https://milanjovanovic.tech/article-covers/clean-architecture-dotnet.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[The REPR Pattern in ASP.NET Core]]></title>
            <link>https://milanjovanovic.tech/blog/repr-pattern-aspnetcore</link>
            <guid isPermaLink="false">https://milanjovanovic.tech/blog/repr-pattern-aspnetcore</guid>
            <pubDate>Sat, 04 Jul 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[The REPR pattern (Request-Endpoint-Response) replaces bloated controllers with focused, single-purpose endpoints.]]></description>
            <content:encoded><![CDATA[<p>Every controller starts small.
Two actions, one dependency, easy to review.
A year later it has 20 actions, 15 constructor parameters, and a merge conflict every sprint.
The <strong>REPR pattern</strong> fixes this by giving each API operation its own class: one request, one endpoint, one response.</p>
<h2>What Is the REPR Pattern?</h2>
<p><strong>REPR</strong> stands for <strong>Request-Endpoint-Response</strong>. It's a pattern for organizing web API code where each endpoint is:</p>
<ol>
<li><strong>Request</strong> - a strongly-typed object representing the input</li>
<li><strong>Endpoint</strong> - a single function handling one HTTP operation</li>
<li><strong>Response</strong> - a strongly-typed object representing the output</li>
</ol>
<p>Instead of grouping dozens of actions into controller classes (which violates the Single Responsibility Principle), each endpoint is its own thing.</p>
<img src="https://milanjovanovic.tech/blogs/articles/repr-pattern-aspnetcore/repr-flow.png" alt="Request-Endpoint-Response flow: an HTTP request maps to a typed request object, a single endpoint function, a command or query handler, then a typed response object and the HTTP response">
<p>This is how <a href="https://milanjovanovic.tech/blog/minimal-apis-dotnet"><strong>Minimal APIs</strong></a> naturally work - and it pairs perfectly with <a href="https://milanjovanovic.tech/blog/cqrs-pattern-the-way-it-should-have-been-from-the-start"><strong>CQRS</strong></a> and <a href="https://milanjovanovic.tech/blog/clean-architecture-and-the-benefits-of-structured-software-design"><strong>Clean Architecture</strong></a>. The pattern itself was popularized by the FastEndpoints library and Ardalis (Steve Smith), but you don't need any library to use it.</p>
<h2>The Problem With Controllers</h2>
<p>Traditional controllers tend to grow into God classes:</p>
<pre><code class="language-csharp">[ApiController]
[Route(&quot;api/orders&quot;)]
public class OrdersController : ControllerBase
{
    private readonly IMediator _mediator;

    // 15 constructor parameters...
    // 20 action methods...
    // 500+ lines of code...

    [HttpPost]
    public async Task&lt;IActionResult&gt; PlaceOrder(PlaceOrderRequest request) { ... }

    [HttpGet(&quot;{id}&quot;)]
    public async Task&lt;IActionResult&gt; GetOrder(Guid id) { ... }

    [HttpPut(&quot;{id}/cancel&quot;)]
    public async Task&lt;IActionResult&gt; CancelOrder(Guid id) { ... }

    [HttpGet]
    public async Task&lt;IActionResult&gt; GetOrders([FromQuery] GetOrdersRequest request) { ... }

    // ... 16 more methods
}
</code></pre>
<p>Problems:</p>
<ul>
<li><strong>Violates SRP</strong> - one class handles many operations with different dependencies</li>
<li><strong>Constructor bloat</strong> - every action's dependencies are injected, even if only one action needs them</li>
<li><strong>Hard to navigate</strong> - finding the right action in a 500-line file is painful</li>
<li><strong>Merge conflicts</strong> - multiple developers editing the same controller file</li>
</ul>
<h2>REPR With Minimal APIs</h2>
<p>Each endpoint becomes a focused, single-purpose function:</p>
<h3>The Request</h3>
<pre><code class="language-csharp">public sealed record PlaceOrderRequest(
    Guid CustomerId,
    List&lt;OrderItemRequest&gt; Items);

public sealed record OrderItemRequest(
    Guid ProductId,
    int Quantity);
</code></pre>
<h3>The Endpoint</h3>
<pre><code class="language-csharp">public sealed class PlaceOrderEndpoint : IEndpoint
{
    public void MapEndpoint(IEndpointRouteBuilder app)
    {
        app.MapPost(&quot;/api/orders&quot;, Handle)
            .WithTags(&quot;Orders&quot;)
            .WithName(&quot;PlaceOrder&quot;)
            .Produces&lt;PlaceOrderResponse&gt;(StatusCodes.Status201Created)
            .ProducesProblem(StatusCodes.Status400BadRequest);
    }

    private static async Task&lt;IResult&gt; Handle(
        PlaceOrderRequest request,
        ICommandHandler&lt;PlaceOrderCommand, Guid&gt; handler,
        CancellationToken cancellationToken)
    {
        var command = new PlaceOrderCommand(request.CustomerId, request.Items);

        var result = await handler.Handle(command, cancellationToken);

        return result.IsSuccess
            ? Results.Created($&quot;/api/orders/{result.Value}&quot;, new PlaceOrderResponse(result.Value))
            : result.ToProblemDetails();
    }
}
</code></pre>
<p><code>IEndpoint</code> is a one-method interface that powers auto-discovery.
I define it in the <a href="https://milanjovanovic.tech/blog/repr-pattern-aspnetcore#auto-discovering-endpoints">auto-discovery section</a> below.</p>
<h3>The Response</h3>
<pre><code class="language-csharp">public sealed record PlaceOrderResponse(Guid OrderId);
</code></pre>
<p>Each endpoint lives in its own file with its own request, response, and handler reference. No shared state, no bloated constructor.</p>
<h2>Organizing REPR Endpoints by Feature</h2>
<pre><code>Api/
  Endpoints/
    Orders/
      PlaceOrderEndpoint.cs
      PlaceOrderRequest.cs
      PlaceOrderResponse.cs
      GetOrderEndpoint.cs
      GetOrderResponse.cs
      CancelOrderEndpoint.cs
    Customers/
      RegisterCustomerEndpoint.cs
      RegisterCustomerRequest.cs
      GetCustomerEndpoint.cs
    Products/
      CreateProductEndpoint.cs
      GetProductsEndpoint.cs
</code></pre>
<p>Or colocate request/response inside the endpoint class for smaller endpoints:</p>
<pre><code class="language-csharp">public sealed class CancelOrderEndpoint : IEndpoint
{
    public sealed record CancelOrderResponse(Guid OrderId, string Status);

    public void MapEndpoint(IEndpointRouteBuilder app)
    {
        app.MapPut(&quot;/api/orders/{orderId:guid}/cancel&quot;, Handle)
            .WithTags(&quot;Orders&quot;);
    }

    private static async Task&lt;IResult&gt; Handle(
        Guid orderId,
        ICommandHandler&lt;CancelOrderCommand&gt; handler,
        CancellationToken cancellationToken)
    {
        var command = new CancelOrderCommand(orderId);

        var result = await handler.Handle(command, cancellationToken);

        return result.IsSuccess
            ? Results.Ok(new CancelOrderResponse(orderId, &quot;Cancelled&quot;))
            : result.ToProblemDetails();
    }
}
</code></pre>
<h2>Auto-Discovering Endpoints</h2>
<p>Instead of manually registering each endpoint, define a small interface and scan the assembly at startup:</p>
<pre><code class="language-csharp">public interface IEndpoint
{
    void MapEndpoint(IEndpointRouteBuilder app);
}
</code></pre>
<pre><code class="language-csharp">public static class EndpointExtensions
{
    public static void MapEndpoints(this IEndpointRouteBuilder app)
    {
        var endpointTypes = typeof(Program).Assembly
            .GetTypes()
            .Where(t =&gt; t is { IsClass: true, IsAbstract: false } &amp;&amp;
                        t.IsAssignableTo(typeof(IEndpoint)));

        foreach (var type in endpointTypes)
        {
            var endpoint = (IEndpoint)Activator.CreateInstance(type)!;

            endpoint.MapEndpoint(app);
        }
    }
}
</code></pre>
<pre><code class="language-csharp">// Program.cs
app.MapEndpoints();
</code></pre>
<p>Now every class implementing <code>IEndpoint</code> is automatically registered.
<code>Activator.CreateInstance</code> works here because endpoint classes are stateless (their dependencies arrive as <code>Handle</code> parameters).
I walk through a production-ready version of this approach (including DI registration of endpoint classes) in <a href="https://milanjovanovic.tech/blog/automatically-register-minimal-apis-in-aspnetcore"><strong>automatically registering Minimal APIs</strong></a>, and <a href="https://milanjovanovic.tech/blog/how-to-structure-minimal-apis"><strong>how I structure Minimal APIs</strong></a> covers where these files should live.</p>
<h2>REPR + CQRS</h2>
<p>REPR and CQRS are natural partners:</p>
<ul>
<li><strong>POST/PUT/DELETE</strong> endpoints map to <strong>Commands</strong></li>
<li><strong>GET</strong> endpoints map to <strong>Queries</strong></li>
</ul>
<pre><code class="language-csharp">// Write endpoint → Command
public sealed class PlaceOrderEndpoint : IEndpoint
{
    public void MapEndpoint(IEndpointRouteBuilder app) =&gt;
        app.MapPost(&quot;/api/orders&quot;, Handle);

    private static async Task&lt;IResult&gt; Handle(
        PlaceOrderRequest request,
        ICommandHandler&lt;PlaceOrderCommand, Guid&gt; handler,
        CancellationToken ct)
    {
        var command = new PlaceOrderCommand(request.CustomerId, request.Items);
        var result = await handler.Handle(command, ct);
        return result.IsSuccess
            ? Results.Created($&quot;/api/orders/{result.Value}&quot;, result.Value)
            : result.ToProblemDetails();
    }
}

// Read endpoint → Query
public sealed class GetOrderEndpoint : IEndpoint
{
    public void MapEndpoint(IEndpointRouteBuilder app) =&gt;
        app.MapGet(&quot;/api/orders/{orderId:guid}&quot;, Handle);

    private static async Task&lt;IResult&gt; Handle(
        Guid orderId,
        IQueryHandler&lt;GetOrderByIdQuery, OrderResponse&gt; handler,
        CancellationToken ct)
    {
        var result = await handler.Handle(new GetOrderByIdQuery(orderId), ct);
        return result.IsSuccess ? Results.Ok(result.Value) : Results.NotFound();
    }
}
</code></pre>
<p>Each endpoint is a thin adapter between HTTP and your <a href="https://milanjovanovic.tech/blog/application-layer-clean-architecture"><strong>Application layer</strong></a>.</p>
<h2>REPR vs MVC Controllers</h2>
<p>How the two approaches compare in practice:</p>
<ul>
<li><strong>Granularity</strong>: REPR gives you one class per operation; controllers give you one class per resource with many actions.</li>
<li><strong>Dependencies</strong>: a REPR endpoint declares only what that operation needs. A controller's constructor accumulates every action's dependencies.</li>
<li><strong>File size</strong>: REPR endpoints stay at 20-50 lines. Controllers routinely grow into hundreds.</li>
<li><strong>Merge conflicts</strong>: rare with one file per operation, common when a team shares one controller file.</li>
<li><strong>Navigation</strong>: with REPR, the file name is the operation. With controllers, you scroll.</li>
<li><strong>Testing</strong>: identical. In both styles the real logic lives in the handler, and the HTTP layer stays thin.</li>
<li><strong>OpenAPI metadata</strong>: REPR endpoints declare <code>Produces</code> metadata explicitly; <code>[ApiController]</code> conventions infer more automatically.</li>
</ul>
<p>The one scenario where controllers still earn their keep: if you depend heavily on MVC-specific features (filters with complex ordering, model binding conventions, view results), migrating to REPR is a bigger lift than the benefit justifies.</p>
<h2>Do You Need a Library?</h2>
<p>Three ways to get REPR in practice:</p>
<ul>
<li><strong>Plain Minimal APIs</strong> (what this article shows): zero dependencies, full control, you own the ~30 lines of auto-discovery code.</li>
<li><strong>FastEndpoints</strong>: a mature library built entirely around REPR, with base classes per endpoint, built-in validation, and its own request pipeline. Great if you want conventions decided for you; the cost is a framework layer between you and ASP.NET Core.</li>
<li><strong>Single-action controllers</strong>: one controller class per operation. Works if your team must stay on MVC, but you keep the controller ceremony without gaining much.</li>
</ul>
<p>I default to plain Minimal APIs. The pattern is simple enough that a library is optional, and staying on the framework's primitives means every ASP.NET Core feature (filters, rate limiting, OpenAPI) works without adapter layers.</p>
<h2>The Vertical Slice Decision</h2>
<p>The REPR pattern treats each API operation as an independent unit: one request, one endpoint function, one response.</p>
<p>Combined with Minimal APIs and CQRS, it gives you focused, maintainable endpoint files that map cleanly to your Application layer commands and queries.</p>
<p>Stop writing 500-line controllers. One endpoint, one file, one responsibility.</p>
<hr>
]]></content:encoded>
            <author>milan@milanjovanovic.tech (Milan Jovanović)</author>
            <enclosure url="https://milanjovanovic.tech/article-covers/repr-pattern-aspnetcore.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[Unit Testing Best Practices in .NET]]></title>
            <link>https://milanjovanovic.tech/blog/unit-testing-best-practices-dotnet</link>
            <guid isPermaLink="false">https://milanjovanovic.tech/blog/unit-testing-best-practices-dotnet</guid>
            <pubDate>Sat, 04 Jul 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Unit tests should be fast, reliable, and maintainable. But poorly written tests become a burden that slows development.]]></description>
            <content:encoded><![CDATA[<p>A test suite is either a safety net or an anchor.
The difference rarely shows up on day one.
It shows up six months in, when a simple rename breaks 40 tests that were all asserting implementation details, and the team starts treating red builds as noise.
These practices keep .NET unit tests fast, trustworthy, and cheap to change.</p>
<h2>What Makes a Good Unit Test?</h2>
<p>A good unit test has four properties:</p>
<ol>
<li><strong>Fast</strong> - runs in milliseconds, not seconds</li>
<li><strong>Isolated</strong> - doesn't depend on databases, file systems, or external services</li>
<li><strong>Repeatable</strong> - produces the same result every time</li>
<li><strong>Self-validating</strong> - passes or fails without manual inspection</li>
</ol>
<p>If your tests take minutes to run, developers stop running them. If they fail randomly, developers stop trusting them. If they're hard to maintain, developers stop writing them.</p>
<h2>The AAA Pattern</h2>
<p>Structure every test with <strong>Arrange, Act, Assert</strong>:</p>
<pre><code class="language-csharp">[Fact]
public void Order_AddLineItem_IncreasesTotalAmount()
{
    // Arrange
    var order = Order.Create(Guid.NewGuid());
    var price = Money.Create(25.00m, &quot;USD&quot;);

    // Act
    order.AddLineItem(Guid.NewGuid(), price, quantity: 2);

    // Assert
    order.TotalAmount.Amount.Should().Be(50.00m);
}
</code></pre>
<p>Clear separation between setup, execution, and verification. One glance tells you what's being tested.</p>
<h2>Name Tests Clearly</h2>
<p>Test names should describe the scenario and expected outcome:</p>
<pre><code class="language-csharp">// Bad - what does this tell you?
[Fact]
public void Test1() { }

// Bad - too vague
[Fact]
public void OrderTest() { }

// Good - describes the behavior
[Fact]
public void Cancel_WhenOrderIsShipped_ReturnsFailure() { }

[Fact]
public void AddLineItem_WithValidProduct_IncreasesTotal() { }

[Fact]
public void Create_WithEmptyEmail_ReturnsValidationError() { }
</code></pre>
<p>Use the pattern: <code>Method_Scenario_ExpectedResult</code> or <code>Given_When_Then</code>. Pick one convention and use it consistently across your project.</p>
<h2>Test One Behavior Per Test</h2>
<p>Each test should verify one logical behavior. Not one assertion - one behavior.</p>
<pre><code class="language-csharp">// Bad - testing multiple behaviors
[Fact]
public void PlaceOrder_WorksCorrectly()
{
    var order = Order.Create(customerId);
    order.AddLineItem(productId, price, 2);
    order.Complete();

    order.LineItems.Should().HaveCount(1);
    order.TotalAmount.Should().Be(Money.Create(50, &quot;USD&quot;));
    order.Status.Should().Be(OrderStatus.Completed);
    order.DomainEvents.Should().ContainSingle(e =&gt; e is OrderCompletedDomainEvent);
}

// Good - one behavior per test
[Fact]
public void AddLineItem_IncreasesLineItemCount()
{
    var order = Order.Create(customerId);
    order.AddLineItem(productId, price, 2);

    order.LineItems.Should().HaveCount(1);
}

[Fact]
public void Complete_RaisesOrderCompletedEvent()
{
    var order = CreateOrderWithItems();
    order.Complete();

    order.DomainEvents.Should().ContainSingle(e =&gt; e is OrderCompletedDomainEvent);
}
</code></pre>
<p>When a test fails, you immediately know which behavior broke.</p>
<h2>Use Domain-Specific Assertions</h2>
<p>Assertion libraries like FluentAssertions (or the free Shouldly and AwesomeAssertions alternatives, worth knowing since FluentAssertions v8 moved to a paid license for commercial use) make tests more readable.
The <strong>FluentAssertions alternatives comparison</strong> covers the licensing and API trade-offs in detail:</p>
<pre><code class="language-csharp">// Without FluentAssertions
Assert.Equal(OrderStatus.Completed, order.Status);
Assert.True(result.IsSuccess);
Assert.NotNull(customer);

// With FluentAssertions
order.Status.Should().Be(OrderStatus.Completed);
result.IsSuccess.Should().BeTrue();
customer.Should().NotBeNull();
</code></pre>
<p>For domain-specific assertions, create extension methods:</p>
<pre><code class="language-csharp">public static class ResultAssertionExtensions
{
    public static void ShouldBeSuccess&lt;T&gt;(this Result&lt;T&gt; result)
    {
        result.IsSuccess.Should().BeTrue(
            $&quot;Expected success but got failure: {result.Error}&quot;);
    }

    public static void ShouldBeFailure&lt;T&gt;(
        this Result&lt;T&gt; result, Error expectedError)
    {
        result.IsFailure.Should().BeTrue();
        result.Error.Should().Be(expectedError);
    }
}
</code></pre>
<pre><code class="language-csharp">[Fact]
public void Cancel_WhenDraft_ReturnsSuccess()
{
    var order = CreateDraftOrder();
    var result = order.Cancel();

    result.ShouldBeSuccess();
}
</code></pre>
<h2>Use Test Data Builders</h2>
<p>Constructing test objects inline makes tests brittle and noisy:</p>
<pre><code class="language-csharp">// Brittle - if Customer constructor changes, every test breaks
[Fact]
public void Test()
{
    var customer = new Customer(
        Guid.NewGuid(),
        &quot;John Doe&quot;,
        Email.Create(&quot;john@example.com&quot;).Value,
        Address.Create(&quot;123 Main St&quot;, &quot;NYC&quot;, &quot;NY&quot;, &quot;10001&quot;, &quot;US&quot;).Value,
        false);
}
</code></pre>
<p>Use a builder:</p>
<pre><code class="language-csharp">public class CustomerBuilder
{
    private Guid _id = Guid.NewGuid();
    private string _name = &quot;John Doe&quot;;
    private Email _email = Email.Create(&quot;john@example.com&quot;).Value;
    private bool _isVip = false;

    public CustomerBuilder WithName(string name) { _name = name; return this; }
    public CustomerBuilder WithEmail(string email) { _email = Email.Create(email).Value; return this; }
    public CustomerBuilder AsVip() { _isVip = true; return this; }

    public Customer Build() =&gt; Customer.Create(_id, _name, _email, _isVip);
}
</code></pre>
<pre><code class="language-csharp">[Fact]
public void VipCustomer_GetsDiscount()
{
    var customer = new CustomerBuilder().AsVip().Build();
    var discount = _discountService.CalculateDiscount(customer);

    discount.Should().Be(10);
}
</code></pre>
<p>Builders isolate your tests from constructor changes and make the test's intent clear. I go deeper on this pattern (composition, factory methods, combining with Bogus) in <strong>test data builders in C#</strong>.</p>
<h2>Don't Test Implementation Details</h2>
<p>Test behavior, not implementation. Tests that verify internal state or call order are brittle.</p>
<pre><code class="language-csharp">// Bad - testing implementation details
[Fact]
public void PlaceOrder_CallsRepositoryAdd()
{
    _mockRepository.Verify(r =&gt; r.Add(It.IsAny&lt;Order&gt;()), Times.Once);
}

// Good - testing observable behavior
[Fact]
public async Task PlaceOrder_ReturnsOrderId()
{
    var result = await _handler.Handle(new PlaceOrderCommand(...), CancellationToken.None);

    result.IsSuccess.Should().BeTrue();
    result.Value.Should().NotBeEmpty();
}
</code></pre>
<p>The first test breaks if you rename the repository method. The second test passes as long as the behavior is correct, regardless of how it's implemented.
The focused guide to <strong>testing CQRS handlers</strong> applies this rule to success paths, validation failures, and persistence boundaries.</p>
<h2>When to Use Mocks (and When Not To)</h2>
<p><strong>Mock external dependencies</strong> - things at the system boundary:</p>
<ul>
<li>Database repositories</li>
<li>HTTP clients</li>
<li>Email services</li>
<li>Message brokers</li>
<li>Time providers</li>
</ul>
<p><strong>Don't mock domain objects</strong> - test them directly:</p>
<img src="https://milanjovanovic.tech/blogs/articles/unit-testing-best-practices-dotnet/mocking-boundary.png" alt="Mocking boundary: domain objects like the Order aggregate and value objects are tested directly, while external dependencies like the database, HTTP clients, and message brokers are mocked">
<pre><code class="language-csharp">// Don't mock the Order - test it directly
[Fact]
public void Order_Cancel_WhenShipped_Fails()
{
    var order = CreateShippedOrder();
    var result = order.Cancel();

    result.IsFailure.Should().BeTrue();
    result.Error.Should().Be(OrderErrors.AlreadyShipped);
}
</code></pre>
<p>If you find yourself mocking too many things, your class has too many dependencies. Consider refactoring.</p>
<p>And know your vocabulary: mocks, stubs, fakes, and spies are different tools with different failure modes. I break down <strong>when to use each test double</strong> separately.</p>
<h2>Parameterized Tests</h2>
<p>Use <code>[Theory]</code> with data sources to test multiple scenarios without duplicating code. I covered <a href="https://milanjovanovic.tech/blog/creating-data-driven-tests-with-xunit"><strong>data-driven tests with xUnit</strong></a> in detail, but here's the short version:</p>
<pre><code class="language-csharp">[Theory]
[InlineData(&quot;&quot;, false)]
[InlineData(&quot;not-an-email&quot;, false)]
[InlineData(&quot;john@example.com&quot;, true)]
[InlineData(&quot;jane@company.co.uk&quot;, true)]
public void Email_Create_ValidatesFormat(string input, bool shouldSucceed)
{
    var result = Email.Create(input);
    result.IsSuccess.Should().Be(shouldSucceed);
}
</code></pre>
<p>For complex test data, use <code>[MemberData]</code>:</p>
<pre><code class="language-csharp">public static IEnumerable&lt;object[]&gt; InvalidOrderData =&gt;
    new List&lt;object[]&gt;
    {
        new object[] { Guid.Empty, &quot;Customer ID is required&quot; },
        new object[] { Guid.NewGuid(), null },  // null items
    };

[Theory]
[MemberData(nameof(InvalidOrderData))]
public void PlaceOrder_WithInvalidData_Fails(Guid customerId, string errorMessage)
{
    // ...
}
</code></pre>
<h2>Test Domain Logic, Not Frameworks</h2>
<p>Focus your unit tests on code you own - domain entities, value objects, and domain services.</p>
<p>Don't unit test:</p>
<ul>
<li>EF Core configurations (use <a href="https://milanjovanovic.tech/blog/testcontainers-integration-testing-using-docker-in-dotnet"><strong>integration tests</strong></a> instead)</li>
<li>ASP.NET Core middleware</li>
<li>Third-party library behavior</li>
<li>DTOs and simple mappings</li>
</ul>
<pre><code class="language-csharp">// Worth unit testing - domain invariant
[Fact]
public void Order_AddLineItem_WhenNotDraft_ThrowsDomainException()
{
    var order = CreateCompletedOrder();
    var act = () =&gt; order.AddLineItem(productId, price, 1);

    act.Should().Throw&lt;DomainException&gt;()
       .WithMessage(&quot;Cannot modify a non-draft order.&quot;);
}

// NOT worth unit testing - it's just EF Core
[Fact]
public void DbContext_CanSaveOrder()
{
    // This tests EF Core, not your code. Use integration tests.
}
</code></pre>
<h2>Organize Tests to Mirror Source</h2>
<pre><code>tests/
  Domain.UnitTests/
    Orders/
      OrderTests.cs
      OrderLineItemTests.cs
    Customers/
      CustomerTests.cs
      EmailTests.cs
  Application.UnitTests/
    Orders/
      PlaceOrderCommandHandlerTests.cs
      CancelOrderCommandHandlerTests.cs
</code></pre>
<p>Mirror the source project structure so tests are easy to find.</p>
<h2>What the Test Should Prove</h2>
<p>Good unit tests protect your domain logic and give you confidence to refactor. Bad unit tests slow you down and break every time you change anything.</p>
<p>Follow these practices:</p>
<ul>
<li>Use AAA structure consistently</li>
<li>Name tests descriptively</li>
<li>Test behaviors, not implementation details</li>
<li>Use builders for test data</li>
<li>Mock only external boundaries</li>
<li>Prefer parameterized tests for multiple scenarios</li>
</ul>
<p>Your unit tests should be assets, not liabilities. And keep them in proportion: unit tests protect your domain logic, but they can't tell you the system works end to end, which is <a href="https://milanjovanovic.tech/blog/the-test-pyramid-is-a-lie-and-what-i-do-instead"><strong>why I don't chase the classic test pyramid shape</strong></a>.</p>
<hr>
]]></content:encoded>
            <author>milan@milanjovanovic.tech (Milan Jovanović)</author>
            <enclosure url="https://milanjovanovic.tech/article-covers/unit-testing-best-practices-dotnet.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[JWT Authentication in ASP.NET Core: Complete Guide]]></title>
            <link>https://milanjovanovic.tech/blog/jwt-authentication-aspnetcore</link>
            <guid isPermaLink="false">https://milanjovanovic.tech/blog/jwt-authentication-aspnetcore</guid>
            <pubDate>Sat, 04 Jul 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[JWT tokens are the standard way to authenticate APIs. Here is a complete guide to setting up JWT authentication in ASP.NET Core - from token generation to…]]></description>
            <content:encoded><![CDATA[<p>JWT authentication is the default choice for securing REST APIs, and ASP.NET Core has excellent built-in support for it.
But there's a gap between &quot;it works&quot; and &quot;it's secure&quot;: weak keys, eternal tokens, and skipped validation checks are all one config line away.</p>
<p>This guide covers the full setup: token generation, validation, refresh tokens, and the configuration mistakes that weaken an otherwise sound design.
If another identity provider issues the token, use the stricter <strong>third-party JWT validation checklist</strong>.</p>
<h2>What Is JWT Authentication?</h2>
<p><strong>JWT</strong> (JSON Web Token) is a compact, URL-safe token format used to represent claims between two parties. It's the most common way to authenticate REST APIs.</p>
<p>The flow:</p>
<ol>
<li>Client sends credentials (username + password)</li>
<li>Server validates credentials and generates a JWT</li>
<li>Client includes the JWT in the <code>Authorization</code> header of subsequent requests</li>
<li>Server validates the JWT on every request</li>
</ol>
<img src="https://milanjovanovic.tech/blogs/articles/jwt-authentication-aspnetcore/jwt-auth-flow.png" alt="JWT authentication sequence: the client posts credentials, the API validates them and returns a signed access token, and the client sends that token on later requests for the API to validate">
<h2>Setting Up JWT Authentication</h2>
<p>Install the required package:</p>
<pre><code class="language-bash">dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
</code></pre>
<p>Configure the authentication middleware:</p>
<pre><code class="language-csharp">builder.Services
    .AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =&gt;
    {
        options.MapInboundClaims = false;

        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidateAudience = true,
            ValidateLifetime = true,
            ValidateIssuerSigningKey = true,
            ValidIssuer = builder.Configuration[&quot;Jwt:Issuer&quot;],
            ValidAudience = builder.Configuration[&quot;Jwt:Audience&quot;],
            IssuerSigningKey = new SymmetricSecurityKey(
                Encoding.UTF8.GetBytes(builder.Configuration[&quot;Jwt:SecretKey&quot;]!)),
            ClockSkew = TimeSpan.Zero  // Disable the default 5-minute grace period
        };
    });

builder.Services.AddAuthorization();
</code></pre>
<p>Note the <code>MapInboundClaims = false</code> line.
By default, the JWT handler silently renames standard claims to legacy XML claim type URIs, so <code>sub</code> arrives as <code>ClaimTypes.NameIdentifier</code> and <code>FindFirst(JwtRegisteredClaimNames.Sub)</code> returns null.
Disabling the mapping keeps claims exactly as they appear in the token, and it is the number one JWT gotcha I see in real projects.</p>
<p>Add the middleware in the correct order:</p>
<pre><code class="language-csharp">app.UseAuthentication();
app.UseAuthorization();
</code></pre>
<h2>Generating JWT Tokens</h2>
<p>Create a token provider service that issues both tokens: a short-lived JWT access token and a random refresh token persisted to the database.
The <code>RefreshToken</code> entity is defined in the <a href="https://milanjovanovic.tech/blog/jwt-authentication-aspnetcore#refresh-tokens">refresh tokens section</a> below.</p>
<pre><code class="language-csharp">public sealed class TokenProvider
{
    private readonly IConfiguration _configuration;
    private readonly AppDbContext _dbContext;

    public TokenProvider(IConfiguration configuration, AppDbContext dbContext)
    {
        _configuration = configuration;
        _dbContext = dbContext;
    }

    public (string AccessToken, string RefreshToken) GenerateTokens(User user)
    {
        var accessToken = GenerateAccessToken(user);

        var refreshToken = new RefreshToken
        {
            Id = Guid.NewGuid(),
            UserId = user.Id,
            Token = Convert.ToBase64String(RandomNumberGenerator.GetBytes(64)),
            ExpiresAt = DateTime.UtcNow.AddDays(7),
            CreatedAt = DateTime.UtcNow
        };

        _dbContext.RefreshTokens.Add(refreshToken);

        return (accessToken, refreshToken.Token);
    }

    private string GenerateAccessToken(User user)
    {
        var claims = new List&lt;Claim&gt;
        {
            new(JwtRegisteredClaimNames.Sub, user.Id.ToString()),
            new(JwtRegisteredClaimNames.Email, user.Email),
            new(JwtRegisteredClaimNames.Name, user.FullName),
            new(&quot;permission&quot;, &quot;orders:read&quot;),
            new(&quot;permission&quot;, &quot;orders:write&quot;)
        };

        // Add role claims
        foreach (var role in user.Roles)
        {
            claims.Add(new(ClaimTypes.Role, role.Name));
        }

        var key = new SymmetricSecurityKey(
            Encoding.UTF8.GetBytes(_configuration[&quot;Jwt:SecretKey&quot;]!));

        var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);

        var tokenDescriptor = new SecurityTokenDescriptor
        {
            Subject = new ClaimsIdentity(claims),
            Expires = DateTime.UtcNow.AddMinutes(30),
            SigningCredentials = credentials,
            Issuer = _configuration[&quot;Jwt:Issuer&quot;],
            Audience = _configuration[&quot;Jwt:Audience&quot;]
        };

        return new JsonWebTokenHandler().CreateToken(tokenDescriptor);
    }
}
</code></pre>
<p>I'm using <code>JsonWebTokenHandler</code> from <code>Microsoft.IdentityModel.JsonWebTokens</code> here, not the legacy <code>JwtSecurityTokenHandler</code>.
It's the same handler ASP.NET Core uses to validate tokens since .NET 8, and it ships transitively with the <code>JwtBearer</code> package.</p>
<p>Register the provider (scoped, because it uses the <code>DbContext</code>):</p>
<pre><code class="language-csharp">builder.Services.AddScoped&lt;TokenProvider&gt;();
</code></pre>
<h2>The Login Endpoint</h2>
<p>The login endpoint returns both tokens.
The access token authenticates requests, and the refresh token is the entry point for the refresh flow covered below.</p>
<pre><code class="language-csharp">public sealed record LoginRequest(string Email, string Password);

public sealed record LoginResponse(string AccessToken, string RefreshToken);
</code></pre>
<pre><code class="language-csharp">app.MapPost(&quot;/api/auth/login&quot;, async (
    LoginRequest request,
    IUserRepository userRepository,
    IPasswordHasher passwordHasher,
    TokenProvider tokenProvider,
    AppDbContext dbContext) =&gt;
{
    var user = await userRepository.GetByEmailAsync(request.Email);

    if (user is null || !passwordHasher.Verify(request.Password, user.PasswordHash))
    {
        return Results.Unauthorized();
    }

    var (accessToken, refreshToken) = tokenProvider.GenerateTokens(user);

    await dbContext.SaveChangesAsync();

    return Results.Ok(new LoginResponse(accessToken, refreshToken));
});
</code></pre>
<p>The <code>SaveChangesAsync</code> call persists the refresh token that <code>GenerateTokens</code> added to the <code>DbContext</code>.</p>
<p><strong>Security note:</strong> Always return the same error for &quot;user not found&quot; and &quot;wrong password.&quot; This prevents user enumeration.</p>
<h2>Protecting Endpoints</h2>
<p>Use <code>RequireAuthorization()</code> on Minimal API endpoints:</p>
<pre><code class="language-csharp">// Requires any authenticated user
app.MapGet(&quot;/api/orders&quot;, GetOrders)
    .RequireAuthorization();

// Requires specific role
app.MapPost(&quot;/api/orders&quot;, CreateOrder)
    .RequireAuthorization(policy =&gt; policy.RequireRole(&quot;Admin&quot;, &quot;Manager&quot;));

// Requires specific claim
app.MapDelete(&quot;/api/orders/{id}&quot;, DeleteOrder)
    .RequireAuthorization(policy =&gt; policy.RequireClaim(&quot;permission&quot;, &quot;orders:delete&quot;));
</code></pre>
<p>Or use the <code>[Authorize]</code> attribute on controllers:</p>
<pre><code class="language-csharp">[Authorize]
[ApiController]
[Route(&quot;api/[controller]&quot;)]
public class OrdersController : ControllerBase
{
    [Authorize(Roles = &quot;Admin&quot;)]
    [HttpDelete(&quot;{id}&quot;)]
    public async Task&lt;IActionResult&gt; Delete(Guid id) { ... }
}
</code></pre>
<h2>Accessing User Claims</h2>
<p>Get the current user's information from the JWT claims:</p>
<pre><code class="language-csharp">public sealed class UserContext : IUserContext
{
    private readonly IHttpContextAccessor _httpContextAccessor;

    public UserContext(IHttpContextAccessor httpContextAccessor)
    {
        _httpContextAccessor = httpContextAccessor;
    }

    public Guid UserId =&gt; Guid.Parse(
        _httpContextAccessor.HttpContext?.User
            .FindFirst(JwtRegisteredClaimNames.Sub)?.Value
        ?? throw new UnauthorizedAccessException());

    public string Email =&gt;
        _httpContextAccessor.HttpContext?.User
            .FindFirst(JwtRegisteredClaimNames.Email)?.Value
        ?? throw new UnauthorizedAccessException();

    public bool IsAuthenticated =&gt;
        _httpContextAccessor.HttpContext?.User.Identity?.IsAuthenticated ?? false;
}
</code></pre>
<p>Looking up <code>JwtRegisteredClaimNames.Sub</code> only works because we set <code>MapInboundClaims = false</code> earlier.
With the default mapping, <code>sub</code> would arrive as <code>ClaimTypes.NameIdentifier</code> and <code>UserId</code> would throw for every authenticated user.</p>
<p>Register it:</p>
<pre><code class="language-csharp">builder.Services.AddHttpContextAccessor();
builder.Services.AddScoped&lt;IUserContext, UserContext&gt;();
</code></pre>
<p>Inject <code>IUserContext</code> into your Application layer to access the current user without depending on ASP.NET Core.
I covered this abstraction in depth in <a href="https://milanjovanovic.tech/blog/getting-the-current-user-in-clean-architecture"><strong>getting the current user in Clean Architecture</strong></a>.</p>
<p>For authorization beyond simple role checks, see my guide to <strong>claims-based authorization</strong>.</p>
<h2>Refresh Tokens</h2>
<p>Access tokens should be short-lived (15-30 minutes). Use refresh tokens for extended sessions.</p>
<img src="https://milanjovanovic.tech/blogs/articles/jwt-authentication-aspnetcore/refresh-token-rotation.png" alt="Refresh token rotation flow: a presented refresh token is checked for validity, an invalid one returns 401, and a valid one is revoked before a new access and refresh token pair is issued">
<pre><code class="language-csharp">public sealed class RefreshToken
{
    public Guid Id { get; set; }
    public Guid UserId { get; set; }
    public string Token { get; set; }
    public DateTime ExpiresAt { get; set; }
    public bool IsRevoked { get; set; }
    public DateTime CreatedAt { get; set; }
}
</code></pre>
<p>This is the entity that <code>TokenProvider.GenerateTokens</code> stores on every login.
The client sends the refresh token back to exchange it for a new token pair:</p>
<pre><code class="language-csharp">public sealed record RefreshRequest(string RefreshToken);
</code></pre>
<pre><code class="language-csharp">app.MapPost(&quot;/api/auth/refresh&quot;, async (
    RefreshRequest request,
    AppDbContext dbContext,
    TokenProvider tokenProvider,
    IUserRepository userRepository) =&gt;
{
    var storedToken = await dbContext.RefreshTokens
        .FirstOrDefaultAsync(r =&gt;
            r.Token == request.RefreshToken &amp;&amp;
            !r.IsRevoked &amp;&amp;
            r.ExpiresAt &gt; DateTime.UtcNow);

    if (storedToken is null)
    {
        return Results.Unauthorized();
    }

    // Rotate the refresh token
    storedToken.IsRevoked = true;

    var user = await userRepository.GetByIdAsync(storedToken.UserId);
    var (accessToken, newRefreshToken) = tokenProvider.GenerateTokens(user!);

    await dbContext.SaveChangesAsync();

    return Results.Ok(new LoginResponse(accessToken, newRefreshToken));
});
</code></pre>
<p><strong>Key security practices:</strong></p>
<ul>
<li>Rotate refresh tokens on every use (issue a new one, revoke the old)</li>
<li>Store refresh tokens in the database (not just in memory)</li>
<li>Set a reasonable expiration (7-14 days)</li>
<li>Revoke all refresh tokens when the user changes their password</li>
</ul>
<p>Rotation also lets you detect stolen tokens: if a revoked token is ever presented again, someone replayed it, and you should revoke the whole session family.
I dig into that mechanism in <strong>refresh token rotation in ASP.NET Core</strong>.</p>
<h2>Configuration</h2>
<p>Store JWT settings in <code>appsettings.json</code>:</p>
<pre><code class="language-json">{
  &quot;Jwt&quot;: {
    &quot;Issuer&quot;: &quot;https://myapp.com&quot;,
    &quot;Audience&quot;: &quot;https://myapp.com&quot;,
    &quot;SecretKey&quot;: &quot;your-256-bit-secret-key-here-minimum-32-chars&quot;
  }
}
</code></pre>
<p>For production, use a proper <strong>secret management</strong> solution - not <code>appsettings.json</code>.</p>
<h2>Common Security Mistakes</h2>
<ol>
<li>
<p><strong>Weak signing keys.</strong> Use at least 256-bit keys for HMAC. Better: use RSA keys.</p>
</li>
<li>
<p><strong>Storing tokens in localStorage.</strong> Vulnerable to XSS. Use <code>HttpOnly</code> cookies for web applications.</p>
</li>
<li>
<p><strong>Long-lived access tokens.</strong> Keep them short (15-30 minutes). Use refresh tokens for longer sessions.</p>
</li>
<li>
<p><strong>Not validating all token properties.</strong> Always validate issuer, audience, lifetime, and signing key.</p>
</li>
<li>
<p><strong>Setting <code>ClockSkew</code> too high.</strong> The default 5-minute clock skew means expired tokens are valid for 5 extra minutes. Set it to <code>TimeSpan.Zero</code> and handle clock synchronization at the infrastructure level.</p>
</li>
<li>
<p><strong>Including sensitive data in the payload.</strong> JWTs are signed but not encrypted by default. Don't put passwords, SSNs, or API keys in claims.</p>
</li>
</ol>
<h2>The Safe Default</h2>
<p>JWT authentication in ASP.NET Core is straightforward:</p>
<ol>
<li>Configure <code>AddJwtBearer</code> with proper validation and <code>MapInboundClaims = false</code></li>
<li>Create a <code>TokenProvider</code> that generates access and refresh tokens</li>
<li>Use <code>RequireAuthorization()</code> on endpoints</li>
<li>Implement refresh token rotation for extended sessions</li>
<li>Store secrets properly and keep access tokens short-lived</li>
</ol>
<p>For more advanced scenarios, consider using an identity provider like <a href="https://milanjovanovic.tech/blog/integrate-keycloak-with-aspnetcore-using-oauth-2"><strong>Keycloak</strong></a> instead of managing JWTs yourself.</p>
<hr>
]]></content:encoded>
            <author>milan@milanjovanovic.tech (Milan Jovanović)</author>
            <enclosure url="https://milanjovanovic.tech/article-covers/jwt-authentication-aspnetcore.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[Polly v8: Resilience Pipelines Explained]]></title>
            <link>https://milanjovanovic.tech/blog/polly-v8-resilience-pipelines</link>
            <guid isPermaLink="false">https://milanjovanovic.tech/blog/polly-v8-resilience-pipelines</guid>
            <pubDate>Sat, 04 Jul 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Polly v8 replaced policies with resilience pipelines: a rewritten core with allocation-free execution, built-in telemetry, and one composition model instead of…]]></description>
            <content:encoded><![CDATA[<p>If you learned Polly before 2023, your muscle memory says <code>Policy.Handle&lt;HttpRequestException&gt;().WaitAndRetryAsync(...)</code> and <code>Policy.WrapAsync(retry, breaker, timeout)</code>.</p>
<p>Polly v8 threw that API out and rebuilt the library around a single concept: the <strong>resilience pipeline</strong>.
It wasn't churn for its own sake.
The rewrite (done in collaboration with Microsoft, who built <code>Microsoft.Extensions.Http.Resilience</code> on top of it) fixed real problems: duplicated sync/async APIs, allocation-heavy execution, bolt-on telemetry, and the perpetually confusing <code>PolicyWrap</code> ordering.</p>
<p>Here's the new model, the migration mapping, and the one behavior (strategy ordering) that deserves more attention than it gets.</p>
<h2>From Policies to Pipelines</h2>
<p>The v8 mental model has three pieces:</p>
<ul>
<li>A <strong>strategy</strong> is one resilience behavior: retry, circuit breaker, timeout, rate limiter, fallback, hedging.</li>
<li>A <strong>pipeline</strong> is an ordered composition of strategies, built once and cached.</li>
<li>Everything is configured through <strong>options classes</strong> with <code>ShouldHandle</code> predicates, instead of fluent <code>Handle</code> chains.</li>
</ul>
<p>Side by side.
Polly v7:</p>
<pre><code class="language-csharp">var retry = Policy
    .Handle&lt;HttpRequestException&gt;()
    .WaitAndRetryAsync(3, attempt =&gt; TimeSpan.FromSeconds(Math.Pow(2, attempt)));

var timeout = Policy.TimeoutAsync(TimeSpan.FromSeconds(10));

var wrapped = Policy.WrapAsync(retry, timeout);

await wrapped.ExecuteAsync(() =&gt; httpClient.GetAsync(url));
</code></pre>
<p>Polly v8:</p>
<pre><code class="language-bash">dotnet add package Polly.Core
</code></pre>
<pre><code class="language-csharp">using Polly;
using Polly.Retry;

ResiliencePipeline pipeline = new ResiliencePipelineBuilder()
    .AddRetry(new RetryStrategyOptions
    {
        ShouldHandle = new PredicateBuilder().Handle&lt;HttpRequestException&gt;(),
        MaxRetryAttempts = 3,
        Delay = TimeSpan.FromSeconds(1),
        BackoffType = DelayBackoffType.Exponential,
        UseJitter = true
    })
    .AddTimeout(TimeSpan.FromSeconds(10))
    .Build();

await pipeline.ExecuteAsync(
    async ct =&gt; await httpClient.GetAsync(url, ct),
    cancellationToken);
</code></pre>
<p>Details worth noticing:</p>
<ul>
<li><strong>One API for sync and async.</strong> <code>ResiliencePipeline</code> has <code>Execute</code> and <code>ExecuteAsync</code>; the v7 split between <code>Policy</code> and <code>AsyncPolicy</code> is gone.</li>
<li><strong>Cancellation is first-class.</strong> The callback receives a <code>CancellationToken</code> that the pipeline manages; a timeout strategy cancels your delegate through it, not by abandoning it.</li>
<li><strong>Jitter is a property</strong>, not a contrib package. <strong>Exponential backoff with jitter</strong> is <code>UseJitter = true</code>.</li>
<li><strong>Generic pipelines</strong> (<code>ResiliencePipeline&lt;HttpResponseMessage&gt;</code>) handle result-based conditions, like retrying on <code>5xx</code> status codes, and are required for result-producing strategies like <strong>fallback</strong> and hedging.</li>
<li><strong>Build once, reuse forever.</strong> Pipelines are thread-safe and designed to be cached. Building one per request wastes the allocation work v8 did; the execution path itself is designed to be allocation-free.</li>
</ul>
<p>The migration mapping, compactly: <code>WaitAndRetryAsync</code> becomes <code>AddRetry</code> with <code>RetryStrategyOptions</code>; <code>AdvancedCircuitBreakerAsync</code> becomes <code>AddCircuitBreaker</code> with failure-ratio options; <code>TimeoutAsync</code> becomes <code>AddTimeout</code>; <code>BulkheadAsync</code> becomes <code>AddConcurrencyLimiter</code> (the <strong>bulkhead pattern</strong> under a more accurate name); <code>FallbackAsync</code> becomes <code>AddFallback</code>; and <code>PolicyWrap</code> disappears entirely, because the pipeline <em>is</em> the composition.
The v8 package still ships the legacy API, so migration can be incremental.</p>
<h2>Ordering: The Same Strategies, Different Machine</h2>
<p><code>PolicyWrap</code> confused everyone about what wrapped what.
Pipelines make it deterministic: <strong>strategies execute in the order added, first added is outermost</strong>.
A call flows inward through each strategy to your delegate, and the outcome flows back out through them in reverse.</p>
<img src="https://milanjovanovic.tech/blogs/articles/polly-v8-resilience-pipelines/strategy-execution-order.png" alt="A pipeline built from a rate limiter, total timeout, retry, circuit breaker, and per-attempt timeout, with the call flowing left to right from the caller inward to the delegate; the first strategy added is the outermost">
<p>This isn't cosmetic.
The same strategies in a different order are a different machine.
The canonical example is timeout placement relative to retry:</p>
<pre><code class="language-csharp">// A: timeout OUTSIDE retry. One 10-second budget for ALL attempts.
new ResiliencePipelineBuilder()
    .AddTimeout(TimeSpan.FromSeconds(10))
    .AddRetry(retryOptions)
    .Build();

// B: timeout INSIDE retry. Each attempt gets its own 10 seconds.
new ResiliencePipelineBuilder()
    .AddRetry(retryOptions)
    .AddTimeout(TimeSpan.FromSeconds(10))
    .Build();
</code></pre>
<img src="https://milanjovanovic.tech/blogs/articles/polly-v8-resilience-pipelines/timeout-placement.png" alt="Two pipelines compared: A puts the timeout outside the retry so all attempts share one 10-second budget, while B puts the timeout inside the retry so each attempt gets its own 10 seconds">
<p>In A, retries race a shared deadline: the third attempt might get 1 second, and <code>TimeoutRejectedException</code> escaping the pipeline means the whole operation is over.
In B, three attempts can burn 30 seconds plus backoff, and the <em>retry</em> sees each timeout and decides whether to go again.
Neither is wrong; they answer different questions, and a robust pipeline often uses both, which is the heart of a sane <strong>timeout strategy</strong>.</p>
<p>The recommended general-purpose order, which is also what Microsoft's standard handler uses:</p>
<ol>
<li>Rate limiter or concurrency limiter (shed load before spending effort on it)</li>
<li>Total timeout (the overall budget)</li>
<li>Retry</li>
<li>Circuit breaker (inside retry, so it sees every raw attempt and its failure stats stay honest; the retry then sees <code>BrokenCircuitException</code> and stops)</li>
<li>Per-attempt timeout</li>
</ol>
<p>The <strong>retry vs circuit breaker</strong> interaction in step 4 is the subtlest part of the ordering, and the one worth internalizing before you tune any thresholds.
And if you add <strong>chaos strategies with Simmy</strong>, they go last, innermost, so injected faults pass through all the real strategies.</p>
<h2>Dependency Injection and Reuse</h2>
<p><code>Polly.Extensions</code> adds the DI registration model.
You register a pipeline under a key, and Polly caches it:</p>
<pre><code class="language-bash">dotnet add package Polly.Extensions
</code></pre>
<pre><code class="language-csharp">builder.Services.AddResiliencePipeline(&quot;database&quot;, pipeline =&gt;
{
    pipeline
        .AddTimeout(TimeSpan.FromSeconds(15))
        .AddRetry(new RetryStrategyOptions
        {
            ShouldHandle = new PredicateBuilder()
                .Handle&lt;NpgsqlException&gt;(ex =&gt; ex.IsTransient),
            MaxRetryAttempts = 3,
            BackoffType = DelayBackoffType.Exponential,
            UseJitter = true
        });
});
</code></pre>
<p>Consume it through <code>ResiliencePipelineProvider</code>:</p>
<pre><code class="language-csharp">public class OrderRepository(
    ResiliencePipelineProvider&lt;string&gt; pipelineProvider,
    NpgsqlDataSource dataSource)
{
    public async Task&lt;Order?&gt; GetByIdAsync(Guid id, CancellationToken ct)
    {
        var pipeline = pipelineProvider.GetPipeline(&quot;database&quot;);

        return await pipeline.ExecuteAsync(
            async token =&gt; await QueryOrderAsync(dataSource, id, token),
            ct);
    }
}
</code></pre>
<p>Registering through DI buys you the second headline feature: <strong>telemetry is on by default</strong>.
Every strategy emits events (retry attempts, breaker state changes, timeouts) through <code>ILogger</code> and <code>System.Diagnostics.Metrics</code>, so the answer to &quot;did the retry actually fire last night?&quot; is in your logs and your OpenTelemetry <strong>metrics</strong> without any instrumentation code.
In v7, that visibility was something you hand-rolled in <code>onRetry</code> callbacks, or more commonly didn't.</p>
<h2>HttpClient: You Might Not Need to Build a Pipeline at All</h2>
<p>For HTTP, <code>Microsoft.Extensions.Http.Resilience</code> packages the whole thing:</p>
<pre><code class="language-bash">dotnet add package Microsoft.Extensions.Http.Resilience
</code></pre>
<pre><code class="language-csharp">builder.Services.AddHttpClient(&quot;catalog&quot;, client =&gt;
{
    client.BaseAddress = new Uri(&quot;https://catalog.internal&quot;);
})
.AddStandardResilienceHandler();
</code></pre>
<p>That one line installs the recommended composition: rate limiter, 30-second total timeout, retry (3 attempts, exponential, jittered, honoring <code>Retry-After</code>), circuit breaker, and a 10-second per-attempt timeout, in exactly the outermost-to-innermost order listed earlier.
It's the productized version of everything this article described, and for service-to-service HTTP it's the right default.
When the defaults don't fit, <a href="https://milanjovanovic.tech/blog/overriding-default-http-resilience-handlers-in-dotnet"><strong>override the standard resilience handlers</strong></a> or use <code>AddResilienceHandler</code> to compose your own; the broader patterns are covered in <a href="https://milanjovanovic.tech/blog/building-resilient-cloud-applications-with-dotnet"><strong>building resilient cloud applications with .NET</strong></a>.</p>
<h2>The Resilience Baseline</h2>
<p>Polly v8 is a better model, not just a new API.</p>
<ul>
<li>Pipelines replace policies and <code>PolicyWrap</code>: one composition mechanism, options-based configuration, unified sync/async, build-once-and-cache.</li>
<li>Ordering is explicit and semantic: first added is outermost, and moving a timeout across a retry changes what your pipeline promises.</li>
<li>DI registration gives you cached pipelines plus logs and metrics for every strategy activation, which turns &quot;is our resilience working?&quot; into a dashboard query.</li>
<li>For HTTP, start with <code>AddStandardResilienceHandler</code> and customize only when you outgrow it.</li>
</ul>
<p>Migrate incrementally; the legacy API still works.
But write new resilience code as pipelines, and spend the time you save thinking about the part the library can't do for you: which strategies, with which thresholds, in which order.</p>
]]></content:encoded>
            <author>milan@milanjovanovic.tech (Milan Jovanović)</author>
            <enclosure url="https://milanjovanovic.tech/article-covers/polly-v8-resilience-pipelines.png" length="0" type="image/png"/>
        </item>
    </channel>
</rss>