The KDS Frontend maintains real-time synchronization across all connected clients using WebSocket technology. When an order is created or updated, all kitchen displays receive the change instantly without requiring manual refresh.
Overview
Real-time synchronization ensures that:
New orders appear on all kitchen displays immediately
Status changes are reflected across all devices in real-time
Multiple users can work simultaneously without conflicts
Kitchen staff always see the current state of all orders
The real-time system uses Socket.IO for WebSocket communication, providing automatic reconnection and fallback support.
Architecture
The real-time synchronization system follows the layered architecture pattern:
Application Interface
Define the contract for real-time services application/order/order-realtime.ts
export interface OrderRealtime {
connect ( events : OrderEvents ) : void ;
disconnect () : void ;
}
Event Types
Define event callbacks for order changes application/order/order-events.ts
export type OrderEvents = {
onCreated ?: ( order : OrderListDto ) => void ;
onUpdated ?: ( order : OrderListDto ) => void ;
};
Infrastructure Implementation
Implement the interface using Socket.IO infraestructure/socket/order-realtime.socket.ts
export class SocketOrderRealtime implements OrderRealtime {
private socket : Socket | null = null ;
constructor ( private readonly baseUrl : string ) {}
connect ( events : OrderEvents ) : void {
// Socket.IO implementation
}
}
Orchestrator Coordination
Coordinate real-time and repository services orchestrators/order/OrderOrchestrator.ts
export class OrderOrchestrator {
constructor (
private readonly repository : OrderRepository ,
private readonly realtime : OrderRealtime ,
) {}
connect ( events : OrderEvents ) : void {
this . realtime . connect ( events );
}
}
WebSocket Connection
Connection Initialization
The Socket.IO connection is established with automatic reconnection support:
infraestructure/socket/order-realtime.socket.ts
export class SocketOrderRealtime implements OrderRealtime {
private socket : Socket | null = null ;
constructor ( private readonly baseUrl : string ) {}
connect ( events : OrderEvents ) : void {
if ( ! this . socket ) {
this . socket = io ( this . baseUrl , {
transports: [ "websocket" ],
reconnection: true ,
reconnectionAttempts: 5 ,
reconnectionDelay: 2000 ,
});
}
// Remove existing listeners to prevent duplicates
this . socket . off ( "order.created" );
this . socket . off ( "order.status.updated" );
// Register event handlers
this . socket . on ( "order.created" , ( order : OrderListDto ) => {
events . onCreated ?.( order );
});
this . socket . on ( "order.status.updated" , ( order : OrderListDto ) => {
events . onUpdated ?.( order );
});
}
disconnect () : void {
this . socket ?. disconnect ();
this . socket = null ;
}
}
Connection Configuration
transports: ['websocket']
Forces the use of WebSocket protocol only (no HTTP long-polling fallback). Benefits:
Lower latency
More efficient
Better for real-time updates
Trade-off:
May not work in restrictive network environments
Enables automatic reconnection if the connection is lost. Behavior:
Automatically attempts to reconnect on connection loss
Useful for handling temporary network issues
Maintains user experience during brief disconnections
Maximum number of reconnection attempts before giving up. Strategy:
Tries to reconnect up to 5 times
After 5 failed attempts, stops trying
Prevents infinite reconnection loops
Delay in milliseconds between reconnection attempts. Timing:
Waits 2 seconds before each reconnection attempt
Prevents overwhelming the server with rapid reconnection requests
Allows time for network or server issues to resolve
Real-time Events
The system listens for two types of events from the backend:
order.created
order.status.updated
order.created Event Triggered when a new order is received by the system. Event Name: order.createdPayload: OrderListDto{
id : string ;
partnerName ?: string ;
partnerImage ?: string ;
displayNumber : string ;
status : OrderStatus ;
priority : OrderPriority ;
activeTimer ?: string ;
courierName ?: string ;
}
Handler Implementation: this . socket . on ( "order.created" , ( order : OrderListDto ) => {
events . onCreated ?.( order );
});
Use Cases:
New order arrives from the ordering system
Order should appear in the “RECEIVED” column
All connected kitchen displays should show the new order
Sound notification can be triggered for new orders
Example: orderOrchestrator . connect ({
onCreated : ( order ) => {
console . log ( `New order received: ${ order . displayNumber } ` );
// Add order to the list
setOrders (( prev ) => [ order , ... prev ]);
// Play notification sound
playNotificationSound ();
},
});
order.status.updated Event Triggered when an order’s status changes (e.g., from PREPARING to READY). Event Name: order.status.updatedPayload: OrderListDto (complete order object with updated status){
id : string ;
partnerName ?: string ;
partnerImage ?: string ;
displayNumber : string ;
status : OrderStatus ; // Updated status
priority : OrderPriority ;
activeTimer ?: string ;
courierName ?: string ;
}
Handler Implementation: this . socket . on ( "order.status.updated" , ( order : OrderListDto ) => {
events . onUpdated ?.( order );
});
Use Cases:
Kitchen staff moves an order to a different status
Order status changes on another device
Backend system automatically updates order status
All displays need to reflect the new status
Example: orderOrchestrator . connect ({
onUpdated : ( order ) => {
console . log ( `Order ${ order . displayNumber } updated to ${ order . status } ` );
// Update the order in the list
setOrders (( prev ) => {
const idx = prev . findIndex (( o ) => o . id === order . id );
if ( idx === - 1 ) return prev ;
const next = [ ... prev ];
next [ idx ] = order ;
return next ;
});
},
});
Integration with React Context
The real-time system integrates seamlessly with the Orders context to manage application state:
contexts/Orders.context.tsx
export const OrdersProvider = ({ children } : { children : React . ReactNode }) => {
const [ orders , setOrders ] = useState < OrderListDto []>([]);
// Upsert function handles both create and update events
const upsert = ( incoming : OrderListDto ) => {
setOrders (( prev ) => {
const idx = prev . findIndex (( o ) => o . id === incoming . id );
// If order doesn't exist, add it to the beginning
if ( idx === - 1 ) return [ incoming , ... prev ];
// If order exists, update it in place
const next = [ ... prev ];
next [ idx ] = incoming ;
return next ;
});
};
useEffect (() => {
let cancelled = false ;
// Load initial data from API
const loadInitial = async () => {
try {
const initial = await orderOrchestrator . listBoard ();
if ( cancelled ) return ;
setOrders ( initial );
} catch ( e : any ) {
if ( cancelled ) return ;
console . error ( "Error loading orders:" , e );
}
};
loadInitial ();
// Connect to real-time updates
orderOrchestrator . connect ({
onCreated: upsert ,
onUpdated: upsert ,
});
// Cleanup on unmount
return () => {
cancelled = true ;
orderOrchestrator . disconnect ();
};
}, []);
return (
< OrdersContext . Provider value = {{ orders }} >
{ children }
</ OrdersContext . Provider >
);
};
Upsert Pattern
The context uses an “upsert” pattern to handle both create and update events efficiently:
Search for Existing Order
Check if the order already exists in the state const idx = prev . findIndex (( o ) => o . id === incoming . id );
Add New Order
If the order doesn’t exist (idx === -1), add it to the beginning if ( idx === - 1 ) return [ incoming , ... prev ];
Update Existing Order
If the order exists, replace it with the updated version const next = [ ... prev ];
next [ idx ] = incoming ;
return next ;
The upsert pattern ensures the UI stays synchronized regardless of whether an event is a creation or an update.
Orchestrator Coordination
The OrderOrchestrator provides a unified interface that coordinates both HTTP and WebSocket operations:
orchestrators/order/OrderOrchestrator.ts
export class OrderOrchestrator {
constructor (
private readonly repository : OrderRepository ,
private readonly realtime : OrderRealtime ,
) {}
// Real-time operations
connect ( events : OrderEvents ) : void {
this . realtime . connect ( events );
}
disconnect () : void {
this . realtime . disconnect ();
}
// HTTP operations
async listBoard () : Promise < OrderListDto []> {
return this . repository . listBoard ();
}
async updateOrderState ( id : string , toStatus : OrderStatus ) : Promise < void > {
await this . repository . updateStatus ( id , toStatus );
}
async getOrderDetail ( id : string ) : Promise < OrderDetailDto > {
return this . repository . getDetail ( id );
}
}
Why Use an Orchestrator?
Single Entry Point Components interact with one service instead of multiple
Simplified Testing Mock the orchestrator to test components in isolation
Coordinated Operations Manage both HTTP and WebSocket operations together
Clear Separation Keep presentation logic separate from infrastructure
Connection Lifecycle
Component Mount
Orders context mounts and initializes
Load Initial Data
Fetch current orders via HTTP API const initial = await orderOrchestrator . listBoard ();
setOrders ( initial );
Establish WebSocket
Connect to real-time server with event handlers orderOrchestrator . connect ({
onCreated: upsert ,
onUpdated: upsert ,
});
Receive Updates
Listen for real-time events and update state // Server broadcasts "order.created" or "order.status.updated"
// → Socket handler receives event
// → Calls onCreated or onUpdated callback
// → Context updates state
// → UI re-renders
Component Unmount
Cleanup: disconnect from WebSocket return () => {
orderOrchestrator . disconnect ();
};
Optimistic Updates
The application implements optimistic updates for better user experience:
contexts/Orders.context.tsx
const updateOrderStatus = async ( id : string , status : OrderStatus ) => {
let previous : OrderListDto | undefined ;
// 1. Save current state for rollback
setOrders (( curr ) => {
previous = curr . find (( o ) => o . id === id );
return curr . map (( o ) => ( o . id === id ? { ... o , status } : o ));
});
try {
// 2. Send update to server
await orderOrchestrator . updateOrderState ( id , status );
// 3. Server broadcasts update via WebSocket
// 4. Socket handler updates state (confirms optimistic update)
} catch ( e ) {
// 5. Rollback on error
if ( ! previous ) return ;
setOrders (( curr ) => curr . map (( o ) => ( o . id === id ? previous ! : o )));
}
};
Optimistic Update Flow
Success Case
Failure Case
Concurrent Updates
Happy Path: Update Succeeds
User drags order to new status
UI updates immediately (optimistic)
HTTP request sent to server
Server updates database
Server broadcasts WebSocket event
All clients receive update (including originator)
State is confirmed and remains unchanged
Result: Instant UI feedback with eventual consistencyError Case: Update Fails
User drags order to new status
UI updates immediately (optimistic)
HTTP request sent to server
Server rejects request (validation error, network issue, etc.)
Error is caught in try-catch block
UI rolls back to previous state
User sees error message
Result: UI is reverted to correct stateEdge Case: Multiple Simultaneous Updates
User A drags order to PREPARING
User B (on another device) drags same order to CANCELLED
Both UIs update optimistically
Server processes requests (typically first-come-first-served)
Server broadcasts the accepted update
Both clients receive WebSocket event
Both UIs converge to the correct state
Result: Eventual consistency across all clients
Error Handling
The real-time system handles various error scenarios:
Scenario: Initial WebSocket connection failsHandling:
Socket.IO automatically retries up to 5 times
2-second delay between attempts
If all attempts fail, app continues with HTTP-only mode
User can manually refresh to retry connection
constructor ( private readonly baseUrl : string ) {
// Connection config handles retries
}
Scenario: Active connection is lost (network issue, server restart)Handling:
Socket.IO automatically attempts to reconnect
Reconnection attempts: 5 times with 2-second delays
During reconnection, app continues to function with local state
Once reconnected, server can resync state if needed
this . socket = io ( this . baseUrl , {
reconnection: true ,
reconnectionAttempts: 5 ,
reconnectionDelay: 2000 ,
});
Scenario: Same event received multiple timesHandling:
The upsert pattern is idempotent
Receiving the same order multiple times has no negative effect
Order is simply updated to the same state
const upsert = ( incoming : OrderListDto ) => {
setOrders (( prev ) => {
const idx = prev . findIndex (( o ) => o . id === incoming . id );
if ( idx === - 1 ) return [ incoming , ... prev ];
const next = [ ... prev ];
next [ idx ] = incoming ; // Safe to update multiple times
return next ;
});
};
Scenario: Events arrive in wrong order due to network latencyHandling:
Each event contains the full order object (not just changes)
Last event received becomes the source of truth
May cause brief inconsistency, but quickly converges to correct state
For critical applications, implement event versioning or timestamps to handle out-of-order events more robustly.
Best Practices
Always Clean Up
Disconnect from WebSocket when component unmounts to prevent memory leaks useEffect (() => {
orderOrchestrator . connect ({ onCreated: upsert , onUpdated: upsert });
return () => {
orderOrchestrator . disconnect (); // Important!
};
}, []);
Remove Duplicate Listeners
Clear existing listeners before adding new ones this . socket . off ( "order.created" );
this . socket . off ( "order.status.updated" );
this . socket . on ( "order.created" , handler );
this . socket . on ( "order.status.updated" , handler );
Handle Null/Undefined
Check if socket exists before calling methods disconnect (): void {
this . socket ?. disconnect (); // Safe navigation
this . socket = null ;
}
Use Idempotent Operations
Design event handlers to be safely called multiple times // Good: Upsert is idempotent
const upsert = ( order ) => {
setOrders ( prev => {
const idx = prev . findIndex ( o => o . id === order . id );
return idx === - 1 ? [ ... prev , order ] : prev . map (( o , i ) => i === idx ? order : o );
});
};
// Bad: Append without checking for duplicates
const append = ( order ) => {
setOrders ( prev => [ ... prev , order ]); // Can create duplicates!
};
Testing Real-time Features
Mock the OrderRealtime interface for testing:
// Mock implementation for tests
class MockOrderRealtime implements OrderRealtime {
private events : OrderEvents = {};
connect ( events : OrderEvents ) : void {
this . events = events ;
}
disconnect () : void {
this . events = {};
}
// Test helper: simulate receiving an event
simulateOrderCreated ( order : OrderListDto ) : void {
this . events . onCreated ?.( order );
}
simulateOrderUpdated ( order : OrderListDto ) : void {
this . events . onUpdated ?.( order );
}
}
// Usage in tests
const mockRealtime = new MockOrderRealtime ();
const orchestrator = new OrderOrchestrator ( mockRepository , mockRealtime );
orchestrator . connect ({
onCreated : ( order ) => {
// Assert order was received
expect ( order . id ). toBe ( "123" );
},
});
// Simulate event
mockRealtime . simulateOrderCreated ({
id: "123" ,
displayNumber: "001" ,
status: "RECEIVED" ,
// ...
});
Architecture Learn about the layered architecture
Order Status Understand order status transitions
Application Hooks API reference for application hooks