Aplicacion de chat en tiempo real con Go (backend) y Svelte 5 (frontend), organizada con 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:
| 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. |
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
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
└─────────────────────┘
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.
IntersectionObserver detecta mensajes visibles en el viewport.highWaterMark (seq mas alto visible). Con debounce de 800ms se envia UN SOLO read_ack.pendingConvID/pendingSeq al momento del scroll para evitar enviar al chat equivocado al cambiar de conversacion.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).
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.
Coordina entidades + repositorios + notifier. No tiene logica pesada. Valida participantes, crea mensajes, actualiza cursores, notifica cambios. Recibe interfaces, no implementaciones concretas.
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.
./run.sh
Levanta el backend en :8080 y el frontend en http://localhost:5173.