DEV Community

ONDŘEJ PODHORNÝ
ONDŘEJ PODHORNÝ

Posted on Originally published at podhorny.dev

MediatR vs. Wolverine: Architecture, Ergonomics, and Feature Matrix

MediatR taught .NET teams to keep HTTP away from business logic. Wolverine asks whether that boundary still needs a marker interface.

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.

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.

This article breaks down the architectural, ergonomic, and operational differences between MediatR and Wolverine to evaluate when each tool makes sense.

Ergonomics: interface ceremony vs. pure POCOs

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.

MediatR: marker interfaces and rigid signatures

In MediatR, every command, query, and handler must implement specific interfaces:

// 1. Command requires marker interface
public record CreateOrder(string CustomerId, decimal Amount)
    : IRequest<OrderResult>;

// 2. Handler ceremony and constructor injection boilerplate
public class CreateOrderHandler : IRequestHandler<CreateOrder, OrderResult>
{
    private readonly ILogger<CreateOrderHandler> _logger;

    public CreateOrderHandler(ILogger<CreateOrderHandler> logger)
    {
        _logger = logger;
    }

    public Task<OrderResult> Handle(CreateOrder request, CancellationToken ct)
    {
        _logger.LogInformation("Processing order for {CustomerId}...", request.CustomerId);
        return Task.FromResult(new OrderResult(Guid.NewGuid()));
    }
}
Enter fullscreen mode Exit fullscreen mode

This pattern introduces several architectural constraints:

  • Interface coupling. Commands and handlers are coupled to IRequest<T> and IRequestHandler<TRequest, TResponse>.
  • Constructor clutter. Dependencies require explicit constructor injection and private field assignments.
  • Forced asynchrony. Handlers must return Task<T> and accept a CancellationToken, even when handling pure in-memory, synchronous computations. This forces artificial state machines and allocations via Task.FromResult.

Wolverine: convention over ceremony

Wolverine strips away external interface constraints entirely:

  // 1. Clean, zero-interface C# POCO
  public record CreateOrder(string CustomerId, decimal Amount);

  // 2. Static class and direct method injection (no constructor DI)
  public static class CreateOrderHandler
  {
      public static OrderResult Handle(CreateOrder cmd, ILogger logger)
      {
          logger.LogInformation("Processing order for {CustomerId}...", cmd.CustomerId);
          return new OrderResult(Guid.NewGuid());
      }
  }
Enter fullscreen mode Exit fullscreen mode

Key ergonomic differences:

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

Comprehensive feature matrix

While MediatR focuses solely on in-process dispatching, Wolverine acts as both an in-memory mediator and an integration bus.

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

Architectural scope: beyond in-process dispatch

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).

Wolverine bridges this gap by unifying local invocation and external messaging into a single abstraction model:

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

Operational trade-offs

Choosing between these frameworks requires weighing specific architectural compromises:

  • Conventions vs. explicit types. 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.
  • Startup performance and AOT. 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.
  • Adoption and maintenance. 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.

Which should you choose?

Choose MediatR if your application is a modular monolith requiring strict in-memory command dispatching, explicit interface-driven navigation, and zero additional infrastructure dependencies.

Choose Wolverine 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.

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.

Top comments (0)