Interaction Styles
The three shapes every messaging pattern reduces to — pub/sub fan-out, point-to-point competing consumers, and request/reply round-trips.
Pick apart any messaging system and you find the same three conversation shapes underneath. A pattern's name and its bells and whistles vary, but the way a message travels from sender to receiver always reduces to one of three styles: one-to-many (pub/sub), one-to-one-of-many (point-to-point), or there-and-back (request/reply).
Think of how people communicate. A speaker at a conference addresses the whole room at once — everyone listening hears it (pub/sub). A help desk has a single ticket line feeding several agents — your ticket goes to whichever agent is free, and to exactly one of them (point-to-point). A phone call is a back-and-forth — you ask, you wait, you get an answer (request/reply). Learn these three shapes and every messaging pattern becomes a variation on a theme you already understand.
Pub/Sub — one sender, many receivers
In pub/sub (publish/subscribe), a sender publishes a message to a named destination and every active receiver subscribed to that destination gets its own copy. The sender does not know or care who is listening — it could be zero receivers or a thousand. This shape is called fan-out: one message in, many copies out.
The sender and receivers are decoupled. You can add a new subscriber tomorrow without touching the publisher, and a slow or absent subscriber does not hold anyone else up.
One published message is copied to every active subscriber — fan-out, one-to-many.
Reach for pub/sub when every interested party needs the same message: broadcasting state changes, notifying multiple services of an event, feeding live dashboards, or invalidating caches across a fleet.
Point-to-Point — one sender, one-of-many receivers
In point-to-point, a sender puts a message on a shared queue and exactly one receiver consumes it. When several receivers read from the same queue, they form a pool of competing consumers — the queue hands each message to whichever consumer is free, spreading the work across all of them. One message in, delivered once, to one worker.
This is how you scale a workload horizontally. Add more workers and throughput goes up; each message is still processed exactly once, and no two workers do the same job.
Each queued message goes to exactly one of the competing workers — load-balanced, one-to-one-of-many. The dotted line is the acknowledgment that removes the message.
Reach for point-to-point when each message represents a unit of work that should be done once: order processing, background jobs, task distribution, or anything where you want to add workers to handle more load.
Pub/sub vs point-to-point is the most consequential choice you make. Pub/sub copies a message to everyone; point-to-point hands a message to one worker. Same starting point, opposite outcomes.
Request/Reply — there and back
In request/reply, a sender issues a request and waits for a response from a receiver before continuing. It is the synchronous shape: the round-trip is part of the flow, and the sender blocks (up to a timeout) until the answer arrives or it gives up. One request out, one matching response back.
Unlike the other two styles, the sender expects a reply and is coupled to it in time — if the receiver is down or slow, the sender waits. That tight coupling is the point: you want the result now, before moving on.
The sender blocks until the response returns through the broker — synchronous, there-and-back. A request that returns data is a query; one that only confirms an action is a command.
Reach for request/reply when the sender needs an answer to proceed: looking up data, calling a service-to-service API, confirming a write succeeded, or any classic remote-procedure call.
The three styles at a glance
| Pub/Sub | Point-to-Point | Request/Reply | |
|---|---|---|---|
| Direction | one → many | one → one-of-many | one ↔ one |
| Receivers per message | every subscriber | exactly one consumer | one responder |
| Coupling | loose (sender ignores receivers) | loose (sender ignores workers) | tight (sender waits for reply) |
| Timing | asynchronous | asynchronous | synchronous |
| Adds receivers to… | reach more listeners (fan-out) | share more work (scale) | distribute load behind one reply |
| Typical use | broadcasts, notifications | jobs, task queues | lookups, RPC, confirmations |
Pitfall: don't force a synchronous request/reply where a one-way style fits. Blocking a sender on a slow downstream service is a common cause of cascading timeouts — if you only need to tell someone something, publish an event or enqueue a job and move on.
In KubeMQ
KubeMQ implements all three interaction styles natively, so you choose the conversation shape rather than wiring it together yourself:
| Interaction style | KubeMQ pattern | Why |
|---|---|---|
| Pub/Sub (fan-out) | Events and Events Store | A publish is copied to every active subscriber on the channel. Events is at-most-once and in-memory; Events Store persists messages so subscribers can also replay. |
| Point-to-Point (competing consumers) | Queues | Each queued message is delivered to one consumer and removed on acknowledgment; multiple consumers on a channel compete for messages and share the load. |
| Request/Reply (round-trip) | RPC (Commands & Queries) | The sender blocks until a responder answers or the timeout expires. A Query returns a payload; a Command returns an execution acknowledgment. |
The snippets below show the send side of each style against localhost:50000, using the same e-commerce orders domain. They are deliberately minimal — see the pattern pages for full subscribe/receive/respond flows.
Pub/Sub — publish an event
err = client.SendEvent(ctx, kubemq.NewEvent().
SetChannel("order-events").
SetMetadata("order.created").
SetBody([]byte(`{"orderId":"ORD-1234","status":"created"}`)),
)client.send_event(
EventMessage(
channel="order-events",
metadata="order.created",
body=b'{"orderId":"ORD-1234","status":"created"}',
)
)await client.sendEvent({
channel: "order-events",
metadata: "order.created",
body: Buffer.from(JSON.stringify({ orderId: "ORD-1234", status: "created" })),
});client.sendEventsMessage(EventMessage.builder()
.channel("order-events")
.metadata("order.created")
.body("{\"orderId\":\"ORD-1234\",\"status\":\"created\"}".getBytes())
.build());await client.SendEventAsync(new EventMessage
{
Channel = "order-events",
Metadata = "order.created",
Body = Encoding.UTF8.GetBytes("{\"orderId\":\"ORD-1234\",\"status\":\"created\"}")
});client.sendEvent(EventMessage(
channel = "order-events",
metadata = "order.created",
body = """{"orderId":"ORD-1234","status":"created"}""".toByteArray()
))kubemq::EventMessage event;
event.channel = "order-events";
event.metadata = "order.created";
event.body = R"({"orderId":"ORD-1234","status":"created"})";
client.sendEvent(event);let event = EventBuilder::new()
.channel("order-events")
.metadata("order.created")
.body(br#"{"orderId":"ORD-1234","status":"created"}"#.to_vec())
.build();
client.send_event(event).await?;msg = KubeMQ::PubSub::EventMessage.new(
channel: "order-events",
metadata: "order.created",
body: '{"orderId":"ORD-1234","status":"created"}'
)
client.send_event(msg)event = KubeMQ.Event.new(
channel: "order-events",
metadata: "order.created",
body: ~s({"orderId":"ORD-1234","status":"created"})
)
KubeMQ.Client.send_event(client, event)Point-to-Point — enqueue a job
msg := kubemq.NewQueueMessage().
SetChannel("order-jobs").
SetBody([]byte(`{"orderId":"ORD-1234","total":99.99}`))
result, err := client.SendQueueMessage(ctx, msg)result = client.send_queue_message(
QueueMessage(
channel="order-jobs",
body=b'{"orderId":"ORD-1234","total":99.99}',
)
)const result = await client.sendQueueMessage(
createQueueMessage({
channel: "order-jobs",
body: JSON.stringify({ orderId: "ORD-1234", total: 99.99 }),
}),
);SendQueueMessageResult result = client.sendQueueMessage(
QueueMessage.builder()
.channel("order-jobs")
.body("{\"orderId\":\"ORD-1234\",\"total\":99.99}".getBytes())
.build());var result = await client.SendQueueMessageAsync(new QueueMessage
{
Channel = "order-jobs",
Body = Encoding.UTF8.GetBytes("{\"orderId\":\"ORD-1234\",\"total\":99.99}")
});val result = client.sendQueueMessage(QueueMessage(
channel = "order-jobs",
body = """{"orderId":"ORD-1234","total":99.99}""".toByteArray()
))kubemq::QueueMessage msg;
msg.channel = "order-jobs";
msg.body = R"({"orderId":"ORD-1234","total":99.99})";
auto result = client.sendQueueMessage(msg);let msg = QueueMessageBuilder::new()
.channel("order-jobs")
.body(br#"{"orderId":"ORD-1234","total":99.99}"#.to_vec())
.build();
let result = client.send_queue_message(msg).await?;msg = KubeMQ::Queues::QueueMessage.new(
channel: "order-jobs",
body: '{"orderId":"ORD-1234","total":99.99}'
)
result = client.send_queue_message(msg)msg = KubeMQ.QueueMessage.new(
channel: "order-jobs",
body: ~s({"orderId":"ORD-1234","total":99.99})
)
{:ok, result} = KubeMQ.Client.send_queue_message(client, msg)Request/Reply — send a command and wait
resp, err := client.SendCommand(ctx, kubemq.NewCommand().
SetChannel("orders.process").
SetBody([]byte(`{"action":"create","orderId":"ORD-1234"}`)).
SetTimeout(10 * time.Second))
log.Printf("executed: %v", resp.Executed)response = client.send_command(
CommandMessage(
channel="orders.process",
body=b'{"action":"create","orderId":"ORD-1234"}',
timeout_in_seconds=10,
)
)
print(f"executed: {response.is_executed}")const response = await client.sendCommand({
channel: "orders.process",
body: Buffer.from(JSON.stringify({ action: "create", orderId: "ORD-1234" })),
timeoutInSeconds: 10,
});
console.log("executed:", response.isExecuted);CommandResponseMessage response = client.sendCommandRequest(
CommandMessage.builder()
.channel("orders.process")
.body("{\"action\":\"create\",\"orderId\":\"ORD-1234\"}".getBytes())
.timeout(10000)
.build());
System.out.println("executed: " + response.isExecuted());var response = await client.SendCommandAsync(new CommandMessage
{
Channel = "orders.process",
Body = Encoding.UTF8.GetBytes("{\"action\":\"create\",\"orderId\":\"ORD-1234\"}"),
Timeout = TimeSpan.FromSeconds(10)
});
Console.WriteLine($"executed: {response.IsExecuted}");val response = client.sendCommand(CommandMessage(
channel = "orders.process",
body = """{"action":"create","orderId":"ORD-1234"}""".toByteArray(),
timeout = 10000
))
println("executed: ${response.isExecuted}")kubemq::CommandMessage cmd;
cmd.channel = "orders.process";
cmd.body = R"({"action":"create","orderId":"ORD-1234"})";
cmd.timeout = 10000;
auto response = client.sendCommand(cmd);
std::cout << "executed: " << response.isExecuted << std::endl;let command = CommandBuilder::new()
.channel("orders.process")
.body(br#"{"action":"create","orderId":"ORD-1234"}"#.to_vec())
.timeout(Duration::from_secs(10))
.build();
let response = client.send_command(command).await?;
println!("executed: {}", response.executed);msg = KubeMQ::CQ::CommandMessage.new(
channel: "orders.process",
timeout: 10,
body: '{"action":"create","orderId":"ORD-1234"}'
)
result = client.send_command(msg)
puts "executed: #{result.executed}"command = KubeMQ.Command.new(
channel: "orders.process",
body: ~s({"action":"create","orderId":"ORD-1234"}),
timeout: 10_000
)
{:ok, response} = KubeMQ.Client.send_command(client, command)
IO.puts("executed: #{response.executed}")How KubeMQ does this →
Events — Pub/Sub
Real-time fan-out: every active subscriber gets a copy, at-most-once.
Events Store — Durable Pub/Sub
Fan-out with persistence, so subscribers can replay from any position.
Queues — Point-to-Point
Competing consumers: each message goes to exactly one worker, with acknowledgment.
RPC — Request/Reply
Synchronous Commands and Queries: send a request, block for the response.
Was this page helpful?
Messaging Fundamentals
What messaging is and why it exists — synchronous vs asynchronous communication, tight vs loose coupling, and the job a message broker does.
Delivery Guarantees
Understand at-most-once, at-least-once, and exactly-once delivery — plus acknowledgements, redelivery, idempotency, and dead-letter queues.