├── .gitignore ├── .env ├── src ├── infrastructure │ ├── event.emitter.ts │ └── database.ts ├── events │ ├── task.events.ts │ └── event.store.ts ├── commands │ └── task.commands.ts ├── queries │ └── task.queries.ts ├── projectors │ └── task.projector.ts └── server.ts ├── Dockerfile ├── tsconfig.json ├── package.json ├── docker-compose.yml └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | DATABASE_URL=postgres://user:password@db:5432/eventsourcing -------------------------------------------------------------------------------- /src/infrastructure/event.emitter.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from "eventemitter3"; 2 | 3 | export const eventEmitter = new EventEmitter(); -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:16-alpine 2 | 3 | WORKDIR /app 4 | 5 | COPY package*.json ./ 6 | COPY tsconfig.json ./ 7 | 8 | RUN npm install 9 | 10 | COPY . . 11 | 12 | RUN npm run build 13 | 14 | CMD ["npm", "start"] -------------------------------------------------------------------------------- /src/events/task.events.ts: -------------------------------------------------------------------------------- 1 | import { Entity, PrimaryGeneratedColumn, Column } from "typeorm"; 2 | 3 | @Entity() 4 | export class Task { 5 | @PrimaryGeneratedColumn() 6 | id!: number; 7 | 8 | @Column() 9 | title: string = ''; 10 | 11 | @Column() 12 | completed: boolean = false; 13 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "CommonJS", 5 | "outDir": "./dist", 6 | "strict": true, 7 | "esModuleInterop": true, 8 | "moduleResolution": "node", 9 | "sourceMap": true, 10 | "emitDecoratorMetadata": true, 11 | "experimentalDecorators": true 12 | } 13 | } -------------------------------------------------------------------------------- /src/infrastructure/database.ts: -------------------------------------------------------------------------------- 1 | import { DataSource } from "typeorm"; 2 | import { Task } from "../events/task.events"; 3 | import { Event } from "../events/event.store"; 4 | 5 | export const AppDataSource = new DataSource({ 6 | type: "postgres", 7 | url: process.env.DATABASE_URL, 8 | synchronize: true, 9 | logging: false, 10 | entities: [Task, Event], 11 | }); -------------------------------------------------------------------------------- /src/commands/task.commands.ts: -------------------------------------------------------------------------------- 1 | import { EventStore } from "../events/event.store"; 2 | import { Task } from "../events/task.events"; 3 | 4 | export class TaskCommandHandler { 5 | constructor(private readonly eventStore: EventStore) {} 6 | 7 | async createTask(task: Omit): Promise { 8 | await this.eventStore.save("TASK_CREATED", task); 9 | } 10 | 11 | async completeTask(taskId: number): Promise { 12 | await this.eventStore.save("TASK_COMPLETED", { taskId }); 13 | } 14 | } -------------------------------------------------------------------------------- /src/queries/task.queries.ts: -------------------------------------------------------------------------------- 1 | import { AppDataSource } from "../infrastructure/database"; 2 | import { Task } from "../events/task.events"; 3 | 4 | export class TaskQueryHandler { 5 | async getAllTasks(): Promise { 6 | return AppDataSource.getRepository(Task).find(); 7 | } 8 | 9 | async getTaskHistory(taskId: number): Promise { 10 | return AppDataSource.getRepository(Event) 11 | .createQueryBuilder("event") 12 | .where("event.payload->>'taskId' = :taskId", { taskId }) 13 | .getMany(); 14 | } 15 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "event-sourcing-app", 3 | "version": "1.0.0", 4 | "scripts": { 5 | "start": "node dist/server.js", 6 | "build": "tsc", 7 | "dev": "tsc-watch --onSuccess \"node dist/server.js\"" 8 | }, 9 | "dependencies": { 10 | "dotenv": "^16.3.1", 11 | "eventemitter3": "^5.0.1", 12 | "express": "^4.18.2", 13 | "pg": "^8.11.3", 14 | "reflect-metadata": "^0.2.2", 15 | "typeorm": "^0.3.17", 16 | "typescript": "^5.2.2" 17 | }, 18 | "devDependencies": { 19 | "@types/express": "^4.17.21", 20 | "@types/node": "^20.5.7", 21 | "tsc-watch": "^6.0.4" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.8" 2 | 3 | services: 4 | app: 5 | build: . 6 | ports: 7 | - "3000:3000" 8 | environment: 9 | - DATABASE_URL=postgres://user:password@db:5432/eventsourcing 10 | depends_on: 11 | db: 12 | condition: service_healthy 13 | 14 | db: 15 | image: postgres:13 16 | environment: 17 | POSTGRES_USER: user 18 | POSTGRES_PASSWORD: password 19 | POSTGRES_DB: eventsourcing 20 | ports: 21 | - "5432:5432" 22 | healthcheck: 23 | test: ["CMD-SHELL", "pg_isready -U user -d eventsourcing"] 24 | interval: 5s 25 | timeout: 5s 26 | retries: 5 27 | -------------------------------------------------------------------------------- /src/events/event.store.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from "eventemitter3"; 2 | import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn } from "typeorm"; 3 | import { AppDataSource } from "../infrastructure/database"; 4 | 5 | @Entity() 6 | export class Event { 7 | @PrimaryGeneratedColumn() 8 | id!: number; 9 | 10 | @Column() 11 | type!: string; 12 | 13 | @Column("jsonb") 14 | payload: any; 15 | 16 | @CreateDateColumn() 17 | createdAt!: Date; 18 | } 19 | 20 | export class EventStore { 21 | constructor( 22 | private readonly emitter: EventEmitter, 23 | private readonly dataSource = AppDataSource 24 | ) {} 25 | 26 | async save(eventType: string, payload: any): Promise { 27 | try { 28 | 29 | const event = new Event(); 30 | event.type = eventType; 31 | event.payload = payload; 32 | 33 | await this.dataSource.manager.save(event); 34 | this.emitter.emit(eventType, payload); 35 | } catch (error) { 36 | console.log("error", error); 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /src/projectors/task.projector.ts: -------------------------------------------------------------------------------- 1 | import { AppDataSource } from "../infrastructure/database"; 2 | import { Task } from "../events/task.events"; 3 | import { eventEmitter } from "../infrastructure/event.emitter"; 4 | 5 | export class TaskProjector { 6 | constructor() { 7 | this.setupEventListeners(); 8 | } 9 | 10 | private setupEventListeners(): void { 11 | eventEmitter.on("TASK_CREATED", async (payload: Omit) => { 12 | const task = new Task(); 13 | task.title = payload.title; 14 | task.completed = payload.completed || false; 15 | await AppDataSource.manager.save(task); 16 | console.log("✅ Task created:", task); 17 | }); 18 | 19 | eventEmitter.on("TASK_COMPLETED", async (payload: { taskId: number }) => { 20 | const task = await AppDataSource.manager.findOneBy(Task, { 21 | id: payload.taskId, 22 | }); 23 | if (task) { 24 | task.completed = true; 25 | await AppDataSource.manager.save(task); 26 | console.log("✅ Task completed:", task); 27 | } 28 | }); 29 | } 30 | } -------------------------------------------------------------------------------- /src/server.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import { AppDataSource } from './infrastructure/database'; 3 | import { eventEmitter } from './infrastructure/event.emitter'; 4 | import { EventStore } from './events/event.store'; 5 | import { TaskCommandHandler } from './commands/task.commands'; 6 | import { TaskQueryHandler } from './queries/task.queries'; 7 | import { TaskProjector } from './projectors/task.projector'; 8 | 9 | const app = express(); 10 | app.use(express.json()); 11 | 12 | const eventStore = new EventStore(eventEmitter); 13 | const taskCommands = new TaskCommandHandler(eventStore); 14 | const taskQueries = new TaskQueryHandler(); 15 | 16 | // Inicializar el proyector 17 | new TaskProjector(); 18 | // Comandos 19 | app.post('/tasks', async (req, res) => { 20 | await taskCommands.createTask(req.body); 21 | res.status(201).send({ message: 'Task created (event stored)!' }); 22 | }); 23 | app.post('/tasks/complete', async (req, res) => { 24 | const { taskId } = req.body; 25 | if (!taskId) { 26 | return res.status(400).send({ message: 'taskId is required' }); 27 | } 28 | await taskCommands.completeTask(taskId); 29 | res.status(200).send({ message: 'Task completed!' }); 30 | }); 31 | // Consultas 32 | app.get('/tasks', async (req, res) => { 33 | const tasks = await taskQueries.getAllTasks(); 34 | res.json(tasks); 35 | }); 36 | 37 | AppDataSource.initialize().then(() => { 38 | app.listen(3000, () => { 39 | console.log('🚀 Server running on port 3000'); 40 | }); 41 | }); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # 🚀 CQRS + Event Sourcing: La Arquitectura que Usan Netflix, Uber y Airbnb (y Ahora Tú) 3 | 4 | ## 🔥 **¿Por Qué Esta Arquitectura Revoluciona el Desarrollo Moderno?** 5 | CQRS (Command Query Responsibility Segregation) + Event Sourcing es el **patrón secreto** detrás de sistemas hiper-escalables como: 6 | - 🏦 Bancos (ej.: Goldman Sachs para transacciones en tiempo real). 7 | - 🛒 E-commerce (ej.: Amazon para el carrito de compras). 8 | - 🎮 Videojuegos (ej.: Riot Games en League of Legends). 9 | 10 | **Traducción:** Si tu app maneja más de 100 usuarios, **necesitas esto YA**. 11 | 12 | --- 13 | 14 | ## 📚 **Teoría en 60 Segundos (o Tu App Morirá)** 15 | ### 1. **CQRS**: Separa las operaciones de **ESCRITURA** (Commands) y **LECTURA** (Queries). 16 | - **Commands**: `POST /tasks` → Crean/actualizan datos (ej.: "Completar tarea"). 17 | - **Queries**: `GET /tasks` → Solo leen datos (ej.: "Mostrar tareas"). 18 | - **Beneficio**: Escalas lecturas y escrituras **POR SEPARADO** (¡adiós a los cuellos de botella!). 19 | 20 | ### 2. **Event Sourcing**: Almacena **CADA CAMBIO** como un evento inmutable. 21 | - Ejemplo: En lugar de guardar "Tarea completada = true", guardas el evento `TASK_COMPLETED`. 22 | - **Beneficio**: Auditoría total, viaje en el tiempo (debugging), y resiliencia a fallos. 23 | 24 | 25 | 26 | --- 27 | 28 | ## 🛠 **Cómo Funciona Este Repo (Código Explicado)** 29 | ### Estructura del Proyecto 30 | ``` 31 | src/ 32 | ├── events/ # Modelos de eventos y almacenamiento 33 | ├── commands/ # Lógica para modificar datos 34 | ├── queries/ # Lógica para leer datos 35 | ├── projectors/ # Proyectores que actualizan vistas 36 | ├── infrastructure/ # Base de datos, EventEmitter, etc. 37 | └── server.ts # API con Express 38 | ``` 39 | 40 | ### Componentes Clave 41 | #### 1. **Event Store (El Corazón del Sistema)** 42 | ```typescript 43 | // events/event.store.ts 44 | @Entity() 45 | export class Event { 46 | @PrimaryGeneratedColumn() 47 | id: number; 48 | 49 | @Column() 50 | type: string; // Ej: "TASK_CREATED" 51 | 52 | @Column('jsonb') 53 | payload: any; // Ej: { "title": "Hacer evento" } 54 | } 55 | 56 | // Guarda eventos y los dispara al sistema 57 | async save(eventType: string, payload: any) { 58 | const event = new Event(); 59 | await this.dataSource.manager.save(event); 60 | this.emitter.emit(eventType, payload); // ¡Notifica a todos! 61 | } 62 | ``` 63 | 64 | #### 2. **Commands (Acciones que Cambian el Estado)** 65 | ```typescript 66 | // commands/task.commands.ts 67 | class TaskCommandHandler { 68 | async createTask(task: Task) { 69 | await this.eventStore.save("TASK_CREATED", task); 70 | } 71 | } 72 | ``` 73 | 74 | #### 3. **Queries (Obtiene Datos sin Modificarlos)** 75 | ```typescript 76 | // queries/task.queries.ts 77 | class TaskQueryHandler { 78 | async getAllTasks() { 79 | return AppDataSource.getRepository(Task).find(); 80 | } 81 | } 82 | ``` 83 | 84 | #### 4. **Projector (Actualiza Vistas al Escuchar Eventos)** 85 | ```typescript 86 | // projectors/task.projector.ts 87 | eventEmitter.on("TASK_CREATED", async (payload) => { 88 | const task = new Task(); 89 | await AppDataSource.manager.save(task); // Actualiza la tabla 'tasks' 90 | }); 91 | ``` 92 | 93 | --- 94 | 95 | ## 🚀 **Cómo Ejecutar el Proyecto (y Dominar el Patrón)** 96 | ### 1. Clona y construye con Docker (¡Solo 2 Comandos!) 97 | ```bash 98 | git clone https://github.com/jaimeirazabal1/event-sourcing-app 99 | cd event-sourcing-app 100 | docker-compose up --build 101 | ``` 102 | 103 | ### 2. Prueba la API como un PRO 104 | ```bash 105 | # Crea una tarea 106 | curl -X POST -H "Content-Type: application/json" -d '{"title":"Dominar CQRS"}' http://localhost:3000/tasks 107 | 108 | # Completa una tarea (cambia el ID) 109 | curl -X POST -H "Content-Type: application/json" -d '{"taskId":1}' http://localhost:3000/tasks/complete 110 | 111 | # Ver todas las tareas 112 | curl http://localhost:3000/tasks 113 | ``` 114 | 115 | ### 3. Explora la Base de Datos 116 | ```bash 117 | //ver lista de contenedores 118 | docker-compose ps 119 | //obtener eventos creados en la base de datos 120 | docker exec -it psql -U user -d eventsourcing -c "SELECT * FROM event;" 121 | ``` 122 | **Salida:** 123 | ``` 124 | id | type | payload | createdAt 125 | ---+---------------+-----------------------------+------------------------- 126 | 1 | TASK_CREATED | {"title": "Dominar CQRS"} | 2023-10-05 12:00:00 127 | 2 | TASK_COMPLETED| {"taskId": 1} | 2023-10-05 12:05:00 128 | ``` 129 | 130 | --- 131 | 132 | ## 💡 **Cómo Practicar y Llegar a Senior (Pasos)** 133 | 1. **Agrega un Nuevo Evento**: Implementa `TASK_UPDATED` y su proyector. 134 | 2. **Crea una Nueva Vista**: Haz una tabla `task_history` que muestre todos los cambios. 135 | 3. **Integra WebSockets**: Usa `eventEmitter` para notificar a clientes en tiempo real. 136 | 4. **Despliega en Kubernetes**: Agrega un archivo `k8s.yaml` y escala los microservicios. 137 | 138 | --- 139 | 140 | ## 🌍 **¿Quién Usa Esto en Producción?** 141 | - **Netflix**: Para sincronizar estados entre microservicios. 142 | - **Uber**: En su sistema de manejo de viajes. 143 | - **EventStoreDB**: Base de datos diseñada específicamente para Event Sourcing. 144 | - **Tu Próximo Empleador**: Si ven esto en tu GitHub, te contratarán al instante. 145 | 146 | --- 147 | 148 | ## 📖 **Recursos para Ser un Experto** 149 | - Libro: **[Patterns, Principles, and Practices of Domain-Driven Design](https://amzn.to/3LJ5WfZ)** (Biblia de DDD/CQRS). 150 | - Video: **[Event Sourcing en 10 Minutos](https://youtu.be/A0g1skWZSJQ)** (Con ejemplos reales). 151 | - Herramienta: **[EventStoreDB](https://www.eventstore.com/)** (Alternativa profesional a PostgreSQL). 152 | 153 | ⚠️ ¿Quieres ser Senior? Clona, modifica y SUBE TU VERSIÓN A GITHUB HOY. 154 | 155 | 🌟 ¡Dale Estrella al Repo Aquí! 🌟 156 | 157 | --------------------------------------------------------------------------------