Skip to content

Socket.io Microservices Communication Convention

This document establishes standardized naming and messaging conventions for Socket.io real-time communication. It ensures consistency across microservices and enables seamless frontend-backend integration.

Goal: Make real-time communication predictable, scalable, and maintainable through clear naming patterns and structured payloads.


Namespaces organize communication channels by service domain.

Format: /{service_name}

Rules:

  • ✅ Use lowercase only
  • ✅ Use _ for multi-word services (snake_case)
  • ❌ No special characters except underscore
  • ❌ No uppercase letters

Examples:

// ✅ Correct
io.of('/data_owner') // Data Owner service
io.of('/appointment') // Appointment management
io.of('/inventory') // Inventory management
io.of('/billing') // Billing system
io.of('/resource_data') // Resource data service
// ❌ Incorrect
io.of('/DATA_OWNER') // ❌ Uppercase
io.of('/data-owner') // ❌ Use underscore, not hyphen
io.of('/data_owner_service') // ❌ Don't use _service suffix, use service name only

Events describe what happened, using past tense to indicate completion.

Format: {resource}:{action}

Rules:

  • resource: Entity name in plural, lowercase (e.g., resources, records)
  • action: Past tense verb (created, updated, deleted, completed)
  • Use underscore for multi-word resources/actions
  • Resource and action separated by colon

Examples:

// ✅ Correct - Clear and descriptive
'resources:created' // New resource registered
'resources:updated' // Resource information modified
'appointments:scheduled' // Appointment booked
'records:created' // Record created
'orders:fulfilled' // Order fulfilled
'results:completed' // Result ready
'records:archived' // Record archived
// ❌ Incorrect
'create_resource' // ❌ Wrong format, use resource:action
'Resource_Created' // ❌ Wrong case
'resources_create' // ❌ Action should be past tense
'resource:created' // ❌ Resource should be plural

Rooms segment messages to specific recipients — individuals, departments, or teams.

Format: {group_type}:{id}

Purpose: Send data only to interested parties without broadcasting to everyone

Common Room Patterns:

// User-specific rooms (individual notifications)
'user:550' // Send to user ID 550 only
'user:admin' // Send to all admin users
'user:manager:finance' // Send to all finance managers
// Department/Branch rooms (team notifications)
'branch:101' // Everyone in branch 101
'department:A' // All staff in department A
'department:operations' // All operations staff
'shift:morning:101' // Morning shift in branch 101
// Location-based rooms
'zone:Zone-A' // Specific zone/unit
'floor:general' // General floor
// Feature-specific rooms
'session:active' // All active sessions
'data:public' // Public data recipients

Frontend Join Example:

// After Socket.io connects, join relevant rooms
const userId = currentUser.id;
const departmentId = currentUser.department;
socket.emit('join', {
user: userId,
department: departmentId,
role: currentUser.role
});

Backend Example:

// Send to specific user
io.to('user:550').emit('resources:created', payload);
// Send to department
io.to('department:operations').emit('appointments:scheduled', payload);
// Send to multiple groups (broadcast to branch + department)
io.to('branch:101').to('department:A').emit('alert:issued', payload);

All events use a consistent JSON structure for predictable parsing and future extensibility.

Standard Payload Format:

interface SocketPayload<T> {
// Required: Unique identifier of the resource
id: string | number;
// Required: The actual data (can be nested)
data: T;
// Required: Metadata about the event
metadata: {
// ISO 8601 timestamp when event occurred
timestamp: string;
// User ID who triggered the event
triggered_by: string;
// Optional: Event version for breaking changes
version?: string;
// Optional: Trace ID for logging/debugging
trace_id?: string;
// Optional: User department (for context)
department?: string;
// Optional: Additional context
context?: Record<string, unknown>;
};
}

Concrete Example — Resource Created:

// Event: resources:created
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"data": {
"reference_number": "REF-95000001",
"first_name": "John",
"last_name": "Smith",
"created_date": "1990-01-15",
"contact_phone": "081-234-5678"
},
"metadata": {
"timestamp": "2025-02-13T10:30:00Z",
"triggered_by": "550e8400-e29b-41d4-a716-446655440100",
"version": "1.0",
"trace_id": "trace-abc-123",
"department": "101"
}
}

Example — Appointment Scheduled:

// Event: appointments:scheduled
{
"id": "550e8400-e29b-41d4-a716-446655440002",
"data": {
"resource_id": "550e8400-e29b-41d4-a716-446655440000",
"operator_id": "550e8400-e29b-41d4-a716-446655440100",
"appointment_date": "2025-02-20T14:00:00Z",
"reason": "Follow-up consultation",
"location": "Department A - Room 5"
},
"metadata": {
"timestamp": "2025-02-13T09:15:00Z",
"triggered_by": "550e8400-e29b-41d4-a716-446655440100",
"department": "A"
}
}

