KubeMQ
LearnConcepts

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/SubPoint-to-PointRequest/Reply
Directionone → manyone → one-of-manyone ↔ one
Receivers per messageevery subscriberexactly one consumerone responder
Couplingloose (sender ignores receivers)loose (sender ignores workers)tight (sender waits for reply)
Timingasynchronousasynchronoussynchronous
Adds receivers to…reach more listeners (fan-out)share more work (scale)distribute load behind one reply
Typical usebroadcasts, notificationsjobs, task queueslookups, 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 styleKubeMQ patternWhy
Pub/Sub (fan-out)Events and Events StoreA 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)QueuesEach 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

publish.go
err = client.SendEvent(ctx, kubemq.NewEvent().
    SetChannel("order-events").
    SetMetadata("order.created").
    SetBody([]byte(`{"orderId":"ORD-1234","status":"created"}`)),
)
publish.py
client.send_event(
    EventMessage(
        channel="order-events",
        metadata="order.created",
        body=b'{"orderId":"ORD-1234","status":"created"}',
    )
)
publish.js
await client.sendEvent({
  channel: "order-events",
  metadata: "order.created",
  body: Buffer.from(JSON.stringify({ orderId: "ORD-1234", status: "created" })),
});
Publish.java
client.sendEventsMessage(EventMessage.builder()
    .channel("order-events")
    .metadata("order.created")
    .body("{\"orderId\":\"ORD-1234\",\"status\":\"created\"}".getBytes())
    .build());
Publish.cs
await client.SendEventAsync(new EventMessage
{
    Channel = "order-events",
    Metadata = "order.created",
    Body = Encoding.UTF8.GetBytes("{\"orderId\":\"ORD-1234\",\"status\":\"created\"}")
});
Publish.kt
client.sendEvent(EventMessage(
    channel = "order-events",
    metadata = "order.created",
    body = """{"orderId":"ORD-1234","status":"created"}""".toByteArray()
))
publish.cpp
kubemq::EventMessage event;
event.channel = "order-events";
event.metadata = "order.created";
event.body = R"({"orderId":"ORD-1234","status":"created"})";

client.sendEvent(event);
publish.rs
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?;
publish.rb
msg = KubeMQ::PubSub::EventMessage.new(
  channel: "order-events",
  metadata: "order.created",
  body: '{"orderId":"ORD-1234","status":"created"}'
)
client.send_event(msg)
publish.exs
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

enqueue.go
msg := kubemq.NewQueueMessage().
    SetChannel("order-jobs").
    SetBody([]byte(`{"orderId":"ORD-1234","total":99.99}`))

result, err := client.SendQueueMessage(ctx, msg)
enqueue.py
result = client.send_queue_message(
    QueueMessage(
        channel="order-jobs",
        body=b'{"orderId":"ORD-1234","total":99.99}',
    )
)
enqueue.ts
const result = await client.sendQueueMessage(
  createQueueMessage({
    channel: "order-jobs",
    body: JSON.stringify({ orderId: "ORD-1234", total: 99.99 }),
  }),
);
Enqueue.java
SendQueueMessageResult result = client.sendQueueMessage(
    QueueMessage.builder()
        .channel("order-jobs")
        .body("{\"orderId\":\"ORD-1234\",\"total\":99.99}".getBytes())
        .build());
Enqueue.cs
var result = await client.SendQueueMessageAsync(new QueueMessage
{
    Channel = "order-jobs",
    Body = Encoding.UTF8.GetBytes("{\"orderId\":\"ORD-1234\",\"total\":99.99}")
});
Enqueue.kt
val result = client.sendQueueMessage(QueueMessage(
    channel = "order-jobs",
    body = """{"orderId":"ORD-1234","total":99.99}""".toByteArray()
))
enqueue.cpp
kubemq::QueueMessage msg;
msg.channel = "order-jobs";
msg.body = R"({"orderId":"ORD-1234","total":99.99})";

auto result = client.sendQueueMessage(msg);
enqueue.rs
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?;
enqueue.rb
msg = KubeMQ::Queues::QueueMessage.new(
  channel: "order-jobs",
  body: '{"orderId":"ORD-1234","total":99.99}'
)
result = client.send_queue_message(msg)
enqueue.exs
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

send_command.go
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)
send_command.py
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}")
send_command.js
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);
SendCommand.java
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());
SendCommand.cs
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}");
SendCommand.kt
val response = client.sendCommand(CommandMessage(
    channel = "orders.process",
    body = """{"action":"create","orderId":"ORD-1234"}""".toByteArray(),
    timeout = 10000
))
println("executed: ${response.isExecuted}")
send_command.cpp
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;
send_command.rs
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);
send_command.rb
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}"
send_command.exs
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 →

Was this page helpful?

On this page