← All articlesPlatform Engineering

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...

Y
Yash Pritwani
14 min read

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:

Loose coupling: Services do not know about each other
Scalability: Add consumers without modifying producers
Resilience: If a consumer is down, messages queue up
Audit trail: Events form a natural log of what happened

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: 128m

NATS 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: 512m

RabbitMQ 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

Feature
NATS
RabbitMQ

|---------|------|----------|

Protocol
Custom (text-based)
AMQP 0-9-1
Memory footprint
10-30MB
200-500MB
Messages/sec (single node)
10M+
50K-100K
Persistence
JetStream
Built-in
Routing
Subject-based wildcards
Exchanges (direct, topic, fanout, headers)
Dead letter queue
JetStream advisory
Built-in
Priority queues
No
Yes
Message TTL
Stream-level
Per-message
Management UI
NATS surveyor
Built-in (excellent)
Client libraries
40+ languages
20+ languages
Clustering
Built-in (super cluster)
Clustering + Quorum queues
Setup complexity
Very low
Low
Best for
High-throughput, low-latency
Complex routing, enterprise

<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.

#event-driven#nats#rabbitmq#messaging#pub-sub

Need help with platform engineering?

TechSaaS provides expert consulting and managed services for cloud infrastructure, DevOps, and AI/ML operations.