Payload Guidelines:

  • ✅ Always include id (primary identifier for the resource)
  • ✅ Always include data (the actual information)
  • ✅ Always include metadata.timestamp (when it happened)
  • ✅ Always include metadata.triggered_by (who did it)
  • ✅ Use ISO 8601 format for timestamps
  • ✅ Use snake_case for field names
  • ❌ Don’t nest deeply (keep structure flat where possible)
  • ❌ Don’t include the entire object if only ID is needed (use reference)

All Socket.io connections must be authenticated at the namespace level.

Backend Middleware Pattern:

// Authenticate connection to namespace
io.of('/data_owner').use(async (socket, next) => {
// Verify JWT token
const token = socket.handshake.auth.token;
if (!token) {
return next(new Error('Authentication failed'));
}
try {
// Verify token with auth service
const user = await authService.verifyToken(token);
socket.data.user = user;
socket.data.departmentId = user.department;
next();
} catch (error) {
next(new Error('Invalid token'));
}
});

Frontend Connection:

// Connect with authentication
const socket = io(`http://localhost:3000/data_owner`, {
auth: {
token: localStorage.getItem('access_token')
}
});
socket.on('connect', () => {
console.log('Connected to Data Owner namespace');
});
socket.on('connect_error', (error) => {
console.error('Connection failed:', error.message);
});

After successful connection, explicitly join relevant rooms.

Frontend Pattern:

socket.on('connect', () => {
// Join personal user room
socket.emit('join:user', { userId: currentUser.id });
// Join department room
socket.emit('join:department', { departmentId: currentUser.department });
// Join specific shift/location if applicable
if (currentUser.shift) {
socket.emit('join:shift', { shiftId: currentUser.shift });
}
});

Backend Handler:

io.of('/data_owner').on('connection', (socket) => {
// User join event
socket.on('join:user', (data) => {
const roomName = `user:${data.userId}`;
socket.join(roomName);
console.log(`Socket ${socket.id} joined ${roomName}`);
});
// Department join event
socket.on('join:department', (data) => {
const roomName = `department:${data.departmentId}`;
socket.join(roomName);
console.log(`Socket ${socket.id} joined ${roomName}`);
});
// Clean up on disconnect
socket.on('disconnect', () => {
console.log(`Socket ${socket.id} disconnected`);
});
});

Emit events to specific rooms, not to all connections.

Good Pattern — Targeted Broadcasting:

// Send to specific user (notification)
io.of('/data_owner').to(`user:${userId}`).emit('resources:created', payload);
// Send to department (team notification)
io.of('/data_owner').to(`department:operations`).emit('alert:high_priority', payload);
// Send to multiple rooms (broadcast to branch and department)
io.of('/data_owner')
.to(`branch:101`)
.to(`department:A`)
.emit('system:alert', payload);

Bad Pattern — Broadcasting to Everyone:

// ❌ Avoid - Sends to all connected clients
io.of('/data_owner').emit('resources:created', payload);
// ❌ Avoid - Inefficient and privacy concern
socket.broadcast.emit('resources:created', payload);

Avoid sending data too frequently. Use debouncing/throttling for high-frequency updates.

Problem — High-Frequency Updates:

// ❌ Too frequent - can overwhelm network
socket.on('status:changed', (statusData) => {
io.to('department:A').emit('resource:status', statusData);
// Fires 60+ times per minute!
});

Solution — Throttle Updates:

// ✅ Better - Limit frequency
const throttledStatusUpdate = throttle((statusData) => {
io.to('department:A').emit('resource:status', statusData);
}, 5000); // Update at most every 5 seconds
socket.on('status:changed', throttledStatusUpdate);

Alternative — Use ACK for Confirmation:

// Backend expects acknowledgment
socket.emit('records:updated', recordData, (ack) => {
if (ack) {
console.log('Frontend received record data');
}
});
// Frontend side
socket.on('records:updated', (data) => {
updateRecordsUI(data);
// Send acknowledgment
return true;
});

When running multiple microservice instances, use Redis adapter for cross-instance broadcasting.

Installation:

Terminal window
npm install socket.io-redis @redis/client

Configuration:

import { createAdapter } from '@socket.io/redis-adapter';
import { createClient } from 'redis';
const pubClient = createClient({
host: process.env.REDIS_HOST,
port: process.env.REDIS_PORT,
password: process.env.REDIS_PASSWORD,
});
const subClient = pubClient.duplicate();
await Promise.all([pubClient.connect(), subClient.connect()]);
io.adapter(createAdapter(pubClient, subClient));

With BullMQ Integration:

