<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: ONDŘEJ PODHORNÝ</title>
    <description>The latest articles on DEV Community by ONDŘEJ PODHORNÝ (@ondej_podhorny).</description>
    <link>https://dev.to/ondej_podhorny</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4142982%2F718877d7-7971-49bb-ad7f-11b711089022.png</url>
      <title>DEV Community: ONDŘEJ PODHORNÝ</title>
      <link>https://dev.to/ondej_podhorny</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ondej_podhorny"/>
    <language>en</language>
    <item>
      <title>MediatR vs. Wolverine: Architecture, Ergonomics, and Feature Matrix</title>
      <dc:creator>ONDŘEJ PODHORNÝ</dc:creator>
      <pubDate>Fri, 25 Sep 2026 13:49:10 +0000</pubDate>
      <link>https://dev.to/ondej_podhorny/mediatr-vs-wolverine-architecture-ergonomics-and-feature-matrix-7dm</link>
      <guid>https://dev.to/ondej_podhorny/mediatr-vs-wolverine-architecture-ergonomics-and-feature-matrix-7dm</guid>
      <description>&lt;p&gt;MediatR taught .NET teams to keep HTTP away from business logic. Wolverine asks whether that boundary still needs a marker interface.&lt;/p&gt;

&lt;p&gt;For years, MediatR has served as the default standard for implementing CQRS and in-process decoupling in .NET applications. It introduced clean boundary separation between HTTP controllers and business logic, providing an accessible, predictable structure for monolithic codebases.&lt;/p&gt;

&lt;p&gt;As distributed requirements, event-driven architectures, and performance constraints have matured, the limitations of simple in-memory mediator patterns have become clearer. Wolverine, part of the Critter Stack ecosystem, approaches this challenge from a fundamentally different perspective: replacing interface ceremony with code generation and integrating messaging capabilities directly into the framework core.&lt;/p&gt;

&lt;p&gt;This article breaks down the architectural, ergonomic, and operational differences between MediatR and Wolverine to evaluate when each tool makes sense.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ergonomics: interface ceremony vs. pure POCOs
&lt;/h2&gt;

&lt;p&gt;The structural difference is evident in handler and message definitions. MediatR enforces strict generic interfaces, whereas Wolverine relies on conventions and dynamic code generation to eliminate boilerplate.&lt;/p&gt;

&lt;h3&gt;
  
  
  MediatR: marker interfaces and rigid signatures
&lt;/h3&gt;

&lt;p&gt;In MediatR, every command, query, and handler must implement specific interfaces:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// 1. Command requires marker interface&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;record&lt;/span&gt; &lt;span class="nc"&gt;CreateOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;CustomerId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;decimal&lt;/span&gt; &lt;span class="n"&gt;Amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IRequest&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;OrderResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// 2. Handler ceremony and constructor injection boilerplate&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CreateOrderHandler&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IRequestHandler&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;CreateOrder&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;OrderResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;ILogger&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;CreateOrderHandler&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_logger&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;CreateOrderHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ILogger&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;CreateOrderHandler&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_logger&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;OrderResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;Handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CreateOrder&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogInformation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Processing order for {CustomerId}..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CustomerId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;OrderResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Guid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NewGuid&lt;/span&gt;&lt;span class="p"&gt;()));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This pattern introduces several architectural constraints:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Interface coupling.&lt;/strong&gt; Commands and handlers are coupled to &lt;code&gt;IRequest&amp;lt;T&amp;gt;&lt;/code&gt; and &lt;code&gt;IRequestHandler&amp;lt;TRequest, TResponse&amp;gt;&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Constructor clutter.&lt;/strong&gt; Dependencies require explicit constructor injection and private field assignments.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Forced asynchrony.&lt;/strong&gt; Handlers must return &lt;code&gt;Task&amp;lt;T&amp;gt;&lt;/code&gt; and accept a &lt;code&gt;CancellationToken&lt;/code&gt;, even when handling pure in-memory, synchronous computations. This forces artificial state machines and allocations via &lt;code&gt;Task.FromResult&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Wolverine: convention over ceremony
&lt;/h3&gt;

&lt;p&gt;Wolverine strips away external interface constraints entirely:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;  &lt;span class="c1"&gt;// 1. Clean, zero-interface C# POCO&lt;/span&gt;
  &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;record&lt;/span&gt; &lt;span class="nc"&gt;CreateOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;CustomerId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;decimal&lt;/span&gt; &lt;span class="n"&gt;Amount&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// 2. Static class and direct method injection (no constructor DI)&lt;/span&gt;
  &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CreateOrderHandler&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;OrderResult&lt;/span&gt; &lt;span class="nf"&gt;Handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CreateOrder&lt;/span&gt; &lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ILogger&lt;/span&gt; &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogInformation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Processing order for {CustomerId}..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CustomerId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
          &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;OrderResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Guid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NewGuid&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Key ergonomic differences:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Zero-interface messaging.&lt;/strong&gt; Messages remain pure POCOs without framework references.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Method-level dependency injection.&lt;/strong&gt; Handlers can be declared as static classes, with dependencies passed directly into the &lt;code&gt;Handle&lt;/code&gt; method parameters alongside the command.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Flexible signatures.&lt;/strong&gt; If an operation is synchronous, return the raw type directly. If a &lt;code&gt;CancellationToken&lt;/code&gt; is needed, declare it; if not, omit it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Comprehensive feature matrix
