20 min read
Event-Driven Systems4,295 words

Event Broker vs. Message Bus

What this is: a precise, opinionated map of two terms that get used interchangeably and shouldn't be. The goal is to give you a defensible mental model, the failure modes, and concrete Spring Boot code so you can answer "what's the difference?" without hand-waving — and, more importantly, so you know which one to reach for and why.

Sits alongside kafka-eda-study-guide.md, inbox-outbox-pattern.md, and eda-vs-event-sourcing-study-guide.md. This guide is the "plumbing vocabulary" layer underneath those.


The one mental model#

A message bus is a communication abstraction/pattern — a shared "language" and topology that endpoints plug into. An event broker is a piece of infrastructure — a running server/cluster that stores and routes events. They live at different layers, and you very often implement a bus on top of a broker.

Two consequences fall straight out of that sentence, and almost every real difference is a restatement of one of them:

  1. Different layer. "Bus" describes the programming model and shape of communication (a shared backbone everyone speaks to). "Broker" describes the runtime component doing the moving and storing. Asking "bus or broker?" is often a category error, like asking "is it a REST API or is it nginx?" — the bus is the contract, the broker is the transport.
  2. Different center of gravity for the payload. A bus is deliberately message-type-agnostic: it carries commands ("do this"), events ("this happened"), and queries/replies over a shared contract. An event broker is optimized for one payload type — events: immutable, past-tense facts, fanned out to whoever is interested via publish/subscribe.

Hold those two and you can reconstruct everything below.


0. First, admit the terms are muddy#

This is a place where sounding senior means naming the ambiguity instead of pretending there's a crisp dictionary. The industry uses these words loosely, and marketing has blurred them further. Pin the vocabulary down before you compare:

TermThe useful definition
MessageThe generic envelope. Three flavors: command, event, query/reply.
CommandAn instruction to do something, aimed at exactly one logical handler, usually expecting it to happen (maybe a reply). PlaceOrder. Sender is coupled to the receiver's contract.
EventA notification that something happened — an immutable, past-tense fact. OrderPlaced. Zero-to-many subscribers. Publisher neither knows nor cares who reacts.
Message brokerInfrastructure that receives, (often) stores, routes, and delivers messages between apps via queues and/or topics. RabbitMQ, ActiveMQ, Kafka. "Broker/hub" topology: apps talk to the broker, not to each other.
Event brokerA message broker whose job is distributing events in an event-driven architecture, pub/sub, fan-out. Kafka, Pulsar, NATS, Solace PubSub+, AWS EventBridge, Google Pub/Sub, Azure Event Hubs. Frequently interchangeable with "message broker" — the difference is emphasis (events + pub/sub + streaming) not a hard technical line.
Message busA pattern (Hohpe & Woolf): a common data model + common command set + shared messaging infrastructure so many apps communicate through one shared set of interfaces. In modern code it shows up two ways: an in-process dispatcher (Spring events, Guava EventBus, Axon's buses) or a distributed bus abstraction (Vert.x EventBus, Spring Cloud Bus, "service bus" libraries) that usually rides on a broker.
ESB (Enterprise Service Bus)The enterprise, heavyweight incarnation of the message-bus pattern: centralized routing, transformation, orchestration, protocol mediation. Mule, IBM Integration Bus, Oracle Service Bus, WSO2.
Event busOverloaded: sometimes an in-process pub/sub dispatcher (Guava), sometimes a distributed bus (Vert.x). Treat "event bus" as "message bus specialized to events," and always ask "in-process or distributed?"

The defensible one-liner to carry into an interview: "Broker" is a noun for infrastructure; "bus" is a noun for a communication pattern/abstraction. They're not competitors — a bus is frequently built on a broker. Where people really disagree is what travels and how coupled the endpoints are.


1. The payload decides everything: command vs. event#

Before comparing bus and broker, internalize the command/event split, because the bus-vs-broker distinction is largely downstream of it.

CommandEvent
Intent"Do this""This happened"
TenseImperative — ShipOrderPast — OrderShipped
RecipientsExactly one logical handlerZero-to-many subscribers
CouplingSender knows the receiver's contract/expectationPublisher is oblivious to subscribers
If nobody handles itA bug (the thing didn't get done)Fine (nobody cared)
Failure ownershipSender often cares about the outcomePublisher already moved on
Natural homeA bus (routed to the one handler; may reply)An event broker (fanned out to all)

A message bus carries all three message flavors and often supports request/reply, because its whole reason to exist is a shared contract many parties speak. An event broker is tuned for events — publish and forget, fan-out, decoupling. This is why "put a command on a broadcast event topic" is a classic design smell (§Problems): you've used broadcast semantics for something that has exactly one rightful owner.


2. What a message bus actually is#

The canonical definition is from Enterprise Integration Patterns (Hohpe & Woolf, 2003): a Message Bus = a common data model + a common command set + a messaging infrastructure, so that different systems communicate through a shared set of interfaces. The metaphor is the hardware bus in a computer: components don't wire to each other pairwise, they all plug into one shared backbone and agree on a protocol.

The essence of "bus" is the shared contract and topology, not any particular server. In practice it shows up at two very different scales:

(a) In-process bus / dispatcher (a mediator inside one JVM). No network, no durability. Publishing a message synchronously invokes registered handlers. Examples: Spring's ApplicationEventPublisher + @EventListener, Guava EventBus, Axon Framework's CommandBus/EventBus/QueryBus. Scope = one process. Dies with the process. This is really an organizational tool — it decouples modules inside a monolith so a PlaceOrder use case doesn't have to call Emailer, Inventory, and Analytics directly.

(b) Distributed bus (an abstraction over transport). A bus-shaped API spanning many nodes. Examples: Vert.x EventBus (send = point-to-point/round-robin to one consumer, publish = broadcast to all, request = reply), Spring Cloud Bus (broadcast management/state-change events to every instance), and "service bus" libraries (MassTransit, NServiceBus in .NET; Axon in Java can distribute its buses). Crucially, these usually ride on a broker — RabbitMQ, Kafka — and hide it behind a bus API. The bus is the programming model; the broker is the plumbing.

Bus emphasis, in one breath: shared contract, endpoints "plug in," carries commands+events+replies, can be synchronous and request/reply, and the abstraction is the product.


3. What an event broker actually is#

An event broker is a running middleware component — a server or cluster — that sits between event producers and consumers. Producers publish events to topics; the broker stores and routes; consumers subscribe. The broker is a piece of infrastructure you deploy, scale, patch, and page someone about at 3am.

What you get from a real broker (and don't get from an in-process bus):

  • Publish/subscribe fan-out — one event, N independent consumers, each unaware of the others.
  • Durability & replay — events persisted to a log; consumers track offsets; you can replay history (Kafka's headline feature). An in-process bus has none of this.
  • Consumer groups / competing consumers — load-balance a stream across instances or give each logical service its own full copy.
  • Ordering & partitioning — Kafka guarantees order within a partition; key selection controls which events share ordering.
  • Delivery semantics you can tune — at-least-once (default and most common), at-most-once, or effectively-/exactly-once with extra machinery; dead-letter queues; retries; backpressure.
  • Producer/consumer decoupling in time — the consumer can be down when the event is produced and still get it later.

Representative brokers: Apache Kafka, Apache Pulsar (streaming logs); RabbitMQ, ActiveMQ Artemis, NATS (broker-style routing/queues); AWS EventBridge / SNS+SQS, Google Pub/Sub, Azure Event Hubs / Event Grid, Solace PubSub+ (managed/cloud event brokers). "Event broker" as a phrase was popularized heavily by Solace and the event-driven-architecture crowd.

Broker emphasis, in one breath: it's infrastructure; producers are fully decoupled from consumers; events (immutable facts) fan out via pub/sub; and it owns durability, ordering, and delivery guarantees. The pipe is "dumb," the endpoints are "smart."


4. They aren't rivals: a bus is usually layered on a broker#

This is the point that makes "vs." collapse, and it's the senior move in the conversation. A message bus is an API/abstraction; an event broker is a transport. Put a bus API over a broker and you get both:

  • Spring Cloud Stream gives you a binder abstraction — Supplier/Function/Consumer beans — over Kafka or RabbitMQ. Your code doesn't touch KafkaTemplate; the broker is swappable config.
  • Spring Cloud Bus literally provides bus semantics (broadcast a state-change to all instances of a service) implemented over a broker (RabbitMQ or Kafka) via Spring Cloud Stream. The classic use: hit one node's /actuator/busrefresh and every instance re-reads its config.
  • MassTransit / NServiceBus (.NET) present a "service bus" programming model on top of RabbitMQ / Azure Service Bus.
  • Axon Framework exposes a CommandBus/EventBus you code against; in a distributed setup those are backed by a real transport.

So the honest framing is a stack:

Your services ─────────────► speak to a BUS ABSTRACTION (Spring Cloud Stream / Bus, Axon, MassTransit) │ ▼ runs on an EVENT BROKER (Kafka / RabbitMQ / Pulsar …)

You can also have a bus with no broker (in-process Spring events), and a broker with no bus abstraction (raw KafkaTemplate + @KafkaListener). The four quadrants all exist. That's exactly why the terms confuse people.


5. Side-by-side comparison#

AxisMessage BusEvent Broker
What it isA pattern / abstraction / API (a "bus" endpoints plug into)A deployed piece of infrastructure (server/cluster)
LayerProgramming model & topologyRuntime transport
Primary payloadAny message: commands, events, queries/repliesEvents (immutable facts)
Comms styleCommand routing + pub/sub + request/reply; shared canonical contractPublish/subscribe fan-out; streaming
CouplingEndpoints share a common data model/command set; can be point-to-pointProducer fully decoupled — doesn't know consumers exist
Topology metaphorShared backbone ("everyone on the bus")Hub with topics + consumer groups
"Smarts"Historically some logic in the bus (ESB routing/transform)Dumb pipe; logic lives in endpoints
Typical scopeIn-process module decoupling or enterprise integrationDistributed microservices / streaming at scale
Durability / replayOften in-memory (in-proc: none); ESB variesDurable log, offsets, replay, DLQ
Failure if process diesIn-flight in-proc messages are lostPersisted; consumer resumes from offset
Delivery guaranteesIn-proc: synchronous best-effortAt-least-once (typical), tunable; ordering per partition
Concrete examplesSpring ApplicationEventPublisher, Guava EventBus, Axon buses, Vert.x EventBus, Spring Cloud Bus, ESBs (Mule/IIB)Kafka, Pulsar, RabbitMQ, NATS, EventBridge, Solace, Pub/Sub, Event Hubs

Read the table as: left column = "how my code talks," right column = "the box that moves the bytes." When they overlap (Spring Cloud Bus, Axon-over-broker), it's because the left is sitting on the right.


6. The ESB — the message bus's enterprise incarnation, and why it fell from grace#

You will get asked this, and it's the sharpest illustration of the bus concept. The Enterprise Service Bus was the 2000s answer to "connect all our systems": put a central bus in the middle that does routing, message transformation, protocol mediation (SOAP↔JMS↔FTP…), and orchestration, all governed by a canonical data model.

It fell out of favor for reasons worth being able to recite (Martin Fowler's "smart endpoints and dumb pipes," 2014):

  • Smart pipe, dumb endpoints. Business logic migrated into the bus (routing rules, transformations, orchestrations). The integration layer became a second, shared codebase — a distributed monolith with its own deploy cycle and its own team as a bottleneck.
  • Canonical-model coupling. Forcing every system through one "universal" schema is a governance tar pit; every new field is a committee.
  • Central point of failure & scaling ceiling. One bus that everything routes through.

The microservices/EDA reaction: dumb pipes, smart endpoints. Keep the transport (an event broker) simple — it just moves events durably — and push routing, transformation, and business decisions into the services. That's precisely why "event broker" rose as the term of choice: it names the dumb-pipe philosophy. (Modern managed integration — iPaaS, EventBridge with rules — brings some routing smarts back, but per-integration and elastic, not one central monolith.)

Senior nuance: don't overcorrect into rebuilding an ESB inside Kafka (a giant central "events service" that reshapes everyone's payloads). Same coupling, new logo.


7. Delivery & failure semantics — where the difference actually bites#

This is the most practically important section, because the biggest real-world mistakes come from assuming an in-process bus behaves like a broker.

In-process message bus (e.g., Spring events):

  • Synchronous by default. Publishing runs the listeners in the caller's thread, and — this trips up nearly everyone — inside the caller's transaction if one is open. A listener throwing can roll your transaction back. It is not async or "fire-and-forget" unless you make it so.
  • No durability. Process crashes between publish and handling → the "message" is gone. No replay, no offsets, no DLQ.
  • Coupled lifetime. Publisher and handler share a JVM and a clock.

Event broker (e.g., Kafka/RabbitMQ):

  • Decoupled in time. Consumer down when produced? It catches up from its offset later.
  • At-least-once is the norm → consumers must be idempotent (see inbox-outbox-pattern.md). "Exactly-once" exists in narrow forms (Kafka transactions/idempotent producer) but end-to-end EOS across external side-effects is mostly a myth; design for dedupe.
  • Ordering is per-partition/queue, not global. Same key → same partition → ordered; across partitions, no promise.
  • Durability, replay, DLQ, backpressure are first-class.

The single most valuable takeaway: crossing a process boundary changes the semantics. An in-process bus is a method-call decoupler; a broker is a durable, time-decoupling integration channel. Reaching for the wrong one is how you get lost events (used a bus where you needed a broker) or needless operational weight and eventual-consistency headaches (used a broker where an in-process call would do).


8. Code — the message bus side#

8a. In-process bus: Spring ApplicationEventPublisher#

One placeOrder use case decoupled from three reactions, all inside one service. This is an in-process message bus.

Kotlin

kotlin
// A domain event (immutable fact). Publisher won't know who listens. data class OrderPlaced(val orderId: String, val amount: BigDecimal) @Service class OrderService( private val orders: OrderRepository, private val events: ApplicationEventPublisher, // <-- the in-process "bus" ) { @Transactional fun placeOrder(cmd: PlaceOrderCommand): String { val order = orders.save(Order.from(cmd)) // Synchronous by default: listeners run HERE, in this thread, in THIS transaction. events.publishEvent(OrderPlaced(order.id, order.amount)) return order.id } } @Component class EmailListener { @EventListener // runs inline, same thread/tx fun on(e: OrderPlaced) { /* send confirmation */ } } @Component class AnalyticsListener { @Async // needs @EnableAsync: separate thread, NO caller tx @EventListener fun on(e: OrderPlaced) { /* update metrics off the hot path */ } } @Component class ProjectionListener { // Only fire AFTER the DB commit succeeds — the correct default for side effects. @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) fun on(e: OrderPlaced) { /* update a read model */ } }

Java

java
public record OrderPlaced(String orderId, BigDecimal amount) {} @Service public class OrderService { private final OrderRepository orders; private final ApplicationEventPublisher events; // the in-process "bus" public OrderService(OrderRepository orders, ApplicationEventPublisher events) { this.orders = orders; this.events = events; } @Transactional public String placeOrder(PlaceOrderCommand cmd) { Order order = orders.save(Order.from(cmd)); events.publishEvent(new OrderPlaced(order.getId(), order.getAmount())); return order.getId(); } } @Component class EmailListener { @EventListener void on(OrderPlaced e) { /* send confirmation */ } } @Component class AnalyticsListener { @Async // requires @EnableAsync @EventListener void on(OrderPlaced e) { /* off-thread metrics */ } } @Component class ProjectionListener { @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) void on(OrderPlaced e) { /* update read model */ } }

The gotchas that separate juniors from seniors here:

  • Default is synchronous, same-thread, same-transaction — not async.
  • @TransactionalEventListener(AFTER_COMMIT) is what you usually want for side effects, so you don't email a customer about an order that then rolls back. But note: an AFTER_COMMIT listener writing to the DB needs care (the tx is already committing).
  • The moment you want durability or cross-service delivery, this pattern is the wrong tool — you graduate to the outbox pattern + a broker (see inbox-outbox-pattern.md). In-process events + a network hop = lost messages on crash.

8b. Distributed bus: Spring Cloud Bus (bus semantics over a broker)#

The cleanest example of "a bus that is explicitly built on a broker." Add the starter; every instance joins the bus over RabbitMQ/Kafka. Broadcasting one event reaches all instances.

gradle
// build.gradle — pick the transport; the bus API is the same either way implementation 'org.springframework.cloud:spring-cloud-starter-bus-amqp' // RabbitMQ // or: implementation 'org.springframework.cloud:spring-cloud-starter-bus-kafka'
yaml
# application.yml — expose the bus refresh endpoint management: endpoints: web: exposure: include: busrefresh

Trigger a fleet-wide config refresh by POSTing to a single node:

bash
# NOTE: endpoint name is VERSION-DEPENDENT. # Newer Spring Cloud: POST /actuator/busrefresh # Older versions: POST /actuator/bus-refresh (hyphenated) curl -X POST http://any-instance:8080/actuator/busrefresh

The point isn't config refresh specifically — it's the shape: one publish → broadcast to every subscriber on the bus, with a real broker doing the moving underneath. You can define custom RemoteApplicationEvents to broadcast your own state changes the same way.

(Other distributed buses worth name-dropping: Vert.x EventBussend() for point-to-point round-robin, publish() for broadcast, request() for reply; Axon's CommandBus/EventBus/QueryBus, which cleanly separate command routing from event fan-out from query handling.)


9. Code — the event broker side#

9a. Kafka via Spring for Apache Kafka — pub/sub fan-out with consumer groups#

The producer publishes OrderPlaced once; two different services each get their own copy because they use different groupIds. That fan-out — plus durability and replay — is the broker's whole value proposition, and it's exactly what the in-process bus in §8a can't do across services.

Kotlin

kotlin
@Service class OrderEventPublisher( private val kafka: KafkaTemplate<String, OrderPlaced>, ) { fun publish(e: OrderPlaced) { // key = orderId → all events for one order land on the same partition → ordered per order kafka.send("orders.v1", e.orderId, e) } } @Component class EmailConsumer { @KafkaListener(topics = ["orders.v1"], groupId = "email-service") fun handle(e: OrderPlaced) { /* send email — idempotently! */ } } @Component class AnalyticsConsumer { // DIFFERENT groupId → its own independent copy of every event (fan-out). // SAME groupId across N instances → the stream is load-balanced (competing consumers). @KafkaListener(topics = ["orders.v1"], groupId = "analytics-service") fun handle(e: OrderPlaced) { /* update metrics */ } }

Java

java
@Service public class OrderEventPublisher { private final KafkaTemplate<String, OrderPlaced> kafka; public OrderEventPublisher(KafkaTemplate<String, OrderPlaced> kafka) { this.kafka = kafka; } public void publish(OrderPlaced e) { kafka.send("orders.v1", e.orderId(), e); // key → partition → per-key ordering } } @Component class EmailConsumer { @KafkaListener(topics = "orders.v1", groupId = "email-service") void handle(OrderPlaced e) { /* send email, idempotently */ } } @Component class AnalyticsConsumer { @KafkaListener(topics = "orders.v1", groupId = "analytics-service") void handle(OrderPlaced e) { /* independent copy — different group */ } }

Notice what you didn't have to do: the publisher names no consumer. Add a third service tomorrow with a new groupId and it retroactively reads history — no producer change. That's maximal producer/consumer decoupling, the broker's signature.

9b. RabbitMQ via Spring AMQP — routing through an exchange#

Different broker model, same "infrastructure moves events" idea. RabbitMQ routes via exchanges → bindings → queues; a topic exchange fans one publish out to every bound queue whose pattern matches.

Kotlin

kotlin
@Configuration class RabbitTopology { @Bean fun ordersExchange() = TopicExchange("orders.exchange") @Bean fun emailQueue() = Queue("email.order-placed") @Bean fun emailBinding(email: Queue, ex: TopicExchange): Binding = BindingBuilder.bind(email).to(ex).with("order.placed") // routing-key pattern } @Service class OrderEventPublisher(private val rabbit: RabbitTemplate) { fun publish(e: OrderPlaced) = rabbit.convertAndSend("orders.exchange", "order.placed", e) // (exchange, routingKey, payload) } @Component class EmailConsumer { @RabbitListener(queues = ["email.order-placed"]) fun handle(e: OrderPlaced) { /* ... */ } }

Java

java
@Service public class OrderEventPublisher { private final RabbitTemplate rabbit; public OrderEventPublisher(RabbitTemplate rabbit) { this.rabbit = rabbit; } public void publish(OrderPlaced e) { rabbit.convertAndSend("orders.exchange", "order.placed", e); } } @Component class EmailConsumer { @RabbitListener(queues = "email.order-placed") void handle(OrderPlaced e) { /* ... */ } }

(Kafka vs. RabbitMQ as brokers is its own topic — log-with-retained-offsets-and-replay vs. queue-with-ack-and-delete. Covered in kafka-eda-study-guide.md. Here they're just two brokers illustrating the same "infrastructure" role.)

9c. The bridge: Spring Cloud Stream (bus abstraction over the broker)#

To make §4 concrete — same behavior as §9a, but your code touches no Kafka API. Swap Kafka↔RabbitMQ by changing a dependency, not code. This is a bus abstraction sitting on an event broker.

Kotlin

kotlin
@Configuration class OrderStreams { // A Supplier bean is bound to an output destination (topic/exchange) by config. @Bean fun orderPlaced(): Supplier<OrderPlaced> = Supplier { nextEventOrNull() } // A Consumer bean is bound to an input destination. @Bean fun onOrderPlaced(): Consumer<OrderPlaced> = Consumer { e -> /* react */ } }
yaml
spring: cloud: stream: bindings: orderPlaced-out-0: { destination: orders.v1 } onOrderPlaced-in-0: { destination: orders.v1, group: analytics-service }

You're writing bus-style functional code; Spring Cloud Stream's binder turns it into Kafka or RabbitMQ calls. That's the whole "bus on a broker" idea in one file.


10. When to reach for which#

  • Decoupling modules inside one deployable (a monolith or a single service)?In-process message bus (Spring events / Guava / Axon). No broker, no ops, no eventual consistency. Just don't mistake it for durable.
  • Async, durable, fan-out communication between services/processes?Event broker (Kafka/RabbitMQ/…). Pair with the outbox pattern so the DB write and the publish can't diverge.
  • You want broker durability but don't want your code coupled to a specific broker, or you need "broadcast to all instances" semantics? → a bus abstraction over a broker (Spring Cloud Stream / Spring Cloud Bus). Best of both, at the cost of another abstraction layer.
  • Command that exactly one service must execute, possibly with a reply? → command semantics (a command bus, or a request/reply channel / dedicated queue), not a broadcast event topic.
  • Connecting many heterogeneous legacy systems with heavy transformation? → historically an ESB; today prefer dumb broker + smart services (or managed iPaaS per-integration), and resist re-centralizing logic in the pipe.

Problems & how to solve them#

Problem: In-process Spring events silently lost on crash / not actually async. Root cause: Assuming @EventListener is durable and asynchronous. It's synchronous, in the caller's thread and transaction, and in-memory only. Fix: For intra-process decoupling, that's fine — use @TransactionalEventListener(AFTER_COMMIT) for side effects. For anything that must survive a crash or cross a process boundary, don't use in-process events as your integration channel: persist via the outbox and publish to a broker.

Problem: A command ends up on a broadcast event topic and multiple services act on it. Root cause: Using pub/sub (event) semantics for something with exactly one rightful owner (a command). Fix: Model it as a command routed to one handler (dedicated queue / command bus). Reserve broadcast topics for past-tense events that many parties may independently react to.

Problem: "We'll just do exactly-once with Kafka." Root cause: Conflating Kafka's in-cluster EOS (idempotent producer + transactions) with end-to-end exactly-once across external side effects (emails, third-party APIs). Fix: Assume at-least-once; make consumers idempotent (dedupe keys / inbox). True end-to-end EOS across side effects generally doesn't exist. See inbox-outbox-pattern.md.

Problem: Ordering bugs — events processed out of sequence. Root cause: Expecting global ordering from a partitioned broker. Fix: Order is guaranteed only within a partition/queue. Key by the entity that needs ordering (e.g., orderId) so its events share a partition; accept that unrelated entities interleave.

Problem: The broker becomes a distributed monolith (ESB reborn). Root cause: Centralizing routing/transformation/business logic in the pipe — a giant "event service" reshaping everyone's payloads. Fix: Dumb pipe, smart endpoints. Keep the broker moving events; keep logic in services. Version topics/schemas (orders.v1) and use a schema registry instead of a central transformer.

Problem: Canonical-data-model paralysis. Root cause: Trying to force one universal schema across all systems (classic message-bus/ESB failure mode). Fix: Bounded-context-local models + explicit published contracts per topic; translate at the edges (anti-corruption layer). See ddd-bounded-contexts-study-guide.md.

Problem: Reached for a broker where a method call would do. Root cause: Cargo-culting "event-driven everywhere." Now you have eventual consistency, ops burden, and debugging pain for logic that lived in one process. Fix: If publisher and subscriber share a deploy and a transaction, an in-process bus (or a plain call) is simpler and stronger. Introduce a broker when you genuinely need time-decoupling, fan-out across services, or durability.


Interview framing / how to sound senior#

The 20-second answer: "They're at different layers, so it's a bit of a false comparison. A message bus is a communication pattern/abstraction — a shared contract that endpoints plug into, carrying commands, events, and replies; it can be in-process or distributed. An event broker is infrastructure — a server that durably stores and routes events via pub/sub. In practice you often build a bus on top of a broker: Spring Cloud Bus and Spring Cloud Stream are bus abstractions running on Kafka or RabbitMQ."

Then show range:

  • Anchor on command vs. event — "a bus is message-type-agnostic; a broker is optimized for events" — and you've explained 80% of the difference from first principles.
  • "Is Kafka a message bus or an event broker?" → "Kafka is a broker (infrastructure). You can use it to implement a message bus by putting a bus abstraction over it. People loosely call it a 'message bus,' but precisely it's the transport, not the contract."
  • Bring up smart endpoints, dumb pipes and the ESB's decline — shows you know the history and why the industry moved.
  • Name the failure-semantics difference (in-process bus = synchronous, non-durable, dies with the process; broker = durable, replayable, at-least-once). This is the practical crux and most candidates miss it.
  • Land the "they compose" point. The senior signal is refusing the false binary and drawing the stack.

Common wrong answers to avoid:

  • "A message bus and an event broker are the same thing." (Layer confusion.)
  • "Spring @EventListener is asynchronous." (It's synchronous by default.)
  • "Kafka gives you exactly-once end-to-end." (Only in-cluster; design for at-least-once + idempotency.)
  • "Events and commands are interchangeable." (Cardinality and coupling differ; it dictates bus-vs-broker fit.)
  • "ESB = message bus = good enterprise architecture." (Know why smart pipes fell out of favor.)

Cheat-sheet#

QuestionFast answer
Bus vs. broker in one lineBus = communication pattern/abstraction; broker = infrastructure that stores & routes events.
What travelsBus: commands + events + replies (shared contract). Broker: events (pub/sub).
Command vs. eventCommand = "do this," one handler, coupled. Event = "happened," 0..N subscribers, decoupled.
Do they compete?No — a bus is usually built on a broker (Spring Cloud Stream/Bus over Kafka/Rabbit).
In-process bus exampleSpring ApplicationEventPublisher + @EventListener; Guava EventBus; Axon buses.
Distributed bus exampleSpring Cloud Bus, Vert.x EventBus, MassTransit/NServiceBus.
Event broker exampleKafka, Pulsar, RabbitMQ, NATS, EventBridge, Solace, Pub/Sub, Event Hubs.
Spring events defaultSynchronous, caller's thread and transaction. Not async, not durable.
Side-effect listener@TransactionalEventListener(AFTER_COMMIT); go durable via outbox + broker.
Kafka fan-out controlDifferent groupId = own copy; same groupId = load-balanced.
Ordering guaranteePer-partition/queue only. Key by entity to keep its events ordered.
Delivery defaultAt-least-once → make consumers idempotent. EOS end-to-end ≈ myth.
ESB's sinSmart pipe (logic in the bus) → distributed monolith. Prefer dumb pipe, smart endpoints.

Self-test#

  1. A colleague says "let's put a message bus between our two microservices." What two clarifying questions do you ask before agreeing? (Hint: in-process vs. distributed; command vs. event; is a broker implied?)
  2. Why is publisher.publishEvent(...) inside an @Transactional method a footgun if the listener sends an email? How does @TransactionalEventListener(AFTER_COMMIT) change it — and what new hazard does an AFTER_COMMIT listener that writes to the DB introduce?
  3. You need three services to react to OrderPlaced, and a fourth service must be the only one to run chargePayment. Which of these is an event and which is a command, and how does that decision shape your Kafka topic/consumer-group design?
  4. Explain "smart endpoints, dumb pipes." How does an event broker embody it and an ESB violate it?
  5. Someone claims their Kafka pipeline is exactly-once because they enabled idempotent producers. Where does that guarantee stop, and what must the consumer still do?
  6. Draw the stack for Spring Cloud Bus broadcasting a config refresh. Which part is the "bus" and which is the "broker"? Why is calling the whole thing "a message bus" imprecise?
  7. Two events for the same orderId are processed out of order by a consumer. Give the most likely cause and the fix. Now: two events for different orders interleave — is that a bug?
  8. When is an in-process message bus strictly better than an event broker, and when does choosing it become a lost-message incident waiting to happen?

Lineage / sources note: "Message Bus" and the command/event distinction are from Hohpe & Woolf, Enterprise Integration Patterns (2003). "Smart endpoints and dumb pipes" and the ESB critique are from Martin Fowler & James Lewis, Microservices (2014). "Event broker" as terminology was popularized by the event-driven-architecture community (notably Solace). Framework specifics — Spring events' synchronous/transactional default, @TransactionalEventListener, Spring Cloud Bus, Spring Cloud Stream binders, Spring for Apache Kafka, Spring AMQP — are stable APIs but version-dependent in details (e.g., the Spring Cloud Bus actuator endpoint is busrefresh in newer releases and bus-refresh in older ones). Verify against the exact Spring Boot / Spring Cloud versions you're running before quoting endpoint names or defaults in an interview.