Getting Started in Real-Time: Commands, Events, and Brokers Demystified

Understand commands, events, and brokers to build real-time APIs. Learn how to avoid polling and design more responsive.

Getting Started in Real-Time: Commands, Events, and Brokers Demystified
Image Credit: Tirachard

In team discussions, I’ve noticed some confusion about messaging terminology. Terms like “request,” “response,” “command,” “event,” and “topic” often come up, but everyone seems to have a slightly different understanding of what they mean.

That’s why I write this article. I want to explain the core concepts of messaging from common message types to the key differences between synchronous and asynchronous messaging, and also point-to-point (queues) and fanout (topics) in message brokers.

By the end of this read, you’ll learn how commands, replies, and events serve distinct purposes in distributed systems, and why brokers are essential for decoupling producers from consumers.We’ll explore what makes point-to-point and fanout scenarios unique, helping you and your team work more confidently with messaging systems.

Synchronous vs. Asynchronous Messaging

When talking about messaging, it’s easy to assume that everything is asynchronous by default.But in practice, many communication patterns rely on synchronous calls, where one party waits for the other to respond immediately.Let’s break down both approaches.

Synchronous Messaging

Synchronous messaging follows a request/response model similar to a traditional function call or HTTP request.The caller sends a message or makes a request and then blocks. It waits until it receives a response.

The two parties become closely linked. One can’t really do its job without the other responding in real time.That tighter coupling can be just what you need for simple use cases, but it becomes trickier when systems get larger or the work grows more complex.

For instance, a HTTP POST request for RegisterAsset would be considered synchronous if the client can’t proceed until the server responds.

Asynchronous Messaging

Asynchronous messaging allows the sender to fire off a message without waiting for an immediate response.The sender can continue doing other work, while the receiver processes the message at its own pace. If a reply is needed, it arrives via a separate message channel.

This pattern comes into its own when you need to handle large bursts of activity or let services work at their own pace. It’s also essential for real-time notifications, where an event is triggered and anyone interested can respond immediately, without the need for constant polling.The trade-off is that you have to plan for scenarios where messages may arrive out of order or some services may be temporarily offline.But the upside is a more resilient, loosely coupled system that keeps moving even if some parts slow down.

Comfort and Habit

In my day-to-day work, even with large enterprises, I am always surprised at how strong the synchronous mindset still is. Even though message-based systems have been around for a long time, many teams stick to request-response APIs and end up polling for updates. In most cases, it’s not that developers don’t see the value of asynchronous approaches; it’s more about comfort and habit; synchronous calls are familiar, and the tooling around them is ubiquitous. But every time I see a service repeatedly calling an endpoint to find out if something has changed, I can’t help but wonder how much time and network overhead we’re wasting. There are so many more efficient ways to push updates in real time, and I’m still waiting for that to become the default in every organization.

Common Message Types

Conversations about messaging often jump straight to how messages move through queues, topics, or other infrastructure. But before we look at any of that, it’s worth clarifying what is actually being passed around.In most systems, we can group messages into three common types: commands, replies, and events.

Knowing the differences helps everyone on the team speak the same language and design consistent, predictable message flows.

Commands (Imperative Requests)

Commands are all about intent: “do this” or “perform this action”. When you send a command, you’re telling another service or component to do something. For example, you might issue a RegisterAsset or a BindDevice. Each command makes a request for a specific piece of work, which may or may not succeed. Because commands are imperative by nature, they set expectations: when a client sends RegisterAsset, the client expects an asset to be registered somewhere.

In a synchronous setting, commands typically map one-to-one to request/response calls (like an HTTP POST). They are requesting an action. Commands can be accepted, rejected, or processed in various ways.

In an asynchronous system, commands are enqueued and processed at the receiver’s pace; the sender doesn’t block while waiting. But the essence is always the same: the system should do something in response.

Replies (Responses or Outcomes)

