Setup Proyecto TypeScript
Setup Proyecto TypeScript
En este capitulo configuraremos el proyecto OrderFlow con TypeScript. Crearemos la estructura de carpetas, las interfaces base y las clases abstractas que usaremos en los siguientes capitulos.
Inicialización
mkdir orderflow && cd orderflow
npm init -y
npm install typescript @types/node tsx -D
npx tsc --init
tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"outDir": "./dist",
"rootDir": "./src",
"declaration": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"paths": {
"@domain/*": ["./src/domain/*"],
"@application/*": ["./src/application/*"],
"@infrastructure/*": ["./src/infrastructure/*"]
}
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
Estructura de Carpetas: Arquitectura por Capas
Organizamos el codigo siguiendo una arquitectura por capas que separa claramente las responsabilidades:
src/
├── domain/
│ ├── order/
│ │ ├── order.aggregate.ts
│ │ ├── order.events.ts
│ │ └── order.repository.ts
│ └── shared/
│ ├── aggregate-root.ts
│ └── domain-event.ts
├── application/
│ ├── commands/
│ │ ├── command.ts
│ │ ├── command-bus.ts
│ │ └── order/
│ │ ├── create-order.command.ts
│ │ └── create-order.handler.ts
│ └── queries/
│ ├── query.ts
│ ├── query-bus.ts
│ └── order/
│ ├── get-order.query.ts
│ └── get-order.handler.ts
├── infrastructure/
│ ├── persistence/
│ │ ├── postgres/
│ │ └── elasticsearch/
│ └── messaging/
│ └── event-bus.ts
└── index.ts
La estructura tiene tres capas principales:
- domain/: Contiene las entidades de negocio, agregados y eventos. No depende de ninguna otra capa.
- application/: Contiene los Commands, Queries y sus Handlers. Depende solo de domain.
- infrastructure/: Contiene implementaciones concretas (bases de datos, mensajeria). Depende de domain y application.
Interfaces Base: Los Bloques Fundamentales
Definimos las clases e interfaces base que seran el fundamento de nuestra implementacion CQRS.
Aggregate Root: Base para Entidades de Dominio
El AggregateRoot es la clase base para todas las entidades raiz de nuestros agregados. Proporciona funcionalidad comun como la generacion de eventos de dominio:
// src/domain/shared/aggregate-root.ts
import { DomainEvent } from "./domain-event";
export abstract class AggregateRoot {
private _events: DomainEvent[] = [];
protected readonly _id: string;
constructor(id: string) {
this._id = id;
}
get id(): string {
return this._id;
}
protected addEvent(event: DomainEvent): void {
this._events.push(event);
}
pullEvents(): DomainEvent[] {
const events = [...this._events];
this._events = [];
return events;
}
}
El metodo addEvent permite que el agregado registre eventos cuando algo importante ocurre. El metodo pullEvents extrae esos eventos para publicarlos, limpiando la lista interna.
Domain Event: Comunicacion entre Modelos
Los eventos de dominio son la forma en que el Write Model comunica cambios al Read Model:
// src/domain/shared/domain-event.ts
export interface DomainEvent {
readonly eventId: string;
readonly aggregateId: string;
readonly occurredAt: Date;
readonly eventType: string;
}
export abstract class BaseDomainEvent implements DomainEvent {
readonly eventId: string;
readonly occurredAt: Date;
constructor(readonly aggregateId: string) {
this.eventId = crypto.randomUUID();
this.occurredAt = new Date();
}
abstract get eventType(): string;
}
Cada evento tiene un eventId unico, sabe a que agregado pertenece (aggregateId), cuando ocurrio (occurredAt) y de que tipo es (eventType).
Command Base: Estructura de Comandos
// src/application/commands/command.ts
export interface Command {
readonly commandId: string;
readonly timestamp: Date;
}
export abstract class BaseCommand implements Command {
readonly commandId: string;
readonly timestamp: Date;
constructor() {
this.commandId = crypto.randomUUID();
this.timestamp = new Date();
}
}
export interface CommandHandler<T extends Command> {
execute(command: T): Promise<void>;
}
Query Base: Estructura de Consultas
// src/application/queries/query.ts
export interface Query<TResult> {
readonly queryId: string;
}
export abstract class BaseQuery<TResult> implements Query<TResult> {
readonly queryId: string;
constructor() {
this.queryId = crypto.randomUUID();
}
}
export interface QueryHandler<TQuery extends Query<TResult>, TResult> {
execute(query: TQuery): Promise<TResult>;
}
Nota como Query<TResult> es generica: el tipo de resultado se define al crear queries especificas.
Dependencias del Proyecto
# Validación
npm install zod
# Base de datos
npm install pg @types/pg
# Elasticsearch
npm install @elastic/elasticsearch
# Mensajería (opcional)
npm install amqplib @types/amqplib
# Testing
npm install vitest -D
package.json Scripts
{
"scripts": {
"dev": "tsx watch src/index.ts",
"build": "tsc",
"start": "node dist/index.js",
"test": "vitest",
"typecheck": "tsc --noEmit"
}
}
Setup Equivalente en Go
mkdir orderflow && cd orderflow
go mod init github.com/company/orderflow
orderflow/
├── cmd/
│ └── api/
│ └── main.go
├── internal/
│ ├── domain/
│ │ └── order/
│ ├── application/
│ │ ├── command/
│ │ └── query/
│ └── infrastructure/
│ ├── postgres/
│ └── elasticsearch/
├── go.mod
└── go.sum
Setup Equivalente en Python
mkdir orderflow && cd orderflow
python -m venv .venv
source .venv/bin/activate
pip install poetry
poetry init
orderflow/
├── src/
│ └── orderflow/
│ ├── domain/
│ │ └── order/
│ ├── application/
│ │ ├── commands/
│ │ └── queries/
│ └── infrastructure/
├── tests/
├── pyproject.toml
└── poetry.lock
# pyproject.toml
[tool.poetry.dependencies]
python = "^3.12"
pydantic = "^2.0"
sqlalchemy = "^2.0"
elasticsearch = "^8.0"
Proximos Pasos
En el siguiente capitulo implementaremos el Command Bus para despachar comandos a sus handlers.
Glosario
Arquitectura por Capas
Definicion: Patron de organizacion de codigo donde el sistema se divide en capas con responsabilidades especificas y dependencias controladas. Las capas superiores dependen de las inferiores, pero no al reves.
Por que es importante: Facilita el mantenimiento y testing. Los cambios en infraestructura no afectan al dominio. Permite reemplazar implementaciones sin tocar la logica de negocio.
Ejemplo practico: domain/ no importa nada de infrastructure/. Si cambias de PostgreSQL a MongoDB, solo modificas infrastructure/, el dominio permanece intacto.
Clase Abstracta
Definicion: Clase que no puede instanciarse directamente, solo heredarse. Puede contener implementaciones parciales que las clases hijas completan.
Por que es importante: Permite definir comportamiento comun que todas las subclases comparten, mientras obliga a implementar metodos especificos.
Ejemplo practico: AggregateRoot es abstracta. No puedes crear new AggregateRoot(), pero OrderAggregate extends AggregateRoot hereda la funcionalidad de eventos.
Pull Events (Extraer Eventos)
Definicion: Patron donde los eventos generados por un agregado se acumulan internamente y se extraen en un momento posterior para publicarlos.
Por que es importante: Separa la generacion de eventos de su publicacion. El agregado se enfoca en logica de negocio; quien lo usa decide cuando publicar los eventos.
Ejemplo practico: order.confirm() agrega OrderConfirmedEvent internamente. Despues de guardar en BD, el handler llama order.pullEvents() para obtener y publicar los eventos pendientes.
Path Mapping (Alias de Rutas)
Definicion: Configuracion de TypeScript que permite usar alias como @domain/ en lugar de rutas relativas largas como ../../domain/.
Por que es importante: Mejora la legibilidad de los imports. Facilita reorganizar carpetas sin cambiar todos los imports.
Ejemplo practico: En lugar de import { Order } from "../../domain/order", usas import { Order } from "@domain/order".
Vitest
Definicion: Framework de testing moderno para JavaScript/TypeScript. Compatible con la API de Jest pero mas rapido gracias a la integracion con Vite.
Por que es importante: Los tests son esenciales en CQRS para verificar que commands y queries funcionan correctamente de forma aislada.
Ejemplo practico: Puedes escribir tests que envien commands y verifiquen que el agregado produce los eventos esperados.