&lt;/h2&gt;

&lt;p&gt;While MediatR focuses solely on in-process dispatching, Wolverine acts as both an in-memory mediator and an integration bus.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;MediatR&lt;/th&gt;
&lt;th&gt;Wolverine&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Scope &amp;amp; Complexity&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Minimal in-process mediator&lt;/td&gt;
&lt;td&gt;Full messaging &amp;amp; integration bus&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Team Onboarding &amp;amp; Ecosystem&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;🟢 De-facto industry standard&lt;/td&gt;
&lt;td&gt;🟡 Emerging, convention-heavy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Coding Style&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;IRequest&lt;/code&gt;, Handlers&lt;/td&gt;
&lt;td&gt;Zero-interface (POCO)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;HTTP Endpoint Integration&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;❌ Minimal API / MVC only&lt;/td&gt;
&lt;td&gt;✅ Wolverine.Http (zero MVC)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cascading Messages&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;❌ Explicit dispatch calls&lt;/td&gt;
&lt;td&gt;✅ Native return types&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Transactional Outbox/Inbox&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;❌ External libraries&lt;/td&gt;
&lt;td&gt;✅ EF Core, PG, SQL, Marten&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Async Message Brokers&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;❌ In-process only&lt;/td&gt;
&lt;td&gt;✅ RabbitMQ, Kafka, ASB, SQS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Retry &amp;amp; Dead Letter Queue&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;❌ Complex in behaviors&lt;/td&gt;
&lt;td&gt;✅ Per-exception &amp;amp; circuit breakers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Stateful Sagas &amp;amp; Workflows&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;❌ Not supported&lt;/td&gt;
&lt;td&gt;✅ First-class stateful sagas&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;OpenTelemetry &amp;amp; Tracing&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;❌ Custom pipeline&lt;/td&gt;
&lt;td&gt;✅ Built-in natively&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Native AOT / Cold Starts&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;🟢 Zero-overhead out-of-the-box&lt;/td&gt;
&lt;td&gt;🟡 Requires code pre-generation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;In-Memory Dispatch&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;⚡ Direct delegate invoke&lt;/td&gt;
&lt;td&gt;Full message envelope context&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Architectural scope: beyond in-process dispatch
&lt;/h2&gt;

&lt;p&gt;MediatR stops at the process boundary. When requirements involve asynchronous background processing, external message queues, or reliable transaction boundaries, developers must build or integrate secondary frameworks (such as MassTransit or custom outbox implementations).&lt;/p&gt;

&lt;p&gt;Wolverine bridges this gap by unifying local invocation and external messaging into a single abstraction model:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cascading messages via return types.&lt;/strong&gt; Handlers can return tuples, for example &lt;code&gt;(OrderResult Result, OrderPlaced Event)&lt;/code&gt;. Wolverine automatically sends the event downstream to message queues or outbox storage while returning the result to the direct caller, avoiding manual dispatcher calls.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Native transactional outbox.&lt;/strong&gt; Ensures consistency between database writes and message broker dispatching natively, without third-party plugins.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resilience and error handling.&lt;/strong&gt; Wolverine provides declarative retry policies, fallback logic, and dead-letter queues configured per message or exception type.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Operational trade-offs
&lt;/h2&gt;

&lt;p&gt;Choosing between these frameworks requires weighing specific architectural compromises:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Conventions vs. explicit types.&lt;/strong&gt; MediatR’s explicit interface hierarchies make code navigation straightforward via standard IDE tools. Wolverine’s convention-based discovery requires developers to understand its method naming and parameter binding rules.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Startup performance and AOT.&lt;/strong&gt; MediatR relies on standard DI service registration. Wolverine uses Lamar and runtime Roslyn compilation by default to maximize throughput. For environments requiring minimal cold-start latencies or Native AOT, teams must incorporate ahead-of-time code pre-generation into their build pipelines.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Adoption and maintenance.&lt;/strong&gt; MediatR has an established track record and extensive documentation across the enterprise ecosystem. Wolverine offers modern ergonomics and broader built-in capabilities, but onboarding requires familiarity with convention-driven patterns.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Which should you choose?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Choose MediatR&lt;/strong&gt; if your application is a modular monolith requiring strict in-memory command dispatching, explicit interface-driven navigation, and zero additional infrastructure dependencies.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Choose Wolverine&lt;/strong&gt; if your application needs an integrated messaging pipeline with transactional outbox support, message broker integration, minimal ceremony, and declarative resilience built directly into the runtime.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;These architectural trade-offs, combined with Marten as a document database on PostgreSQL, were the primary reason I decided to rebuild the backend of Codebase Pulse on top of the Critter Stack.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>csharp</category>
    </item>
  </channel>
</rss>
