Commands, Events, and Messages Explained

March 28, 2024#Software Development
Series: NServiceBus
Article
Author image.

Kyle McMaster, Senior Consultant

This post is the third in a series on NServiceBus, a popular messaging framework for building distributed systems. In the previous post, we covered the basics of NServiceBus and how to set up a simple messaging system using the framework. In this post, we’ll take a closer look at the concepts of messages, commands, and events in distributed systems.

When building distributed architectures, there are a few types of messages that can be sent between components in your system. Messages, commands, and events represent distinct types of objects that facilitate communication between different parts of a system. Understanding the difference between them is key to designing effective interactions and data flow within distributed architectures.

What are Messages? 📨

First, it’s important to understand that not every message sent in your application will be a command or an event. Messages are a more general term that encompasses commands, events, and simply data without intent or an associated action related to it. They are the units of communication that are passed between different parts of a system. A good example of a message that is neither a command nor an event is a request for information, such as a query to a database. These messages are not commands because they don’t imply an action, and they are not events because they don’t signal that something has happened. Instead, they are simply requests for information.

In NServiceBus, messages are simply the unit of communication that is passed between endpoints. These messages can be defined using the IMessage interface and communicated using the Send method on the IMessageSession or IMessageHandlerContext interfaces. They are then received by handlers that implement the IHandleMessages<T> interface, where T is the message type. The following is an idiomatic example of how messages are defined, sent, and received in NServiceBus:

public class MyMessage : IMessage
{
  public string Content { get; init; }
}

public class MyMessageSender(IMessageSession _messageSession)
{
  public async Task SendMessage()
  {
    var message = new MyMessage
    {
      Content = "NServiceBus rocks!"
    };
    await _messageSession.Send(message);
  }
}

public class MyMessageHandler : IHandleMessages<MyMessage>
{
  public Task Handle(MyMessage message, IMessageHandlerContext context)
  {
    Console.WriteLine($"Received message with content: {message.Content}");
    return Task.CompletedTask;
  }
}

What are Commands? 💪

Commands are messages that represent an intention to perform an action or to produce a side effect. They are imperative meaning they tell the recipient to do something. Commands are used to trigger a specific action in the receiving system, such as creating a new record in a database, updating a value, or sending a notification. Commands should typically be processed by one recipient. In NServiceBus, the ICommand interface is used to mark classes as commands. Commands in distributed systems don’t return a value, but they can trigger messages that notify other parts of the system to satisfy any request/response requirements. Here is an example of how commands are defined and sent in NServiceBus.

public class CreateContributorCommand : ICommand
{
  public string Name { get; init; }
  public string PhoneNumber { get; init; }
}

public class ContributorService(IMessageSession _messageSession)
{
  public async Task Create(string name, string phoneNumber)
  {
    var command = new CreateContributorCommand
    {
      Name = name,
      PhoneNumber = phoneNumber
    };
    await _messageSession.Send(command);
  }
}

public class CreateContributorCommandHandler(IRepository<Contributor> _repository)
  : IHandleMessages<CreateContributorCommand>
{
  public async Task Handle(CreateContributorCommand message, IMessageHandlerContext context)
  {
    var contributor = new Contributor(message.Name, message.PhoneNumber);
    await _repository.Add(contributor);
  }
}

In this code, we define a CreateContributorCommand that represents the intention to create a new contributor in the system. The command is sent from a domain service and it is processed by a handler that creates a new contributor using a Repository.

What are Events? 💥

Events are messages that represent something that has happened in the system. Events are typically used to notify other parts of the system that a particular state change has occurred, such as a new record being created, an update to an existing record, or a user logging in. These events represent changes that have already taken place. Events can be processed by multiple recipients. In NServiceBus, the IEvent interface is used to mark classes as events.

public class ContributorCreatedEvent : IEvent
{
  public int Id { get; init; }
  public string Name { get; init; }
  public string PhoneNumber { get; init; }
}

public class CreateContributorCommandHandler(IRepository<Contributor> _repository)
  : IHandleMessages<CreateContributorCommand>
{
  public async Task Handle(CreateContributorCommand message, IMessageHandlerContext context)
  {
    // .. Create a new contributor in the system

    await context.Publish(new ContributorCreatedEvent
    {
      Id = contributor.Id,
      Name = message.Name,
      PhoneNumber = message.PhoneNumber
    });
  }
}

public class ContributorCreatedEventHandler(ILogger<ContributorCreatedEventHandler> _logger) 
  : IHandleMessages<ContributorCreatedEvent>
{
  public Task Handle(ContributorCreatedEvent message, IMessageHandlerContext context)
  {
    _logger.LogInformation("Received {EventName} for {ContributorId}",
      nameof(ContributorCreatedEvent),
      message.ContributorId);
    return Task.CompletedTask;
  }
}

Note that this is the CreateContributorCommandHandler from the previous section but we are adding the functionality of publishing an event. This is common in distributed systems when events are used to notify other parts of the system that a command has been processed. In this case, the ContributorCreatedEvent is published after a new contributor has been created in the system. If you’ve been following along with our NServiceBus content series, you’ll recognize the ContributorCreatedEventHandler from the previous post in this series.

NServiceBus does enforce the distinction between commands and events which is important because it helps to clarify the intent of the message and how it should be processed by the receiving system. Commands are sent using the Send method, while events are published using the Publish method. For example, if you were to pass an IEvent using the Send method, an exception would be thrown with a message similar to “Best practice violation for message type ‘ContributorCreatedEvent’. Events can have multiple recipients, so they should be published.”. This helpful guidance towards best practices is one of the features of NServiceBus that I enjoy most.

Conclusion 🎬

In this post, we’ve covered the basic definitions of messages, commands, and events in distributed systems. NServiceBus provides a simple and effective way to implement these patterns using the IMessage, ICommand, and IEvent interfaces. By following these conventions, you can build robust and reliable messaging systems that can scale with your application’s needs. Be sure to check out the next post in this series on NServiceBus to learn more about asynchronous messaging patterns and how to implement them in your applications. 👋

Further Reading 📚


Copyright © 2024 NimblePros - All Rights Reserved