// Use BullMQ for event queuing + Socket.io for real-time updates
import Queue from 'bull';
const resourceQueue = new Queue('resources', {
redis: {
host: process.env.REDIS_BULLMQ_HOST,
port: process.env.REDIS_BULLMQ_PORT,
}
});
// Process event and broadcast via Socket.io
resourceQueue.process(async (job) => {
const { resourceId, action } = job.data;
// Emit to interested parties
io.to(`resource:${resourceId}`).emit(`resource:${action}`, job.data);
return { success: true };
});

When modifying event structure, create a new event version.

Pattern — Event Versioning:

// Version 1 (old)
emit('resources:created', {
id: '123',
name: 'Resource Name'
});
// Version 2 (new structure)
emit('resources:created:v2', {
id: '123',
data: {
first_name: 'John',
last_name: 'Smith'
},
metadata: {
timestamp: '2025-02-13T10:30:00Z',
triggered_by: 'user-123'
}
});
// Gradual deprecation
// Frontend listens to both versions initially
socket.on('resources:created', handleV1);
socket.on('resources:created:v2', handleV2);
// Remove v1 listener after migration complete

Track all Socket.io events for debugging and auditing.

Middleware for Logging:

io.of('/data_owner').use((socket, next) => {
// Attach logging to socket
socket.onAny((event, ...args) => {
logger.debug('Socket.io Event', {
event,
userId: socket.data.user?.id,
namespace: socket.nsp.name,
timestamp: new Date().toISOString(),
dataSize: JSON.stringify(args).length
});
});
next();
});

Handle disconnections and reconnections gracefully.

Frontend Pattern:

socket.on('disconnect', () => {
console.warn('Disconnected from server');
// UI should show offline state
updateUIStatus('offline');
});
socket.on('reconnect', () => {
console.log('Reconnected to server');
// Rejoin rooms
socket.emit('rejoin', { userId: currentUser.id });
updateUIStatus('online');
});
socket.on('reconnect_error', (error) => {
console.error('Reconnection failed:', error);
});
socket.io.engine.on('upgrade', (transport) => {
console.log('Transport upgraded to:', transport.name);
});

  • Configure Socket.io with namespace per service
  • Implement authentication middleware
  • Set up Redis adapter for multi-instance support
  • Define event naming convention (resource:action)
  • Implement room management (join/leave handlers)
  • Add logging middleware for debugging
  • Set up error handling
  • Create payload validation
  • Test cross-instance broadcasting
  • Connect with authentication token
  • Join relevant rooms after connection
  • Listen to event namespaces
  • Handle reconnection logic
  • Implement retry mechanism
  • Add error boundary for connection failures
  • Store connection state in state management
  • Test with multiple browser tabs
  • Monitor connection health

Scenario: Staff updates a resource’s status in Department A, which should notify:

  • The assigned operator (in same department)
  • All department staff (for awareness)
  • The resource’s dashboard (real-time display)

Implementation:

// Backend - data-owner service
resourceRouter.patch('/:resourceId/status', async (req, res) => {
const { resourceId } = req.params;
const statusData = req.body;
const operator = req.user;
// Update database
await updateStatus(resourceId, statusData);
// Broadcast via Socket.io
const payload = {
id: resourceId,
data: statusData,
metadata: {
timestamp: new Date().toISOString(),
triggered_by: operator.id,
department: operator.department
}
};
// To specific resource's dashboard
io.of('/data_owner').to(`resource:${resourceId}`).emit('status:updated', payload);
// To department staff
io.of('/data_owner').to(`department:A`).emit('resource:status:updated', {
...payload,
resource_ref: resource.reference_number // Include context for staff
});
// To operator's own device
io.of('/data_owner').to(`user:${operator.id}`).emit('status:confirmed', payload);
res.json({ success: true });
});
// Frontend - Department Dashboard
socket.on('status:updated', (payload) => {
// Update resource card in real-time
updateResourceStatus(payload.id, payload.data);
// Check for flagged status
if (isFlagged(payload.data)) {
showAlert(`Status flagged for resource ${payload.id}`);
}
});
// Frontend - Resource Monitor Display
socket.on('resource:status:updated', (payload) => {
// Update display instantly
refreshStatusView(payload.id, payload.data);
playNotificationSound();
});


ComponentPatternExample
Namespace/{service_name}/data_owner, /appointment
Event{resource}:{action}resources:created, status:updated
Room{type}:{id}user:550, department:A, zone:Zone-A
Payload.idUUID or string550e8400-e29b-41d4-a716-446655440000
Payload.dataBusiness objectResource record, status data, etc.
TimestampISO 86012025-02-13T10:30:00Z
Triggered ByUser ID550e8400-e29b-41d4-a716-446655440100