Best Practices
The Wolverine community certainly wants you to be successful with Wolverine, so we're using this page to gather whatever advice we can offer. This advice falls into two areas, generic tips for creating maintainable code when using asynchronous messaging that would apply to any messaging or command executor tool, and specific tips for Wolverine itself.
Dividing Handlers
Wolverine does not enforce or require today any kind of explicit assignment of incoming message handler to endpoint (i.e. Rabbit MQ queue, AWS SQS queue, Kafka topic). That being said, you will frequently want to define your message routing to isolate incoming message types into separate endpoints for the sake of throughput or parallelized work. Likewise, if message ordering is important, you may want to purposely route multiple message types to the same listening endpoint.
While Wolverine will happily allow you to implement multiple message handler methods or multiple HTTP endpoints in the same class, you may get better results by only allowing a single message handler method per class. Especially for larger, more complex message handling or HTTP request handling.
Avoid Abstracting Wolverine
This is generic advice for just about any infrastructure tool. You will lose out on Wolverine functionality by trying to abstract it, and very likely just create an abstraction that merely mimics a subset of Wolverine for little gain.
If you are concerned about testability of your message handlers, we recommend using cascading messages instead anyway that would completely remove the need for abstracting Wolverine.
Lean on Wolverine Error Handling
Rather than explicitly catching exceptions in message handlers, we recommend leaning on Wolverine's configurable error handling policies. This will save you explicit code that can obfuscate your actual functionality, while still providing robust responses to errors and Wolverine's built in observability (error logging, circuit breakers, execution statistics).
Pre-Generate Types to Optimize Production Usage
Wolverine has admittedly an unusual runtime architecture in that it depends much more on runtime generated code than the IoC container tricks that many other .NET frameworks do today. That's great for performance, and definitely helps Wolverine to enable much lower ceremony code, but that comes with a potentially significant memory usage and cold start problem.
If you see any of the issues I just described, or want to get in front of this issue, utilize the "pre-generated types" functionality described in Working with Code Generation.
Prefer Pure Functions for Business Logic
As much as possible, we recommend that you try to create pure functions for any business logic or workflow routing logic that is responsible for "deciding" what to do next. The goal here is to make that code relatively easy to test inside of isolated unit tests that are completely decoupled from infrastructure. Moreover, using pure functions allows you to largely eschew the usage of mock objects inside of unit tests which can become problematic when overused.
Wolverine has a lot of specific functionality to move infrastructure concerns out of the way of your business or workflow logic. For tips on how to create pure functions for your Wolverine message handlers or HTTP endpoints, see:
- A-Frame Architecture with Wolverine
- Testing Without Mocks: A Pattern Language by Jim Shore
- Compound Handlers in Wolverine
- Isolating Side Effects from Wolverine Handlers
Make All Side Effects Apparent from the Root Message Handler
INFO
This advice arose from Jeremy's involvement with a legacy system using a Wolverine competitor where a message handler had a huge dependency tree of services that depended on other services and deep, deep down the call stack, a service published a message through that service bus. The point here is to make your code easy to reason about by being able to easily scan and see the side effects and outcomes of the original message.
Very frequently, you'll need to publish additional messages from either an HTTP endpoint or a message handler. You can technically have the current IMessageBus
for the message injected into your handler's dependencies and have outgoing messages published deeper in your call stack -- but we strongly recommend not doing that because it can make your code and your system hard to reason about.
Instead, as much as possible, the Wolverine team recommends that these outgoing, "cascading" messages only be published from the root method like shown below:
// Using cascading messages is certainly fine
public static SecondMessage Handle(FirstMessage message, IService1 service1)
{
return new SecondMessage();
}
// This is fine too if you prefer the more explicit code model and don't mind
// the tighter coupling
public static async ValueTask<SecondMessage> Handle(FirstMessage message, IService1 service1, IMessageBus bus)
{
// Little more coupling, but some folks will prefer the more explicit style
await bus.PublishAsync(new SecondMessage());
}
public static SecondMessage? Handle(FirstMessage message, IService1 service1)
{
// Call into another service to *decide* whether or not to send
// the cascading service
if (service1.ComplicatedLogicTest(message))
{
return BuildUpComplicatedMessage();
}
return null; // no cascading message
}
This advice does not mean that you have to cascade every possible follow up step from a message handler, just to make your code easy to reason about by not hiding "side effect" actions deep in the stack trace underneath the message handler.
Consider this case as an anti-pattern to avoid:
- Your message handler method calls a method on
IService1
- Which might call a method on
IService2
- Which might call
IMessageBus.PublishAsync()
to publish a new message as part of you original message handling
In the case above, it can become very easy to lose sight of the workflow of the system, and the Wolverine team has encountered systems build using other messaging frameworks that suffered from this problem.
Keep Your Call Stacks Short
Honestly, as a follow up to the previous statement, we highly advise you to make your "call stacks" short within message handlers to help make your code easier to reason about. And by "call stack," we mean how many other different code files or types underneath the message handler will you have to jump through to really understand how a command or event message is handled? What we've found in our own development is that for whatever value layering provides for loose coupling, it can easily do even more damage for your ability to reason about and modify the code as a whole.
To be blunt, the Wolverine team is not a fan of Onion/Clean Architecture approaches with a lot of layering. Wolverine leans hard into that "A-Frame Architecture" idea as a way of creating loose coupling between technical concerns and business logic with simpler code than we think you can achieve with more typical layered, hexagonal architecture approaches.
Attaining IMessageBus
The Wolverine team recommendation is to utilize cascading messages as much as possible to publish additional messages, but when you do need to attain a reference to an IMessageBus
object, we suggest to take that service into your message handler or HTTP endpoint methods as a method argument like so:
public static async Task HandleAsync(MyMessage message, IMessageBus messageBus)
{
// handle the message..;
}
or if you really prefer the little extra ceremony of constructor injection because that's how the .NET ecosystem has worked for the past 15-20 years, do this:
public class MyMessageHandler
{
private readonly IMessageBus _messageBus;
public MyMessageHandler(IMessageBus messageBus)
{
_messageBus = messageBus;
}
public async Task HandleAsync(MyMessage message)
{
// handle the message..;
}
}
Avoid ever trying to resolve IMessageBus
at runtime through a scoped container like this:
services.AddScoped<IService>(s => {
var bus = s.GetRequiredService<IMessageBus>();
return new Service { TenantId = bus.TenantId };
});
The usage above will give you a completely different IMessageBus
object than the current MessageContext
being used by Wolverine to track behavior and state.
IoC Container Usage
TIP
Wolverine is trying really hard not to use an IoC container whatsoever at runtime. It's not going to work with Wolverine to try to pass state between the outside world into Wolverine through ASP.Net Core scoped containers for example.
Honestly, you'll get better performance and better results all the way around if you can avoid doing any kind of "opaque" service registrations in your IoC container that require runtime resolution. In effect, this means to stay away from any kind of Scoped
or Transient
Lambda registration like:
services.AddScoped<IDatabase>(s => {
var c = s.GetRequiredService<IConfiguration>();
return new Database(c.GetConnectionString("foo");
});
This might be more about preference than a hard advantage (it is a performance improvement though), but the Wolverine team recommends using method injection over the older, traditional constructor injection approach as shown below:
// Wolverine prefers this:
public static class Message1Handler
{
public static Task HandleAsync(Message1 message, IService service)
{
// Do stuff
}
}
// This certainly works with Wolverine, but it's more code and more
// runtime overhead
public class Message1Handler
{
private readonly IService1 _service1;
public Message1Handler(IService1 service1)
{
_service1 = service1;
}
public Task HandleAsync(Message1 message)
{
// Do stuff
}
}
By and large, Wolverine kind of wants you to use fewer abstractions and keep a shorter call stack. See the earlier section about "pure function" handlers for alternatives to jamming more abstracted services into an IoC container in order to create separation of concerns and testability.
Vertical Slice Architecture
TIP
Despite its name, "Vertical Slice Architecture" is really just an idea about organizing code and not what I would normally think of as a true architectural pattern. You could technically follow any kind of ports and adapter style of coding like the Clean Architecture while still organizing your code in vertical slices instead of horizontal layers.
There's nothing stopping you from using Wolverine as part of a typical Clean Architecture or Onion Architecture project layout where technical concerns are generally spread out into different projects per technical concern. Wolverine though has quite a bit of features specifically to support a "Vertical Slice Architecture" code layout, and you may be able to utilize Wolverine to create a maintainable codebase with much less complexity than the state of the art Clean/Onion layered approach.
See Low Ceremony Vertical Slice Architecture with Wolverine
Graceful Shutdown of Nodes
Hey, don't worry about this quite so much anymore. One of the big changes in Wolverine 3.0 was making Wolverine a lot more tolerant of how the application was shut down. Go forth, debug as needed, and just get things done!
It's helpful to note that Wolverine operates in Balanced
mode by default, which enables it to operate as a cluster of nodes. Ideally, the node process should be gracefully shut down to prevent failures when others attempt to communicate with it. A health check process determines stale nodes
If you are running Wolverine in a container, ensure that the orchestrator correctly sends a TERM signal and that there is enough time before it forcefully kills it. For reference, you can check Pod termination in Kubernetes.