Skip to content

utkarshhxd/Notify-EventDrivenNotificationService

Repository files navigation

Notification System

A small event-driven notification system built with Node.js, TypeScript, BullMQ, Redis, PostgreSQL, Prisma, and Express.

The project accepts notification events through an API, queues them in Redis, processes them in a worker, stores delivery records in PostgreSQL, and exposes a Bull Board dashboard to inspect queue activity.

What This Project Does

  • Accepts events through an HTTP API
  • Queues jobs with BullMQ
  • Processes notifications asynchronously in a worker
  • Stores notification history in PostgreSQL
  • Applies simple per-user rate limiting
  • Prevents duplicate processing with queue-level and database-level idempotency
  • Exposes a Bull Board dashboard for queue monitoring

Architecture

The system is split into three apps plus one shared package:

  • apps/producer-api Receives incoming events and pushes them into the notification-queue.
  • apps/worker Pulls jobs from the queue, checks user preferences, applies rate limiting, simulates sending notifications, and stores the result in the database.
  • apps/dashboard Hosts the Bull Board UI for queue inspection.
  • packages/shared Contains shared TypeScript types and the Prisma schema.

Flow

  1. A client sends an event to POST /api/events.
  2. The producer validates the payload using Zod.
  3. The producer adds the event to the BullMQ queue.
  4. The worker consumes the job.
  5. The worker reads user preferences from PostgreSQL.
  6. The worker selects channels: EMAIL, SMS, and/or PUSH.
  7. The worker applies a Redis-based rate limit of 5 notifications per minute per user.
  8. The worker writes notification records to PostgreSQL.
  9. The worker marks each notification as SENT or FAILED.
  10. You can inspect queue state in Bull Board and fetch notification history through the API.

Tech Stack

  • Node.js
  • TypeScript
  • Express
  • BullMQ
  • Redis
  • PostgreSQL
  • Prisma
  • Zod
  • Docker Compose

Project Structure

.
├── apps
│   ├── dashboard
│   │   └── src/index.ts
│   ├── producer-api
│   │   └── src/index.ts
│   └── worker
│       └── src
│           ├── handlers
│           │   ├── email.ts
│           │   ├── push.ts
│           │   └── sms.ts
│           └── index.ts
├── packages
│   └── shared
│       ├── prisma/schema.prisma
│       └── src
│           ├── index.ts
│           └── types.ts
├── docker-compose.yml
├── package.json
├── test-event.ps1
└── tsconfig.json

Prerequisites

Make sure you have these installed:

  • Node.js 18+ recommended
  • npm
  • Docker Desktop

Environment Variables

The project uses a root .env file:

DATABASE_URL="postgresql://notification_user:secret_password@localhost:5432/notification_db"
REDIS_URL="redis://localhost:6379"

These defaults match the provided docker-compose.yml.

Installation

From the project root, install dependencies:

npm install

Start Dependencies

Start Redis and PostgreSQL:

docker compose up -d

To verify containers are running:

docker ps

Initialize the Database

Push the Prisma schema to PostgreSQL:

npm run db:push

This creates the required tables:

  • UserPreferences
  • Notification

Run the Project

Open three terminals in the project root and run one command in each.

Terminal 1: Worker

npm run start:worker

Terminal 2: Producer API

npm run start:producer

Terminal 3: Dashboard

npm run start:dashboard

Default URLs

  • Producer API: http://localhost:3001
  • Dashboard: http://localhost:3000/admin/queues
  • Redis: localhost:6379
  • PostgreSQL: localhost:5432

API Reference

POST /api/events

Accepts an event and queues it for processing.

Request Body

{
  "eventId": "evt-123",
  "eventType": "ORDER_PLACED",
  "userId": "user_123",
  "payload": {
    "orderAmount": 250,
    "items": ["Laptop", "Mouse"]
  },
  "timestamp": "2026-03-31T16:30:00Z"
}

Validation Rules

  • eventId must be a string
  • eventType must be a string
  • userId must be a string
  • payload must be an object
  • timestamp must be a valid ISO datetime string

Success Response

{
  "message": "Event accepted and queued for processing",
  "jobId": "evt-123"
}

GET /api/notifications/:userId

Returns notification records for a user ordered by newest first.

Example

Invoke-WebRequest -UseBasicParsing http://localhost:3001/api/notifications/user_123

