On this page
- 1. · Concept & Definition
- 2. · Why It Matters
- 3. · How It Works
- 4. · Java / Jakarta EE Code Examples
- Deep Dive — Messaging Versus Synchronous Calls
- Deep Dive — Commands and Events
- 5. · Configuration, Defaults & Diagnostics
- 6. · Production Usage Patterns
- 7. · Common Pitfalls & Misconceptions
- 8. · Best Practices
- 9. · Architect's Perspective & Trade-offs
- 10. · Interview Q&A
1· Concept & Definition
Message-oriented middleware (MOM) is infrastructure that accepts a message from a producer, stores it, and delivers it to one or more consumers. The broker is the server that does this. Messages are addressed to a destination, which is either a queue (each message goes to exactly one consumer) or a topic (each message goes to every subscriber). The defining property is that sender and receiver never interact directly.

2· Why It Matters
- It removes temporal coupling: the receiver may be restarting, redeploying or overloaded, and the sender is unaffected.
- It levels load: a burst that would overwhelm a synchronous service becomes queue depth instead of errors.
- It enables fan-out: adding a new consumer of an event requires no change to the producer.
- It changes the failure model: instead of an immediate error the caller gets nothing, so progress must be tracked elsewhere.
- It introduces duplicates, reordering and eventual consistency, which the consumer must handle (Chapter 2).
- It adds a system to operate — storage, clustering, credentials, monitoring — that must be designed for, not assumed.
3· How It Works
MOM
| Property | Synchronous call (HTTP/gRPC) | Messaging |
|---|---|---|
| Availability requirement | Both sides up at the same time | Only the broker |
| Back pressure | Errors, timeouts, retries by the caller | Queue depth, consumer pace |
| Coupling | Caller knows the callee's address and contract | Both know a destination and a message contract |
| Result | Returned immediately | Arrives later, or via a reply message |
| Failure visibility | Immediate status code | Delayed; needs monitoring and dead-letter handling |
| Fan-out | Caller calls each service | Publish once, any number of subscribers |
Producer
A producer creates a message and sends it to a destination. It chooses the delivery mode (persistent or not), an optional time to live, a priority, and any properties used for routing or filtering. A send is complete when the broker acknowledges it — for persistent messages that means the message is on disk — so a producer must still handle send failures, and must decide what happens to the business operation if the send fails.
Consumer
| Consumption style | API | Characteristics |
|---|---|---|
| Message-driven bean | @MessageDriven + onMessage | Container-managed, pooled, transactional — the Jakarta EE default |
| Asynchronous listener | setMessageListener | Callback per message; the application manages threads |
| Synchronous receive | receive() / receive(timeout) | Blocking pull; simple, used in batch and tests |
| No-wait receive | receiveNoWait() | Returns null when the queue is empty |
| Shared subscription | createSharedConsumer / createSharedDurableConsumer | Several instances share one topic subscription (JMS 2.0+) |
Broker
| Broker responsibility | What it means in production |
|---|---|
| Durable storage | Journal or database; disk throughput bounds persistent send rate |
| Routing | Queues, topics, address settings, wildcard hierarchies |
| Redelivery | Retries with delay, delivery counters, dead-letter addresses |
| Flow control | Producer blocking or paging when memory limits are reached |
| Clustering | Replication or shared store for failover; client reconnection |
| Security | Authentication, per-destination authorisation, TLS |
Queue and Topic

| Aspect | Queue | Topic |
|---|---|---|
| Delivery | Exactly one consumer receives each message | Every subscriber receives a copy |
| Scaling | Add consumers to increase throughput | Add subscribers to add behaviour |
| Retention | Message stays until consumed or expired | Kept only for durable subscriptions |
| Ordering | Per queue, broken by competing consumers | Per subscription |
| Typical content | Commands and tasks | Events and notifications |
| Coupling | Sender expects the work to be done | Publisher does not know the reactions |

