Files
pos-system/services/chat-service-net/docs/en/ARCHITECTURE.md

11 KiB

Chat Service Architecture Documentation

Detailed architecture for Chat Service with SignalR, scalability patterns, and AI integration.

Architecture Overview

graph TB
    subgraph "Clients"
        WEB[Web App]
        MOB[Mobile App]
        BOT[Bot Client]
    end
    
    subgraph "Load Balancer"
        LB[NGINX/Traefik]
        SS[Sticky Sessions]
    end
    
    subgraph "Chat Service Instances"
        CS1[Instance 1<br/>SignalR Hub]
        CS2[Instance 2<br/>SignalR Hub]
        CS3[Instance 3<br/>SignalR Hub]
    end
    
    subgraph "Backplane"
        RD[(Redis<br/>Pub/Sub)]
    end
    
    subgraph "Storage"
        PG[(PostgreSQL<br/>Messages)]
        RC[(Redis<br/>Cache)]
    end
    
    subgraph "AI Service"
        AI[OpenAI<br/>GPT-4]
    end
    
    WEB & MOB & BOT --> LB
    LB --> SS --> CS1 & CS2 & CS3
    CS1 & CS2 & CS3 <--> RD
    CS1 & CS2 & CS3 --> PG
    CS1 & CS2 & CS3 --> RC
    CS1 & CS2 & CS3 --> AI
    
    style LB fill:#3498db,stroke:#2980b9,color:#fff
    style RD fill:#e74c3c,stroke:#c0392b,color:#fff
    style PG fill:#27ae60,stroke:#1e8449,color:#fff
    style AI fill:#9b59b6,stroke:#7d3c98,color:#fff

SignalR Hub Architecture

Connection Lifecycle

sequenceDiagram
    participant Client
    participant Hub as ChatHub
    participant Groups
    participant DB as Database
    participant Redis
    
    Client->>Hub: Connect (JWT Token)
    Hub->>Hub: OnConnectedAsync()
    Hub->>DB: Load user rooms
    Hub->>Groups: AddToGroupAsync(roomId)
    Hub->>Redis: Publish(UserOnline)
    Hub-->>Client: Connected
    
    Note over Client,Hub: User is now online
    
    Client->>Hub: SendMessage(roomId, content)
    Hub->>DB: Save message
    Hub->>Redis: Publish(NewMessage)
    Redis-->>Hub: Broadcast to all instances
    Hub->>Groups: Clients.Group(roomId)
    Hub-->>Client: ReceiveMessage

Hub Implementation

public class ChatHub : Hub<IChatClient>
{
    private readonly IChatRoomRepository _roomRepository;
    private readonly IMessageRepository _messageRepository;
    private readonly IAIService _aiService;
    
    public async Task JoinRoom(Guid roomId)
    {
        var userId = Context.UserIdentifier;
        
        // Add to SignalR Group
        await Groups.AddToGroupAsync(Context.ConnectionId, roomId.ToString());
        
        // Notify other members
        await Clients.Group(roomId.ToString())
            .UserJoined(userId, roomId);
        
        // Persist state to DB
        await _roomRepository.AddParticipantAsync(roomId, userId);
    }
    
    public async Task SendMessage(Guid roomId, string content)
    {
        var message = new Message(roomId, Context.UserIdentifier, content);
        await _messageRepository.AddAsync(message);
        
        // Broadcast message
        await Clients.Group(roomId.ToString())
            .ReceiveMessage(message.ToDto());
        
        // Check AI trigger
        if (content.StartsWith("@gpt "))
        {
            await StreamAIResponse(roomId, content[5..]);
        }
    }
    
    public async IAsyncEnumerable<string> StreamAIResponse(
        Guid roomId, 
        string prompt,
        [EnumeratorCancellation] CancellationToken ct = default)
    {
        // Get chat history for context
        var history = await _messageRepository.GetHistoryAsync(roomId, 20);
        
        await foreach (var chunk in _aiService.StreamAsync(prompt, history, ct))
        {
            yield return chunk;
        }
    }
}

Scalability Architecture

Redis Backplane

graph LR
    subgraph "Instance 1"
        C1[Client A] --> H1[Hub 1]
    end
    
    subgraph "Instance 2"
        C2[Client B] --> H2[Hub 2]
    end
    
    subgraph "Redis"
        CH[Channel: chat.messages]
    end
    
    H1 -->|Publish| CH
    CH -->|Subscribe| H2
    H2 -->|Deliver| C2
    
    style CH fill:#e74c3c,stroke:#c0392b,color:#fff

Scaling Options

Option Pros Cons
Redis Backplane Simple, on-premise Requires Redis cluster management
Azure SignalR Serverless, no sticky sessions Vendor lock-in, cost
Sticky Sessions Simplest Imperfect for failover
// Redis Backplane Configuration
builder.Services.AddSignalR()
    .AddStackExchangeRedis(options =>
    {
        options.Configuration.ChannelPrefix = 
            RedisChannel.Literal("ChatService");
        options.Configuration.AbortOnConnectFail = false;
    });

// Azure SignalR Service
builder.Services.AddSignalR()
    .AddAzureSignalR(options =>
    {
        options.ConnectionString = azureSignalRConnectionString;
        options.ServerStickyMode = ServerStickyMode.Required;
    });

User & Group Management

User Mapping

