El objetivo de este repositorio es mostrar como implementaría, a grandes rasgos, un sistema que tenga funcionalidades semejantes a Twitter (ahora X).
- Go como lenguaje de programación
- Gin-Gonic para implementar la API REST
- MariaDB para persistir los datos
- JWT para autenticar a los usuarios
.
├── cmd
│ └── api
│ └── main.go -> Punto de entrada. Configura el router y el contenedor.
├── docs -> Documentación del sistema
├── internal -> Código privado de la aplicación
│ ├── configs -> Configuraciones de la aplicación
│ ├── domain -> Estructuras que definen las entidades del sistema
│ ├── helpers -> Funciones auxiliares que necesitan ser inicializadas
│ ├── infraestructure -> Código relacionado con la comunicación externa saliente
│ │ └── repository -> Implementación de los repositorios para persistencia de datos
│ ├── interfaces -> Código relacionado con la comunicación externa entrante
│ │ ├── controller -> Funciones para manejar comunicación HTTP
│ │ └── dto -> Objetos de transferencia de datos
│ ├── middleware -> Middlewares para el framework gin-gonic
│ ├── usecase -> Funciones que manejan la lógica de negocio
│ ├── test -> Pruebas de la aplicación
│ └── container.go -> Definición del contenedor para inyección de dependencias
├── pkg -> Código de librería que puede ser usado por aplicaciones externas
├── scripts -> Scripts de base de datos
├── go.mod -> Definición de dependencias de golang
└── README.md -> Este archivoEl sistema está diseñado para usar inyección de dependencias basada en interfaces de golang. Esto permite realizar
pruebas unitarias fácilmente en los controladores, servicios y repositorios, ya que podemos simular cada una de sus
dependencias. También permite cambiar, por ejemplo, la tecnología de persistencia sin realizar grandes cambios en el
código.
En este modelo, la autenticación se maneja mediante los middlewares de gin-gonic. Los controllers analizan las
solicitudes HTTP, validando su cuerpo y parámetros, y también convierten errores de negocio en errores de API con el
código de estado y formato correctos. Los usecases manejan la lógica de negocio y las validaciones, y orquestan el uso
de las dependencias como clientes HTTP externos o clientes de persistencia. Finalmente, los repositories son los
encargados de manejar la persistencia de los diferentes modelos del domain.
-
Clonar el repositorio
git clone git@github.com:sruta/twitter-demo.git
-
Utilizar docker para iniciar la base de datos y el sistema
docker compose up -d --build
-
Para el primer uso conectarse a la base de datos con las credenciales presentes en
./docker-compose.ymly crear el schematwitter_demo -
Ejecutar el script presente en
./scripts/setup_db.sqlpara crear las tablas necesarias -
La aplicación ya se encuentra lista para utilizar en
http://localhost:8080
Se han implementado 3 tipos de tests a modo de ejemplo:
-
Tests unitarios para
usecase/user.go. Prueban los métodos mockeando la base de datos.go test -v ./internal/usecase -
Tests de integración para
controller/user.go. Prueban los métodos utilizando la implementación real del usecase.go test -v ./internal/interfaces/controller -
Tests end-to-end para la API. Para ejecutarlo primero se debe iniciar la base de datos y la aplicación utilizando docker compose.
go test -v ./test/e2e/
El test end-to-end realiza las siguientes solicitudes mientras va validando los resultados:
- Creación de un usuario A
- Creación de un usuario B
- Login del usuario A
- Login del usuario B
- Obtención del usuario A por ID
- Obtención del usuario B por ID
- Creación de un tweet para el usuario B
- Obtención de un timeline vacío para el usuario A
- Creación de un follower del usuario A al usuario B
- Obtención de un timeline con un tweet para el usuario A
Para poder soportar una mayor carga se debe poder escalar los distintos componentes del sistema de manera independiente según la necesidad. El siguiente enfoque separa el sistema según los distintos flujos de información:
- Escrituras de tweets
- Lecturas de timelines
- Búsquedas de tweets/usuarios/tags
- Load Balancer: distribuye la carga entre los distintos scopes según el tipo de operación sobre los datos
- CDN: almacena las imágenes/videos para que se distribuya más rápido a los usuarios
- Write API Scope: maneja las operaciones de escritura sobre los datos. Se encarga de recibir los tweets, likes, followers, etc. Este scope tiene un sistema de colas para desacoplar la escritura de los tweets y la creación de los timelines.
- Read API Scope: maneja las operaciones de lectura sobre los datos. Se encarga de recibir las solicitudes de timelines, tweets, etc. Utiliza caches para mejorar la velocidad de respuesta.
- Search API Scope: maneja las operaciones de búsqueda sobre los datos. Se encarga de recibir las solicitudes de búsqueda de tweets, usuarios, tags, etc. Utiliza un motor de búsqueda especial para mejorar la velocidad de respuesta.
- Media Store: almacena los videos/imágenes de los tweets.
- Relational Database: almacena los datos de los usuarios, tweets, followers, etc. Utiliza réplicas para mejorar la velocidad de respuesta, los scopes de write interactúan con la instancia master y los scopes de read interactúan con las instancias replica.
- Key-Value Store: almacena los timelines de los usuarios y tweets. Es la primera opción de búsqueda para el scope de read antes de ir a la base de datos relacional.
- Full Text Search Engine: almacena de manera optimizada los tweets, usuarios y tags para su posterior búsqueda.
- Graph Database: almacena las relaciones entre los usuarios (followers/followed).
El diseño puede seguir iterándose para mejorar la escalabilidad, la velocidad de respuesta y separación de responsabilidades. Pueden existir scopes de read y write para cada una de las entidades del sistema (tweets, usuarios, timelines, search, media, etc.) de modo de poder escalar cada uno de ellos de manera independiente.
-
Crear un nuevo usuario:
POST /api/v1/userCuerpo:
{ "email": "un_email@un_dominio.com", "password": "12345", "username": "un_nombre_de_usuario" }Respuesta exitosa:
{ "id": 1, "email": "un_email@un_dominio.com", "username": "un_nombre_de_usuario", "created_at": "2025-01-01T00:00:00Z", "updated_at": "2025-01-01T00:00:00Z" } -
Login:
POST /api/v1/loginCuerpo:
{ "email": "un_email@un_dominio.com", "password": "12345" }Respuesta exitosa:
{ "token": "un_token_JWT" }
El header Authorization: Bearer {token_recibido_en_el_login} debe ser enviado en las solicitudes para poder acceder
a los siguientes recursos.
-
Obtener un usuario por ID:
GET /api/v1/user/:idRespuesta exitosa:
{ "id": 1, "email": "un_email@un_dominio.com", "username": "un_nombre_de_usuario", "created_at": "2025-01-01T00:00:00Z", "updated_at": "2025-01-01T00:00:00Z" } -
Modificar un usuario por ID:
PUT /api/user/:idCuerpo:
{ "id": 1, "username": "un_nombre_de_usuario_editado" }Respuesta exitosa:
{ "id": 1, "email": "un_email@un_dominio.com", "username": "un_nombre_de_usuario_modificado", "created_at": "2025-01-01T00:00:00Z", "updated_at": "2025-02-01T00:00:00Z" } -
Crear un tweet:
POST /api/v1/tweetCuerpo:
{ "user_id": 1, "text": "el_texto_de_un_tweet" }Respuesta exitosa:
{ "id": 1, "user_id": 1, "text": "el_texto_de_un_tweet", "created_at": "2025-01-01T00:00:00Z", "updated_at": "2025-01-01T00:00:00Z" } -
Obtener un tweet por ID:
GET /api/v1/tweet/:idRespuesta exitosa:
{ "id": 1, "user_id": 1, "text": "el_texto_de_un_tweet", "created_at": "2025-01-01T00:00:00Z", "updated_at": "2025-01-01T00:00:00Z" } -
Modificar un tweet por ID:
PUT /api/tweet/:idCuerpo:
{ "id": 1, "user_id": 1, "text": "el_texto_de_un_tweet_modificado" }Respuesta exitosa:
{ "id": 1, "user_id": 1, "text": "el_texto_de_un_tweet_modificado", "created_at": "2025-01-01T00:00:00Z", "updated_at": "2025-02-01T00:00:00Z" } -
Crear un follower:
POST /api/v1/followerCuerpo:
{ "follower_id": 1, "followed_id": 2 }Respuesta exitosa:
{ "follower_id": 1, "followed_id": 2, "created_at": "2025-01-01T00:00:00Z" } -
Obtener el timeline del usuario logueado:
GET /api/v1/timelineRespuesta exitosa:
[ { "id": 1, "user_id": 1, "text": "el_texto_de_un_tweet", "created_at": "2025-01-01T00:00:00Z", "updated_at": "2025-01-01T00:00:00Z", "user": { "id": 1, "email": "un_email@un_dominio.com", "username": "un_nombre_de_usuario", "created_at": "2025-01-01T00:00:00Z", "updated_at": "2025-01-01T00:00:00Z" } } ]
Cuando la solicitud no es exitosa, el servidor responde con el siguiente formato:
{
"code": 400,
"message": "una_descripcion_del_error"
}200para solicitudesGET,PUTyDELETEexitosas201para solicitudesPOSTexitosas400para solicitudes fallidas cuando el cliente envía datos incorrectos401para solicitudes fallidas cuando el cliente debería estar autenticado y no lo está403para solicitudes fallidas cuando el cliente no está autorizado para realizar la acción404para solicitudes fallidas cuando la entidad solicitada no se encuentra500para solicitudes fallidas cuando el sistema falla por sí mismo


