We've had a quick introduction to Nimbus, so let's look at some messaging patterns in a bit more detail.

Picture this: you're running a successful company that sends people a "Good morning!" text message every day. (It's amazing what people will pay for, isn't it?) People pay $5/month for your inspirational text message and business is great.

Let's say you have some clever logic that decides what to send people in the morning. Let's call that the Thinker. The Thinker is quite fast and it can churn out many inspirational thoughts per second. The Thinker code initially looks something like this:

Scenario 1: Big Ball of Mud

public class Thinker
{
    private readonly SMSGateway _smsGateway;

    public Thinker(SMSGateway smsGateway)
    {
        _smsGateway = smsGateway;
    }

    public void SendSomethingInspirational(Subscriber[] subscribers)
    {
        foreach (var subscriber in subscribers)
        {
            var inspirationalThought = ThinkOfSomethingNiceToSay();
            _smsGateway.SendSMS(subscriber.PhoneNumber, inspirationalThought);
        }
    }

    private string ThinkOfSomethingNiceToSay()
    {
        throw new NotImplementedException();
    }
}

which means our logical design looks like this:

Thinker coupled to SMS gateway

That's a bit silly - we've coupled our Thinker to our SMS gateway, which means two things:

  1. The Thinker can only generate messages as fast as the SMS gateway can receive them; and
  2. If the SMS gateway falls down, the Thinker can't work.

Let's try decoupling them and see how we go.

Scenario 2: Decoupled Thinker from SMS gateway

In this scenario, our code looks like this:

public class Thinker
{
    private readonly IBus _bus;

    public Thinker(IBus bus)
    {
        _bus = bus;
    }

    public void SendSomethingInspirational(Subscriber[] subscribers)
    {
        foreach (var subscriber in subscribers)
        {
            var inspirationalThought = ThinkOfSomethingNiceToSay();
            _bus.Send(new SendSMSCommand(subscriber.PhoneNumber, inspirationalThought));
        }
    }

    private string ThinkOfSomethingNiceToSay()
    {
        throw new NotImplementedException();
    }
}

and we have a handler that looks something like this:

public class SendSMSCommandHandler: IHandleCommand<SendSMSCommand>
{
    private readonly SMSGateway _smsGateway;

    public SendSMSCommandHandler(SMSGateway smsGateway)
    {
        _smsGateway = smsGateway;
    }

    public async Task Handle<T>(T)
    {
    }

    public async Task Handle(SendSMSCommand busCommand)
    {
        _smsGateway.SendSMS(busCommand.PhoneNumber, busCommand.Message);
    }
}

Our topology now looks something like this:

Decoupled Thinker from SMS sender

This is much better. In this scenario, our Thinker can generate inspirational thoughts as fast as it can think and simply queue them for delivery. If the SMS gateway is slow or goes down, the Thinker isn't affected and the texts can be delivered later by the retry logic built into the bus itself.

What? Retry logic? Did we forget to mention that we get that for free? If your SendSMSCommandHandler class blows up when it's trying to send a message, don't worry about handling exceptions or failing gracefully. Just fail. Nimbus will catch any exception you throw and automatically put the message back onto the queue for another attempt. If the gateway has a long outage, there are compensatory actions we can take pretty cheaply, too. (Dead letter queues are a topic for another day, but they're there.)

So... business is great, and we've hit the front page of Reddit. Everyone wants our inspirational thoughts. As far as our Thinker is concerned, that's no problem - it can generate thousands of happy thoughts per second all morning. Our telco's SMS delivery gateway looks like it's getting a bit swamped, though. Even though we've decoupled our Thinker from our SMS sender, messages are still taking too long to arrive and the SMS gateway itself is just too slow.

Scenario 3: Scaling out our command handlers

This is where we discover that distributed systems are pure awesome.

When designed well, a good system will allow us to scale parts out as necessary. We're going to scale out our SMS sending architecture and solve our throughput problem. All we need to do is:

  1. Spin up another SendSMSCommandHandler server; and
  2. Point it at a different telco's SMS gateway.

Job done.

What - we didn't have to reconfigure our Thinker to send to two gateways? And what about the first SMS gateway? Doesn't it need to know about load balancing? Well... no.

This is what our system now looks like:

Scaled out SMS sender

Stuff we get for free out of this design includes:

  • Zero code changes to our Thinker
  • Zero code changes to our existing SMS sender
  • Automatic, in-built load-balancing between our two SMS senders

Implicit load-balancing is part and parcel of a queue-based system like Nimbus. Under the covers, there's a message queue (we'll talk about queues soon) for each type of command. Every application instance that can handle that type of command just pulls messages from the head of the command queue as fast as it can. This means that there's no load-balancer to configure and there are no pools to balance - it all just works. If one handler is faster than another (say, for instance, you have different hardware between the two) then the load will be automatically distributed between the two just because each node will pull commands at a different rate.

How cool is that?

Stay tuned for more messaging patterns and how to use them with Nimbus.