Event-Driven Architecture with NATS and RabbitMQ
Build event-driven systems with NATS and RabbitMQ. Pub/sub patterns, message persistence, dead letter queues, and choosing between lightweight NATS and...
Event-Driven Architecture Fundamentals
Event-driven architecture (EDA) is a design pattern where services communicate by producing and consuming events. Instead of Service A calling Service B directly, Service A publishes an event ("user registered") and any interested service can react to it.
<div style="margin:2.5rem auto;max-width:600px;width:100%;text-align:center;"><svg viewBox="0 0 600 220" xmlns="http://www.w3.org/2000/svg" style="width:100%;height:auto;"><rect width="600" height="220" rx="12" fill="#1a1a2e"/><rect x="230" y="15" width="140" height="35" rx="8" fill="#6366f1" opacity="0.9"/><text x="300" y="38" text-anchor="middle" fill="#ffffff" font-size="12" font-family="system-ui" font-weight="bold">API Gateway</text><rect x="30" y="80" width="100" height="50" rx="8" fill="#3b82f6" opacity="0.8"/><text x="80" y="100" text-anchor="middle" fill="#ffffff" font-size="10" font-family="system-ui">Auth</text><text x="80" y="115" text-anchor="middle" fill="#ffffff" font-size="10" font-family="system-ui">Service</text><rect x="160" y="80" width="100" height="50" rx="8" fill="#a855f7" opacity="0.8"/><text x="210" y="100" text-anchor="middle" fill="#ffffff" font-size="10" font-family="system-ui">User</text><text x="210" y="115" text-anchor="middle" fill="#ffffff" font-size="10" font-family="system-ui">Service</text><rect x="290" y="80" width="100" height="50" rx="8" fill="#2dd4bf" opacity="0.8"/><text x="340" y="100" text-anchor="middle" fill="#1a1a2e" font-size="10" font-family="system-ui">Order</text><text x="340" y="115" text-anchor="middle" fill="#1a1a2e" font-size="10" font-family="system-ui">Service</text><rect x="420" y="80" width="100" height="50" rx="8" fill="#f59e0b" opacity="0.8"/><text x="470" y="100" text-anchor="middle" fill="#1a1a2e" font-size="10" font-family="system-ui">Payment</text><text x="470" y="115" text-anchor="middle" fill="#1a1a2e" font-size="10" font-family="system-ui">Service</text><line x1="265" y1="50" x2="80" y2="78" stroke="#e2e8f0" stroke-width="1" opacity="0.5"/><line x1="285" y1="50" x2="210" y2="78" stroke="#e2e8f0" stroke-width="1" opacity="0.5"/><line x1="315" y1="50" x2="340" y2="78" stroke="#e2e8f0" stroke-width="1" opacity="0.5"/><line x1="335" y1="50" x2="470" y2="78" stroke="#e2e8f0" stroke-width="1" opacity="0.5"/><ellipse cx="80" cy="175" rx="35" ry="12" fill="none" stroke="#3b82f6" stroke-width="1.5"/><line x1="45" y1="175" x2="45" y2="190" stroke="#3b82f6" stroke-width="1.5"/><line x1="115" y1="175" x2="115" y2="190" stroke="#3b82f6" stroke-width="1.5"/><ellipse cx="80" cy="190" rx="35" ry="12" fill="none" stroke="#3b82f6" stroke-width="1.5"/><line x1="80" y1="130" x2="80" y2="163" stroke="#94a3b8" stroke-width="1" stroke-dasharray="3,3"/><ellipse cx="340" cy="175" rx="35" ry="12" fill="none" stroke="#2dd4bf" stroke-width="1.5"/><line x1="305" y1="175" x2="305" y2="190" stroke="#2dd4bf" stroke-width="1.5"/><line x1="375" y1="175" x2="375" y2="190" stroke="#2dd4bf" stroke-width="1.5"/><ellipse cx="340" cy="190" rx="35" ry="12" fill="none" stroke="#2dd4bf" stroke-width="1.5"/><line x1="340" y1="130" x2="340" y2="163" stroke="#94a3b8" stroke-width="1" stroke-dasharray="3,3"/><rect x="155" y="160" width="150" height="30" rx="6" fill="#a855f7" opacity="0.3"/><text x="230" y="180" text-anchor="middle" fill="#a855f7" font-size="10" font-family="system-ui">Message Bus / Events</text><line x1="210" y1="130" x2="210" y2="158" stroke="#94a3b8" stroke-width="1" stroke-dasharray="3,3"/><line x1="470" y1="130" x2="470" y2="175" stroke="#94a3b8" stroke-width="1" stroke-dasharray="3,3"/><line x1="305" y1="175" x2="470" y2="175" stroke="#94a3b8" stroke-width="0.5" stroke-dasharray="3,3" opacity="0.3"/></svg><p style="margin-top:0.75rem;font-size:0.85rem;color:#94a3b8;font-style:italic;line-height:1.4;">Microservices architecture: independent services communicate through an API gateway and event bus.</p></div>
Benefits:
NATS: The Cloud-Native Messaging System
NATS is an incredibly lightweight messaging system. The server binary is 15MB, uses 10MB of RAM, and handles millions of messages per second.
# docker-compose.yml
services:
nats:
image: nats:2.10-alpine
container_name: nats
command: ["-js", "-sd", "/data", "-m", "8222"]
ports:
- "4222:4222" # Client connections
- "8222:8222" # HTTP monitoring
volumes:
- nats-data:/data
mem_limit: 128mNATS Core: Simple Pub/Sub
import { connect, StringCodec, JSONCodec } from 'nats';
const nc = await connect({ servers: 'nats://nats:4222' });
const jc = JSONCodec();
// Publisher
nc.publish('user.registered', jc.encode({
userId: '123',
email: '[email protected]',
name: 'Jane Doe',
timestamp: Date.now(),
}));
// Subscriber (receives messages while connected)
const sub = nc.subscribe('user.registered');
for await (const msg of sub) {
const data = jc.decode(msg.data);
console.log('New user:', data.email);
await sendWelcomeEmail(data);
}
// Wildcard subscriptions
nc.subscribe('user.>'); // user.registered, user.updated, user.deleted
nc.subscribe('orders.*.shipped'); // orders.123.shipped, orders.456.shipped
// Request-Reply (synchronous over async)
const response = await nc.request('auth.validate', jc.encode({ token: 'abc' }), {
timeout: 5000,
});
const authResult = jc.decode(response.data);NATS JetStream: Persistent Messaging
JetStream adds persistence, replay, and exactly-once delivery:
const js = nc.jetstream();
const jsm = await nc.jetstreamManager();
// Create a stream (persists messages)
await jsm.streams.add({
name: 'EVENTS',
subjects: ['events.>'],
retention: 'limits',
max_msgs: 1000000,
max_age: 7 * 24 * 60 * 60 * 1e9, // 7 days
storage: 'file',
num_replicas: 1,
});
// Publish to stream
await js.publish('events.order.created', jc.encode({
orderId: 'ord-789',
items: [{ sku: 'WIDGET-1', qty: 2 }],
}));
// Durable consumer (survives restarts, tracks position)
await jsm.consumers.add('EVENTS', {
durable_name: 'order-processor',
filter_subject: 'events.order.>',
ack_policy: 'explicit',
deliver_policy: 'all', // Start from the beginning
max_deliver: 3, // Max delivery attempts
});
const consumer = await js.consumers.get('EVENTS', 'order-processor');
const messages = await consumer.consume({ max_messages: 10 });
for await (const msg of messages) {
try {
const event = jc.decode(msg.data);
await processOrder(event);
msg.ack();
} catch (error) {
msg.nak(5000); // Negative ack, retry after 5 seconds
}
}<div style="margin:2.5rem auto;max-width:600px;width:100%;text-align:center;"><svg viewBox="0 0 600 180" xmlns="http://www.w3.org/2000/svg" style="width:100%;height:auto;"><rect width="600" height="180" rx="12" fill="#1a1a2e"/><rect x="20" y="20" width="70" height="35" rx="6" fill="#3b82f6" opacity="0.8"/><text x="55" y="42" text-anchor="middle" fill="#ffffff" font-size="10" font-family="system-ui">Web</text><rect x="20" y="65" width="70" height="35" rx="6" fill="#3b82f6" opacity="0.8"/><text x="55" y="87" text-anchor="middle" fill="#ffffff" font-size="10" font-family="system-ui">Mobile</text><rect x="20" y="110" width="70" height="35" rx="6" fill="#3b82f6" opacity="0.8"/><text x="55" y="132" text-anchor="middle" fill="#ffffff" font-size="10" font-family="system-ui">IoT</text><rect x="150" y="20" width="120" height="130" rx="10" fill="#6366f1" opacity="0.9"/><text x="210" y="50" text-anchor="middle" fill="#ffffff" font-size="12" font-family="system-ui" font-weight="bold">Gateway</text><line x1="165" y1="60" x2="255" y2="60" stroke="#ffffff" stroke-width="0.5" opacity="0.3"/><text x="210" y="80" text-anchor="middle" fill="#ffffff" font-size="9" font-family="system-ui">Rate Limit</text><text x="210" y="95" text-anchor="middle" fill="#ffffff" font-size="9" font-family="system-ui">Auth</text><text x="210" y="110" text-anchor="middle" fill="#ffffff" font-size="9" font-family="system-ui">Load Balance</text><text x="210" y="125" text-anchor="middle" fill="#ffffff" font-size="9" font-family="system-ui">Transform</text><text x="210" y="140" text-anchor="middle" fill="#ffffff" font-size="9" font-family="system-ui">Cache</text><rect x="340" y="15" width="95" height="35" rx="6" fill="#a855f7" opacity="0.8"/><text x="387" y="37" text-anchor="middle" fill="#ffffff" font-size="10" font-family="system-ui">Service A</text><rect x="340" y="60" width="95" height="35" rx="6" fill="#2dd4bf" opacity="0.8"/><text x="387" y="82" text-anchor="middle" fill="#1a1a2e" font-size="10" font-family="system-ui">Service B</text><rect x="340" y="105" width="95" height="35" rx="6" fill="#f59e0b" opacity="0.8"/><text x="387" y="127" text-anchor="middle" fill="#1a1a2e" font-size="10" font-family="system-ui">Service C</text><rect x="490" y="55" width="80" height="45" rx="6" fill="none" stroke="#e2e8f0" stroke-width="1"/><text x="530" y="82" text-anchor="middle" fill="#e2e8f0" font-size="10" font-family="system-ui">DB / Cache</text><defs><marker id="arrow7" markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto"><path d="M0,0 L8,3 L0,6" fill="#e2e8f0"/></marker></defs><line x1="92" y1="37" x2="148" y2="55" stroke="#e2e8f0" stroke-width="1" marker-end="url(#arrow7)"/><line x1="92" y1="82" x2="148" y2="85" stroke="#e2e8f0" stroke-width="1" marker-end="url(#arrow7)"/><line x1="92" y1="127" x2="148" y2="115" stroke="#e2e8f0" stroke-width="1" marker-end="url(#arrow7)"/><line x1="272" y1="55" x2="338" y2="32" stroke="#e2e8f0" stroke-width="1" marker-end="url(#arrow7)"/><line x1="272" y1="85" x2="338" y2="77" stroke="#e2e8f0" stroke-width="1" marker-end="url(#arrow7)"/><line x1="272" y1="115" x2="338" y2="122" stroke="#e2e8f0" stroke-width="1" marker-end="url(#arrow7)"/><line x1="437" y1="77" x2="488" y2="77" stroke="#e2e8f0" stroke-width="1" marker-end="url(#arrow7)"/></svg><p style="margin-top:0.75rem;font-size:0.85rem;color:#94a3b8;font-style:italic;line-height:1.4;">API gateway pattern: a single entry point handles auth, rate limiting, and routing to backend services.</p></div>
RabbitMQ: The Feature-Rich Broker
RabbitMQ provides advanced routing, dead letter queues, priority queues, and management UI out of the box.
# docker-compose.yml
services:
rabbitmq:
image: rabbitmq:3.13-management-alpine
container_name: rabbitmq
environment:
RABBITMQ_DEFAULT_USER: admin
RABBITMQ_DEFAULT_PASS: secret
ports:
- "5672:5672" # AMQP
- "15672:15672" # Management UI
volumes:
- rabbitmq-data:/var/lib/rabbitmq
mem_limit: 512mRabbitMQ Patterns
Direct Exchange (point-to-point):
import pika
import json
connection = pika.BlockingConnection(pika.ConnectionParameters('rabbitmq'))
channel = connection.channel()
# Declare exchange and queue
channel.exchange_declare(exchange='orders', exchange_type='direct', durable=True)
channel.queue_declare(queue='order-processing', durable=True)
channel.queue_bind(queue='order-processing', exchange='orders', routing_key='created')
# Publish
channel.basic_publish(
exchange='orders',
routing_key='created',
body=json.dumps({'orderId': '123', 'total': 49.99}),
properties=pika.BasicProperties(
delivery_mode=2, # Persistent message
content_type='application/json',
),
)
# Consume
def process_order(ch, method, properties, body):
order = json.loads(body)
print(f"Processing order {order['orderId']}")
# Process...
ch.basic_ack(delivery_tag=method.delivery_tag)
channel.basic_qos(prefetch_count=1)
channel.basic_consume(queue='order-processing', on_message_callback=process_order)
channel.start_consuming()Topic Exchange (pattern matching):
# Publisher
channel.exchange_declare(exchange='events', exchange_type='topic', durable=True)
channel.basic_publish(exchange='events', routing_key='user.registered.us',
body=json.dumps(user_data))
channel.basic_publish(exchange='events', routing_key='order.shipped.eu',
body=json.dumps(order_data))
# Subscriber (US users only)
channel.queue_bind(queue='us-notifications', exchange='events',
routing_key='user.*.us')
# Subscriber (all shipments)
channel.queue_bind(queue='shipping-tracker', exchange='events',
routing_key='order.shipped.*')Dead Letter Queue (handle failures):
# Main queue with DLQ
channel.queue_declare(
queue='order-processing',
durable=True,
arguments={
'x-dead-letter-exchange': 'dlx',
'x-dead-letter-routing-key': 'failed-orders',
'x-message-ttl': 300000, # 5 minute TTL
'x-max-length': 10000, # Max queue size
},
)
# Dead letter queue
channel.exchange_declare(exchange='dlx', exchange_type='direct')
channel.queue_declare(queue='failed-orders', durable=True)
channel.queue_bind(queue='failed-orders', exchange='dlx', routing_key='failed-orders')NATS vs RabbitMQ Comparison
|---------|------|----------|
<div style="margin:2.5rem auto;max-width:600px;width:100%;text-align:center;"><svg viewBox="0 0 600 170" xmlns="http://www.w3.org/2000/svg" style="width:100%;height:auto;"><rect width="600" height="170" rx="12" fill="#1a1a2e"/><circle cx="60" cy="85" r="25" fill="#f59e0b" opacity="0.85"/><text x="60" y="82" text-anchor="middle" fill="#1a1a2e" font-size="9" font-family="system-ui" font-weight="bold">Trigger</text><text x="60" y="94" text-anchor="middle" fill="#1a1a2e" font-size="8" font-family="system-ui">webhook</text><polygon points="175,55 210,85 175,115 140,85" fill="#6366f1" opacity="0.85"/><text x="175" y="88" text-anchor="middle" fill="#ffffff" font-size="9" font-family="system-ui">If</text><rect x="250" y="35" width="100" height="40" rx="6" fill="#2dd4bf" opacity="0.85"/><text x="300" y="55" text-anchor="middle" fill="#1a1a2e" font-size="10" font-family="system-ui">Send Email</text><text x="300" y="67" text-anchor="middle" fill="#1a1a2e" font-size="8" font-family="system-ui">SMTP</text><rect x="250" y="95" width="100" height="40" rx="6" fill="#a855f7" opacity="0.85"/><text x="300" y="115" text-anchor="middle" fill="#ffffff" font-size="10" font-family="system-ui">Log Event</text><text x="300" y="127" text-anchor="middle" fill="#ffffff" font-size="8" font-family="system-ui">database</text><rect x="400" y="55" width="100" height="40" rx="6" fill="#3b82f6" opacity="0.85"/><text x="450" y="75" text-anchor="middle" fill="#ffffff" font-size="10" font-family="system-ui">Update CRM</text><text x="450" y="87" text-anchor="middle" fill="#ffffff" font-size="8" font-family="system-ui">API call</text><circle cx="545" cy="75" r="18" fill="none" stroke="#2dd4bf" stroke-width="2"/><text x="545" y="79" text-anchor="middle" fill="#2dd4bf" font-size="9" font-family="system-ui">Done</text><defs><marker id="arrow10" markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto"><path d="M0,0 L8,3 L0,6" fill="#e2e8f0"/></marker></defs><line x1="87" y1="85" x2="138" y2="85" stroke="#e2e8f0" stroke-width="1.5" marker-end="url(#arrow10)"/><line x1="210" y1="72" x2="248" y2="55" stroke="#e2e8f0" stroke-width="1.5" marker-end="url(#arrow10)"/><line x1="210" y1="98" x2="248" y2="115" stroke="#e2e8f0" stroke-width="1.5" marker-end="url(#arrow10)"/><line x1="352" y1="55" x2="398" y2="68" stroke="#e2e8f0" stroke-width="1.5" marker-end="url(#arrow10)"/><line x1="352" y1="115" x2="398" y2="82" stroke="#e2e8f0" stroke-width="1.5" marker-end="url(#arrow10)"/><line x1="502" y1="75" x2="525" y2="75" stroke="#e2e8f0" stroke-width="1.5" marker-end="url(#arrow10)"/><text x="225" y="45" text-anchor="middle" fill="#2dd4bf" font-size="8" font-family="system-ui">true</text><text x="225" y="120" text-anchor="middle" fill="#a855f7" font-size="8" font-family="system-ui">false</text></svg><p style="margin-top:0.75rem;font-size:0.85rem;color:#94a3b8;font-style:italic;line-height:1.4;">Workflow automation: triggers, conditions, and actions chain together to eliminate manual processes.</p></div>
When to Choose Each
Choose NATS when: You need ultra-high throughput (millions/sec), want minimal resource usage, your routing needs are simple (subject-based), or you are building cloud-native microservices.
Choose RabbitMQ when: You need complex routing (topic exchanges, headers), require dead letter queues and priority queues, need protocol compatibility (AMQP, MQTT, STOMP), or your team is already familiar with AMQP.
At TechSaaS, we use n8n for our event-driven workflows instead of a raw message broker. For client architectures that need messaging infrastructure, we recommend NATS for its simplicity and performance (15MB binary, 10MB RAM, handles millions of messages). RabbitMQ is our recommendation only when clients need its advanced routing and management UI for teams that prefer visual queue management.
Need help with platform engineering?
TechSaaS provides expert consulting and managed services for cloud infrastructure, DevOps, and AI/ML operations.