Event Bus Pattern
The event bus pattern covers any architecture where services communicate via an intermediate message broker rather than directly: Kafka, RabbitMQ, NATS, and similar systems. It underlies Saga Choreography, Event Sourcing, and CQRS.
The rule
Services PUBLISH events to topics via edges. Topics have SUBSCRIPTION edges to consumers. Services never call each other directly through the bus — they always go through the broker.
Compensating events and read-model updates travel through the same topics, not via direct service-to-service edges.
CORRECT: Producer → [Topic] → Consumer A
→ Consumer B
WRONG: Producer → Consumer A (direct call, bypasses bus)Kafka topology
Use type: 'pulse' for Kafka edges to visually distinguish them from synchronous HTTP connections.
Topology
const topology = new TopologyBuilder(true);
const producersGroup = topology.createGroup('producers', { label: 'Producers' });
producersGroup
.addProcess({ id: 'order-svc', label: 'Order Service', state: 'running' })
.addProcess({ id: 'payment-svc', label: 'Payment Service', state: 'running' })
.autoResize();
const kafkaGroup = topology.createGroup('kafka', { label: 'Kafka Cluster' });
kafkaGroup
.addProcess({ id: 'topic-orders', label: 'orders topic', shape: 'cylinder', state: 'running' })
.addProcess({ id: 'topic-payments', label: 'payments topic', shape: 'cylinder', state: 'running' })
.addProcess({ id: 'topic-events', label: 'events topic', shape: 'cylinder', state: 'running' })
.autoResize();
const consumersGroup = topology.createGroup('consumers', { label: 'Consumers' });
consumersGroup
.addProcess({ id: 'inventory-svc', label: 'Inventory Service', state: 'running' })
.addProcess({ id: 'notify-svc', label: 'Notification Service', state: 'running' })
.addProcess({ id: 'analytics-svc', label: 'Analytics Service', state: 'running' })
.autoResize();
// Producers publish TO topics (producers dial the broker)
topology.connect('order-svc', 'topic-orders', { protocol: 'kafka', type: 'pulse', label: 'publish' });
topology.connect('payment-svc', 'topic-payments', { protocol: 'kafka', type: 'pulse', label: 'publish' });
topology.connect('order-svc', 'topic-events', { protocol: 'kafka', type: 'pulse', label: 'publish' });
// Consumers connect TO broker (consumers dial the broker, broker pushes records)
topology.connect('inventory-svc', 'topic-orders', { protocol: 'kafka', type: 'pulse', label: 'consume' });
topology.connect('notify-svc', 'topic-orders', { protocol: 'kafka', type: 'pulse', label: 'consume' });
topology.connect('analytics-svc', 'topic-events', { protocol: 'kafka', type: 'pulse', label: 'consume' });Edge direction for Kafka consumers: Consumers open the TCP connection to the broker — they dial the broker, not the other way. Use connect('consumer', 'topic'), not connect('topic', 'consumer'). See Edge Direction Guide.
Animation
const flow = new FlowBuilder();
flow.scenario('order-placed', 'Order Placed', 'Order event fans out to multiple consumers');
flow
.from('order-svc').to('topic-orders')
.showMessage('[PUBLISH] OrderPlaced {orderId: "ORD-42", items: 3, total: 149.97}')
.parallel([
['topic-orders', 'inventory-svc'],
['topic-orders', 'notify-svc'],
])
.showMessage('[FANOUT] Two consumer groups receive the event simultaneously')
.from('inventory-svc').to('topic-events')
.showMessage('[PUBLISH] InventoryReserved {orderId: "ORD-42", sku: "SKU-7"}')
.from('analytics-svc').to('topic-events')
.showMessage('[CONSUME] Recording order metrics');
return flow;Saga Choreography
In a choreographed saga, each service listens for events and emits its own events in response. There is no central orchestrator — the saga state is implied by the sequence of events flowing through topics.
Topology
const topology = new TopologyBuilder(true);
const kafkaGroup = topology.createGroup('kafka', { label: 'Kafka' });
kafkaGroup
.addProcess({ id: 't-order', label: 'order-events', shape: 'cylinder', state: 'running' })
.addProcess({ id: 't-inventory', label: 'inventory-events', shape: 'cylinder', state: 'running' })
.addProcess({ id: 't-payment', label: 'payment-events', shape: 'cylinder', state: 'running' })
.addProcess({ id: 't-shipping', label: 'shipping-events', shape: 'cylinder', state: 'running' })
.autoResize();
const servicesGroup = topology.createGroup('services', { label: 'Domain Services' });
servicesGroup
.addProcess({ id: 'order-svc', label: 'Order Service', state: 'running' })
.addProcess({ id: 'inventory-svc', label: 'Inventory Svc', state: 'running' })
.addProcess({ id: 'payment-svc', label: 'Payment Service', state: 'running' })
.addProcess({ id: 'shipping-svc', label: 'Shipping Service', state: 'running' })
.autoResize();
// Each service publishes to its own topic and consumes from upstream topics
topology.connect('order-svc', 't-order', { protocol: 'kafka', type: 'pulse', label: 'publish' });
topology.connect('inventory-svc', 't-order', { protocol: 'kafka', type: 'pulse', label: 'consume' });
topology.connect('inventory-svc', 't-inventory', { protocol: 'kafka', type: 'pulse', label: 'publish' });
topology.connect('payment-svc', 't-inventory', { protocol: 'kafka', type: 'pulse', label: 'consume' });
topology.connect('payment-svc', 't-payment', { protocol: 'kafka', type: 'pulse', label: 'publish' });
topology.connect('shipping-svc', 't-payment', { protocol: 'kafka', type: 'pulse', label: 'consume' });
topology.connect('shipping-svc', 't-shipping', { protocol: 'kafka', type: 'pulse', label: 'publish' });CQRS with Event Sourcing
In CQRS, commands (writes) go through one path and queries (reads) go through another. The event store is the source of truth; read models are projections built from events.
Key topology rules
- The Command Handler publishes domain events to the event store / event bus
- Projectors consume events and update read models (SQL read DB, Elasticsearch, etc.)
- The Query Handler reads only from the read models, never from the event store directly
- There is no direct connection between the write side and the read side — they communicate exclusively through the event bus
// Write side
topology.connect('command-handler', 'event-store', { protocol: 'tcp', label: 'append event' });
topology.connect('command-handler', 'event-bus', { protocol: 'kafka', type: 'pulse', label: 'publish' });
// Projectors consume from event bus and update read models
topology.connect('projector-sql', 'event-bus', { protocol: 'kafka', type: 'pulse', label: 'consume' });
topology.connect('projector-es', 'event-bus', { protocol: 'kafka', type: 'pulse', label: 'consume' });
topology.connect('projector-sql', 'read-db', { protocol: 'postgresql', label: 'upsert' });
topology.connect('projector-es', 'search-idx', { protocol: 'http', label: 'index' });
// Read side
topology.connect('query-handler', 'read-db', { protocol: 'postgresql', label: 'SELECT' });
topology.connect('query-handler', 'search-idx', { protocol: 'http', label: 'search' });