A scalable messaging system built with NestJS, MongoDB, Elasticsearch, and Kafka.
The system follows a microservice architecture designed for scalability and maintainability. It consists of three main services:
- Handles all REST API requests related to messaging
- Publishes messages to
create-messagetopic withconversationIdas the key - Uses key-based partitioning to ensure message order within conversations
- Consumes messages from
create-messagetopic - Stores messages in MongoDB
- Publishes to
message-insertedtopic after successful storage - Ensures data persistence before indexing
- Consumes messages from
message-insertedtopic - Performs bulk indexing in Elasticsearch
- Optimized for search performance
- Partitions: 5 (scalable based on traffic)
- Batch Size: 100
- Purpose: Initial message creation and storage
- Key:
conversationId(ensures ordered processing within conversations)
- Partitions: 5 (scales linearly with
create-message) - Batch Size: 100
- Purpose: Elasticsearch indexing
- Optimization: Bulk indexing for better performance
messaging-api: Produces tocreate-messagemessaging-storage-worker: Produces tomessage-inserted
messaging-storage-worker: Consumes fromcreate-messagemessaging-search-worker: Consumes frommessage-inserted
- Node.js (v18 or higher)
- MongoDB (v6 or higher)
- Elasticsearch (v8 or higher)
- Kafka (v3 or higher)
- pnpm (v8 or higher)
- Clone the repository:
git clone <repository-url>
cd messaging-system- Install dependencies for each service:
# Install messaging-api dependencies
cd messaging-api
pnpm install
# Install messaging-storage-worker dependencies
cd ../messaging-storage-worker
pnpm install
# Install messaging-search-worker dependencies
cd ../messaging-search-worker
pnpm install- Set up environment variables for all the services:
# messaging-api/.env.local
JWT_SECRET=''
MONGODB_URI=mongodb://localhost:27017/
MONGODB_DBNAME=''
MONGODB_USER=''
MONGODB_PASSWORD=''
MONGODB_AUTH_SOURCE=''
ES_NODE_URI='http://localhost:9200'
ES_USER=''
ES_PASSWORD=''
KAFKA_CLIENT_ID='messaging-api'
KAFKA_BROKERS='localhost:9092'
# messaging-storage-worker/.env.local
JWT_SECRET=''
MONGODB_URI=mongodb://localhost:27017/
MONGODB_DBNAME=''
MONGODB_USER=''
MONGODB_PASSWORD=''
MONGODB_AUTH_SOURCE=''
KAFKA_CLIENT_ID='messaging-storage-worker'
KAFKA_GROUP_ID='messaging-storage-worker'
KAFKA_BROKERS='localhost:9092'
# messaging-search-worker/.env.local
JWT_SECRET=''
ES_NODE_URI='http://localhost:9200'
ES_USER=elastic
ES_PASSWORD=''
KAFKA_CLIENT_ID='messaging-search-worker'
KAFKA_GROUP_ID='messaging-search-worker'
KAFKA_BROKERS='localhost:9092'Note:
- The
JWT_SECRETis used for authentication - Each service has its own Kafka client ID and group ID for proper message handling
- Start the services:
# Start messaging-api
cd messaging-api
pnpm start:dev
# Start messaging-storage-worker
cd messaging-storage-worker
pnpm start:dev
# Start messaging-search-worker
cd messaging-search-worker
pnpm start:devCreates a new message in a conversation.
curl --location 'http://localhost:4000/api/messages' \
--header 'Content-Type: application/json' \
--data '{
"id": "0088b8e7-936a-4752-833a-a319ab53bd78",
"conversationId": "a12533b3-8a32-46cc-a638-da129eb92ff4",
"senderId": "f1a50c56-5242-4581-806a-05c9ffb3039f",
"content": "Sit vero sint.\nDolor quos unde cumque id modi ipsa.\nAutem harum rem omnis non dolorum eaque est ut."
}'Request Body:
id(string, UUID): Unique identifier for the messageconversationId(string, UUID): ID of the conversation this message belongs tosenderId(string, UUID): ID of the user sending the messagecontent(string): The message content
Retrieves messages for a specific conversation with pagination support.
curl --location 'http://localhost:4000/api/conversations/acbfaceb-69ee-49f6-8c17-d22d431d1aa9/messages?lastMessageId=10812e3e-38e9-4bfc-85f2-fc541788e7b6&lastPaginationId=2025-03-29T13%3A51%3A25.253Z'Query Parameters:
conversationId(path parameter, UUID): ID of the conversationlastMessageId(optional, string): ID of the last message for paginationlastPaginationId(optional, string): Timestamp of the last message for paginationsortBy(optional, enum): Sort order for messagesDATE_CREATED_ASC: Sort by creation date ascendingDATE_CREATED_DESC: Sort by creation date descending (default)
Searches messages within a conversation with pagination support.
curl --location 'http://localhost:4000/api/conversations/acbfaceb-69ee-49f6-8c17-d22d431d1aa9/messages/search?q=gab&sortBy=DATE_CREATED_DESC&sortBy=DATE_CREATED_ASC&lastMessageId=36fee290-750a-4500-bd07-d5b70969b322&lastPaginationId=2025-03-30T10%3A09%3A15.756Z'Query Parameters:
conversationId(path parameter, UUID): ID of the conversationq(required, string): Search termlastMessageId(optional, string): ID of the last message for paginationlastPaginationId(optional, string): Timestamp of the last message for paginationsortBy(optional, enum): Sort order for messagesDATE_CREATED_ASC: Sort by creation date ascendingDATE_CREATED_DESC: Sort by creation date descending (default)
All endpoints return responses in the following format:
{
"messages": [
{
"id": "string",
"conversationId": "string",
"senderId": "string",
"content": "string",
"timestamp": "string",
"metadata": {
"type": "string",
"timestamp": "string"
}
}
],
"hasMore": boolean,
"nextLastMessageId": "string",
"nextPaginationId": "string"
}The API returns appropriate HTTP status codes and error messages:
400 Bad Request: Invalid input parameters or validation errors404 Not Found: Resource not found500 Internal Server Error: Server-side errors
Error response format:
{
"statusCode": number,
"message": "string",
"error": "string"
}-
Horizontal Scaling
- Each service can be scaled independently
- Kafka partitions can be increased based on traffic
- Multiple instances of storage and search workers can be deployed
-
Performance Optimization
- Batch processing for MongoDB and Elasticsearch operations
- Key-based partitioning for ordered message processing
- Bulk indexing for Elasticsearch
-
Data Consistency
- Messages are stored in MongoDB before indexing
- Kafka ensures reliable message delivery
- Partition keys maintain message order within conversations
-
Health Checks
- Each service exposes health check endpoints
- Monitor Kafka consumer lag
- Track MongoDB and Elasticsearch performance
-
Logging
- Structured logging for all services
- Error tracking and monitoring
- Performance metrics collection
The messaging system uses a single collection called Messages with the following schema:
interface Messages {
id: string; // Unique message identifier
conversationId: string; // Groups messages by conversation
senderId: string; // Identifies message sender
content: string; // Message content
metadata?: Record<string, any>; // Optional metadata for extensibility
timestamp: Date; // Message creation timestamp
}-
Indexing Strategy:
-
Compound Indexes:
// Primary index for DESC timestamp order { conversationId: 1, timestamp: -1, id: 1 } // Secondary index for ASC timestamp order { conversationId: 1, timestamp: 1, id: 1 }
-
Benefits:
- Perfect index coverage for pagination queries
- Supports both ascending and descending sorts
- Eliminates need for index intersection
- Optimal for range-based queries on timestamp
- Ensures consistent ordering with ID as tie-breaker
-
-
Index Usage:
- Conversations are queried by
conversationId - Range queries on
timestampfor pagination idas tie-breaker for same timestamps- No separate indexes needed for individual fields
- Conversations are queried by
-
Data Consistency:
- Messages are first stored in MongoDB before being indexed in Elasticsearch
- Each message has a guaranteed unique ID to prevent duplicates
- Timestamps are server-generated for consistency
Messages are indexed in Elasticsearch for full-text search capabilities:
- Index name:
messages - Indexed fields:
id,conversationId,content,timestamp - Optimized for content-based search within conversations
-
Message Creation:
Client -> messaging-api -> Kafka (create-message) -> messaging-storage-worker -> MongoDB -> Kafka (message-inserted) -> messaging-search-worker -> Elasticsearch -
Message Retrieval:
- Regular queries: MongoDB (sorted by timestamp)
- Search queries: Elasticsearch (full-text search)
-
Advanced Pagination Strategy:
- Hybrid cursor-based pagination using both timestamp and message ID
- Compound sorting strategy:
{ timestamp: -1, id: 1 } - Prevents message skipping in high-concurrency scenarios
- Efficient index utilization with no offset penalties
- Page size of 20 messages for optimal response times
-
Query Optimization:
- Uses compound indexes for efficient pagination queries
- Avoids table scans with proper index coverage
- Leverages MongoDB's index intersection
- Cursor-based navigation eliminates deep page performance issues
-
Elasticsearch Search Optimization:
- Implements
search_afterfor deep pagination efficiency - Maintains consistency with MongoDB pagination pattern
- Uses compound sort
[timestamp, id]for deterministic ordering
- Implements
-
Data Consistency:
- Two-phase commit pattern for message storage and indexing
- Kafka ensures reliable message delivery between services
- Maintains message order within conversations using partition keys
-
Authentication:
- JWT-based authentication for API access
- Configurable MongoDB authentication
- Elasticsearch security with username/password
-
Data Validation:
- DTO-based request validation
- Schema-level validation for MongoDB
- Type checking for all message fields