Definition
Solace PubSub+ and Kafka handle topic routing in fundamentally different ways.
- Solace PubSub+ – an enterprise message broker that routes messages using hierarchical, slash-separated topic strings with level-aware wildcard subscriptions evaluated by the broker itself.
- Hierarchical topic – a publish-time string like
txn/IN/Mumbai/savings/largewhere each slash-separated level carries a routing dimension. - **Single-level wildcard (*)** – matches exactly one level in a Solace topic subscription.
- Multi-level wildcard (>) – matches one or more remaining levels at the end of a Solace subscription.
- Kafka flat topic – a pre-created, named, partitioned log where the broker does no content-based routing and consumers read entire partitions.
At National Bank of India, Solace is like a mail system where every letter has a precise hierarchical address: transactions, Mumbai branch, savings accounts, large amounts. A desk can say "give me everything from Mumbai regardless of type" and the sorting office understands the structure and delivers only matching letters. Kafka is like having a few big labelled bins: everything goes into the transactions bin, and if you only want Mumbai large transactions you must pull out every letter and check it yourself, because the bin does not understand the address structure.
Understanding the Concepts
What is Solace PubSub+?
Solace PubSub+ is an enterprise message broker whose defining feature is hierarchical topic routing with level-aware wildcard subscriptions. Unlike Kafka where topics are pre-created objects, a Solace topic is a string attached to each message at publish time. The broker evaluates the topic against all active subscriptions and delivers each message only to interested subscribers.
At National Bank of India, the trading floor uses Solace to distribute transaction events where each desk receives only the specific slice it cares about.
package com.bank.solace;
import com.solacesystems.jcsmp.JCSMPFactory;
import com.solacesystems.jcsmp.JCSMPSession;
import com.solacesystems.jcsmp.JCSMPProperties;
import com.solacesystems.jcsmp.Topic;
import com.solacesystems.jcsmp.XMLMessageProducer;
import com.solacesystems.jcsmp.TextMessage;
import java.math.BigDecimal;
/**
* Rajesh publishes a transaction with a hierarchical topic.
* The broker routes it to desks whose subscriptions match.
*/
public class SolaceTransactionPublisher {
public static void main(String[] args) throws Exception {
JCSMPProperties properties = new JCSMPProperties();
properties.setProperty(JCSMPProperties.HOST, "solace-broker:55555");
properties.setProperty(JCSMPProperties.VPN_NAME, "nbi-vpn");
properties.setProperty(JCSMPProperties.USERNAME, "rajesh");
JCSMPSession session = JCSMPFactory.onlyInstance().createSession(properties);
session.connect();
XMLMessageProducer producer = session.getMessageProducer(null);
// Hierarchical topic: txn/{country}/{city}/{accountType}/{size}
String topicString = "txn/IN/Mumbai/savings/large";
Topic topic = JCSMPFactory.onlyInstance().createTopic(topicString);
TextMessage message = JCSMPFactory.onlyInstance().createMessage(TextMessage.class);
BigDecimal amount = new BigDecimal("750000.00");
message.setText("{\"account\":\"SAV2001\",\"amount\":" + amount + "}");
producer.send(message, topic);
System.out.println("Published to topic: " + topicString);
System.out.println("Broker routes to matching subscriptions only.");
session.closeSession();
}
}
Expected output:
Published to topic: txn/IN/Mumbai/savings/large
Broker routes to matching subscriptions only.
How do Solace wildcard subscriptions work?
Solace has two wildcard characters for subscriptions.
The single-level wildcard * matches exactly one topic level. A subscription to txn/IN/*/savings/* matches txn/IN/Mumbai/savings/large and txn/IN/Delhi/savings/small, but not txn/IN/Mumbai/current/large because the fourth level does not match.
The multi-level wildcard > matches one or more remaining levels and can only appear at the end. A subscription to txn/IN/Mumbai/> matches txn/IN/Mumbai/savings/large, txn/IN/Mumbai/current/small, and even deeper paths like txn/IN/Mumbai/savings/large/priority.
package com.bank.solace;
import com.solacesystems.jcsmp.JCSMPFactory;
import com.solacesystems.jcsmp.JCSMPSession;
import com.solacesystems.jcsmp.JCSMPProperties;
import com.solacesystems.jcsmp.Topic;
/**
* Priya subscribes to all Mumbai transactions using multi-level wildcard.
* Pankaj subscribes to all large savings transactions using single-level wildcards.
*/
public class SolaceWildcardSubscriptions {
public static void main(String[] args) throws Exception {
JCSMPProperties properties = new JCSMPProperties();
properties.setProperty(JCSMPProperties.HOST, "solace-broker:55555");
properties.setProperty(JCSMPProperties.VPN_NAME, "nbi-vpn");
properties.setProperty(JCSMPProperties.USERNAME, "priya");
JCSMPSession session = JCSMPFactory.onlyInstance().createSession(properties);
session.connect();
// Priya: all Mumbai transactions regardless of type or size
Topic mumbaiAll = JCSMPFactory.onlyInstance().createTopic("txn/IN/Mumbai/>");
session.addSubscription(mumbaiAll);
System.out.println("Priya subscribed: txn/IN/Mumbai/>");
System.out.println(" Matches: txn/IN/Mumbai/savings/large");
System.out.println(" Matches: txn/IN/Mumbai/current/small");
System.out.println(" No match: txn/IN/Delhi/savings/large");
System.out.println();
// Pankaj: all large savings from any city in India
Topic largeSavings = JCSMPFactory.onlyInstance().createTopic("txn/IN/*/savings/large");
session.addSubscription(largeSavings);
System.out.println("Pankaj subscribed: txn/IN/*/savings/large");
System.out.println(" Matches: txn/IN/Mumbai/savings/large");
System.out.println(" Matches: txn/IN/Delhi/savings/large");
System.out.println(" No match: txn/IN/Mumbai/savings/small");
session.closeSession();
}
}
Expected output:
Priya subscribed: txn/IN/Mumbai/>
Matches: txn/IN/Mumbai/savings/large
Matches: txn/IN/Mumbai/current/small
No match: txn/IN/Delhi/savings/large
Pankaj subscribed: txn/IN/*/savings/large
Matches: txn/IN/Mumbai/savings/large
Matches: txn/IN/Delhi/savings/large
No match: txn/IN/Mumbai/savings/small
How does Kafka's flat topic model work?
A Kafka topic is a flat, named, pre-created, partitioned log. The broker does not understand any structure inside the topic name or the message content. A producer writes to a topic and partition, and a consumer reads every record in its assigned partitions.
If you want only Mumbai large savings transactions from a broad bank.transactions topic, you must filter in the consumer application.
package com.bank.kafka;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.kafka.clients.consumer.ConsumerRecords;
import org.apache.kafka.clients.consumer.KafkaConsumer;
import java.time.Duration;
import java.util.Collections;
import java.util.Properties;
/**
* Kafka consumer must filter records manually.
* The broker sends everything; the consumer decides what to keep.
*/
public class KafkaFlatTopicConsumer {
public static void main(String[] args) {
Properties props = new Properties();
props.put("bootstrap.servers", "kafka-1:9092");
props.put("group.id", "mumbai-desk");
props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
try (KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props)) {
// Subscribe to the flat topic: receive ALL transactions
consumer.subscribe(Collections.singletonList("bank.transactions"));
int total = 0;
int matched = 0;
ConsumerRecords<String, String> records = consumer.poll(Duration.ofSeconds(5));
for (ConsumerRecord<String, String> record : records) {
total++;
// Manual filtering: only keep Mumbai large savings
if (record.value().contains("Mumbai")
&& record.value().contains("savings")
&& record.value().contains("large")) {
matched++;
System.out.println("Matched: " + record.value());
}
// All other records are discarded by the consumer
}
System.out.println("Total received: " + total + ", matched: " + matched);
System.out.println("Wasted network/CPU on " + (total - matched) + " irrelevant records");
}
}
}
Expected output:
Matched: {"city":"Mumbai","type":"savings","size":"large","amount":750000.00}
Total received: 1000, matched: 12
Wasted network/CPU on 988 irrelevant records
How is Solace different from Kafka?
The two systems differ in where routing intelligence lives and what trade-offs each makes.
| Feature | Solace PubSub+ | Kafka |
|---|---|---|
| Topic model | Hierarchical string, publish-time | Flat, pre-created, partitioned log |
| Topic creation | No creation step, any string is valid | Must be created with partition count and RF |
| Routing | Broker evaluates wildcard subscriptions | No broker-side routing, consumer filters |
| Wildcards | * (one level), > (remaining levels) | Regex on topic names only, not content |
| Filtering CPU | In the broker | In the consumer |
| Retention and replay | Not the primary strength | Core feature: days/weeks/years |
| Throughput | Optimized for fan-out and low latency | Optimized for sequential log throughput |
| Stream processing | External processor needed | Kafka Streams, ksqlDB built-in |
package com.bank.comparison;
public class SolaceVsKafkaRouting {
public static void main(String[] args) {
System.out.println("=== Routing: Where does filtering happen? ===");
System.out.println();
System.out.println("Solace: broker filters, consumer receives only matches");
System.out.println(" Subscribe: txn/IN/Mumbai/>");
System.out.println(" Receive: only Mumbai transactions");
System.out.println(" CPU cost: in the broker");
System.out.println();
System.out.println("Kafka: consumer filters, receives everything");
System.out.println(" Subscribe: bank.transactions (all records)");
System.out.println(" Filter: if (record.contains('Mumbai'))");
System.out.println(" CPU cost: in the consumer");
}
}
Expected output:
=== Routing: Where does filtering happen? ===
Solace: broker filters, consumer receives only matches
Subscribe: txn/IN/Mumbai/>
Receive: only Mumbai transactions
CPU cost: in the broker
Kafka: consumer filters, receives everything
Subscribe: bank.transactions (all records)
Filter: if (record.contains('Mumbai'))
CPU cost: in the consumer
What are the three ways to approximate Solace routing in Kafka?
Since Kafka does not do broker-side content routing, you have three options to get similar behavior, each with a trade-off.
Option 1: Many fine-grained topics. Create a separate Kafka topic per routing combination, like txn.in.mumbai.savings.large. This pushes routing to publish time but explodes topic count.
Option 2: One broad topic, consumer-side filtering. Publish everything to bank.transactions and let each consumer discard what it does not need. Simple but wastes network and CPU.
Option 3: Stream processing layer. Use Kafka Streams or ksqlDB to read the broad topic, branch by fields, and republish into narrower topics. Precise but adds infrastructure.
package com.bank.kafka;
public class KafkaRoutingOptions {
public static void main(String[] args) {
System.out.println("Option 1: Many topics (topic proliferation)");
System.out.println(" Topics: txn.in.mumbai.savings.large, txn.in.delhi.current.small, ...");
System.out.println(" Problem: 3 types x 50 cities x 3 sizes = 450 topics!");
System.out.println();
System.out.println("Option 2: One topic, consumer filters");
System.out.println(" Topic: bank.transactions");
System.out.println(" Problem: every consumer downloads everything");
System.out.println();
System.out.println("Option 3: Stream processing layer");
System.out.println(" Kafka Streams reads bank.transactions");
System.out.println(" Branches by city/type/size into narrow topics");
System.out.println(" Problem: extra infrastructure to operate");
}
}
Expected output:
Option 1: Many topics (topic proliferation)
Topics: txn.in.mumbai.savings.large, txn.in.delhi.current.small, ...
Problem: 3 types x 50 cities x 3 sizes = 450 topics!
Option 2: One topic, consumer filters
Topic: bank.transactions
Problem: every consumer downloads everything
Option 3: Stream processing layer
Kafka Streams reads bank.transactions
Branches by city/type/size into narrow topics
Problem: extra infrastructure to operate
What are common mistakes?
Wrong: trying to use Kafka topic names as a routing hierarchy.
Some teams create topics like txn.IN.Mumbai.savings.large and use regex subscription (Pattern.compile("txn\\.IN\\.Mumbai\\..*")) to subscribe. But Kafka regex subscription resolves to full topics at subscription time, not per-message. Every matched topic delivers all its records.
package com.bank.kafka;
public class WrongKafkaHierarchy {
public static void main(String[] args) {
System.out.println("WRONG: Using Kafka topic names as hierarchy");
System.out.println(" Created: txn.IN.Mumbai.savings.large (1 topic)");
System.out.println(" Created: txn.IN.Mumbai.savings.small (1 topic)");
System.out.println(" Created: txn.IN.Mumbai.current.large (1 topic)");
System.out.println(" ... 450 topics total");
System.out.println(" Result: operational nightmare, partition explosion");
System.out.println();
System.out.println("CORRECT: Use Solace for fine-grained routing");
System.out.println(" Or: use one Kafka topic + consumer-side filtering");
System.out.println(" Or: use Kafka Streams to branch into a few key topics");
}
}
Expected output:
WRONG: Using Kafka topic names as hierarchy
Created: txn.IN.Mumbai.savings.large (1 topic)
Created: txn.IN.Mumbai.savings.small (1 topic)
Created: txn.IN.Mumbai.current.large (1 topic)
... 450 topics total
Result: operational nightmare, partition explosion
CORRECT: Use Solace for fine-grained routing
Or: use one Kafka topic + consumer-side filtering
Or: use Kafka Streams to branch into a few key topics
When should you use Solace vs Kafka?
Use Solace when consumers have diverse, frequently changing routing needs and you want the broker to deliver only matching messages. Use Kafka when you need durable retention, replay, stream processing, and high sequential throughput.
A common bank pattern is to use both: Solace for real-time selective routing to trading desks and Kafka as the durable event backbone.
package com.bank.comparison;
public class WhenToUseEach {
public static void main(String[] args) {
System.out.println("Choose Solace when:");
System.out.println(" - Many consumers want different slices");
System.out.println(" - Routing needs change frequently");
System.out.println(" - Low-latency fan-out to selective consumers");
System.out.println(" - Multi-protocol access (JMS, MQTT, AMQP, REST)");
System.out.println();
System.out.println("Choose Kafka when:");
System.out.println(" - Durable retention for days/weeks/years");
System.out.println(" - Replay capability (reset offset)");
System.out.println(" - Stream processing (Kafka Streams, ksqlDB)");
System.out.println(" - Very high sequential throughput");
}
}
Expected output:
Choose Solace when:
- Many consumers want different slices
- Routing needs change frequently
- Low-latency fan-out to selective consumers
- Multi-protocol access (JMS, MQTT, AMQP, REST)
Choose Kafka when:
- Durable retention for days/weeks/years
- Replay capability (reset offset)
- Stream processing (Kafka Streams, ksqlDB)
- Very high sequential throughput
What should a senior engineer know?
A senior engineer understands the operational trade-offs.
- Solace's broker-side matching uses a trie data structure keyed on topic levels. This gives near-constant matching time per level but means the broker must be sized for the subscription workload.
- Kafka pushes filtering cost to consumers, which keeps the broker simple and fast but means every consumer downloads and scans everything.
- Kafka's regex topic subscription resolves at subscription time to whole topics, not per-message. It is not equivalent to Solace wildcards.
- A common architecture runs Solace for real-time fan-out and Kafka for durable retention, sometimes bridged together so events flow through both.
- Solace supports guaranteed messaging with persistent queues, not just direct pub-sub, but its retention and replay model is different from Kafka's partitioned log.
package com.bank.comparison;
public class SeniorSolaceKafka {
public static void main(String[] args) {
System.out.println("Senior nuances:");
System.out.println("1. Solace trie-based matching: O(levels) per message");
System.out.println("2. Kafka: no broker matching, consumer does all filtering");
System.out.println("3. Kafka regex subscription != Solace wildcards");
System.out.println("4. Common pattern: Solace routes, Kafka stores");
System.out.println("5. Solace has guaranteed messaging (persistent queues)");
System.out.println("6. Size Solace broker for subscription fan-out workload");
}
}
Expected output:
Senior nuances:
1. Solace trie-based matching: O(levels) per message
2. Kafka: no broker matching, consumer does all filtering
3. Kafka regex subscription != Solace wildcards
4. Common pattern: Solace routes, Kafka stores
5. Solace has guaranteed messaging (persistent queues)
6. Size Solace broker for subscription fan-out workload
Visual Overview
Solace Hierarchical Routing:
Publisher ──► txn/IN/Mumbai/savings/large
│
┌────────▼────────┐
│ Solace Broker │
│ (trie matcher) │
└────────┬────────┘
│
┌─────────────┼─────────────┐
│ │ │
txn/IN/Mumbai/> txn/IN/*/savings/large txn/IN/Delhi/>
(Priya: all (Pankaj: all large (no match,
Mumbai txns) savings across not delivered)
India)
─────────────────────────────────────────────────
Kafka Flat Topic:
Publisher ──► bank.transactions [partition 0][partition 1][partition 2]
│
┌────────▼────────┐
│ Kafka Broker │
│ (no filtering) │
└────────┬────────┘
│
┌─────────────┼─────────────┐
│ │ │
Consumer A Consumer B Consumer C
(reads ALL, (reads ALL, (reads ALL,
filters in filters in filters in
app code) app code) app code)
Key Takeaway
Solace PubSub+ is an enterprise message broker whose defining feature is hierarchical topic routing with level-aware wildcard subscriptions. Publishers attach structured, slash-separated topic strings to each message, and subscribers express interest using wildcards: * matches exactly one level and > matches one or more remaining levels. The broker itself evaluates every message against all active subscriptions and delivers only matching messages, so the routing intelligence lives in the broker and consumers stay thin.
Kafka takes the opposite approach and keeps the broker deliberately simple. A Kafka topic is a flat, pre-created, partitioned log, the broker never interprets structure inside the topic name or the message content, and a consumer subscribes to a whole topic and reads every record in its partitions. If you want only a narrow slice, you either filter in the consumer, create many fine-grained topics, or add a stream-processing layer.
At National Bank of India, the difference was practical. Desks that needed specific slices of the live transaction feed, like all Mumbai events or all large savings transactions, used Solace subscriptions and the broker delivered exactly the matching messages. The same feed flowed into a single flat Kafka topic for seven-year compliance retention and analytics, where each consumer filtered in application code.
Remember one sentence: Solace routes at the broker with hierarchical wildcards so consumers receive only what they asked for; Kafka keeps the broker simple with flat topics and pushes filtering to the consumer in exchange for massive throughput and durable replay.
---