4· Java / Jakarta EE Code Examples
@ApplicationScoped
public class OrderEvents {
// injected, container-managed
@Inject JMSContext context;
@Resource(lookup = "java:/jms/topic/orders.events")
private Topic ordersEvents;
@Resource(lookup = "java:/jms/queue/orders.commands")
private Queue ordersCommands;
public void publishPlaced(long orderId, String payload) { // event → topic
context.createProducer()
.setDeliveryMode(DeliveryMode.PERSISTENT)
.setProperty("eventType", "OrderPlaced")
.setProperty("orderId", orderId)
.send(ordersEvents, payload);
}
public void requestPicking(String payload) { // command → queue
context.createProducer()
.setTimeToLive(Duration.ofMinutes(10).toMillis())
.send(ordersCommands, payload);
}
}@MessageDriven(activationConfig = {
@ActivationConfigProperty(propertyName = "destinationType",
propertyValue = "jakarta.jms.Queue"),
@ActivationConfigProperty(propertyName = "destinationLookup",
propertyValue = "java:/jms/queue/orders.commands"),
@ActivationConfigProperty(propertyName = "maxSession", propertyValue = "10")
})
public class PickingListener implements MessageListener {
@Inject PickingService picking;
@Override
// container transaction
public void onMessage(Message message) {
try {
// JMS 2.0+, no casting
String body = message.getBody(String.class);
picking.handle(body);
} catch (JMSException e) {
// rollback → redelivery
throw new EJBException(e);
}
}
}// Declaring destinations from the application, so deployment needs no manual setup
@JMSDestinationDefinition(
name = "java:/jms/queue/orders.commands",
interfaceName = "jakarta.jms.Queue",
destinationName = "ordersCommands")
@JMSDestinationDefinition(
name = "java:/jms/topic/orders.events",
interfaceName = "jakarta.jms.Topic",
destinationName = "ordersEvents")
@ApplicationScoped
public class MessagingConfig { }// Synchronous receive — batch jobs and tests
public Optional<String> drainOne() {
try (JMSContext ctx = factory.createContext(JMSContext.AUTO_ACKNOWLEDGE)) {
JMSConsumer consumer = ctx.createConsumer(ordersCommands);
String body = consumer.receiveBody(String.class, 2_000); // null after 2s
return Optional.ofNullable(body);
}
}# Broker view of the same destinations (Artemis CLI)
> queue stat
NAME ADDRESS CONSUMER_COUNT MESSAGE_COUNT MESSAGES_ADDED DELIVERING
ordersCommands ordersCommands 10 412 98,441 10
billing.ordersEvents ordersEvents 1 0 98,441 0
DLQ DLQ 0 37 37 0Deep Dive — Messaging Versus Synchronous Calls
The choice is not stylistic. It follows from what the caller needs at the moment of the call.
| Question | If yes | If no |
|---|---|---|
| Does the caller need the result to continue? | Synchronous call | Message |
| Must the work survive the callee being down? | Message | Either |
| Can the work be retried safely? | Message with at-least-once | Reconsider the design |
| Do several systems need to react? | Topic | Queue or call |
| Is the load spiky? | Message (queue absorbs it) | Either |
| Is an answer needed within a request timeout? | Synchronous call | Message |
A common hybrid: the request is handled synchronously up to the point where a durable decision is recorded, and everything that can happen afterwards — notifications, projections, downstream updates — travels as messages. The user gets an immediate answer, and the slow or unreliable parts no longer sit in the request path.
Deep Dive — Commands and Events
| Command | Event | |
|---|---|---|
| Meaning | Do this | This happened |
| Naming | Imperative: ReservePicking | Past tense: OrderPlaced |
| Destination | Queue | Topic |
| Consumers | Exactly one logical handler | Any number, unknown to the publisher |
| Failure | Sender expects the work to happen | Publisher is unaffected |
| Coupling | Sender knows what should happen | Publisher knows only what occurred |
| Versioning | Handler and sender evolve together | Must tolerate unknown consumers |
Mixing the two is the most common modelling error in messaging systems: publishing SendInvoiceEmail as an "event" makes the publisher responsible for downstream behaviour, and consuming OrderPlaced as if it were a command makes every new subscriber a change to the producer's meaning.
5· Configuration, Defaults & Diagnostics
| Setting | Where | Typical value / note |
|---|---|---|
| Connection factory | Server resource or @JMSConnectionFactory | Pooled; separate XA factory if JTA is required |
maxSession | MDB activation config | Consumer concurrency per instance |
| Delivery mode | Producer | PERSISTENT by default — decide per message type |
| Time to live | Producer | Bound staleness; avoids infinite backlogs |
| Address settings | Broker | Max size, paging, redelivery delay, DLQ address |
| Prefetch / consumer window | Client or broker | Large values harm fairness across consumers |
| Symptom | Likely cause |
|---|---|
| Queue depth grows steadily | Consumers slower than producers, or all failing |
| Messages consumed but nothing happens | Exception swallowed in onMessage |
| No consumer receives a topic message | Non-durable subscriber was offline |
JMSRuntimeException: connection failure | Broker restart without client reconnection configured |
| Producer blocks | Broker flow control — address size limit reached |
6· Production Usage Patterns
- Queues for commands, topics for events, with naming conventions that make the difference visible.
- Destinations declared as deployment artefacts (definitions or infrastructure as code), not created by hand.
- Message-driven beans for consumption, so transactions and pooling are container-managed.
- A schema or contract per message type, versioned alongside the service.
- Dashboards for depth, consumer count, delivery rate and dead-letter depth on every destination.
7· Common Pitfalls & Misconceptions
- Using messaging where the caller must wait — adding latency and a correlation problem for nothing.
- Publishing commands as events, which moves responsibility to the wrong side.
- Assuming a topic keeps messages for subscribers that are offline.
- Ignoring send failures, leaving the business operation committed but unannounced (see the outbox pattern, Volume IV Chapter 6).
- No time to live, so a backlog of obsolete messages is processed hours later.
- Treating the broker as a database, keeping messages as the record of state.
8· Best Practices
- Model each message as a command or an event, and name it accordingly.
- Make the payload self-describing and versioned; never send internal entities.
- Set delivery mode and TTL deliberately for each destination.
- Instrument depth and lag from day one — they are the early warning for everything else.
9· Architect's Perspective & Trade-offs
| Option | Coupling | Latency | Failure handling | Fits |
|---|---|---|---|---|
| Synchronous HTTP | Tight | Lowest | Immediate error | Queries, user-facing answers |
| Queue (command) | Loose | Low to medium | Retry, dead-letter | Work that must happen once |
| Topic (event) | Loosest | Low to medium | Per subscriber | Notifying many systems |
| Log-based stream (Kafka) | Loose | Low | Offset replay | High volume, replay, analytics |
10· Interview Q&A
What is message-oriented middleware?
Message-oriented middleware is infrastructure, usually a message broker, that sits between applications and transfers messages from producers to consumers. A producer sends a message to a named destination; the broker stores it reliably and delivers it to one or more consumers, possibly much later. Because the broker holds the message, the producer and consumer never need to be running at the same time, and neither needs to know the other's location. Examples include ActiveMQ Artemis, IBM MQ and RabbitMQ, typically used through Jakarta Messaging. This decoupling in time and location is the main reason for using messaging.
What is the difference between a queue and a topic?
A queue implements point-to-point messaging: each message is delivered to exactly one consumer, even if several consumers are listening, so queues are used for work that must be done once, such as processing a payment. A topic implements publish-subscribe: each message is delivered to every current subscriber, so topics are used to broadcast facts that several systems care about, such as an order having been placed. Choosing between them is a design decision about who should act on a message. Many brokers also offer shared subscriptions, which combine a topic with several competing consumers per subscriber.
What are competing consumers?
Competing consumers are several consumer instances reading from the same queue. The broker gives each message to only one of them, so they share the workload and throughput scales by adding consumers; if one fails, the others continue. For example, five instances of an invoice service can process invoice requests in parallel. The trade-off is ordering: messages are processed concurrently by different consumers, so a later message can finish before an earlier one. If order matters for related messages, such as updates to the same account, use message groups or partitioning so each group is handled by one consumer at a time.
When should a message be a command rather than an event?
Use a command when the sender needs a specific action to be performed by a specific recipient, for example ChargeCard or SendInvoice. Commands are named in the imperative, go to a queue, are processed by exactly one consumer, and the sender usually cares about the outcome. Use an event when the sender is announcing that something has already happened, for example OrderPlaced or PaymentCaptured. Events are named in the past tense, are published to a topic, and the sender does not know or care who reacts. Mixing the two, such as an event that secretly expects one consumer to act, creates hidden coupling.
What happens to topic messages published while a subscriber is offline?
It depends on the subscription type. With a non-durable subscription, the broker delivers messages only while the subscriber is connected; anything published while it is offline is simply never delivered to it. With a durable subscription, the broker remembers the subscription and stores messages for it while the subscriber is disconnected, delivering them when it reconnects. Shared durable subscriptions, from JMS 2.0, let several consumer instances share one durable subscription. For any subscriber that must not miss events, such as a service updating its own data from OrderPlaced events, durable subscriptions are essential, and their backlog must be monitored.
How do you decide between messaging and a synchronous call?
Ask whether the caller needs the result to continue. If a user is waiting for a price quote or a stock check, call synchronously, with timeouts and fallbacks. If the work can complete later, such as sending a confirmation email or updating a search index, use messaging. Messaging is also the better choice when the work must survive the receiving system being down, when several systems need to react to the same fact, or when load spikes must be smoothed. The cost is eventual consistency and more complex error handling, so do not use messaging merely to avoid designing a good API.
What does introducing a broker cost an organisation?
A broker is a stateful, business-critical system that someone must operate. It needs durable storage, clustering and failover, backups, capacity planning and upgrades. It needs security: authentication, authorisation per destination and encryption. Teams need monitoring of queue depth, consumer lag and dead-letter queues, with alerts and runbooks for poison messages. Message formats need versioning and governance, because many systems depend on them. And debugging asynchronous flows requires correlation IDs and distributed tracing across producers and consumers. These costs are worth paying when decoupling is needed, but they should be planned and staffed from the start.
How does messaging change the consistency model of a system?
With synchronous calls inside one transaction, a change is either fully visible or not at all. With messaging, each system updates its own data when it processes a message, so the overall system becomes eventually consistent: for a while, the order may be placed but the stock not yet reserved. Intermediate states become visible to users and other services. Messages can arrive more than once or out of order, so consumers must be idempotent and tolerant of ordering. The design must define how long inconsistency may last, how users see pending states, and how reconciliation or compensating actions fix anything that goes wrong.