Skip to content

Messaging

When a customer places an order, several things must happen: billing must charge the card, the warehouse must prepare a shipment, and a confirmation email must go out. None of these needs to finish before the buyer sees “order placed”. If the Order service called each downstream service synchronously, a slow email provider could stall the checkout, and any one of those services being down would fail the sale outright.

You want one service to trigger work in others without waiting for them and without being coupled to whether they are currently available. Some of that work should fan out — one order event should drive billing, shipping, and notifications independently, and you would like to add a fourth reaction later without touching the Order service at all. A direct synchronous call gives you none of this: it blocks, it couples the two services in time, and it knows about exactly one recipient. How do you let services collaborate while staying decoupled in both time and identity?

Messaging is the asynchronous style. Instead of calling a service, a producer hands a message to a message broker — infrastructure that stores the message durably and delivers it to consumers. The producer does not wait for the work to be done and does not even need to know who the consumers are. The broker decouples the two sides in time: a consumer that is down simply processes its backlog when it returns.

Brokers offer two delivery shapes:

  • Queues (point-to-point) — each message is delivered to exactly one consumer. This carries commands: “charge this order”. Multiple instances of a consumer compete for messages, which load-balances the work.
  • Topics (publish/subscribe) — each message is delivered to every subscriber. This carries events: “an order was placed”. Billing, shipping, and notifications each subscribe independently, and a new subscriber can be added without changing the producer.
flowchart LR
  P[Order Service<br/>producer] -->|OrderPlaced| Broker[(Message Broker<br/>topic)]
  Broker --> B[Billing Service]
  Broker --> S[Shipping Service]
  Broker --> N[Notification Service]
  B -.->|buffered if down| Broker
Messaging — a producer publishes one event to a broker, which fans it out to independent consumers

A small wrapper over a broker that publishes an OrderPlaced event and consumes it on the other side. The publish call returns as soon as the broker has the message; the consumer acknowledges only after it has finished its work, so an unprocessed message is redelivered. Each example is self-contained and idiomatic to its language.

type OrderPlaced = { orderId: string; customerId: string; total: number };
// Publish: hand the event to the broker and return immediately.
async function publishOrderPlaced(broker: Broker, event: OrderPlaced): Promise<void> {
await broker.publish('orders.placed', JSON.stringify(event));
}
// Consume: process each message, then acknowledge it.
function consumeOrders(broker: Broker): void {
broker.subscribe('orders.placed', async (raw, ack) => {
const event = JSON.parse(raw) as OrderPlaced;
await chargeCustomer(event.customerId, event.total);
await ack(); // ack only after the work succeeded
});
}

What you gain:

  • Loose coupling. The producer does not block, does not know its consumers, and does not depend on them being up. New consumers subscribe without any change to the producer, so the system grows by addition rather than modification.
  • Buffering and resilience. The broker absorbs traffic spikes and holds messages while a consumer is down or deploying. Slowness in one consumer never propagates back to the producer.
  • Natural fan-out. A single published event drives many independent reactions, which is exactly what one-to-many interactions need.

What it costs you:

  • The broker is critical infrastructure. Everything now depends on the broker being available, durable, and correctly operated. It must be clustered and monitored, and it becomes a new thing that can fail.
  • Eventual consistency. Because work happens later, there is a window where the order exists but the email has not been sent. Callers cannot read a result back immediately the way a synchronous call returns one.
  • Operational complexity. You must handle duplicate delivery (most brokers are at-least-once, so consumers must be idempotent), out-of-order messages, and poison messages that need a dead-letter queue. Debugging a flow spread across publishers and subscribers is harder than following a single call stack.
  • Remote Procedure Invocation — the synchronous alternative, when the caller truly needs an immediate answer.
  • Saga — uses messaging to coordinate a multi-service transaction.
  • Event Sourcing — a reliable way to produce the events you publish.
How does messaging decouple two services in time?
What is the difference between a queue and a topic?
Why must messaging consumers usually be idempotent?
Which is a real cost of adopting messaging?