I’ve been able to talk and write a bit about Wolverine in the last couple weeks. This builds on the previous blog posts in this list:
Wolverine on the JetBrains Webinar series — but watch out, the ICommandBus interface shown in that webinar was consolidated and changed to IMessageBus in the latest release. The rest of the syntax and the concepts are all unchanged though.
Wolverine on DotNetRocks — a conversation about Wolverine with a bonus rant from me about prescriptive, hexagonal architectures
This post is a little bonus content that I accidentally cut from the previous post.
Last time I talked about Wolverine’s support for the transactional outbox pattern for messages that just absolutely have to be delivered. About the same day that I was writing that post, I was also talking with a colleague through a very different messaging scenario where a stream of status updates were being streamed to WebSocket connected clients. In this case, the individual messages being broadcast only had temporary validity, and were quickly obsolete. There’s absolutely no need for message persistence or guaranteed delivery. There’s also no good reason to even attempt to deliver a message in this case that’s more than a few seconds old.
To that end, let’s go back yet again to the command handler for the DebitAccount command, but in this version I’m going to cascade an AccountUpdated message that would ostensibly be broadcast through WebSockets to any connected client:
[Transactional]
public static IEnumerable<object> Handle(
DebitAccount command,
Account account,
IDocumentSession session)
{
account.Balance -= command.Amount;
// This just marks the account as changed, but
// doesn't actually commit changes to the database
// yet. That actually matters as I hopefully explain
session.Store(account);
// Conditionally trigger other, cascading messages
if (account.Balance > 0 && account.Balance < account.MinimumThreshold)
{
yield return new LowBalanceDetected(account.Id);
}
else if (account.Balance < 0)
{
yield return new AccountOverdrawn(account.Id);
}
// Send out a status update message that is maybe being
// broadcast to websocket-connected clients
yield return new AccountUpdated(account.Id, account.Balance);
}
Now I need to switch to the Wolverine bootstrapping and configure some explicit routing of the AccountUpdated message. In this case, I’m going to let the WebSocket messaging of the AccountUpdated messages happen from a non-durable, local queue:
builder.Host.UseWolverine(opts =>
{
// Middleware introduced in previous posts
opts.Handlers.AddMiddlewareByMessageType(typeof(AccountLookupMiddleware));
opts.UseFluentValidation();
// Explicit routing for the AccountUpdated
// message handling. This has precedence over conventional routing
opts.PublishMessage<AccountUpdated>()
.ToLocalQueue("signalr")
// Throw the message away if it's not successfully
// delivered within 10 seconds
// THIS CONFIGURATION ITEM WAS ADDED IN v0.9.6
.DeliverWithin(10.Seconds())
// Not durable
.BufferedInMemory();
var rabbitUri = builder.Configuration.GetValue<Uri>("rabbitmq-broker-uri");
opts.UseRabbitMq(rabbitUri)
// Just do the routing off of conventions, more or less
// queue and/or exchange based on the Wolverine message type name
.UseConventionalRouting()
.ConfigureSenders(x => x.UseDurableOutbox());
});
The call to DeliverWithin(10.Seconds()) puts a rule on the local “signalr” queue that all messages published to that queue will have an effective expiration date of 10 seconds from the point at which the message was published. If the web socket publishing is backed up, or there’s a couple failure/retry cycles that delays the message, Wolverine will discard the message before it’s processed.
This option is perfect for transient status messages that have short shelf lives. Wolverine also lets you happily mix and match durable messaging and transient messages in the same message batch, as I hope is evident in the sample handler method in the first code sample.
Lastly, I used a fluent interface to apply the “deliver within” rule at the local queue level. That can also be applied at the message type level with an attribute like this alternative usage:
// The attribute directs Wolverine to send this message with
// a "deliver within 5 seconds, or discard" directive
[DeliverWithin(5)]
public record AccountUpdated(Guid AccountId, decimal Balance);
Or lastly, I can set the “deliver within” rule on a message by message basis at the time of sending the message like so:
// "messaging" is a Wolverine IMessageContext or IMessageBus service
// Do the deliver within rule on individual messages
await messaging.SendAsync(new AccountUpdated(account.Id, account.Balance),
new DeliveryOptions { DeliverWithin = 5.Seconds() });
I’ll try to sneak in one more post before mostly shutting down for Christmas and New Year’s. Next time up I’d like to talk about Wolverine’s support for grown up “clone n’ go” development through its facilities for configuring infrastructure like Postgresql or Rabbit MQ for you based on your application configuration.
I’ve been able to talk and write a bit about Wolverine in the last couple weeks. This builds on the last two blog posts in this list:
Wolverine on the JetBrains Webinar series — but watch out, the ICommandBus interface shown in that webinar was consolidated and changed to IMessageBus in the latest release. The rest of the syntax and the concepts are all unchanged though.
Wolverine on DotNetRocks — a conversation about Wolverine with a bonus rant from me about prescriptive, hexagonal architectures
Alright, back to the sample message handler from my previous two blog posts here’s the shorthand version:
[Transactional]
public static async Task Handle(
DebitAccount command,
Account account,
IDocumentSession session,
IMessageContext messaging)
{
account.Balance -= command.Amount;
// This just marks the account as changed, but
// doesn't actually commit changes to the database
// yet. That actually matters as I hopefully explain
session.Store(account);
// Conditionally trigger other, cascading messages
if (account.Balance > 0 && account.Balance < account.MinimumThreshold)
{
await messaging.SendAsync(new LowBalanceDetected(account.Id));
}
else if (account.Balance < 0)
{
await messaging.SendAsync(new AccountOverdrawn(account.Id));
// Give the customer 10 days to deal with the overdrawn account
await messaging.ScheduleAsync(new EnforceAccountOverdrawnDeadline(account.Id), 10.Days());
}
}
and just for the sake of completion, here is a longer hand, completely equivalent version of the same handler:
[Transactional]
public static async Task Handle(
DebitAccount command,
Account account,
IDocumentSession session,
IMessageContext messaging)
{
account.Balance -= command.Amount;
// This just marks the account as changed, but
// doesn't actually commit changes to the database
// yet. That actually matters as I hopefully explain
session.Store(account);
if (account.Balance > 0 && account.Balance < account.MinimumThreshold)
{
await messaging.SendAsync(new LowBalanceDetected(account.Id));
}
else if (account.Balance < 0)
{
await messaging.SendAsync(new AccountOverdrawn(account.Id));
// Give the customer 10 days to deal with the overdrawn account
await messaging.ScheduleAsync(new EnforceAccountOverdrawnDeadline(account.Id), 10.Days());
}
}
To review just a little bit, that Wolverine style message handler at runtime is committing changes to an Account in the underlying database and potentially sending out additional messages based on the state of the Account. For folks who are experienced with asynchronous messaging systems who hear me say that Wolverine does not support any kind of 2 phase commits between the database and message brokers, you’re probably already concerned with some potential problems in that code above:
Maybe the database changes fail, but there are “ghost” messages already queued that pertain to data changes that never actually happened
Maybe the messages actually manage to get through to their downstream handlers and are applied erroneously because the related database changes have not yet been applied. That’s a race condition that absolutely happens if you’re not careful (ask me how I know 😦 )
Maybe the database changes succeed, but the messages fail to be sent because of a network hiccup or who knows what problem happens with the message broker
Needless to say, there’s genuinely a lot of potential problems from those handful lines of code up above. Some of you reading this have probably already said to yourself that this calls for using some sort of transactional outbox — and Wolverine thinks so too!
The general idea of an “outbox” is to obviate the lack of true 2 phase commits by ensuring that outgoing messages are held until the database transaction is successful, then somehow guaranteeing that the messages will be sent out afterward. In the case of Wolverine and its integration with Marten, the order of operations in the message handler (in either version) shown above is to:
Tell Marten that the Account document needs to be persisted. Nothing happens at this point other than marking the document as changed
The handler creates messages that are registered with the current IMessageContext. Again, the messages do not actually go out here, instead they are routed by Wolverine to know exactly how and where they should be sent later
The Wolverine + Marten [Transactional] middleware is calling the Marten IDocumentSession.SaveChangesAsync() method that makes the changes to the Account document and also creates new database records to persist any outgoing messages in the underlying Postgresql application database in one single, native database transaction. Even better, with the Marten integration, all the database operations are even happening in one single batched database call for maximum efficiency.
When Marten successfully commits the database transaction, it tells Wolverine to “flush” the outgoing messages to the sending agents in Wolverine (depending on configuration and exact transport type, the messages might be sent “inline” or batched up with other messages to go out later).
To be clear, Wolverine also supports a transactional outbox with EF Core against either Sql Server or Postgresql. I’ll blog and/or document that soon.
The integration with Marten that’s in the WolverineFx.Marten Nuget isn’t that bad (I hope). First off, in my application bootstrapping I chain the IntegrateWithWolverine() call to the standard Marten bootstrapping like this:
using Wolverine.Marten;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddMarten(opts =>
{
// This would be from your configuration file in typical usage
opts.Connection(Servers.PostgresConnectionString);
opts.DatabaseSchemaName = "wolverine_middleware";
})
// This is the wolverine integration for the outbox/inbox,
// transactional middleware, saga persistence we don't care about
// yet
.IntegrateWithWolverine()
// Just letting Marten build out known database schema elements upfront
// Helps with Wolverine integration in development
.ApplyAllDatabaseChangesOnStartup();
For the moment, I’m going to say that all the “cascading messages” from the DebitAccount message handler are being handled by local, in memory queues. At this point — and I’d love to have feedback on the applicability or usability of this approach — each endpoint has to be explicitly enrolled into the durable outbox or inbox (for incoming, listening endpoints) mechanics. Knowing both of those things, I’m going to add a little bit of configuration to make every local queue durable:
builder.Host.UseWolverine(opts =>
{
// Middleware introduced in previous posts
opts.Handlers.AddMiddlewareByMessageType(typeof(AccountLookupMiddleware));
opts.UseFluentValidation();
// The nomenclature might be inconsistent here, but the key
// point is to make the local queues durable
opts.Policies
.AllLocalQueues(x => x.UseDurableInbox());
});
If instead I chose to publish some of the outgoing messages with Rabbit MQ to other processes (or just want the messages queued), I can add the WolverineFx.RabbitMQ Nuget and change the bootstrapping to this:
builder.Host.UseWolverine(opts =>
{
// Middleware introduced in previous posts
opts.Handlers.AddMiddlewareByMessageType(typeof(AccountLookupMiddleware));
opts.UseFluentValidation();
var rabbitUri = builder.Configuration.GetValue<Uri>("rabbitmq-broker-uri");
opts.UseRabbitMq(rabbitUri)
// Just do the routing off of conventions, more or less
// queue and/or exchange based on the Wolverine message type name
.UseConventionalRouting()
.ConfigureSenders(x => x.UseDurableOutbox());
});
I just threw a bunch of details at you all, so let me try to anticipate a couple questions you might have and also try to answer them:
Do the messages get delivered before the transaction completes? No, they’re held in memory until the transaction completes, then get sent
What happens if the message delivery fails? The Wolverine sending agents run in a hosted service within your application. When message delivery fails, the sending agent will try it again up to a configurable amount of times (100 is the default). Read the next question though before the “100” number bugs you:
What happens if the whole message broker is down? Wolverine’s sending agents have a crude circuit breaker and will stop trying to send message batches if there are too many failures in a period of time, then resume sending after a periodic “ping” message gets though. Long story short, Wolverine will buffer outgoing messages in the application database until Wolverine is able to reach the message broker.
What happens if the application process fails between the transaction succeeding and the message getting to the broker? The message will be recovered and sent by either another active node of the application if running in a cluster, or by restarting the single application process.
So you can do this in a cluster without sending the message multiple times? Yep.
What if you have zillions of stored messages and you restart the application, will it overwhelm the process and cause harm? It’s paged, distributes a bit between nodes, and there’s some back pressure to keep it from having too many outgoing messages in memory.
Can I use Sql Server instead? Yes. But for the moment, it’s like the scene in Blues Brothers when Elwood asks what kinds of music they have and the waitress replies “we have both kinds, Country and Western.”
Can I tell Wolverine to throw away a message that’s old and maybe out of date if it still hasn’t been processed? Yes, and I’ll show a bit of that in the next post.
What about messages that are routed to a non-durable endpoint as part of an outbox’d transaction? Good question! Wolverine is still holding those messages in memory until the message being processed successfully finishes, then kicks them out to in memory sending agents. Those sending agents have their own internal queues and retry loops for maximum resiliency. And actually for that matter, Wolverine has a built in in memory outbox to at least deal with ordering between the message processing and actually sending outgoing messages.
Next Time
WordPress just cut off the last section, so I’ll write a short follow up on mixing in non-durable message queues with message expirations. Next week I’ll keep on this sample application by discussing how Wolverine & its friends try really hard for a “clone n’go” developer workflow where you can be up and running mere minutes with all the database & message broker infrastructure up and going after a fresh clone of the codebase.
Yesterday I blogged about the new Wolverine alpha release with a sample that hopefully showed off how Wolverine’s different approach can lead to better developer productivity and higher performance than similar tools. Today I want to follow up on that by extending the code sample with other functionality, but then diving into how Wolverine (hopefully) makes automated unit or integration testing easier than what you may be used to.
From yesterday’s sample, I showed this small message handler for applying a debit to a bank account from an incoming message:
public static class DebitAccountHandler
{
[Transactional]
public static void Handle(DebitAccount command, Account account, IDocumentSession session)
{
account.Balance -= command.Amount;
session.Store(account);
}
}
Today let’s extend this to:
Raise an event if the balance gets below a specified threshold for the account
Or raises a different event if the balance goes negative, but also…
Sends a second “timeout” message to carry out some kind of enforcement action against the account if it is still negative by then
Here’s the new event and command messages:
public record LowBalanceDetected(Guid AccountId) : IAccountCommand;
public record AccountOverdrawn(Guid AccountId) : IAccountCommand;
// We'll change this in a little bit
public class EnforceAccountOverdrawnDeadline : IAccountCommand
{
public Guid AccountId { get; }
public EnforceAccountOverdrawnDeadline(Guid accountId)
{
AccountId = accountId;
}
}
Now, we could extend the message handler to raise the necessary events and the overdrawn enforcement command message like so:
[Transactional]
public static async Task Handle(
DebitAccount command,
Account account,
IDocumentSession session,
IMessageContext messaging)
{
account.Balance -= command.Amount;
// This just marks the account as changed, but
// doesn't actually commit changes to the database
// yet. That actually matters as I hopefully explain
session.Store(account);
if (account.Balance > 0 && account.Balance < account.MinimumThreshold)
{
await messaging.SendAsync(new LowBalanceDetected(account.Id));
}
else if (account.Balance < 0)
{
await messaging.SendAsync(new AccountOverdrawn(account.Id));
// Give the customer 10 days to deal with the overdrawn account
await messaging.ScheduleAsync(new EnforceAccountOverdrawnDeadline(account.Id), 10.Days());
}
}
And just to add a little more context, here’s part of what the message handler for the EnforceAccountOverdrawnDeadlinecould look like:
public static void Handle(EnforceAccountOverdrawnDeadline command, Account account)
{
// Don't do anything if the balance has been corrected
if (account.Balance >= 0) return;
// Dunno, send in the goons? Report them to a credit agency? Guessing
// nothing pleasant happens here
}
Alrighty then, back to the new version of the message handler that raises extra event messages depending on the state of the account. You’ll notice that I used method injection to pass in the Wolverine IMessageContext for the current message being handled. That gives me access to spawn additional messages and even schedule the execution of a command for a later time. You should notice that I now had to make the handler method asynchronous as the various SendAsync() calls return ValueTask, so it’s a little uglier now. Don’t worry, we’re going to come back to that, so don’t settle for this quite yet.
I’m going to leave this for the next post, but if you’re experienced with asynchronous messaging you’re screaming that there’s a potential race condition or risk of phantom data or messages between the extra messages going out and the Account being committed. Tomorrow I’ll discuss how Wolverine’s transactional outbox support removes those very real, very common problems in asynchronous message processing.
So let’s jump into what a unit test could look like for the message handler for the DebitAccount method. To start with, I’ll use Wolverine’s built in TestMessageContext to act as a “spy” on the method. A couple tests might look like this using my typical testing stack of xUnit.Net, Shouldly, and NSubstitute:
public class when_the_account_is_overdrawn : IAsyncLifetime
{
private readonly Account theAccount = new Account
{
Balance = 1000,
MinimumThreshold = 100,
Id = Guid.NewGuid()
};
private readonly TestMessageContext theContext = new TestMessageContext();
// I happen to like NSubstitute for mocking or dynamic stubs
private readonly IDocumentSession theDocumentSession = Substitute.For<IDocumentSession>();
public async Task InitializeAsync()
{
var command = new DebitAccount(theAccount.Id, 1200);
await DebitAccountHandler.Handle(command, theAccount, theDocumentSession, theContext);
}
[Fact]
public void the_account_balance_should_be_negative()
{
theAccount.Balance.ShouldBe(-200);
}
[Fact]
public void raises_an_account_overdrawn_message()
{
// ShouldHaveMessageOfType() is an extension method in
// Wolverine itself to facilitate unit testing assertions like this
theContext.Sent.ShouldHaveMessageOfType<AccountOverdrawn>()
.AccountId.ShouldBe(theAccount.Id);
}
[Fact]
public void raises_an_overdrawn_deadline_message_in_10_days()
{
var scheduledTime = theContext.ScheduledMessages()
// Also an extension method in Wolverine for testing
.ShouldHaveEnvelopeForMessageType<EnforceAccountOverdrawnDeadline>()
.ScheduledTime;
// Um, do something to verify that the scheduled time is 10 days from this moment
// and also:
// https://github.com/JasperFx/wolverine/issues/110
}
public Task DisposeAsync()
{
return Task.CompletedTask;
}
}
It’s not horrendous, and I’ve seen much, much worse in real life code. All the same though, let’s aim for easier code to test by removing more infrastructure code and trying to get to purely synchronous code. To get there, I’m first going to start with the EnforceAccountOverdrawnDeadline message type and change it slightly to this:
// I'm hard coding the delay time for execution, just
// go with that for now please:)
public record EnforceAccountOverdrawnDeadline(Guid AccountId) : TimeoutMessage(10.Days()), IAccountCommand;
And now back the the Handle(DebitAccount) handler, we’ll use Wolverine’s concept of cascading messages to simplify the handler and make it completely synchronous:
[Transactional]
public static IEnumerable<object> Handle(
DebitAccount command,
Account account,
IDocumentSession session)
{
account.Balance -= command.Amount;
// This just marks the account as changed, but
// doesn't actually commit changes to the database
// yet. That actually matters as I hopefully explain
session.Store(account);
if (account.Balance > 0 && account.Balance < account.MinimumThreshold)
{
yield return new LowBalanceDetected(account.Id);
}
else if (account.Balance < 0)
{
yield return new AccountOverdrawn(account.Id);
// Give the customer 10 days to deal with the overdrawn account
yield return new EnforceAccountOverdrawnDeadline(account.Id);
}
}
Now, we’re able to mostly use state-based testing, eliminate the fake IMessageContext, and work with strictly synchronous code. Here’s the new version of the test class from before:
public class when_the_account_is_overdrawn
{
private readonly Account theAccount = new Account
{
Balance = 1000,
MinimumThreshold = 100,
Id = Guid.NewGuid()
};
// I happen to like NSubstitute for mocking or dynamic stubs
private readonly IDocumentSession theDocumentSession = Substitute.For<IDocumentSession>();
private readonly object[] theOutboundMessages;
public when_the_account_is_overdrawn()
{
var command = new DebitAccount(theAccount.Id, 1200);
theOutboundMessages = DebitAccountHandler.Handle(command, theAccount, theDocumentSession)
.ToArray();
}
[Fact]
public void the_account_balance_should_be_negative()
{
theAccount.Balance.ShouldBe(-200);
}
[Fact]
public void raises_an_account_overdrawn_message()
{
// ShouldHaveMessageOfType() is an extension method in
// Wolverine itself to facilitate unit testing assertions like this
theOutboundMessages.ShouldHaveMessageOfType<AccountOverdrawn>()
.AccountId.ShouldBe(theAccount.Id);
}
[Fact]
public void raises_an_overdrawn_deadline_message_in_10_days()
{
var scheduledTime = theOutboundMessages
// Also an extension method in Wolverine for testing
.ShouldHaveEnvelopeForMessageType<EnforceAccountOverdrawnDeadline>();
}
[Fact]
public void should_not_raise_account_balance_low_event()
{
theOutboundMessages.ShouldHaveNoMessageOfType<LowBalanceDetected>();
}
}
The second version of both the handler method and the accompanying unit test is arguably simpler because:
We were able to make the handler method synchronous which helpfully removes some boilerplate code, which is especially helpful if you use xUnit.Net because that allows us to eschew the IAsyncLifetime thing.
Except for verifying that the account data was stored, all of the unit test code is now using state-based testing, which is generally easier to understand and write than interaction-based tests that necessarily depend on mock objects
Wolverine in general also made the handler method easier to test through the middleware I introduced in my previous post that “pushes” in the Account data to the handler method instead of making you jump through data access code and potential mock/stub object setup to inject the data inputs.
At the end of the day, I think that Wolverine not only does quite a bit to simplify your actual application code by doing more to isolate business functionality away from infrastructure, Wolverine also leads to more easily testable code for effective Test Driven Development.
But what about……….?
I meant to also show Wolverine’s built in integration testing support, but to be honest, I’m about to meet a friend for lunch and I’ve gotta wrap this up in the next 10 minutes. In subsequent posts I’m going to stick with this example and extend that into integration testing across the original message and into the cascading messages. I’ll also get into the very important details about Wolverine’s transactional outbox support.
TL;DR — Wolverine’s runtime model is significantly different than other tools with similar functionality in the .NET world in a way that leads to simpler application code and more efficient runtime execution.
I was able to push a new version of Wolverine today based on the newly streamlined API worked out in this GitHub issue. Big thanks to Oskar, Eric, and Blake for their help in coming to what I feel turned out be a great improvement in usability — even though I took some convincing to get there. Also some huge thanks to Babu for the website scaffolding and publishing, and Khalid for all his graphics help and general encouragement.
The Wolverine docs — such as they are — are up on the Wolverine website.
In a nutshell, Wolverine is a mediator and message bus tool. There’s plenty of those tools already in the .NET space, so let me drop right into how Wolverine’s execution pipeline is genuinely unique and potentially does much more than older tools to improve developer productivity.
I’m going to build a very crude banking service that includes a message endpoint that will need to:
Accept a message to apply a debit to a given account
Verify that the debit amount is non-zero before you do anything else
Load the information for the designated account from the database
Apply the debit to the current balance of the account
Persist the changes in balance back to the database
I’ll introduce “cascaded” messages tomorrow for things business rules like an account reaching a low balance or being overdrawn, but I’m ignoring that today to make this a smaller post.
While Wolverine supports EF Core and SQL Server as well, I unsurprisingly want to use Marten as a lower ceremony approach to application persistence in this particular case.
Before I even try to write the message handler, let me skip a couple design steps and say that I’m going to utilize three different sets of middleware to deal with cross cutting concerns:
I’m going to use Wolverine’s built in Fluent Validation middleware to apply any known validation rules for the incoming messages. I’m honestly not sure I’d use this in real life, but this was built out and demo’d today as a way to demonstrate what’s “special” about Wolverine’s runtime architecture.
Custom middleware to load and push account data related to the incoming message to the message handler, or log and abort the message processing when the account data referenced in the message does not exist — and this more than anything is where the example code will show off Wolverine’s different approach. This example came directly from a common use case in a huge system at my work that uses NServiceBus.
Keeping in mind that we’ll be using some Wolverine middleware in a second, here’s the simple message handler to implement exactly the numbered list above:
public static class DebitAccountHandler
{
// This explicitly adds the transactional middleware
// The Fluent Validation middleware is applied because there's a validator
// The Account argument is passed in by the AccountLookupMiddleware middleware
[Transactional] // This could be done w/ a policy, but I'm opting to do this explicitly here
public static void Handle(
// The actual command message
DebitAccount command,
// The current data for the account stored in the database
// This will be "pushed" in by middleware
Account account,
// The Marten document session service scoped to the
// current message being handled.
// Wolverine supports method injection similar to ASP.NET minimal api
IDocumentSession session)
{
// decrement the balance
account.Balance -= command.Amount;
// Just telling Marten that this account document changed
// so that it can be persisted by the middleware
session.Store(account);
}
}
I would argue that that handler method is very easy to understand. By removing away so many infrastructure concerns, a developer is able to mostly focus on business logic in isolation even without having to introduce all the baggage of some sort of hexagonal architecture style. Moreover, using Wolverine’s middleware allowed me to write purely synchronous code, which also reduces the code noise. Finally, but being able to “push” the business entity state into the method, I’m much more able to quickly write unit tests for the code and do TDD as I work.
Every message processing tool has middleware strategies for validation or transaction handling, but let’s take the example of the account data instead. When reviewing a very large system at work that uses NServiceBus, I noticed a common pattern of needing to load an entity from the database related to the incoming message and aborting the message handling if the entity does not exist. It’s an obvious opportunity for using middleware to eliminate the duplicated code.
First off, we need some way to “know” what the account id is for the incoming message. In this case I chose to use a marker interface just because that’s easy:
public interface IAccountCommand
{
Guid AccountId { get; }
}
And the DebitAccountCommand becomes:
public record DebitAccount(Guid AccountId, decimal Amount) : IAccountCommand;
The actual middleware implementation is this:
// This is *a* way to build middleware in Wolverine by basically just
// writing functions/methods. There's a naming convention that
// looks for Before/BeforeAsync or After/AfterAsync
public static class AccountLookupMiddleware
{
// The message *has* to be first in the parameter list
// Before or BeforeAsync tells Wolverine this method should be called before the actual action
public static async Task<(HandlerContinuation, Account?)> BeforeAsync(
IAccountCommand command,
ILogger logger,
IDocumentSession session,
CancellationToken cancellation)
{
var account = await session.LoadAsync<Account>(command.AccountId, cancellation);
if (account == null)
{
logger.LogInformation("Unable to find an account for {AccountId}, aborting the requested operation", command.AccountId);
}
return (account == null ? HandlerContinuation.Stop : HandlerContinuation.Continue, account);
}
}
There’s also a Fluent Validation validator for the command as well (again, not sure I’d actually do it this way myself, but it shows off Wolverine’s middleware capabilities):
public class DebitAccountValidator : AbstractValidator<DebitAccount>
{
public DebitAccountValidator()
{
RuleFor(x => x.Amount).GreaterThan(0);
}
}
Stepping back to the actual application, I first added the WolverineFx.Marten Nuget reference to a brand new ASP.Net Core web api application, and made the following Program file to bootstrap the application:
using AppWithMiddleware;
using IntegrationTests;
using Marten;
using Oakton;
using Wolverine;
using Wolverine.FluentValidation;
using Wolverine.Marten;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddMarten(opts =>
{
// This would be from your configuration file in typical usage
opts.Connection(Servers.PostgresConnectionString);
opts.DatabaseSchemaName = "wolverine_middleware";
}).IntegrateWithWolverine()
// Just letting Marten build out known database schema elements upfront
.ApplyAllDatabaseChangesOnStartup();
builder.Host.UseWolverine(opts =>
{
// Custom middleware to load and pass account data into message
// handlers
opts.Handlers.AddMiddlewareByMessageType(typeof(AccountLookupMiddleware));
// This will register all the Fluent Validation validators, and
// apply validation middleware where the command type has
// a validator
opts.UseFluentValidation();
});
var app = builder.Build();
// One Minimal API that just delegates directly to Wolverine
app.MapPost("/accounts/debit", (DebitAccount command, IMessageBus bus) => bus.InvokeAsync(command));
return await app.RunOaktonCommands(args);
After all of those pieces are put together, let’s finally talk about how Wolverine’s runtime execution is really different. Wolverine’s “special sauce” is that instead of forcing you to write your code wrapped around the framework, Wolverine conforms to your application code by generating code at runtime (don’t worry, it can be done ahead of time as well to minimize cold start time).
For example, here’s the runtime code that’s generated for the DebitAccountHandler.Handle() method:
// <auto-generated/>
#pragma warning disable
using FluentValidation;
using Microsoft.Extensions.Logging;
using Wolverine.FluentValidation;
using Wolverine.Marten.Publishing;
namespace Internal.Generated.WolverineHandlers
{
// START: DebitAccountHandler1928499868
public class DebitAccountHandler1928499868 : Wolverine.Runtime.Handlers.MessageHandler
{
private readonly FluentValidation.IValidator<AppWithMiddleware.DebitAccount> _validator;
private readonly Wolverine.Marten.Publishing.OutboxedSessionFactory _outboxedSessionFactory;
private readonly Wolverine.FluentValidation.IFailureAction<AppWithMiddleware.DebitAccount> _failureAction;
private readonly Microsoft.Extensions.Logging.ILogger<AppWithMiddleware.DebitAccount> _logger;
public DebitAccountHandler1928499868(FluentValidation.IValidator<AppWithMiddleware.DebitAccount> validator, Wolverine.Marten.Publishing.OutboxedSessionFactory outboxedSessionFactory, Wolverine.FluentValidation.IFailureAction<AppWithMiddleware.DebitAccount> failureAction, Microsoft.Extensions.Logging.ILogger<AppWithMiddleware.DebitAccount> logger)
{
_validator = validator;
_outboxedSessionFactory = outboxedSessionFactory;
_failureAction = failureAction;
_logger = logger;
}
public override async System.Threading.Tasks.Task HandleAsync(Wolverine.Runtime.MessageContext context, System.Threading.CancellationToken cancellation)
{
await using var documentSession = _outboxedSessionFactory.OpenSession(context);
var debitAccount = (AppWithMiddleware.DebitAccount)context.Envelope.Message;
(var handlerContinuation, var account) = await AppWithMiddleware.AccountLookupMiddleware.BeforeAsync((AppWithMiddleware.IAccountCommand)context.Envelope.Message, ((Microsoft.Extensions.Logging.ILogger)_logger), documentSession, cancellation).ConfigureAwait(false);
if (handlerContinuation == Wolverine.HandlerContinuation.Stop) return;
Wolverine.FluentValidation.Internals.FluentValidationExecutor.ExecuteOne<AppWithMiddleware.DebitAccount>(_validator, _failureAction, debitAccount);
AppWithMiddleware.DebitAccountHandler.Handle(debitAccount, account, documentSession);
// Commit the unit of work
await documentSession.SaveChangesAsync(cancellation).ConfigureAwait(false);
}
}
// END: DebitAccountHandler1928499868
}
It’s auto-generated code, so it’s admittedly ugly as sin, but there’s some things I’d like you to notice or just no:
This class is only instantiated one single time in the application and held in memory for the rest of the application instance’s lifecycle
That handler code is managing service lifecycle and service disposal, but yet there’s no IoC tool anywhere in sight
The code only has to create and resolve the Fluent Validation validator one single time, and it’s inlined access the complete rest of the way
The code up above minimizes the number of objects allocated per message being handled compared to other tools that utilize some kind of Russian Doll middleware model by dodging the need for framework adapter objects that are created and destroyed for each message execution. Less garbage collector thrashing from fewer object allocations means better performance and scalability
The Fluent Validation middleware wouldn’t even apply to message types that don’t have any known validators, and can optimize itself. Contrast that with Fluent Validation middleware strategies with something like MediatR where it would create a decorator object for each request and loop through an empty enumerable even when there are not validator for a given message type. That’s not a lot of overhead per se, but that adds up when there’s a lot of framework cruft.
You might have to take my word for it, but having built other frameworks and having spent a long time poring over the internals of other similar frameworks, Wolverine is going to do a lot fewer object allocations, indirections, and dictionary lookups at runtime than other tools with similar capabilities
I’m following this up immediately tomorrow by adding some “cascading” messages and diving into the built in testing support within Wolverine.
The fine folks at DotNetRocks graciously allowed me to come on and talk about Wolverine and its combination with Marten into the new “Critter Stack” for highly productive server side development in .NET.
Wolverine is a new framework (but based on previous tools dating back over a decade) for server side .NET development that acts as both an in-process mediator tool and message bus for asynchronous messaging between processes. In an admittedly crowded field, Wolverine stands apart from older tools in a couple important ways:
While Wolverine will happily take care of infrastructure concerns like error handling, logging, distributed tracing with OpenTelemetry, serialization, performance metrics, and interacting directly with message brokers, Wolverine does a much better job of keeping out of your application code
Wolverine’s runtime model — including its robust middleware strategy — completely bypasses the performance problems the older tools incur
Application code testability is a first class goal with Wolverine, and it shows. Unit testing is certainly easier with Wolverine keeping more infrastructure concerns out of your code, while also adding some unique test automation support for integration testing
Developer productivity is enhanced by aiming for baking in infrastructure setup and configuration directly into Wolverine. And because some of Wolverine’s productivity boost admittedly comes from coding convention magic, Wolverine can tell you exactly what it’s going to do at runtime through its built in diagnostics.
The usage is already going to change next week based on a lot of early user feedback, but you can easily get the gist of what Wolverine is like in usage through the JetBrains webinar on Wolverine a couple weeks back:
Alba is a small library that enables easy integration testing of ASP.Net Core routes completely in process within an NUnit/xUnit.Net/MSTest project. Alba 7.1 just dropped today with .NET 7 support, improved JSON handling for Minimal API endpoints, and multipart form support.
Quickstart with Minimal API
Keeping things almost absurdly simple, let’s say that you have a Minimal API route (taken from the Alba tests) like so:
app.MapPost("/go", (PostedMessage input) => new OutputMessage(input.Id));
Now, over in your testing project, you could write a crude test for the route above like so:
[Fact]
public async Task sample_test()
{
// This line only matters if you use Oakton for the command line
// processing
OaktonEnvironment.AutoStartHost = true;
// I'm doing this inline to make the sample easier to understand,
// but you'd want to share the AlbaHost between tests because
// this is expensive
await using var host = await AlbaHost.For<MinimalApiWithOakton.Program>();
var guid = Guid.NewGuid();
var result = await _host.PostJson(new PostedMessage(guid), "/go")
.Receive<OutputMessage>();
result.Id.ShouldBe(guid);
}
A couple notes about the code above:
The test is bootstrapping your actual application using its configuration, but using the TestServer in place of Kestrel as the web server.
The call to PostJson() is using the application’s JSON serialization configuration, just in case you’ve customized the JSON serialization. Likewise, the call to Receive<T>() is also using the application’s JSON serialization mechanism to be consistent. This functionality was improved in Alba 7 to “know” whether to use MVC Core or Minimal API style JSON serialization (but you can explicitly override that in mixed applications on a case by case basis)
When the test executes, it’s running through your entire application’s ASP.Net Core pipeline including any and all registered middleware
If you choose to use Alba with >= .NET 6 style application bootstrapping inside of an inferred Program.Main() method, be aware that you will need to grant your test project visibility to the internals of your main project something like this:
I think most people by now are somewhat familiar with the testing pyramid idea (or testing trophy or any other number of shapes). Just to review, it’s the idea that a software system is best served by being backed by a mix of automated tests between solitary unit tests, intermediate integration tests, and some number of end to end, black box tests.
We can debate what the exact composition of your test pyramid should be on a particular project until the cows come home. For my part, I want more fast running, easier to write tests and fewer potentially nasty Selenium/Playwright/Cypress.io tests that tend towards being slow and brittle. I like Alba in particular because it allows our teams at work to test at the HTTP web service layer through to the database completely within process — meaning the tests can be executed on demand without any kind of deployment. In short, Alba sits in the middle of the pyramid graphic above and makes those very valuable kind of tests easier to write, execute, and debug for the developers working on the system.
Marten was conceived and launched way back in 2016 as an attempt to quickly improve the performance and stability of a mission critical web application by utilizing Postgresql and its new JSON capabilities as a replacement for a 3rd party document database – and do that in a hurry before the next busy season. My former colleagues and I did succeed in that endeavor, but more importantly for the longer run, Marten was also launched as an open source project on GitHub and quickly attracted attention from other developers. The addition of an originally small feature set for event sourcing dramatically increased interest and participation in Marten.
Fast forward to today, and we have a vibrant community of engaged users and a core team of contributors that are constantly improving the tool and discussing ideas about how to make it even better. The giant V4 release last year brought an overhaul of almost all the library internals and plenty of new capabilities. V5 followed early in 2022 with more multi-tenancy options and better tooling for development lifecycles and database management based on early issues with V4.
At this point, I’d list the strong points of Marten that we’ve already achieved as:
A very useful document database option that provides the powerful developer productivity you expect from NoSQL solutions while also supporting a strong consistency model that’s usually missing from NoSQL databases.
A wide range of viable hosting options by virtue of being on top of Postgresql. No cloud vendor lock-in with Marten!
Quite possibly the easiest way to build an application using Event Sourcing in .NET with both event storage and user defined view projections in the box
A great local development story through the simple ability to run Postgresql in a Docker container and Marten’s focus on an “it just works” style database schema management subsystem
The aforementioned core team and active user base makes Marten a viable OSS tool for teams wanting some reassurance that Marten is going to be well supported in the future
Great! But now it’s time to talk about the next steps we’re planning to take Marten to even greater heights in the forthcoming Marten V6 that’s being planned now. The overarching theme is to remove the most common hurdles for not choosing Marten. By and large, I think the biggest themes for Marten are:
Scalability, so Marten can be used for much larger data sets. From user feedback, Marten is able to handle data sets of 10 million events today, but there’s opportunities to go far, far larger than that.
Improvements to operational support. Database migrations when documents change, rebuilding projections without downtime, usage metrics, and better support for using multiple databases for multi-tenancy
Marten is in good shape as a purely storage option for Event Sourcing, but users are very often asking for an array of subscription options to propagate events captured by Marten
More powerful options for aggregating event data into more complex projected views
Improving the Linq and other querying support is a seemingly never-ending battle
The lack of professional support for Marten. Obviously a lot of shops and teams are perfectly comfortable with using FOSS tools knowing that they may have to roll up their sleeves and pitch in with support, but other shops are not comfortable with this at all and will not allow FOSS usage for critical functions. More on this later.
First though, Marten is getting a new “critter” friend in the larger JasperFx project family:
Wolverine is a new/old OSS command bus and messaging tool for .NET. It’s what was formerly being developed as Jasper, but the Marten team decided to rebrand the tool as a natural partner with Marten (both animals plus Weasel are members of the Mustelidae family). While both Marten and Wolverine are happily usable without each other, we think that the integration of these tools gives us the opportunity to build a full fledged platform for building applications in .NET using a CQRS architecture with Event Sourcing. Moreover, we think there’s a significant gap in .NET for this kind of tooling and we hope to fill that.
So, onto future plans…
There’s a couple immediate ways to improve the scalability of Marten we’re planning to build in Marten V6. The first idea is to utilize Postgresql table sharding in a couple different ways.
First, we can enable sharding on document tables based on user defined criteria through Marten configuration. The big challenge there is to provide a good migration strategy for doing this as it requires at least a 3 step process of copying the existing table data off to the side before creating the new tables.
The next idea is to shard the event storage tables as well, with the immediate idea being to shard off of archived status to effectively create a “hot” storage of recent events and a “cold” storage of older events that are much less frequently accessed. This would allow Marten users to keep the active “hot” event storage to a much smaller size and therefore greatly improve potential performance even as the database continues to grow.
We’re not done “sharding” yet, but this time we need to shift to the asynchronous projection support in Marten. The core team has some ideas to improve the throughput of the asynchronous projection code as it is, but today it’s limited to only running on one single application node with “hot/cold” rollover support. With some help from Wolverine, we’re hoping to build a “sharded” asynchronous projection that can shard the processing of single projections and distribute the projection work across potentially many nodes as shown in the following diagram:
The asynchronous projection sharding is going to be a big deal for Marten all by itself, but there’s some other potentially big wins for Marten V6 with better tooling for projection rebuilds and asynchronous projections in general:
Some kind of user interface to monitor and manage the asynchronous projections
Faster projection rebuilds
Zero downtime projection rebuilds
Marten + Wolverine == “Critter Stack”
Again, both Marten and Wolverine will be completely usable independently, but we think there’s some potential synergy through the combination. One of the potential advantages of combining the tools is to use Wolverine’s messaging to give Marten a full fledged subscription model for Marten events. All told we’re planning three different mechanisms for propagating Marten events to the rest of your system:
Through Wolverine’s transactional outbox right at the point of event capture when you care more about immediate delivery than strict ordering (this is already working)
Through Martens asynchronous daemon when you do need strict ordering
If this works out, through CDC event streaming straight from the database to Kafka/Pulsar/Kinesis
That brings me to the last topic I wanted to talk about in this post. Marten and Wolverine in their current form will remain FOSS under the MIT license, but it’s past time to make a real business out of these tools.
I don’t know how this is exactly going to work out yet, but the core Marten team is actively planning on building a business around Marten and now Wolverine. I’m not sure if this will be the front company, but I personally have formed a new company named “Jasper Fx Software” for my own activity – but that’s going to be limited to just being side work for at least awhile.
The general idea – so far – is to offer:
Support contracts for Marten
Consulting services, especially for help modeling and maximizing the usage of the event sourcing support
Training workshops
Add on products that add the advanced features I described earlier in this post
Maybe success leads us to offering a SaaS model for Marten, but I see that as a long way down the road.
What think you gentle reader? Does any of this sound attractive? Should we be focusing on something else altogether?
I wrote a lot about Test Driven Development back in the days of the now defunct CodeBetter site. You can read a little of the old precursor content from this old MSDN Magazine article I wrote in 2008.As time permits or my ambition level waxes and wanes, I’ll be resurrecting and rewriting some of my old “Shade Tree Developer” content on team dynamics, design fundamentals, and Agile software practices from those days.This is just a preface to a new blog series on my thinking about how to effectively do TDD in your daily coding work.
I’m giving an internal talk at work this week about applying Test Driven Development (TDD) within one of our largest systems. Our developers today certainly build tests for new code today with a mix of unit tests and integration tests, but there’s room for improvement to help our developers do more effective unit testing with less effort and end up with more useful tests.
That being said, it’s not all that helpful to just yell at your developers and tell them they should “just” write more or better tests or say that they should “just do TDD.” So instead of yelling, let’s talk through some possible strategies and mental tools for applying TDD in real world code. But first, here’s a quick rundown of…
What we don’t want:
Tests that require a lot of setup code just to establish inputs. Not only does that keep developers from being productive when writing tests, it’s a clear sign that you may have harmful coupling problems within your code structure.
Tests that only duplicate the implementation of the code under test. This frequently happens from overusing mock objects. Tests written this way are often brittle when the actual code needs to be refactored, and can even serve to prevent developers from trying to make code improvements through refactoring. These tests are also commonly caused by attempts to “shut up the code coverage check” in CI with tests retrofitted onto existing code.
Tests that “blink,” meaning that they do not consistently pass or fail even if the actual functionality is correct. This is all too painfully common with integration tests that deal with asynchronous code. Selenium tests are notoriously bad for this.
Slow feedback cycles between writing code and knowing whether or not that code actually works
Developers needing to spend a lot of time in the debugger trying to trace down problems in the code.
Instead, let’s talk about…
What we do want:
Fast feedback cycles for development. It’s hard to overstate how important that is for developers to be productive.
Developers to be able to efficiently use their time while constantly switching between writing tests and the code to make those tests pass
The tests are fine-grained enough to allow our developers to find and remove problems in the code
The existing tests are useful for refactoring. Or at least not a significant cause of friction when trying to refactor code.
Test tests clearly express the intent of the code and act as a form of documentation.
The code should generally exhibit useful qualities of cohesion and coupling between various pieces of code
And more than anything, I would like developers to be able to use TDD to help them think through their code as they build it. TDD is a couple things, but the most important two things to me are as a heuristic to think through code structure and as a rapid feedback cycle. Having the tests around later to facilitate safe refactoring in the codebase is important too, especially if you’re going to be working on a codebase for years that’s likely going to outgrow its original purpose.
So what’s next?
I’ve already started working on the actual content of how to do TDD with examples mostly pulled from my open source projects. Right now, I’m thinking about writing over the next couple months about:
Using responsibility driven design as a way to structure code in a way that’s conducive to easier unit testing
Some real world examples of building open source features with TDD
My old “Jeremy’s Rules of TDD” which really just amount to some heuristics for improving the properties of cohesion or coupling in your code based on testability. I’m going to supplement that by stealing from Jim Shore’s excellent book on Testing without Mocks
A discussion of state-based vs interaction based testing and when you would choose either
Switching between top down code construction or bottom up coding using TDD
What code deserves a test, and what could you let slide without?
Choosing between solitary unit tests, sociable unit tests, or pulling in infrastructure to write integration tests on a case by case basis
Dealing with data intensive testing. Kind of a big deal working for a company whose raison d’etre is data analytics
This is the second part of a 3 or 4 part series where I’m formulating my thoughts about an ongoing initiative at MedeAnalytics. I started yesterday with a related post called On Giving Technical Guidance to Others that’s a synopsis of an impromptu lecture I game our architecture team about all the things I wish I’d known before becoming any kind of technical leader. I’ll follow this post up hopefully as soon as tomorrow with my reasoning about why prescriptive architectures are harmful and my own spin on the SOLID Principles.
I’m part of an architectural team that’s been charged with modernizing and improving our very large, existing systems. We have an initiative just getting off the ground to break off part of one of our monoliths into a separate system to begin a strangler application strategy to modernize the system over time. This gives us a chance to work out how we want our systems to be structured and built going forward in a smaller subset of work instead of trying to boil the ocean to update the entire monolith codebase at one time.
As part of that effort, I’m trying to put some stakes in the ground to:
Ban all usage of formal, prescriptive architectural styles like the Onion Architecture or Clean Architecture because I find that they do more harm than good. Rather, I’m pushing hard for vertical slice or feature folder code organization while still satisfying the need for separation of concerns and decent structuring of the code
Generally choose lower code ceremony approaches whenever possible because that promotes easier evolution of the code, and in the end, the only truly guaranteed path to good code is adaptation and evolution in the face of feedback about the code.
Be very cautious about how we abstract database access to avoid causing unnecessary complexity or poor performance, which means I probably want to ban any usage of naive IRepository<T> type abstractions
Put the SOLID Principles into a little bit of perspective as we do this work and make sure our developers and architects have a wider range of mental tools in their design toolbox than just an easy to remember but hard to interpret or apply acronym developed by C++ developers before many of our developers were even born
The rest of this post is just trying to support those opinions.
Let’s talk a little bit about the qualities you want in your code. Quite a few folks are going to say that the most important quality is that the code satisfies the business needs and delivers value to the business! If you’ll please get that bit of self righteousness out of your system, let’s move on to the kind of technical quality that’s necessary to continue to efficiently deliver business value over time.
You can understand what the code is doing, navigate within the codebase, and generally find code where you would expect it to be based on the evident and documented rules of the system architecture.
The code exhibits separation of concerns, meaning that you’re generally able to reason about and change one responsibility of the code at a time (data access, business logic, validation logic, data presentation, etc.). Cohesion and coupling are the alpha and omega of software design. I’m a very strong believer in evolutionary approaches to designing software as the only truly reliable method to arrive at good code, but that’s largely dependent upon the qualities of cohesion and coupling within your code.
Rapid feedback is vital to effective coding, so testability of the code is a major factor for me. This can mean that code is structured in a way that it’s easy to unit test in isolation (i.e., you can effectively test business rules without having to run the full stack application or in one hurtful extreme, be forced to use a tool like Selenium). This version of testability is very largely a restatement of cohesion and coupling. Alternatively, if the code depends on some kind of infrastructure that’s easy to deal with in integration testing (like Marten!) and the integration tests run “fast enough,” I say you can relax separation of concerns and jumble things together as long as the code is still easy to reason about.
I don’t know a pithy way to describe this, but the code needs to carefully expose the places where system state is changed or “mutated” to make the code’s behavior predictable and prevent bugs. Whether that’s adopting command query segregation, using elements of functional programming, or the uni-directional data flow in place of two way data binding in user interface development, system state changes are an awfully easy way to introduce bugs in code and should be dealt with consciously and with some care.
I think most of us would say that code should be “simple,” and I’d add that I personally want code to be written in a low ceremony way that reduces noise in the code. The problem with that whole statement is that it’s very subjective:
The killer problem sometimes being that one dev's well engineered code is another dev's over-complicated code. And one dev's "clean" approach is another dev's "too much magic" https://t.co/f4dsIybX3t
Which is just to say that saying the words “just write simple code!” isn’t necessarily all that helpful or descriptive. What’s helpful is to have some mental tools to help developers judge whether or not their code is “good” and move in the direction of more successful code. Bet yet, do that without introducing unnecessary complexity or code ceremony through well-intentioned prescriptive architectures like “Onion” or “Clean” that purposely try to force developers to write code “the right way.”
And next time on Jeremy tries to explain his twitter ranting…
This has inevitably taken longer than I wished to write, so I’m breaking things up. I will follow up tomorrow and Thursday with my analysis of SOLID, an explanation of why I think the Onion/Clean Architecture style of code organization is best avoided, and eventually some thoughts on database abstractions.
I’m still working on my promised SOLID/Anti-Onion/Anti-Clean/Database Abstraction post, but it’s as usual taking longer than I’d like and I’m publishing this section separately.
Just as a quirk of circumstances, I pretty well went straight from being a self-taught “Shadow IT” developer to being a lead developer and de facto architect on a mission critical supply chain application for a then Fortune 500 company. The system was an undeniable success in the short term, but it came at a cost to me because as a first time lead I had zero ability to enable the other developers working with me to be productive. As such, I ended up writing the mass majority of the code and inevitably became the bottleneck on all subsequent production issues. That doesn’t scale.
The following year I had another chance to lead a follow up project and vowed to do a better job with the other developers (plus I was getting a lot of heat from various management types to do so). In a particular case that I remember to this day, I wrote up a detailed Word document for a coding assignment for another developer. I got all the way down to class and method names and even had some loose sample code I think. I handed that off, patted myself on the back for being a better lead, and went off on my merry way.
As you might have surmised, when I got his code back later it was unusable because he did exactly what I said to do — which turned out to be wrong based on factors I hadn’t anticipated. Worse, he only did exactly what I said to do and missed some concerns that I didn’t think needed to be explicitly called out. I’ve thought a lot about this over the years and come to some conclusions about how I should have tried to work differently with that developer. Before diving into that, let’s first talk about you for awhile!
Congratulations! You’ve made it to some kind of senior technical role in your company. You’ve attained enough skill and knowledge to be recognized for your individual contributions, and now your company is putting you in a position to positively influence other developers, determine technical strategies, and serve as a steward for your company’s systems.
Hopefully you’ll still be hands on in the coding and testing, but increasingly, your role is going to involve trying to create and evolve technical guidance for other developers within your systems. More and more, your success is going to be dependent on your ability to explain ideas, concepts, and approaches to other developers. Not that I’m the fount of all wisdom about this, but here’s some of the things I wish I’d understood before being put into technical leadership roles:
It’s crucial to provide the context, reasoning, and applicability behind any technical guidance. Explaining why or when are we doing this is just as important as the “what” or “how.”
Being too specific in the guidance or instructions to another developer can easily come with the unintended consequence of turning off their brains and will frequently lead to poor results. Expanding on my first point, it’s better to explain the goals, how their work fits into the larger system, and the qualities of the code you’re hoping to achieve rather than try to make them automatons just following directions. It’s quite possible that JIRA-driven development exacerbates this potential problem.
You need to provide some kind of off-ramp to developers to understand the limitations of the guidance. The last thing you want is for developers to blindly follow guidance that is inappropriate for a circumstance that wasn’t anticipated during the formulation of said guidance
Recommendations about technology usage probably needs to come as some kind of decision tree with multiple options to its applicability because there’s just about never a one size fits all tool
By all means, allow and encourage the actual developers to actively look for better approaches because they’re the ones closest to their code. Especially with talented younger developers, you never want to take away their sense of initiative or close them off from providing feedback, adjustments, or flat out innovation to the “official” guidance. At the very least, you as a senior technical person need to pay attention when a developer tells you that the current approach is confusing or laborious or feels too complicated.
Treat every possible recommendation or technical guidance as a theory that hasn’t yet been perfectly proven.
I’ve talked a lot about giving technical guidance, but you should never think that you or any other individual are responsible for doing all the thinking within a software ecosystem. What you might be responsible for is facilitating the sharing of learning and knowledge through the company. I was lucky enough early in my career to spend just a little bit of time working with Martin Fowler who effectively acts as a sort of industry wide, super bumble bee gathering useful knowledge from lots of different projects and cross-pollinating what he’s learned to other teams and other projects. Maybe you don’t impact the entire software development industry like he has, but you can at least facilitate that within your own team or maybe within your larger organization.
As an aside, a very helpful technique to use when trying to explain something in code to another developer is to ask them to explain it back to you in their own words — or conversely, I try to do this when I’m the one getting the explanation to make sure I’ve really understood what I’m being told. My wife is an educator and tells me this is a common technique for teachers as well.
Next time…
In my next post I’m going to cover a lot of ground about whyI think prescriptive architectural styles like the “Onion” or “Clean” are harmful, alternatives, a discussion about what use is SOLID these days (more than none, but much less than the focus many people put on it is really worth), and a discussion about database abstractions I find to be harmful that tend to be side effects of prescriptive architectures.