Skip to main content
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:
1

Application Interface

Define the contract for real-time services
application/order/order-realtime.ts
export interface OrderRealtime {
  connect(events: OrderEvents): void;
  disconnect(): void;
}
2

Event Types

Define event callbacks for order changes
application/order/order-events.ts
export type OrderEvents = {
  onCreated?: (order: OrderListDto) => void;
  onUpdated?: (order: OrderListDto) => void;
};
3

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
  }
}
4

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

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 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();
  },
});

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:
1

Search for Existing Order

Check if the order already exists in the state
const idx = prev.findIndex((o) => o.id === incoming.id);
2

Add New Order

If the order doesn’t exist (idx === -1), add it to the beginning
if (idx === -1) return [incoming, ...prev];
3

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

1

Component Mount

Orders context mounts and initializes
2

Load Initial Data

Fetch current orders via HTTP API
const initial = await orderOrchestrator.listBoard();
setOrders(initial);
3

Establish WebSocket

Connect to real-time server with event handlers
orderOrchestrator.connect({
  onCreated: upsert,
  onUpdated: upsert,
});
4

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
5

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

Happy Path: Update Succeeds
  1. User drags order to new status
  2. UI updates immediately (optimistic)
  3. HTTP request sent to server
  4. Server updates database
  5. Server broadcasts WebSocket event
  6. All clients receive update (including originator)
  7. State is confirmed and remains unchanged
Result: Instant UI feedback with eventual consistency

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

1

Always Clean Up

Disconnect from WebSocket when component unmounts to prevent memory leaks
useEffect(() => {
  orderOrchestrator.connect({ onCreated: upsert, onUpdated: upsert });
  
  return () => {
    orderOrchestrator.disconnect(); // Important!
  };
}, []);
2

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);
3

Handle Null/Undefined

Check if socket exists before calling methods
disconnect(): void {
  this.socket?.disconnect(); // Safe navigation
  this.socket = null;
}
4

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

Build docs developers (and LLMs) love