Apache Pulsar implements a flexible pub-sub messaging model that supports multiple messaging patterns, from traditional queue semantics to streaming workloads.
Topics serve as named channels for message transmission
Subscriptions track which messages have been consumed
Unlike traditional message queues, Pulsar’s pub-sub model allows multiple independent subscriptions on the same topic, each maintaining its own consumption state.
From pulsar-client-api/src/main/java/org/apache/pulsar/client/api/Message.java, Pulsar messages contain:
public interface Message<T> { // Core message components T getValue(); // Deserialized message payload byte[] getData(); // Raw payload bytes MessageId getMessageId(); // Unique message identifier // Metadata String getKey(); // Optional message key byte[] getOrderingKey(); // Key for ordering in Key_Shared mode Map<String, String> getProperties(); // User-defined properties // Timestamps long getPublishTime(); // When producer sent the message long getEventTime(); // Application-defined event timestamp // Producer information String getProducerName(); // Name of the producing client String getTopicName(); // Topic where message was published // Advanced features long getSequenceId(); // Producer-assigned sequence int getRedeliveryCount(); // How many times redelivered byte[] getSchemaVersion(); // Schema version used Optional<EncryptionContext> getEncryptionCtx(); // Encryption info}
Messages are delivered exactly once using transactions (from pulsar-client-api/src/main/java/org/apache/pulsar/client/api/transaction/Transaction.java):
// Using transactions for exactly-once semanticsTransaction txn = client.newTransaction() .withTransactionTimeout(1, TimeUnit.MINUTES) .build() .get();try { // Produce and consume within transaction producer.newMessage(txn).value(data).send(); consumer.acknowledgeAsync(msgId, txn).get(); // Commit - messages visible atomically txn.commit().get();} catch (Exception e) { // Abort - no messages are visible txn.abort().get();}
// From Consumer.java - negativeAcknowledge methodconsumer.negativeAcknowledge(msg);// Message will be redelivered after configured delay
Negative acknowledgments trigger redelivery after a delay (configurable via negativeAckRedeliveryDelay). Use this when you want to retry processing a message.
For partitioned topics, producers can control message routing:
// Key-based routing (default for partitioned topics)producer.newMessage() .key("user-123") .value(data) .send();// All messages with same key go to same partition// Custom routingproducer.newMessage() .orderingKey(customKey.getBytes()) .value(data) .send();