Event System

Redis Pub/Sub event architecture, webhook:events channel, best-effort delivery, and publisher/subscriber patterns

MQUAD uses Redis Pub/Sub for event-driven communication. Kafka is NOT used.

Architecture

survey-service ──┐
                 ├──► Redis Pub/Sub ──► webhook-service ──► External Webhooks
data-service ────┘     webhook:events       │
                                           └──► delivery records in webhook_db

Channel

All events publish to a single Redis channel: webhook:events

Event Schema

{
  "event_id": "uuid-string",
  "event_type": "survey.created",
  "tenant_id": "tenant-uuid",
  "timestamp": "2026-05-13T12:00:00.000Z",
  "data": {
    "survey_id": "uuid",
    "survey_name": "Patient Survey"
  }
}

Event Producers

Survey Service

EventTriggerPayload
survey.createdSurvey createdsurvey_id, survey_name, created_by
survey.updatedSurvey updatedsurvey_id, survey_name, updated_by
survey.deletedSurvey deletedsurvey_id, survey_name
survey.publishedSurvey publishedsurvey_id, survey_name, version
survey.unpublishedSurvey unpublishedsurvey_id, survey_name

Data Service

EventTriggerPayload
response.submittedResponse submittedresponse_id, survey_id, submitted_by
response.deletedResponse deletedresponse_id, survey_id
response.invalidatedResponse invalidatedresponse_id, survey_id, reason

Best-Effort Delivery

Event publishing failures must not break the main flow:

async def publish_event(event_type, tenant_id, data):
    try:
        redis = get_redis_client()
        event = {
            "event_id": str(uuid4()),
            "event_type": event_type,
            "tenant_id": tenant_id,
            "timestamp": datetime.utcnow().isoformat() + "Z",
            "data": data
        }
        await redis.publish("webhook:events", json.dumps(event, default=str))
        return True
    except Exception as e:
        logger.warning(f"Failed to publish event {event_type}: {e}")
        return False  # Never raise — don't break the request

Event Consumer (Webhook Service)

The webhook-service subscribes to webhook:events and dispatches to configured URLs:

@celery_app.task
async def process_webhook_event(event_data: dict):
    webhooks = await webhook_repo.find_by_event_type(
        event_data["event_type"], event_data["tenant_id"]
    )
    for webhook in webhooks:
        await deliver_webhook(webhook, event_data)