Replies come into play when the sender needs to know what happened after issuing a command. They confirm whether an operation was successful, failed, or encountered some exception. For instance, a RegisterAssetResponse might include a success flag and the ID of the newly registered asset, or it could provide an error code if something went wrong.

Replies are straightforward in a synchronous system: you send a command, and the reply arrives in the same connection right away.

In an asynchronous setup, the reply is usually a separate message traveling back through another channel. By decoupling the request and response channels, the receiver can process commands at its own pace and send back replies whenever it’s done.

Events (Facts)

Events announce that “something has happened”. They don’t ask a system to do anything, but rather let anyone who is interested know that a certain event has occurred, such as AssetRegistered. Once published, events are simply out there for anyone to consume. This publish/subscribe paradigm means that the event producer doesn’t have to worry about who needs the information. He simply broadcasts the fact, and any number of subscribers can respond in their own way.

Message Type: Event

Events are perfect for scenarios where multiple services or components need the same information, but use it differently. One microservice might update a dashboard, another might trigger a notification email, and yet another might record analytics. Each subscriber gets the full event, making the system more extensible: you can add new event handlers later without touching the original event producer.

Immutability of Messages

Here is a really important principles in messaging:

Messages are not subject to change.

In simple words, an immutable message is written in stone. Once created and sent, it can’t be changed afterward. This concept might seem strict at first, but it solves a host of potential issues and makes your system more predictable.

Audit and Traceability
When messages remain unchanged after they’re published, you have a crystal-clear audit trail. If you ever need to debug a problem or reconstruct a sequence of events, you can trust that the messages you retrieve accurately reflect what was originally sent. No one can go back and rewrite history.

Consistent State Across Services
In a distributed system, different services might process the same message at different times. If the content of a message could be altered mid-flight, some services might see one version while others see a different version. By locking the message content, you ensure that every subscriber is operating on the same, unchanging information.

Easier Reasoning About Data Flow
Immutability reduces the mental overhead of wondering, “Which version of the message am I handling?” or “Did this get updated after I subscribed?” When messages can’t be changed, what you see is what you get. The message you read today will look identical tomorrow, next week, or next year, making event replay (common in streaming systems) far more reliable.

Simple Example of an Event in C#

If you’re using C#, one way to enforce immutability is to use record types or classes with read-only properties:

public record DeviceBound
{
    public required Guid MessageId { get; init; }
    public required Guid AssetId { get; init; }
    public required Guid DeviceId { get; init; }
    public required DateTime BoundAt { get; init; }

    public required Guid CorrelationId { get; init; }    
    public required string Source { get; init; }
    public DateTime CreatedAt { get; init; } = DateTime.UtcNow; 
}

Once you construct the DeviceBound event, you cannot change the data values. If you wanted to change something, you’d publish a new event instead. This mirrors what happens in the real world: once you declare a fact, such as “Asset 123 was bound to device 5678 at 3:00 pm”, you don’t go back and change it; you send out a new event when the situation changes.

Shift to a “New Fact” Mentality

Adopting immutability pushes you to think in terms of facts rather than states. Instead of saying, “Update the existing message so it’s always current,” you say, “Publish a new message that reflects the latest truth. This perspective is at the heart of event sourcing and event-driven architectures. Instead of carrying around a single mutable state, you accumulate a set of immutable facts that accurately represent how your system evolves over time.

Keeping messages immutable is a small design choice with a big impact.

Message Brokers

One of the most important concepts in asynchronous systems is the message broker. A broker sits in the middle between producers and consumers (also called receivers), acting like a post office that accepts messages and delivers them to the right place. Because producers and consumers don’t talk directly to each other, they can evolve independently. A producer only needs to know how to send messages to the broker, not which specific consumers are listening or how many there are.

Point-to-Point (Queues)

In a point-to-point model, messages go into a queue. Each message is then consumed by exactly one receiver. If multiple consumers are listening to the same queue, the broker’s job is to distribute the messages among them, with each piece of work being handled by exactly one consumer instance.
A practical example might be a geofencing service that needs to handle individual “asset entered geofence” events. Each time an asset crosses a boundary, a message is queued.

Point-to-Point Model.

Only one instanceof the geofencing consumer will pick up each message, perform any necessary logic (such as alerting, logging, or updating a map), and then acknowledge completion.This setup scales well because you can spin up more consumers when you have a spike in geofencing events, and the broker automatically balances the load.

Fanout (Topics)

In a fanout, or publish-subscribe model, messages are published to a topic. Any number of consumers can listen to that topic, and each consumer gets a copy of each message. Topics are perfect for scenarios where multiple services need to respond to the same event, such as an AssetRegisteredTopic announcing that a new asset has been added.

Fanout model.

Because each consumer does different worker, the event can trigger multiple tasks in parallel. The original producer doesn’t need to know who is subscribing or how the data will be used. The producer simply sends the AssetRegistered message to the topic. This flexibility enables decoupling: new services can join the topic whenever they need to act on the same event, without changing the publisher or existing subscribers.

Bringing It All Together

Imagine a REST API POST endpoint for registering assets. From the outside, this flow appears synchronous: the caller makes an HTTP request to POST /assets, and once the asset is successfully registered, they immediately receive a reply indicating success. Here’s what it looks like:

Client Sends a Command
The client calls your REST endpoint, effectively issuing a synchronous command RegisterAsset. Because it’s HTTP-based, the client awaits a response (success, failure, or error).

public record RegisterAssetRequest : AuthenticatedRequest
{
    public required string Name { get; init; }
    public string? Description { get; init; }
    public Dictionary<string, string>? Attributes { get; init; }
}

Synchronous Reply
Once the service has finished registering the asset in the database, it returns a response, usually a JSON response with details such as the new asset ID. At this point, the caller knows that the asset has been successfully registered.

{
  "value": {
    "id": "345abc89-410b-4005-912a-4952f41cb878"
  },
  "statusCode": 201,
  "location": "/assets/345abc89-410b-4005-912a-4952f41cb878"
}

Asynchronous Event Publication
The same service immediately publishes an AssetRegistered event to a topic. It doesn’t wait for a specific service to consume it; it simply broadcasts the fact that “Asset X was registered at time Y”. This event is immutable and can be saved or processed later by any interested subscriber.

Subscriber Reactions
Other services (feature slices, microservices, serverless functions, etc.) subscribe to the AssetRegistered topic. One updates a search index, and maybe another logs analytics. They each receive the exact same event and handle it in their own way, independent of each other and independent of the original request..

Register Asset example.

Loose Coupling and High Cohesion

The command (the incoming POST request) and its response remain narrowly focused on the “register asset” operation. They don’t need to know what else is happening in the system. This is highly cohesive: all the logic associated with registering an asset lives in a feature slice, from input validation to saving to the database to publishing the event.

At the same time, other services that need this data (e.g., search indexing, notifications) are loosely coupled to the original service because they rely on the published event rather than a direct synchronous call. They don’t need to respond immediately, nor do they block the registration process. Each service can evolve independently as long as they all agree on the event contract (i.e., the schema of the AssetRegistered event).

Conclusion

Shifting from a synchronous mindset to an event-driven one isn’t just about reducing polling; it’s about building systems that stay responsive under load, scale more gracefully, and remain easier to evolve over time. By understanding the differences between commands, responses, and events, and by using brokers for point-to-point and fan-out scenarios, you lay the foundation for a resilient, decoupled infrastructure.

Messages become the system’s source of truth, helping you accurately track changes and allowing each service to move at its own pace. Ultimately, using asynchronous messaging means fewer bottlenecks, more real-time insight, and an easier path to growth as your application and team expand.

Cheers!

This article was originally published on here.

Subscribe to Rico Fritzsche

Don’t miss out on the latest issues. Sign up now to get access to the library of members-only issues.
jamie@example.com
Subscribe