Manual Testing

Option 1: Use the Included PowerShell Script

The project includes test-event.ps1, which posts a sample event to the producer API.

Run:

.\test-event.ps1

The script:

  • Generates a new GUID for eventId
  • Sends an ORDER_PLACED event
  • Uses user_123 as the test user
  • Prints the API response

Option 2: Send a Request Manually

$body = @{
    eventId = [guid]::NewGuid().ToString()
    eventType = "ORDER_PLACED"
    userId = "user_123"
    payload = @{
        orderAmount = 250.00
        items = @("Laptop", "Mouse")
    }
    timestamp = (Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ")
} | ConvertTo-Json

Invoke-RestMethod `
  -Uri "http://localhost:3001/api/events" `
  -Method Post `
  -Body $body `
  -ContentType "application/json"

Verify Notification Records

After sending an event, fetch the stored notifications:

Invoke-WebRequest -UseBasicParsing http://localhost:3001/api/notifications/user_123

If the worker processed the event successfully, you should see one record per enabled channel.

Dashboard

Bull Board is available at:

http://localhost:3000/admin/queues

Use it to:

  • Inspect queued jobs
  • Inspect completed jobs
  • Inspect failed jobs
  • Monitor queue activity while testing

Notification Processing Rules

Channel Selection

The worker checks UserPreferences for the current user.

  • If no preferences exist, all channels are enabled by default
  • If preferences exist, the worker only sends to opted-in channels

Rate Limiting

The worker uses Redis to enforce a per-user rate limit:

  • Maximum 5 notifications per minute per user

If the limit is exceeded, the job fails.

Global Worker Limiter

The worker is configured with:

  • Concurrency: 5
  • Global processing limiter: 100 jobs per 1000 ms

Idempotency

The project uses two idempotency safeguards:

  • Queue-level idempotency using jobId = eventId
  • Database-level uniqueness on (eventId, channel)

This prevents duplicate channel delivery records for the same event.

Simulated Delivery Behavior

The delivery handlers in the worker simulate provider behavior:

  • Email handler waits about 500ms and randomly fails about 20% of the time
  • SMS handler waits about 300ms and randomly fails about 10% of the time
  • Push handler waits about 200ms and currently does not simulate failure

Because of this, occasional FAILED records are expected during testing.

Database Models

UserPreferences

Stores per-user notification settings.

Fields:

  • userId
  • emailOptIn
  • smsOptIn
  • pushOptIn
  • createdAt
  • updatedAt

Notification

Stores per-channel notification delivery records.

Fields:

  • id
  • eventId
  • eventType
  • userId
  • channel
  • status
  • payload
  • error
  • createdAt
  • updatedAt

Unique constraint:

  • (eventId, channel)

Useful Commands

Install dependencies:

npm install

Start infrastructure:

docker compose up -d

Stop infrastructure:

docker compose down

Reset database schema from Prisma:

npm run db:push

Start producer:

npm run start:producer

Start worker:

npm run start:worker

Start dashboard:

npm run start:dashboard

Troubleshooting

Port Already in Use

If you see EADDRINUSE, another process is already using the port.

Default ports:

  • 3000 for dashboard
  • 3001 for producer API
  • 5432 for PostgreSQL
  • 6379 for Redis

Check listeners:

Get-NetTCPConnection -State Listen -LocalPort 3000,3001,5432,6379

Producer or Worker Fails on Startup

Make sure:

  • npm install has been run from the repo root
  • Docker containers are up
  • .env exists and points to the correct Redis and PostgreSQL instances
  • npm run db:push has been executed at least once

API Is Up but No Notifications Are Stored

Check the worker terminal output. The producer only queues events. The worker is responsible for processing and writing records to PostgreSQL.

Random Failures During Tests

Some failures are intentional because the email and SMS handlers simulate provider errors. Retry with a new event if needed.

Current Limitations

  • No automated test suite is configured yet
  • No single-command script exists yet to start all three apps together
  • Delivery handlers are mocked and do not call real providers
  • No authentication is implemented on the API or dashboard

Suggested Next Improvements

  • Add a root dev script to start all apps together
  • Add automated tests for API validation and worker behavior
  • Add retry/backoff tuning per channel
  • Add user preference seed data and a management endpoint
  • Replace mock handlers with real email/SMS/push providers

License

No license file is currently included in this repository.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors