proyecto-clean-architecture Svelte Themes

Proyecto Clean Architecture

Chat con Clean Architecture en Go y Svelte

Quantex Chat — Clean Architecture

Aplicacion de chat en tiempo real con Go (backend) y Svelte 5 (frontend), organizada con Clean Architecture.

Qué es Clean Architecture

Clean Architecture es un estilo de organizar el código en círculos concéntricos donde el centro es lo mas importante.

La regla clave:

Una capa mas interna no conoce ni depende de una más externa. Las dependencias solo apuntan hacia adentro.

Esto significa que el dominio:

  • Define sus propias interfaces según lo que necesita.
  • Las capas externas implementan esas interfaces sin que el dominio sepa cómo lo hacen.
Capa Qué contiene
domain/ Entidades con comportamiento, reglas de negocio e interfaces que el dominio necesita.
client/ Servidores (HTTP, WebSocket), persistencia (memoria), inyección de dependencias y cualquier detalle de infraestructura.

Estructura del proyecto

back/
├── main.go                                    # Punto de entrada, inyeccion de dependencias
├── go.mod
│
├── domain/                                    # CAPA INTERNA — lógica de negocio
│   ├── user/                                  # Entidad User (ID, Name)
│   ├── conversation/                          # Entidad Conversation (ID, Participant1, Participant2)
│   ├── message/                               # Entidad Message (ID, SenderID, ConversationID, SeqID, Text, Timestamp, Deleted)
│   │                                          # + DeriveStatus: estado derivado de cursores (sent/delivered/read)
│   ├── cursor/                                # Entidad ReadCursor (LastReadSeq, LastDeliveredSeq)
│   ├── notifier/                              # Interfaz Notifier (push en tiempo real)
│   └── service.go                             # ChatService: orquesta todos los casos de uso
│
└── client/                                    # CAPA EXTERNA — infraestructura
    ├── persistence/memory/                    # Repos en memoria (User, Conversation, Message, Cursor)
    └── server/
        ├── rest/handler.go                    # API REST completa + CORS
        └── ws/                                # WebSocket: handler, registry, notifier

front/
├── src/
│   ├── App.svelte                             # Layout principal
│   └── lib/
│       ├── api.js                             # Cliente REST
│       ├── stores.svelte.js                   # Estado reactivo + read tracking
│       └── components/                        # Sidebar, ChatArea, MessageBubble, UserSetup

Flujo de una petición (ejemplo: enviar mensaje)

POST /conversations/{id}/messages  {"text": "Hola"}  [X-User-ID: uuid]
       │
       ▼
 ┌─────────────────────┐
 │  rest.Handler        │  Deserializa JSON, extrae X-User-ID
 │  handleSendMessage() │
 └──────────┬───────────┘
            │
            ▼
 ┌─────────────────────┐
 │  domain.ChatService  │  Valida participante, crea Message, guarda
 │  SendMessage()       │  Notifica al otro via Notifier
 └──────────┬───────────┘
            │
            ▼
 ┌─────────────────────┐
 │  memory.Repo         │  Asigna SeqID atomico, persiste
 │  Save()              │
 └──────────┬───────────┘
            │
            ▼
 ┌─────────────────────┐
 │  ws.Notifier         │  Envia new_message al otro usuario por WebSocket
 └─────────────────────┘

Sistema de estados de mensajes

Cada mensaje tiene un estado derivado (no almacenado) calculado en tiempo de consulta:

Estado Visual Condicion
sent Un tick gris El servidor lo guardo
delivered Doble tick gris El dispositivo del receptor lo recibio (delivery_ack)
read Doble tick azul El receptor lo vio en pantalla (read_ack via cursor)

El estado se deriva comparando el SeqID del mensaje contra los cursores (LastReadSeq, LastDeliveredSeq) del otro participante.

Lectura progresiva (implementado)

  • Al abrir un chat, el frontend hace scroll al primer mensaje no leido (no al final).
  • Un IntersectionObserver detecta mensajes visibles en el viewport.
  • Se mantiene un highWaterMark (seq mas alto visible). Con debounce de 800ms se envia UN SOLO read_ack.
  • El ack pendiente se captura con pendingConvID/pendingSeq al momento del scroll para evitar enviar al chat equivocado al cambiar de conversacion.
  • Flush en: debounce timeout, cambio de chat, logout, cierre de pestana.
  • El sidebar muestra unread badges reales del servidor (no optimistas).
  • Mini avatar del otro usuario debajo del ultimo mensaje leido (estilo Instagram).

Mi punto de vista: por qué lo pensé así

Las entidades

User — ID y Name. Factory NewUser valida nombre no vacio.

Conversation — ID, Participant1, Participant2. No pueden ser iguales. HasParticipant y OtherParticipant como comportamiento.

Message — ID (UUID), SenderID, ConversationID, SeqID (asignado por repo), Text, Timestamp, Deleted (soft-delete).

ReadCursor — ConversationID, UserID, LastReadSeq, LastDeliveredSeq. Metodos AdvanceRead/AdvanceDelivery que solo avanzan (nunca retroceden).

Las interfaces

Cada repositorio esta definido en el dominio. El dominio dice "necesito poder guardar y buscar X", no le importa como. El Notifier dice "necesito notificar a un usuario" — no sabe si es WebSocket, SSE o palomas mensajeras.

El orquestador (ChatService)

Coordina entidades + repositorios + notifier. No tiene logica pesada. Valida participantes, crea mensajes, actualiza cursores, notifica cambios. Recibe interfaces, no implementaciones concretas.

El client (persistencia + servidor)

Un solo persistor (memory) implementa todas las interfaces. Cambiar a SQLite es implementar las mismas interfaces sin tocar el dominio.

El handler REST traduce HTTP ↔ dominio. No tiene lógica de negocio: no valida campos (eso lo hace la entidad), no decide cómo guardar (eso lo hace el repo). Solo mapea errores de dominio a códigos HTTP.

El WebSocket maneja el flujo en vivo: mensajes nuevos, ACKs de delivery/read, y notificaciones de cambio de estado.


Cómo ejecutar

./run.sh

Levanta el backend en :8080 y el frontend en http://localhost:5173.

Top categories

Loading Svelte Themes