graph TB
    subgraph "User Mapping"
        U1[User ID: user-123]
        C1[Connection 1<br/>Phone]
        C2[Connection 2<br/>Laptop]
        C3[Connection 3<br/>Tablet]
    end
    
    U1 --> C1 & C2 & C3
    
    style U1 fill:#3498db,stroke:#2980b9,color:#fff

IUserIdProvider

public class ClaimsUserIdProvider : IUserIdProvider
{
    public string? GetUserId(HubConnectionContext connection)
    {
        // Get User ID from JWT Claims
        return connection.User?
            .FindFirst(ClaimTypes.NameIdentifier)?.Value;
    }
}

// Registration
builder.Services.AddSingleton<IUserIdProvider, ClaimsUserIdProvider>();

Group State Management

// Problem: SignalR Groups don't persist on restart
// Solution: Store in database

public class GroupStateService
{
    private readonly IDistributedCache _cache;
    private readonly IChatRoomRepository _repository;
    
    public async Task RestoreUserGroups(string userId, string connectionId)
    {
        var rooms = await _repository.GetUserRoomsAsync(userId);
        
        foreach (var room in rooms)
        {
            await _hubContext.Groups.AddToGroupAsync(
                connectionId, 
                room.Id.ToString()
            );
        }
    }
}

AI Integration Flow

sequenceDiagram
    participant User
    participant Hub as ChatHub
    participant AI as AIService
    participant OpenAI
    participant DB as Database
    
    User->>Hub: "@gpt Explain DDD"
    Hub->>DB: Load history (20 messages)
    Hub->>AI: StreamAsync(prompt, history)
    AI->>OpenAI: ChatCompletion (stream: true)
    
    loop Streaming
        OpenAI-->>AI: Token chunk
        AI-->>Hub: yield chunk
        Hub-->>User: ReceiveAIChunk
    end
    
    Hub->>DB: Save AI response
    Hub-->>User: AIResponseComplete

Domain Model

Aggregate Roots

erDiagram
    ChatRoom ||--o{ Message : contains
    ChatRoom ||--o{ Participant : has
    ChatRoom {
        uuid id PK
        string name
        enum type
        uuid owner_id
        timestamp created_at
    }
    
    Message {
        uuid id PK
        uuid room_id FK
        uuid sender_id
        string content
        enum type
        timestamp sent_at
    }
    
    Participant {
        uuid id PK
        uuid room_id FK
        uuid user_id
        enum role
        timestamp joined_at
        timestamp last_read
    }

Domain Events

Event Trigger Handler
MessageSentEvent New message Update last_activity, broadcast
UserJoinedRoomEvent User joins room Notify members
UserLeftRoomEvent User leaves room Notify members
RoomCreatedEvent Create new room Add creator as admin

MessagePack Protocol

JSON vs MessagePack Comparison

Metric JSON MessagePack
Size 100% ~50-70%
Parse time 1x 0.5-0.8x
Type safety Weak Strong

Configuration

// Server
builder.Services.AddSignalR()
    .AddMessagePackProtocol(options =>
    {
        options.SerializerOptions = 
            MessagePackSerializerOptions.Standard
                .WithSecurity(MessagePackSecurity.UntrustedData)
                .WithCompression(MessagePackCompression.Lz4BlockArray);
    });

Resiliency Patterns

Automatic Reconnect

stateDiagram-v2
    [*] --> Connected
    Connected --> Disconnected: Connection lost
    Disconnected --> Reconnecting: Auto retry
    Reconnecting --> Connected: Success
    Reconnecting --> Reconnecting: Retry with backoff
    Reconnecting --> Disconnected: Max retries exceeded
    Disconnected --> [*]: User logout

Stateful Reconnect (.NET 8+)

// Server configuration
builder.Services.AddSignalR(options =>
{
    options.StatefulReconnectBufferSize = 32 * 1024; // 32KB
});

// With UseStatefulReconnect
app.MapHub<ChatHub>("/chatHub", options =>
{
    options.AllowStatefulReconnects = true;
});

Deployment Architecture

Kubernetes with Sticky Sessions

apiVersion: apps/v1
kind: Deployment
metadata:
  name: chatservice
spec:
  replicas: 3
  template:
    spec:
      containers:
      - name: chatservice
        image: chatservice:latest
        ports:
        - containerPort: 8080
        env:
        - name: REDIS_URL
          valueFrom:
            secretKeyRef:
              name: chat-secrets
              key: redis-url
---
apiVersion: v1
kind: Service
metadata:
  name: chatservice
  annotations:
    service.beta.kubernetes.io/aws-load-balancer-sticky-sessions: "true"
spec:
  type: LoadBalancer
  sessionAffinity: ClientIP
  sessionAffinityConfig:
    clientIP:
      timeoutSeconds: 3600
  ports:
  - port: 80
    targetPort: 8080

Ingress with WebSocket Support

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: chatservice
  annotations:
    nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
    nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
    nginx.ingress.kubernetes.io/upstream-hash-by: "$request_uri"
    nginx.ingress.kubernetes.io/affinity: "cookie"
spec:
  rules:
  - host: chat.goodgo.io
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: chatservice
            port:
              number: 80

Health Checks

builder.Services.AddHealthChecks()
    .AddNpgSql(connectionString, name: "postgresql")
    .AddRedis(redisConnectionString, name: "redis")
    .AddSignalRHub<ChatHub>(name: "signalr-hub");

References