@ronaldjdevfs/forge 1.3.0-beta → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/package.json +1 -1
- package/skills/forge/SKILL.md +57 -128
- package/skills/forge/command/forge.md +59 -14
- package/skills/forge/reference/adr.md +242 -0
- package/skills/forge/reference/anti-corruption-layer.md +340 -0
- package/skills/forge/reference/api-design.md +7 -0
- package/skills/forge/reference/api-versioning.md +354 -0
- package/skills/forge/reference/architectural-depth-checklist.md +311 -0
- package/skills/forge/reference/architecture-template.md +41 -0
- package/skills/forge/reference/assay.md +6 -0
- package/skills/forge/reference/bounded-contexts.md +311 -0
- package/skills/forge/reference/chain.md +6 -0
- package/skills/forge/reference/cohesion-checklist.md +256 -0
- package/skills/forge/reference/cqrs.md +286 -0
- package/skills/forge/reference/data-patterns.md +6 -0
- package/skills/forge/reference/di-strategies.md +6 -0
- package/skills/forge/reference/errors.md +5 -0
- package/skills/forge/reference/events.md +8 -0
- package/skills/forge/reference/evolutionary-architecture.md +300 -0
- package/skills/forge/reference/forge.md +7 -0
- package/skills/forge/reference/hooks.md +6 -0
- package/skills/forge/reference/idempotency.md +283 -0
- package/skills/forge/reference/inscribe.md +5 -0
- package/skills/forge/reference/inspect.md +6 -0
- package/skills/forge/reference/modular-monolith.md +252 -0
- package/skills/forge/reference/observability.md +5 -0
- package/skills/forge/reference/quench.md +5 -0
- package/skills/forge/reference/relocate.md +6 -0
- package/skills/forge/reference/sagas.md +359 -0
- package/skills/forge/reference/security-patterns.md +6 -0
- package/skills/forge/reference/smelt.md +6 -0
- package/skills/forge/reference/temper.md +6 -0
- package/skills/forge/reference/testing-patterns.md +6 -0
- package/skills/forge/reference/transactional-outbox.md +311 -0
- package/skills/forge/scripts/architecture.mjs +10 -5
- package/skills/forge/scripts/assay.mjs +2 -2
- package/skills/forge/scripts/chain.mjs +31 -5
- package/skills/forge/scripts/context.mjs +24 -4
- package/skills/forge/scripts/detect.mjs +39 -30
- package/skills/forge/scripts/forge-boot.mjs +108 -0
- package/skills/forge/scripts/forge-config.mjs +182 -3
- package/skills/forge/scripts/forge-state.mjs +1 -1
- package/skills/forge/scripts/forgeSentinel-lib.mjs +2 -2
- package/skills/forge/scripts/forgeSentinel.mjs +2 -2
- package/skills/forge/scripts/forgeSmith.mjs +2 -2
- package/skills/forge/scripts/graph.mjs +65 -9
- package/skills/forge/scripts/hook.mjs +2 -2
- package/skills/forge/scripts/inspect.mjs +56 -48
- package/skills/forge/scripts/parse-imports.mjs +0 -2
- package/skills/forge/scripts/posttool.mjs +211 -17
- package/skills/forge/scripts/recommendation-engine.mjs +125 -0
- package/skills/forge/scripts/rollback.mjs +5 -3
- package/skills/forge/templates/feature/acl-gateway.ts.md +52 -0
- package/skills/forge/templates/feature/acl-repository.ts.md +36 -0
- package/skills/forge/templates/feature/acl-translator.ts.md +24 -0
- package/skills/forge/templates/feature/cqrs-query.ts.md +40 -0
- package/skills/forge/templates/feature/outbox-repository.ts.md +36 -0
- package/skills/forge/templates/feature/saga-orchestrator.ts.md +72 -0
- package/skills/forge/templates/platform/outbox-relayer.ts.md +80 -0
- package/skills/forge/tests/core.test.mjs +288 -4
- package/src/cli.js +26 -13
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
# Modular Monolith
|
|
2
|
+
|
|
3
|
+
No todo proyecto necesita microservicios. El modular monolith es el punto óptimo para la mayoría de los equipos: la modularidad de features de Forge con la simplicidad operativa de un solo deploy.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Espectro Monolith → Modular → Microservices
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
Monolith clásico Modular Monolith Microservicios
|
|
11
|
+
┌──────────────────┐ ┌──────────────────┐ ┌──┐ ┌──┐ ┌──┐ ┌──┐
|
|
12
|
+
│ Todo en una capa │ │ ┌─┐ ┌─┐ ┌─┐ ┌─┐│ │F1│ │F2│ │F3│ │F4│
|
|
13
|
+
│ Sin boundaries │ │ │F1│ │F2│ │F3│ │F4││ └──┘ └──┘ └──┘ └──┘
|
|
14
|
+
│ BD compartida │ │ └─┘ └─┘ └─┘ └─┘│ BD separadas
|
|
15
|
+
│ Sin features │ │ Platform/Shared/Infra │ APIs síncronas
|
|
16
|
+
└──────────────────┘ └──────────────────┘ │ Eventos asíncronos
|
|
17
|
+
│ Deploy independiente
|
|
18
|
+
└───────────────────────────
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
| Aspecto | Monolith clásico | Modular Monolith | Microservicios |
|
|
22
|
+
|---|---|---|---|
|
|
23
|
+
| **Boundaries** | No existen | Features con R8-R9 | Servicios independientes |
|
|
24
|
+
| **Comunicación** | Llamadas directas | Inyección de interfaces + eventos | HTTP/gRPC + eventos |
|
|
25
|
+
| **Deploy** | Todo junto | Todo junto | Independiente |
|
|
26
|
+
| **BD** | Compartida | Schemas separados por feature | BD independiente |
|
|
27
|
+
| **Equipo** | 1 equipo | 1-2 equipos | 3+ equipos |
|
|
28
|
+
| **Forge** | No aplica | Modelo nativo | Modelo nativo + split |
|
|
29
|
+
| **Escala** | < 20 features | < 40 features | Sin límite |
|
|
30
|
+
|
|
31
|
+
El modular monolith NO es un monolith clásico. La diferencia son los **boundaries explícitos**: cada feature respeta R8 (no imports directos) y R9 (no ciclos), igual que en microservicios, pero todo corre en el mismo proceso.
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Marco de Decisión
|
|
36
|
+
|
|
37
|
+
### Cuándo mantener Modular Monolith
|
|
38
|
+
|
|
39
|
+
| Condición | Señal |
|
|
40
|
+
|---|---|
|
|
41
|
+
| Equipo pequeño (< 8 personas) | Un solo equipo puede mantener todas las features |
|
|
42
|
+
| Dominio cohesionado | Los bounded contexts están estrechamente relacionados |
|
|
43
|
+
| Latencia crítica | El overhead de red de microservicios es inaceptable |
|
|
44
|
+
| Fase inicial | El dominio no está lo suficientemente entendido para partir |
|
|
45
|
+
| Baja carga operativa | Sin equipo de infraestructura dedicado |
|
|
46
|
+
| Deploy semanal/mensual | La velocidad de deploy no es el cuello de botella |
|
|
47
|
+
|
|
48
|
+
### Cuándo considerar partir a microservicios
|
|
49
|
+
|
|
50
|
+
| Señal | Síntoma |
|
|
51
|
+
|---|---|
|
|
52
|
+
| **Team scaling** | Dos equipos distintos modifican la misma feature frecuentemente |
|
|
53
|
+
| **Deployment bottleneck** | Un cambio en una feature requiere validar todo el monolith |
|
|
54
|
+
| **Boundaries maduros** | Los bounded contexts están estables y bien definidos |
|
|
55
|
+
| **Failure isolation** | Una feature con alta criticidad (ej. pagos) arrastra a las demás en fallos |
|
|
56
|
+
| **Diferentes características** | Una feature necesita escalar distinto (ej. Search vs Catalog) |
|
|
57
|
+
| **Stack divergence** | Una feature se beneficiaría de un stack tecnológico distinto |
|
|
58
|
+
|
|
59
|
+
### Contra-señales (no partir)
|
|
60
|
+
|
|
61
|
+
| Falsa señal | Realidad |
|
|
62
|
+
|---|---|
|
|
63
|
+
| "Es lo moderno" | Microservicios son una decisión de negocio, no técnica |
|
|
64
|
+
| "Escalabilidad" | El cuello de botella suele ser BD, no el monolith |
|
|
65
|
+
| "Equipos autónomos" | Sin boundaries bien definidos, los microservicios serán un distributed monolith |
|
|
66
|
+
| "Rendimiento" | El overhead de red puede empeorar la latencia |
|
|
67
|
+
| "Está de moda" | Moda tecnológica no justifica la complejidad operativa |
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## Arquitectura Interna del Modular Monolith
|
|
72
|
+
|
|
73
|
+
### Comunicación entre features
|
|
74
|
+
|
|
75
|
+
En el modular monolith, la comunicación entre features sigue exactamente las mismas reglas que en microservicios:
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
// ✅ Permitido: inyección de interfaz desde shared
|
|
79
|
+
// src/features/orders/application/use-cases/PlaceOrderUseCase.ts
|
|
80
|
+
import { IInventoryService } from "src/shared/contracts/catalog";
|
|
81
|
+
import { IUnitOfWork } from "src/shared/contracts/data";
|
|
82
|
+
|
|
83
|
+
// ❌ Prohibido: import directo a otra feature (R8)
|
|
84
|
+
import { StockEntity } from "src/features/inventory/domain/StockEntity";
|
|
85
|
+
|
|
86
|
+
// ❌ Prohibido: import directo a infra de otra feature (R1 + R8)
|
|
87
|
+
import { prisma } from "src/features/inventory/adapters/out/persistence/prisma";
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Shared Kernel
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
// src/shared/contracts/catalog/IInventoryService.ts
|
|
94
|
+
export interface IInventoryService {
|
|
95
|
+
reserveStock(orderId: string, items: LineItem[]): Promise<ReservationResult>;
|
|
96
|
+
releaseStock(reservationId: string): Promise<void>;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Los DTOs de los contratos viven junto a la interfaz
|
|
100
|
+
export type ReservationResult = {
|
|
101
|
+
success: boolean;
|
|
102
|
+
reservationId?: string;
|
|
103
|
+
insufficientItems: { sku: string; available: number }[];
|
|
104
|
+
};
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
La implementación se inyecta en tiempo de construcción:
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
// src/features/inventory/adapters/out/shared/InventoryService.ts
|
|
111
|
+
export class InventoryServiceImpl implements IInventoryService {
|
|
112
|
+
constructor(private readonly stockRepo: IStockRepository) {}
|
|
113
|
+
// implementación real que la feature Inventory expone al Shared Kernel
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Base de datos
|
|
118
|
+
|
|
119
|
+
En el modular monolith, cada feature tiene su propio schema dentro de la misma BD:
|
|
120
|
+
|
|
121
|
+
```sql
|
|
122
|
+
-- Schema por feature, misma base de datos
|
|
123
|
+
CREATE SCHEMA IF NOT EXISTS orders;
|
|
124
|
+
CREATE SCHEMA IF NOT EXISTS catalog;
|
|
125
|
+
CREATE SCHEMA IF NOT EXISTS inventory;
|
|
126
|
+
|
|
127
|
+
CREATE TABLE orders.orders ( … );
|
|
128
|
+
CREATE TABLE catalog.products ( … );
|
|
129
|
+
CREATE TABLE inventory.stock ( … );
|
|
130
|
+
|
|
131
|
+
-- No existen foreign keys entre schemas de distintas features
|
|
132
|
+
-- La integridad referencial se maneja en la aplicación, no en la BD
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
En Prisma esto se configura con `schema` por modelo:
|
|
136
|
+
|
|
137
|
+
```prisma
|
|
138
|
+
generator client {
|
|
139
|
+
provider = "prisma-client-js"
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
datasource db {
|
|
143
|
+
provider = "postgresql"
|
|
144
|
+
url = env("DATABASE_URL")
|
|
145
|
+
schemas = ["orders", "catalog", "inventory"]
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
model Order {
|
|
149
|
+
id String @id @default(uuid())
|
|
150
|
+
// ...
|
|
151
|
+
@@schema("orders")
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
model Product {
|
|
155
|
+
id String @id @default(uuid())
|
|
156
|
+
// ...
|
|
157
|
+
@@schema("catalog")
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### Eventos dentro del monolith
|
|
162
|
+
|
|
163
|
+
Dentro del modular monolith, los eventos pueden ser sincrónicos (in-process event bus) o asíncronos (message broker):
|
|
164
|
+
|
|
165
|
+
```ts
|
|
166
|
+
// Sincrónico: EventBus in-process, sin serialización
|
|
167
|
+
// platform/events/EventBus.ts
|
|
168
|
+
export interface IEventBus {
|
|
169
|
+
publish(event: DomainEvent): Promise<void>;
|
|
170
|
+
subscribe<T extends DomainEvent>(eventType: string, handler: (event: T) => Promise<void>): void;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Uso en feature
|
|
174
|
+
// orders/application/use-cases/PlaceOrderUseCase.ts
|
|
175
|
+
await this.eventBus.publish(new OrderPlacedEvent(order));
|
|
176
|
+
// inventory/adapters/in/events/OrderPlacedHandler.ts
|
|
177
|
+
eventBus.subscribe("OrderPlaced", async (event) => {
|
|
178
|
+
await this.stockService.reserveStock(event.orderId, event.items);
|
|
179
|
+
});
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
**Regla: dentro del monolith, puedes elegir síncrono o asíncrono.** Pero si planeas partir a microservicios en el futuro, usa asíncrono desde el día 1 (message broker). La migración será solo cambiar la URL del broker.
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
## Integración con Forge
|
|
187
|
+
|
|
188
|
+
| Elemento | Modular Monolith | Microservicios |
|
|
189
|
+
|---|---|---|
|
|
190
|
+
| **Reglas R1-R9** | Se aplican igual | Se aplican igual |
|
|
191
|
+
| **Contratos en shared/** | Interfaces + DTOs | Mismos contratos (packages publicados) |
|
|
192
|
+
| **Eventos** | In-process o broker | Broker siempre |
|
|
193
|
+
| **Platform** | Monolítica, compartida | Por servicio o shared library |
|
|
194
|
+
| **Infra** | Schemas separados en misma BD | BD independientes |
|
|
195
|
+
| **Deploy** | Un solo artefacto | N artefactos |
|
|
196
|
+
| **forge cast** | Crea feature en el monolith | Crea feature + esqueleto de servicio |
|
|
197
|
+
| **forge inspect** | Auditoría normal | Auditoría + health check distribuido |
|
|
198
|
+
| **forge relocate** | Extraer feature del monolith | Dividir servicio o fusionar |
|
|
199
|
+
|
|
200
|
+
### Transición guiada: Modular Monolith → Microservicios
|
|
201
|
+
|
|
202
|
+
```
|
|
203
|
+
Fase 1: Asegurar boundaries
|
|
204
|
+
- Verificar que R8 y R9 se cumplen
|
|
205
|
+
- shared/contracts/ debe contener todas las interfaces entre features
|
|
206
|
+
- Schemas separados por feature
|
|
207
|
+
|
|
208
|
+
Fase 2: Extraer eventos
|
|
209
|
+
- Migrar de event bus in-process a broker (RabbitMQ, Kafka)
|
|
210
|
+
- Verificar que los handlers de eventos son idempotentes
|
|
211
|
+
|
|
212
|
+
Fase 3: Extraer feature como servicio
|
|
213
|
+
1. forge relocate extrae la feature a un nuevo repo
|
|
214
|
+
2. La feature origen ahora inyecta un cliente HTTP/gRPC en vez de la impl directa
|
|
215
|
+
3. El contrato en shared/ se convierte en API contract
|
|
216
|
+
4. Los eventos viajan por el broker existente
|
|
217
|
+
|
|
218
|
+
Fase 4: Iterar
|
|
219
|
+
- Repetir Fase 3 para cada feature que se beneficie de partir
|
|
220
|
+
- Dejar las demás en el monolith
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
---
|
|
224
|
+
|
|
225
|
+
## Anti-patrones
|
|
226
|
+
|
|
227
|
+
| Anti-patrón | Problema | Solución |
|
|
228
|
+
|---|---|---|
|
|
229
|
+
| **Distributed Monolith** | Microservicios que se llaman sincrónicamente en cadena. Si un servicio falla, todos fallan. | Usar eventos asíncronos entre servicios. Si la latencia lo exige sincrónico, reconsiderar si deberían ser un solo servicio. |
|
|
230
|
+
| **Shared Database (microservices)** | Microservicios que comparten BD. Los boundaries son ficticios. | Migrar a schemas separados o BD independientes. Empezar con schemas si el split es futuro. |
|
|
231
|
+
| **Nano-services** | Microservicio por entidad. Cada servicio es un CRUD sin lógica. | Fusionar en features por dominio de negocio. El overhead de operar N servicios pequeños es mayor que el beneficio. |
|
|
232
|
+
| **Premature splitting** | Partir antes de entender los bounded contexts. | Esperar a que los contexts estén estables. El monolith modular permite partir sin reescribir. |
|
|
233
|
+
| **Monolith con carpetas** | Solo hay separación por carpetas, no por boundaries. Cualquier código importa cualquier otro. | Implementar R8 y R9. Si duele, significa que los boundaries están mal diseñados o no existen. |
|
|
234
|
+
|
|
235
|
+
---
|
|
236
|
+
|
|
237
|
+
## Conexión con Forge
|
|
238
|
+
|
|
239
|
+
| Comando | Acción |
|
|
240
|
+
|---|---|
|
|
241
|
+
| `forge cast` | Crea feature en el monolith modular con boundaries correctos |
|
|
242
|
+
| `forge inspect` | Verifica que los boundaries entre features son saludables |
|
|
243
|
+
| `forge relocate` | Extrae una feature del monolith a un servicio independiente |
|
|
244
|
+
| `forge reforge` | Rediseña boundaries mal definidos dentro del monolith |
|
|
245
|
+
| `forge graph` | Visualiza dependencias entre features; identifica candidatos a partir |
|
|
246
|
+
|
|
247
|
+
## Ver también
|
|
248
|
+
|
|
249
|
+
- `reference/bounded-contexts.md` — contexts como módulos del monolith
|
|
250
|
+
- `reference/anti-corruption-layer.md` — ACL entre módulos
|
|
251
|
+
- `reference/sagas.md` — coordinación entre módulos
|
|
252
|
+
- `reference/events.md` — comunicación asíncrona entre módulos
|
|
@@ -64,3 +64,8 @@ export interface IHealthCheck {
|
|
|
64
64
|
- Tracing distribuido con baggage contextual
|
|
65
65
|
- Health checks sin autenticación (solo para orquestadores)
|
|
66
66
|
- Alertas basadas en métricas, no en logs
|
|
67
|
+
|
|
68
|
+
## Ver también
|
|
69
|
+
|
|
70
|
+
- `reference/security-patterns.md` — audit logging como práctica de seguridad
|
|
71
|
+
- `reference/testing-patterns.md` — tests de instrumentación y health checks
|
|
@@ -71,4 +71,9 @@ node .opencode/skills/forge/scripts/detect.mjs --severity ERROR
|
|
|
71
71
|
|
|
72
72
|
# Solo un tipo específico
|
|
73
73
|
node .opencode/skills/forge/scripts/detect.mjs --type layers
|
|
74
|
+
|
|
75
|
+
## Ver también
|
|
76
|
+
|
|
77
|
+
- `reference/evolutionary-architecture.md` — fitness functions como validación continua
|
|
78
|
+
- `reference/hooks.md` — integración de quench en pre-commit hook
|
|
74
79
|
```
|
|
@@ -49,3 +49,9 @@ El backup se almacena en `.forge/backups/<target>--<timestamp>/` y preserva la e
|
|
|
49
49
|
| Feature | `src/application/use-cases/<name>/` | `src/features/<name>/` |
|
|
50
50
|
| Shared | `src/utils/`, `src/helpers/`, `src/lib/` | `src/shared/<name>/` |
|
|
51
51
|
| Infra | `src/database/`, `src/providers/` | `src/infra/<name>/` |
|
|
52
|
+
|
|
53
|
+
## Ver también
|
|
54
|
+
|
|
55
|
+
- `reference/anti-corruption-layer.md` — aislamiento de legacy durante migración
|
|
56
|
+
- `reference/modular-monolith.md` — decisión de estructura al migrar features
|
|
57
|
+
- `reference/reforge.md` — refactor post-migración
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
# Sagas — Transacciones Multi-Feature
|
|
2
|
+
|
|
3
|
+
Los procesos de negocio rara vez caben en una sola feature. Una saga coordina múltiples pasos a través de bounded contexts, garantizando consistencia eventual sin ACID distribuido.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## El Problema
|
|
8
|
+
|
|
9
|
+
Una transacción como "realizar un pedido" cruza múltiples features:
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
Orders: crear pedido → Inventory: reservar stock → Payments: cobrar → Shipping: preparar envío
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
En un monolith con una sola BD, esto sería una transacción ACID. Cuando cada feature tiene su propio schema o BD, no hay transacción distribuida. Las sagas resuelven este problema.
|
|
16
|
+
|
|
17
|
+
### Una saga no es:
|
|
18
|
+
- Una transacción ACID (no hay rollback, hay compensación)
|
|
19
|
+
- Un workflow BPMN con motor externo (aunque puede parecerse)
|
|
20
|
+
- Un event pipeline unidireccional (tiene lógica de decisión)
|
|
21
|
+
|
|
22
|
+
### Una saga es:
|
|
23
|
+
- Una secuencia de transacciones locales
|
|
24
|
+
- Cada paso tiene una acción compensatoria si falla
|
|
25
|
+
- La saga completa tiene consistencia eventual garantizada
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## Coreografía (Choreography)
|
|
30
|
+
|
|
31
|
+
Cada participante publica eventos y reacciona a eventos de otros. No hay coordinador central.
|
|
32
|
+
|
|
33
|
+
```mermaid
|
|
34
|
+
sequenceDiagram
|
|
35
|
+
participant Orders
|
|
36
|
+
participant Inventory
|
|
37
|
+
participant Payments
|
|
38
|
+
participant Shipping
|
|
39
|
+
|
|
40
|
+
Orders->>Orders: crear pedido
|
|
41
|
+
Orders->>Inventory: OrderPlaced (event)
|
|
42
|
+
Inventory->>Inventory: reservar stock
|
|
43
|
+
alt stock suficiente
|
|
44
|
+
Inventory->>Orders: StockReserved (event)
|
|
45
|
+
Orders->>Payments: cobrar (event)
|
|
46
|
+
Payments->>Payments: procesar pago
|
|
47
|
+
else stock insuficiente
|
|
48
|
+
Inventory->>Orders: StockReservationFailed (event)
|
|
49
|
+
Orders->>Orders: cancelar pedido
|
|
50
|
+
end
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Implementación coreografía
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
// Cada feature publica y escucha eventos
|
|
57
|
+
// features/orders/adapters/in/events/OrderSaga.ts
|
|
58
|
+
export class OrderSaga {
|
|
59
|
+
constructor(
|
|
60
|
+
private readonly orderRepo: IOrderRepository,
|
|
61
|
+
private readonly eventBus: IEventBus,
|
|
62
|
+
) {
|
|
63
|
+
// Escucha respuestas de otros features
|
|
64
|
+
this.eventBus.subscribe("StockReserved", this.onStockReserved.bind(this));
|
|
65
|
+
this.eventBus.subscribe("StockReservationFailed", this.onStockFailed.bind(this));
|
|
66
|
+
this.eventBus.subscribe("PaymentProcessed", this.onPaymentProcessed.bind(this));
|
|
67
|
+
this.eventBus.subscribe("PaymentFailed", this.onPaymentFailed.bind(this));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async onStockReserved(event: StockReservedEvent): Promise<void> {
|
|
71
|
+
await this.orderRepo.updateStatus(event.orderId, "stock_reserved");
|
|
72
|
+
this.eventBus.publish(new RequestPaymentCommand(event.orderId, event.amount));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async onStockFailed(event: StockReservationFailedEvent): Promise<void> {
|
|
76
|
+
await this.orderRepo.updateStatus(event.orderId, "cancelled_no_stock");
|
|
77
|
+
// No hay compensación adicional: el pedido nunca se confirmó
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async onPaymentProcessed(event: PaymentProcessedEvent): Promise<void> {
|
|
81
|
+
await this.orderRepo.updateStatus(event.orderId, "paid");
|
|
82
|
+
this.eventBus.publish(new RequestShippingCommand(event.orderId));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async onPaymentFailed(event: PaymentFailedEvent): Promise<void> {
|
|
86
|
+
await this.orderRepo.updateStatus(event.orderId, "payment_failed");
|
|
87
|
+
// Compensación: liberar stock
|
|
88
|
+
this.eventBus.publish(new ReleaseStockCommand(event.orderId));
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Pros y contras
|
|
94
|
+
|
|
95
|
+
| Pro | Contra |
|
|
96
|
+
|---|---|
|
|
97
|
+
| Sin punto central de fallo | Lógica de saga distribuida: difícil de entender el flujo completo |
|
|
98
|
+
| Cada feature es autónomo | Seguimiento complejo (¿en qué paso está cada pedido?) |
|
|
99
|
+
| Fácil de añadir nuevos participantes | Riesgo de ciclos de eventos |
|
|
100
|
+
| Escala horizontalmente | Testing de integración más complejo |
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## Orquestación (Orchestration)
|
|
105
|
+
|
|
106
|
+
Un coordinador central (orchestrator) dirige cada paso y decide el flujo.
|
|
107
|
+
|
|
108
|
+
```mermaid
|
|
109
|
+
sequenceDiagram
|
|
110
|
+
participant CheckoutSaga as Orchestrator
|
|
111
|
+
participant Orders
|
|
112
|
+
participant Inventory
|
|
113
|
+
participant Payments
|
|
114
|
+
participant Shipping
|
|
115
|
+
|
|
116
|
+
CheckoutSaga->>Orders: crearPedido()
|
|
117
|
+
Orders-->>CheckoutSaga: ok
|
|
118
|
+
CheckoutSaga->>Inventory: reservarStock()
|
|
119
|
+
Inventory-->>CheckoutSaga: ok
|
|
120
|
+
CheckoutSaga->>Payments: procesarPago()
|
|
121
|
+
alt pago exitoso
|
|
122
|
+
Payments-->>CheckoutSaga: ok
|
|
123
|
+
CheckoutSaga->>Shipping: prepararEnvio()
|
|
124
|
+
else pago falla
|
|
125
|
+
Payments-->>CheckoutSaga: failed
|
|
126
|
+
CheckoutSaga->>Inventory: liberarStock() ← compensación
|
|
127
|
+
CheckoutSaga->>Orders: cancelarPedido() ← compensación
|
|
128
|
+
end
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Implementación orquestación
|
|
132
|
+
|
|
133
|
+
El orchestrator es un feature independiente:
|
|
134
|
+
|
|
135
|
+
```
|
|
136
|
+
src/features/checkout-saga/
|
|
137
|
+
domain/
|
|
138
|
+
SagaInstance.entity.ts ← estado de cada saga activa
|
|
139
|
+
ISagaStepExecutor.ts ← interfaz para ejecutar pasos
|
|
140
|
+
application/
|
|
141
|
+
use-cases/
|
|
142
|
+
ExecuteCheckoutSaga.ts ← orquesta la saga
|
|
143
|
+
adapters/
|
|
144
|
+
out/
|
|
145
|
+
saga-steps/
|
|
146
|
+
CreateOrderStep.ts ← llama a Orders
|
|
147
|
+
ReserveStockStep.ts ← llama a Inventory
|
|
148
|
+
ProcessPaymentStep.ts ← llama a Payments
|
|
149
|
+
PrepareShippingStep.ts ← llama a Shipping
|
|
150
|
+
CancelOrderStep.ts ← compensación de Orders
|
|
151
|
+
ReleaseStockStep.ts ← compensación de Inventory
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
```ts
|
|
155
|
+
// features/checkout-saga/application/use-cases/ExecuteCheckoutSaga.ts
|
|
156
|
+
export class ExecuteCheckoutSaga {
|
|
157
|
+
constructor(
|
|
158
|
+
private readonly sagaRepo: ISagaInstanceRepository,
|
|
159
|
+
private readonly stepFactory: SagaStepFactory,
|
|
160
|
+
) {}
|
|
161
|
+
|
|
162
|
+
async execute(command: StartCheckoutCommand): Promise<void> {
|
|
163
|
+
const saga = SagaInstance.start("CHECKOUT", {
|
|
164
|
+
orderId: command.orderId,
|
|
165
|
+
customerId: command.customerId,
|
|
166
|
+
items: command.items,
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
const steps = [
|
|
170
|
+
this.stepFactory.createOrderStep(),
|
|
171
|
+
this.stepFactory.reserveStockStep(),
|
|
172
|
+
this.stepFactory.processPaymentStep(),
|
|
173
|
+
this.stepFactory.prepareShippingStep(),
|
|
174
|
+
];
|
|
175
|
+
|
|
176
|
+
for (const step of steps) {
|
|
177
|
+
try {
|
|
178
|
+
await step.execute(saga.context);
|
|
179
|
+
saga.advance(step.name);
|
|
180
|
+
await this.sagaRepo.save(saga);
|
|
181
|
+
} catch (error) {
|
|
182
|
+
saga.fail(step.name, error.message);
|
|
183
|
+
await this.compensate(saga, steps);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
saga.complete();
|
|
189
|
+
await this.sagaRepo.save(saga);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
private async compensate(saga: SagaInstance, steps: SagaStep[]): Promise<void> {
|
|
193
|
+
// Ejecuta compensaciones en orden inverso
|
|
194
|
+
const executedSteps = steps.slice(0, saga.currentStepIndex);
|
|
195
|
+
for (const step of executedSteps.reverse()) {
|
|
196
|
+
try {
|
|
197
|
+
await step.compensate(saga.context);
|
|
198
|
+
} catch (err) {
|
|
199
|
+
// La compensación falló — requiere intervención manual
|
|
200
|
+
saga.requireManualIntervention();
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### Pros y contras
|
|
208
|
+
|
|
209
|
+
| Pro | Contra |
|
|
210
|
+
|---|---|
|
|
211
|
+
| Flujo completo visible en un lugar | Punto central de fallo |
|
|
212
|
+
| Fácil de testear el orchestrator | El orchestrator conoce todos los participantes |
|
|
213
|
+
| Compensaciones explícitas y centralizadas | Riesgo de orchestrator con lógica de negocio |
|
|
214
|
+
| Trazabilidad: cada saga tiene estado | Más boilerplate que coreografía |
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
## Cuándo Elegir Cada Una
|
|
219
|
+
|
|
220
|
+
| Situación | Recomendación |
|
|
221
|
+
|---|---|
|
|
222
|
+
| 2-3 features, flujo simple | Coreografía |
|
|
223
|
+
| 4+ features, flujo con ramas | Orquestación |
|
|
224
|
+
| Equipos autónomos que no quieren coordinar | Coreografía |
|
|
225
|
+
| Requisito de trazabilidad y monitoreo | Orquestación |
|
|
226
|
+
| Flujo con muchas compensaciones | Orquestación |
|
|
227
|
+
| Feature nuevo que se suma al flujo existente | Coreografía (solo escucha/publica) |
|
|
228
|
+
|
|
229
|
+
---
|
|
230
|
+
|
|
231
|
+
## Manejo de Fallos
|
|
232
|
+
|
|
233
|
+
### Retry con Backoff
|
|
234
|
+
|
|
235
|
+
```ts
|
|
236
|
+
export class RetryPolicy {
|
|
237
|
+
constructor(
|
|
238
|
+
private readonly maxRetries: number = 3,
|
|
239
|
+
private readonly baseDelay: number = 100,
|
|
240
|
+
) {}
|
|
241
|
+
|
|
242
|
+
async execute<T>(fn: () => Promise<T>): Promise<T> {
|
|
243
|
+
for (let attempt = 1; attempt <= this.maxRetries; attempt++) {
|
|
244
|
+
try {
|
|
245
|
+
return await fn();
|
|
246
|
+
} catch (error) {
|
|
247
|
+
if (attempt === this.maxRetries) throw error;
|
|
248
|
+
const delay = this.baseDelay * Math.pow(2, attempt - 1);
|
|
249
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
### Dead Letter Queue (DLQ)
|
|
257
|
+
|
|
258
|
+
Cuando una saga no puede completarse ni compensarse después de N reintentos:
|
|
259
|
+
|
|
260
|
+
```ts
|
|
261
|
+
if (saga.retryCount >= 5) {
|
|
262
|
+
saga.sendToDLQ({
|
|
263
|
+
reason: "Payment service unavailable after 5 retries",
|
|
264
|
+
context: saga.context,
|
|
265
|
+
lastError: error.message,
|
|
266
|
+
});
|
|
267
|
+
// Requiere intervención humana
|
|
268
|
+
await this.sagaRepo.markFailed(saga);
|
|
269
|
+
await this.notificationService.alertOperations(
|
|
270
|
+
`Saga ${saga.id} requiere intervención manual`
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
### Fallback Humano
|
|
276
|
+
|
|
277
|
+
Para casos donde la compensación automática no es posible (ej. un pago ya procesado que no puede revertirse automáticamente):
|
|
278
|
+
|
|
279
|
+
```ts
|
|
280
|
+
if (error.name === "IrreversibleError") {
|
|
281
|
+
saga.requireManualIntervention({
|
|
282
|
+
reason: "Payment already captured, manual refund required",
|
|
283
|
+
instructions: `Login to payment dashboard, refund transaction ${event.paymentId}, then resume saga ${saga.id}`,
|
|
284
|
+
severity: "HIGH",
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
---
|
|
290
|
+
|
|
291
|
+
## Conexión con el Modelo de Forge
|
|
292
|
+
|
|
293
|
+
### Reglas y Sagas
|
|
294
|
+
|
|
295
|
+
| Regla | Aplicación |
|
|
296
|
+
|---|---|
|
|
297
|
+
| **R8** (no cross-feature imports) | La saga se comunica con features mediante eventos o interfaces. Nunca importa directamente. |
|
|
298
|
+
| **R5** (domain → infra) | La saga opera en application/use-cases/. No hay infra en dominio de saga. |
|
|
299
|
+
| **R9** (no ciclos) | Las sagas coreografiadas deben auditarse para evitar ciclos de eventos (A→B→C→A). |
|
|
300
|
+
|
|
301
|
+
### Ubicación de la saga en la arquitectura
|
|
302
|
+
|
|
303
|
+
```
|
|
304
|
+
src/features/
|
|
305
|
+
checkout-saga/ ← orchestrator como feature
|
|
306
|
+
domain/
|
|
307
|
+
SagaInstance.entity.ts
|
|
308
|
+
ISagaStepExecutor.ts
|
|
309
|
+
application/
|
|
310
|
+
use-cases/
|
|
311
|
+
ExecuteCheckoutSaga.ts
|
|
312
|
+
adapters/
|
|
313
|
+
out/
|
|
314
|
+
saga-steps/
|
|
315
|
+
CreateOrderStep.ts ← llama a contracts de Orders
|
|
316
|
+
ReserveStockStep.ts ← llama a contracts de Inventory
|
|
317
|
+
ProcessPaymentStep.ts ← llama a contracts de Payments
|
|
318
|
+
saga-persistence/
|
|
319
|
+
PostgresSagaRepository.ts
|
|
320
|
+
orders/
|
|
321
|
+
adapters/
|
|
322
|
+
in/
|
|
323
|
+
events/ ← escucha eventos de la saga
|
|
324
|
+
http/
|
|
325
|
+
commands/ ← la saga llama aquí via contract
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
---
|
|
329
|
+
|
|
330
|
+
## Anti-patrones
|
|
331
|
+
|
|
332
|
+
| Anti-patrón | Problema | Solución |
|
|
333
|
+
|---|---|---|
|
|
334
|
+
| **Saga sin compensación** | Un paso falla y el sistema queda inconsistente. | Cada paso debe tener compensación definida explícitamente. |
|
|
335
|
+
| **Orchestrator con lógica de negocio** | El orchestrator decide descuentos, valida reglas, calcula montos. | El orchestrator solo coordina. La lógica de negocio vive en los use cases de cada feature. |
|
|
336
|
+
| **Coreografía sin trazabilidad** | Múltiples eventos volando, nadie sabe el estado global de un proceso. | Usar saga log + event store. Monitorear con `forge chain`. |
|
|
337
|
+
| **Timeout único** | Todas las operaciones tienen el mismo timeout. Operaciones lentas (pagos) fallan donde rápidas (stock) no. | Timeout configurable por paso de saga. |
|
|
338
|
+
| **Compensación que falla sin alerta** | La compensación falla silenciosamente. El sistema cree que compensó pero no lo hizo. | Alertas de operaciones para compensaciones fallidas. Siempre. |
|
|
339
|
+
| **Saga como transacción ACID** | Se intenta revertir un paso que ya tuvo efectos visibles para el usuario. | Las sagas ofrecen consistencia eventual. Los efectos visibles deben diseñarse para ser reversibles (o aceptar que no lo son). |
|
|
340
|
+
|
|
341
|
+
---
|
|
342
|
+
|
|
343
|
+
## Conexión con Forge
|
|
344
|
+
|
|
345
|
+
| Comando | Acción |
|
|
346
|
+
|---|---|
|
|
347
|
+
| `forge cast checkout-saga` | Crea feature orchestrator con estructura de saga |
|
|
348
|
+
| `forge inspect` | Detecta features que se comunican sincrónicamente (candidatos a saga) |
|
|
349
|
+
| `forge graph` | Visualiza el flujo de la saga como grafo de dependencias entre features |
|
|
350
|
+
| `forge chain` | Verifica que la saga no introduce ciclos (R9) |
|
|
351
|
+
| `forge assay` | Evalúa si la estrategia de coordinación (coreografía vs orquestación) es la adecuada |
|
|
352
|
+
|
|
353
|
+
## Ver también
|
|
354
|
+
|
|
355
|
+
- `reference/bounded-contexts.md` — contexts que la saga coordina
|
|
356
|
+
- `reference/events.md` — eventos como unidad de la saga
|
|
357
|
+
- `reference/cqrs.md` — commands como pasos de la saga
|
|
358
|
+
- `reference/transactional-outbox.md` — entrega confiable de eventos de saga
|
|
359
|
+
- `reference/idempotency.md` — retry seguro de pasos de saga
|
|
@@ -85,3 +85,9 @@ export const createUserSchema = z.object({
|
|
|
85
85
|
- Helmet para headers de seguridad (CSP, X-Frame-Options, etc.)
|
|
86
86
|
- Secretos en variables de entorno, nunca en código
|
|
87
87
|
- Auditoría de acciones sensibles: login, delete, role change, export
|
|
88
|
+
|
|
89
|
+
## Ver también
|
|
90
|
+
|
|
91
|
+
- `reference/api-design.md` — middleware de seguridad, validación de entrada
|
|
92
|
+
- `reference/observability.md` — audit logging como práctica de observabilidad
|
|
93
|
+
- `reference/testing-patterns.md` — tests de middleware y seguridad
|
|
@@ -29,3 +29,9 @@ Extrae código reutilizable desde features hacia `shared/`.
|
|
|
29
29
|
- El código extraído NO debe importar de infraestructura
|
|
30
30
|
- El código extraído debe ser puro o depender solo de otros componentes shared
|
|
31
31
|
- Si el código depende de platform, considerar moverlo a platform en lugar de shared
|
|
32
|
+
|
|
33
|
+
## Ver también
|
|
34
|
+
|
|
35
|
+
- `reference/relocate.md` — operación similar de extracción
|
|
36
|
+
- `reference/data-patterns.md` — identificación de qué extraer a shared
|
|
37
|
+
- `reference/errors.md` — errores tipados como candidatos a smelt
|
|
@@ -47,3 +47,9 @@ Templa la arquitectura aplicando reglas de inyección de dependencias, seguridad
|
|
|
47
47
|
- ❌ Importar tsyringe en archivos de dominio
|
|
48
48
|
- ❌ Mezclar DI manual y contenedor en el mismo feature
|
|
49
49
|
- ❌ Proxies automáticos o decoradores ocultos que dificulten el rastreo
|
|
50
|
+
|
|
51
|
+
## Ver también
|
|
52
|
+
|
|
53
|
+
- `reference/di-strategies.md` — selección de estrategia DI antes de temperar
|
|
54
|
+
- `reference/patterns.md` — convenciones de naming en DI
|
|
55
|
+
- `reference/testing-patterns.md` — testabilidad que DI disciplinada habilita
|
|
@@ -67,3 +67,9 @@ it("POST /users returns 201", async () => {
|
|
|
67
67
|
- Nombrar tests como unidades de comportamiento, no métodos
|
|
68
68
|
- Coverage mínimo sugerido: 85% use cases, 75% adapters
|
|
69
69
|
- Los mappers se testean con fixtures: entrada conocida → salida esperada
|
|
70
|
+
|
|
71
|
+
## Ver también
|
|
72
|
+
|
|
73
|
+
- `reference/anti-corruption-layer.md` — tests de ACL y traducción entre contexts
|
|
74
|
+
- `reference/di-strategies.md` — DI testing con mocks manuales
|
|
75
|
+
- `reference/temper.md` — DI disciplinada que habilita testabilidad
|