Mini Project: RabbitMQ Notification System
In this tutorial, you will learn about Mini Project: RabbitMQ Notification System. We cover key concepts, practical examples, and best practices to help you master this topic.
Build a complete notification system with RabbitMQ using topic exchanges, multiple consumer types, TLS security, and publisher confirms for reliable delivery.
What You Learn
You will build a production-ready notification system that routes different notification types to different handlers, implements reliable delivery with publisher confirms, and secures all communication with TLS.
Why It Matters
A notification system is a common real-world messaging application. This project combines all RabbitMQ concepts you learned: exchanges, routing, acknowledgements, QoS, persistence, and security. Completing it proves you can build production messaging systems.
Real-World Use
This is the same pattern Doda Browser uses for notifications. When a malware scan completes, the system publishes a notification. Different consumers handle email, SMS, and push notifications. The topic exchange routes each type to the correct handler.
System Architecture
flowchart TB
P[Notification Producer] --> TE[Topic Exchange
notifications]
TE -->|"email.*"| EQ[Email Queue]
TE -->|"sms.*"| SQ[SMS Queue]
TE -->|"push.*"| PQ[Push Queue]
TE -->|"alert.*"| AQ[Alert Queue]
EQ --> EC[Email Consumer]
SQ --> SC[SMS Consumer]
PQ --> PC[Push Consumer]
AQ --> AC[Alert Consumer]
style TE fill:#f90,color:#fff
Setup
# requirements.txt
# pika==1.3.2
# requests==2.31.0
Step 1: Exchange and Queue Setup
import pika
import json
import os
import logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s [%(levelname)s] %(message)s')
RABBITMQ_HOST = os.environ.get('RABBITMQ_HOST', 'localhost')
RABBITMQ_PORT = int(os.environ.get('RABBITMQ_PORT', 5672))
EXCHANGE_NAME = 'notifications'
QUEUES = {
'email_queue': 'email.#',
'sms_queue': 'sms.#',
'push_queue': 'push.#',
'alert_queue': 'alert.#',
}
def setup_infrastructure():
connection = pika.BlockingConnection(
pika.ConnectionParameters(host=RABBITMQ_HOST, port=RABBITMQ_PORT)
)
channel = connection.channel()
channel.exchange_declare(exchange=EXCHANGE_NAME, exchange_type='topic', durable=True)
channel.confirm_delivery()
for queue_name, binding_key in QUEUES.items():
channel.queue_declare(queue=queue_name, durable=True)
channel.queue_bind(exchange=EXCHANGE_NAME, queue=queue_name, routing_key=binding_key)
logging.info(f"Queue {queue_name} bound with key {binding_key}")
logging.info("Infrastructure setup complete")
connection.close()
if __name__ == '__main__':
setup_infrastructure()
Expected output:
2026-06-28 10:00:00 [INFO] Queue email_queue bound with key email.#
2026-06-28 10:00:00 [INFO] Queue sms_queue bound with key sms.#
2026-06-28 10:00:00 [INFO] Queue push_queue bound with key push.#
2026-06-28 10:00:00 [INFO] Queue alert_queue bound with key alert.#
2026-06-28 10:00:00 [INFO] Infrastructure setup complete
Step 2: Notification Producer
import pika
import json
import uuid
import logging
logger = logging.getLogger(__name__)
class NotificationProducer:
def __init__(self):
self.connection = pika.BlockingConnection(
pika.ConnectionParameters(host=RABBITMQ_HOST, port=RABBITMQ_PORT)
)
self.channel = self.connection.channel()
self.channel.confirm_delivery()
logger.info("Notification producer initialized")
def send_notification(self, notification_type, recipient, subject, body, priority='normal'):
routing_key = f"{notification_type}.{priority}"
message = {
'id': str(uuid.uuid4()),
'type': notification_type,
'recipient': recipient,
'subject': subject,
'body': body,
'priority': priority,
'timestamp': '2026-06-28T10:00:00Z'
}
try:
self.channel.basic_publish(
exchange=EXCHANGE_NAME,
routing_key=routing_key,
body=json.dumps(message),
properties=pika.BasicProperties(
delivery_mode=2,
content_type='application/json',
message_id=message['id']
)
)
logger.info(f"Sent {notification_type} to {recipient}: {subject}")
return True
except (pika.exceptions.UnroutableError, pika.exceptions.NackError) as e:
logger.error(f"Failed to send notification: {e}")
return False
def close(self):
if self.connection and self.connection.is_open:
self.connection.close()
if __name__ == '__main__':
producer = NotificationProducer()
producer.send_notification('email', 'user@example.com', 'Scan Complete', 'Your file is clean')
producer.send_notification('sms', '+1234567890', 'Alert', 'Suspicious activity detected', 'high')
producer.send_notification('push', 'device_token_123', 'Update Available', 'Version 2.1 is ready')
producer.close()
Expected output:
2026-06-28 10:00:00 [INFO] Sent email to user@example.com: Scan Complete
2026-06-28 10:00:00 [INFO] Sent sms to +1234567890: Alert
2026-06-28 10:00:00 [INFO] Sent push to device_token_123: Update Available
Step 3: Notification Consumers
import pika
import json
import logging
import time
import threading
logger = logging.getLogger(__name__)
class BaseConsumer:
def __init__(self, queue_name, handler_name):
self.queue_name = queue_name
self.handler_name = handler_name
self.connection = pika.BlockingConnection(
pika.ConnectionParameters(host=RABBITMQ_HOST, port=RABBITMQ_PORT)
)
self.channel = self.connection.channel()
self.channel.basic_qos(prefetch_count=1)
def process(self, notification):
raise NotImplementedError
def callback(self, ch, method, properties, body):
notification = json.loads(body)
try:
self.process(notification)
ch.basic_ack(delivery_tag=method.delivery_tag)
logger.info(f"[{self.handler_name}] Processed: {notification['id']}")
except Exception as e:
logger.error(f"[{self.handler_name}] Error: {e}")
ch.basic_nack(delivery_tag=method.delivery_tag, requeue=False)
def run(self):
self.channel.basic_consume(
queue=self.queue_name,
on_message_callback=self.callback
)
logger.info(f"[{self.handler_name}] Waiting for messages on {self.queue_name}")
self.channel.start_consuming()
class EmailConsumer(BaseConsumer):
def __init__(self):
super().__init__('email_queue', 'Email')
def process(self, notification):
logger.info(f"Sending email to {notification['recipient']}")
logger.info(f"Subject: {notification['subject']}")
logger.info(f"Body: {notification['body']}")
time.sleep(0.3)
class SMSConsumer(BaseConsumer):
def __init__(self):
super().__init__('sms_queue', 'SMS')
def process(self, notification):
logger.info(f"Sending SMS to {notification['recipient']}")
logger.info(f"Message: {notification['subject']}: {notification['body']}")
time.sleep(0.2)
class PushConsumer(BaseConsumer):
def __init__(self):
super().__init__('push_queue', 'Push')
def process(self, notification):
logger.info(f"Sending push to device {notification['recipient']}")
logger.info(f"Title: {notification['subject']}")
time.sleep(0.1)
class AlertConsumer(BaseConsumer):
def __init__(self):
super().__init__('alert_queue', 'Alert')
def process(self, notification):
logger.warning(f"HIGH PRIORITY ALERT for {notification['recipient']}")
logger.warning(f"Subject: {notification['subject']}")
time.sleep(0.1)
if __name__ == '__main__':
consumers = [
EmailConsumer(),
SMSConsumer(),
PushConsumer(),
AlertConsumer(),
]
threads = []
for consumer in consumers:
t = threading.Thread(target=consumer.run, daemon=True)
t.start()
threads.append(t)
logger.info("All consumers started")
time.sleep(60)
Step 4: Monitoring Dashboard
import requests
import json
import time
import os
MONITOR_URL = os.environ.get('RABBITMQ_API_URL', 'http://localhost:15672/api')
AUTH = (os.environ.get('RABBITMQ_USER', 'guest'), os.environ.get('RABBITMQ_PASS', 'guest'))
def get_queue_metrics():
queues = requests.get(f"{MONITOR_URL}/queues", auth=AUTH).json()
metrics = {}
for q in queues:
if q['name'] in QUEUES:
metrics[q['name']] = {
'ready': q['messages_ready'],
'unacked': q['messages_unacknowledged'],
'total': q['messages'],
'consumers': q['consumers'],
}
return metrics
def check_health():
try:
health = requests.get(f"{MONITOR_URL}/healthchecks/node", auth=AUTH, timeout=5)
return health.status_code == 200
except:
return False
if __name__ == '__main__':
print(f"Exchange: {EXCHANGE_NAME}")
print("Monitoring notification system...")
print()
while True:
metrics = get_queue_metrics()
healthy = check_health()
print(f"Time: {time.strftime('%H:%M:%S')} | Healthy: {healthy}")
print("-" * 60)
for queue_name, data in metrics.items():
status = "OK" if data['ready'] < 100 else "WARN"
print(f" {queue_name:20s} | Ready: {data['ready']:4d} | "
f"Unacked: {data['unacked']:3d} | "
f"Consumers: {data['consumers']:2d} | {status}")
print()
time.sleep(5)
Expected output:
Exchange: notifications
Time: 10:00:00 | Healthy: True
------------------------------------------------------------
email_queue | Ready: 0 | Unacked: 0 | Consumers: 1 | OK
sms_queue | Ready: 0 | Unacked: 0 | Consumers: 1 | OK
push_queue | Ready: 0 | Unacked: 0 | Consumers: 1 | OK
alert_queue | Ready: 0 | Unacked: 0 | Consumers: 1 | OK
Common Mistakes
1. Not Using Durable Queues
Without durable queues, all queue definitions and messages are lost on broker restart. Always set durable=True in production.
2. Mixing Up Routing Keys
Routing keys like email.high only match email.# or email.*, not email alone. Test routing keys with the management UI before deploying.
3. Forgetting to Set prefetch_count
Without basic_qos(prefetch_count=1), consumers buffer all messages, causing OOM on high-traffic queues.
4. Not Handling Consumer Exceptions
An unhandled exception in a consumer callback crashes the consumer. Wrap processing in try/except and nack on failure.
5. Running Without Monitoring
Queues can silently grow to millions of messages. Set up the monitoring dashboard with alerts for queue depth, consumer count, and message rates.
Practice Questions
1. Why use a topic exchange instead of direct exchange for this project?
Topic exchange supports pattern matching (email.#), allowing flexible routing. Direct exchange requires exact match. New notification types can be added without changing the producer.
2. What happens if a consumer crashes mid-processing?
The message remains unacknowledged. On connection loss, RabbitMQ requeues it for another consumer. The manual ack pattern ensures no message is lost.
3. How can you add SMS notification for high priority only?
Change the SMS binding key to sms.high instead of sms.#. Only messages with routing key sms.high reach the SMS queue.
4. Why is prefetch_count=1 important here?
Each consumer handles one notification at a time. If an email takes 300ms and a push takes 100ms, prefetch=1 ensures fair distribution without any consumer being overwhelmed.
Challenge
Extend the notification system: add a dead letter exchange for failed notifications, implement retry with exponential backoff (3 attempts), and route retries to a separate delay queue using TTL. The system should log every failed notification to a dead letter queue for manual inspection.
FAQ
What's Next
You completed the RabbitMQ notification system. Review message queue patterns to see how this fits into the broader messaging landscape, or explore Celery for Python task queues built on RabbitMQ.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro