Socket.io Microservices Communication Convention
Overview
Section titled “Overview”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.
1. Naming Conventions
Section titled “1. Naming Conventions”1.1 Namespaces (The “Where”)
Section titled “1.1 Namespaces (The “Where”)”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:
// ✅ Correctio.of('/data_owner') // Data Owner serviceio.of('/appointment') // Appointment managementio.of('/inventory') // Inventory managementio.of('/billing') // Billing systemio.of('/resource_data') // Resource data service
// ❌ Incorrectio.of('/DATA_OWNER') // ❌ Uppercaseio.of('/data-owner') // ❌ Use underscore, not hyphenio.of('/data_owner_service') // ❌ Don't use _service suffix, use service name only1.2 Events (The “What”)
Section titled “1.2 Events (The “What”)”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 plural1.3 Rooms (The “Who”)
Section titled “1.3 Rooms (The “Who”)”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 recipientsFrontend Join Example:
// After Socket.io connects, join relevant roomsconst userId = currentUser.id;const departmentId = currentUser.department;
socket.emit('join', { user: userId, department: departmentId, role: currentUser.role});Backend Example:
// Send to specific userio.to('user:550').emit('resources:created', payload);
// Send to departmentio.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);2. Payload Structure
Section titled “2. Payload Structure”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_casefor field names - ❌ Don’t nest deeply (keep structure flat where possible)
- ❌ Don’t include the entire object if only ID is needed (use reference)
3. Communication Logic
Section titled “3. Communication Logic”3.1 Connection & Authentication
Section titled “3.1 Connection & Authentication”All Socket.io connections must be authenticated at the namespace level.
Backend Middleware Pattern:
// Authenticate connection to namespaceio.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 authenticationconst 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);});3.2 Joining Rooms
Section titled “3.2 Joining Rooms”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`); });});3.3 Event Broadcasting
Section titled “3.3 Event Broadcasting”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 clientsio.of('/data_owner').emit('resources:created', payload);
// ❌ Avoid - Inefficient and privacy concernsocket.broadcast.emit('resources:created', payload);3.4 Throttling & Performance
Section titled “3.4 Throttling & Performance”Avoid sending data too frequently. Use debouncing/throttling for high-frequency updates.
Problem — High-Frequency Updates:
// ❌ Too frequent - can overwhelm networksocket.on('status:changed', (statusData) => { io.to('department:A').emit('resource:status', statusData); // Fires 60+ times per minute!});Solution — Throttle Updates:
// ✅ Better - Limit frequencyconst 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 acknowledgmentsocket.emit('records:updated', recordData, (ack) => { if (ack) { console.log('Frontend received record data'); }});
// Frontend sidesocket.on('records:updated', (data) => { updateRecordsUI(data); // Send acknowledgment return true;});4. Maintenance & Scaling
Section titled “4. Maintenance & Scaling”4.1 Redis Adapter for Multi-Instance
Section titled “4.1 Redis Adapter for Multi-Instance”When running multiple microservice instances, use Redis adapter for cross-instance broadcasting.
Installation:
npm install socket.io-redis @redis/clientConfiguration:
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 updatesimport 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.ioresourceQueue.process(async (job) => { const { resourceId, action } = job.data;
// Emit to interested parties io.to(`resource:${resourceId}`).emit(`resource:${action}`, job.data);
return { success: true };});4.2 Versioning & Breaking Changes
Section titled “4.2 Versioning & Breaking Changes”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 initiallysocket.on('resources:created', handleV1);socket.on('resources:created:v2', handleV2);
// Remove v1 listener after migration complete4.3 Logging & Monitoring
Section titled “4.3 Logging & Monitoring”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();});4.4 Reconnection & Error Handling
Section titled “4.4 Reconnection & Error Handling”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);});5. Implementation Checklist
Section titled “5. Implementation Checklist”Backend Setup
Section titled “Backend Setup”- 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
Frontend Setup
Section titled “Frontend Setup”- 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
6. Real-World Example
Section titled “6. Real-World Example”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 serviceresourceRouter.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 Dashboardsocket.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 Displaysocket.on('resource:status:updated', (payload) => { // Update display instantly refreshStatusView(payload.id, payload.data); playNotificationSound();});References
Section titled “References”Summary Table
Section titled “Summary Table”| Component | Pattern | Example |
|---|---|---|
| Namespace | /{service_name} | /data_owner, /appointment |
| Event | {resource}:{action} | resources:created, status:updated |
| Room | {type}:{id} | user:550, department:A, zone:Zone-A |
| Payload.id | UUID or string | 550e8400-e29b-41d4-a716-446655440000 |
| Payload.data | Business object | Resource record, status data, etc. |
| Timestamp | ISO 8601 | 2025-02-13T10:30:00Z |
| Triggered By | User ID | 550e8400-e29b-41d4-a716-446655440100 |