Skip to content

Event-Driven Architecture

Background and asynchronous work in dichit-backend is event/queue-driven rather than blocking the request lifecycle.

What runs asynchronously

Concern Mechanism
Email / SMS / push notifications BullMQ queue processors → mail, SMS, FCM providers
Document/PDF rendering Queue (see src/application/queue/processors)
Payment webhook handling HTTP webhooks (pg/zwitch, sentry, whatsapp) + queues
Cross-instance real-time fan-out Redis Pub/Sub (ws:global)
Program/cycle state changes Queue processors orchestrating side effects

Queues (BullMQ on Redis)

flowchart LR
    H["Request handler / webhook"] -->|"add job"| BQ["BullMQ queue (Redis)"]
    BQ --> W["Worker (same process, app bootstrap)"]
    W --> A["Processor (application/queue/processors)"]
    A --> EXT["S3 / mail / SMS / FCM / PG"]
  • Queue processors and services live in src/application/queue.
  • Workers are started from the main app at boot (app.tsregisterQueueProcessors), unless disabled in tests.
  • Bull Board is available for monitoring at /admin/queues by default (see infrastructure/monitoring.md).

WebSocket pub/sub

Real-time events are published through a Redis pub/sub adapter and delivered by every WS instance (see websocket-architecture.md):

sequenceDiagram
    participant A as Instance A (publisher)
    participant R as Redis (ws:global)
    participant B as Instance B (subscriber)
    A->>R: publish(event)
    R-->>B: deliver event
    B->>B: deliverFromPubSub → socket rooms

Why async

  • Keeps HTTP/WS responses fast (< 200 ms target).
  • Retry semantics via BullMQ job attempts.
  • Decouples providers: mail/SMS/FCM can fail without failing the request.

Best practices

  1. Prefer queues for anything not needed in the response.
  2. Keep processors idempotent and re-runnable.
  3. Never log sensitive payloads (OTPs, tokens) in jobs.
  4. Use the existing queue infrastructure instead of adding schedulers/spawns.
  5. When a state transition has side effects, put them in a processor, not a controller.