eva4j 1.0.16 → 1.0.18
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/AGENTS.md +220 -5
- package/DOMAIN_YAML_GUIDE.md +188 -3
- package/FUTURE_FEATURES.md +33 -52
- package/QUICK_REFERENCE.md +8 -4
- package/bin/eva4j.js +70 -2
- package/config/defaults.json +1 -0
- package/docs/CAMUNDA_DMN_GUIDE.md +1380 -0
- package/docs/KAFKA_PRODUCTION_CONFIG.md +441 -0
- package/docs/RABBITMQ_PRODUCTION_CONFIG.md +227 -0
- package/docs/commands/ADD_RABBITMQ_CLIENT.md +192 -0
- package/docs/commands/EVALUATE_SYSTEM.md +290 -10
- package/docs/commands/GENERATE_RABBITMQ_EVENT.md +341 -0
- package/docs/commands/GENERATE_RABBITMQ_LISTENER.md +595 -0
- package/docs/commands/GENERATE_TEMPORAL_FLOW.md +52 -12
- package/docs/commands/INDEX.md +27 -3
- package/docs/prototype/TEMPORAL_COMMUNICATION_PATTERNS.md +731 -0
- package/docs/prototype/TEMPORAL_DESIGN_METHODOLOGY.md +740 -0
- package/docs/prototype/system/RISKS.md +277 -0
- package/docs/prototype/system/customers.yaml +133 -0
- package/docs/prototype/system/inventory.yaml +109 -0
- package/docs/prototype/system/notifications.yaml +131 -0
- package/docs/prototype/system/orders.yaml +241 -0
- package/docs/prototype/system/payments.yaml +256 -0
- package/docs/prototype/system/products.yaml +168 -0
- package/docs/prototype/system/system.yaml +269 -0
- package/examples/domain-endpoints-multi-aggregate.yaml +140 -0
- package/examples/domain-events.yaml +26 -0
- package/examples/domain-read-models.yaml +113 -0
- package/examples/system/customer.yaml +89 -0
- package/examples/system/orders.yaml +119 -0
- package/examples/system/product.yaml +27 -0
- package/examples/system/system.yaml +80 -0
- package/package.json +1 -1
- package/read-model-spec.md +664 -0
- package/src/agents/design-gap-analyst-temporal.agent.md +452 -0
- package/src/agents/design-gap-analyst.agent.md +383 -0
- package/src/agents/design-reviewer-temporal.agent.md +412 -0
- package/src/agents/design-reviewer.agent.md +34 -5
- package/src/agents/implement-use-cases.prompt.md +179 -0
- package/src/agents/ux-gap-analyst.agent.md +412 -0
- package/src/commands/add-rabbitmq-client.js +261 -0
- package/src/commands/add-temporal-client.js +22 -2
- package/src/commands/build.js +267 -11
- package/src/commands/evaluate-system.js +700 -13
- package/src/commands/generate-entities.js +560 -24
- package/src/commands/generate-http-exchange.js +3 -0
- package/src/commands/generate-kafka-event.js +3 -0
- package/src/commands/generate-kafka-listener.js +3 -0
- package/src/commands/generate-rabbitmq-event.js +665 -0
- package/src/commands/generate-rabbitmq-listener.js +205 -0
- package/src/commands/generate-record.js +2 -2
- package/src/commands/generate-resource.js +4 -1
- package/src/commands/generate-temporal-activity.js +970 -33
- package/src/commands/generate-temporal-flow.js +98 -38
- package/src/commands/generate-temporal-system.js +708 -0
- package/src/commands/generate-usecase.js +4 -1
- package/src/skills/build-system-yaml/SKILL.md +343 -2
- package/src/skills/build-system-yaml/references/domain-yaml-spec.md +253 -26
- package/src/skills/build-system-yaml/references/module-spec.md +90 -9
- package/src/skills/build-system-yaml/references/system-yaml-spec.md +36 -0
- package/src/skills/build-temporal-system/SKILL.md +752 -0
- package/src/skills/build-temporal-system/references/temporal-communication-patterns.md +167 -0
- package/src/skills/build-temporal-system/references/temporal-domain-yaml-spec.md +449 -0
- package/src/skills/build-temporal-system/references/temporal-module-spec.md +353 -0
- package/src/skills/build-temporal-system/references/temporal-system-yaml-spec.md +326 -0
- package/src/skills/implement-use-case/SKILL.md +350 -0
- package/src/skills/implement-use-case/references/use-case-patterns.md +980 -0
- package/src/skills/requirements-elicitation/SKILL.md +228 -0
- package/src/skills/requirements-elicitation/references/interview-framework.md +260 -0
- package/src/skills/requirements-elicitation/references/output-templates.md +368 -0
- package/src/utils/bounded-context-diagram.js +844 -0
- package/src/utils/config-manager.js +4 -2
- package/src/utils/domain-validator.js +495 -17
- package/src/utils/naming.js +20 -0
- package/src/utils/system-validator.js +169 -11
- package/src/utils/system-yaml-parser.js +318 -0
- package/src/utils/temporal-validator.js +497 -0
- package/src/utils/validator.js +3 -1
- package/src/utils/yaml-to-entity.js +281 -9
- package/templates/aggregate/AggregateRepository.java.ejs +4 -0
- package/templates/aggregate/AggregateRepositoryImpl.java.ejs +8 -0
- package/templates/aggregate/AggregateRoot.java.ejs +38 -4
- package/templates/aggregate/DomainEventHandler.java.ejs +116 -22
- package/templates/aggregate/JpaAggregateRoot.java.ejs +4 -4
- package/templates/aggregate/JpaEntity.java.ejs +2 -2
- package/templates/base/docker/rabbitmq-services.yaml.ejs +12 -0
- package/templates/base/resources/parameters/develop/kafka.yaml.ejs +5 -0
- package/templates/base/resources/parameters/develop/rabbitmq.yaml.ejs +15 -0
- package/templates/base/resources/parameters/develop/temporal.yaml.ejs +0 -3
- package/templates/base/resources/parameters/local/kafka.yaml.ejs +5 -0
- package/templates/base/resources/parameters/local/rabbitmq.yaml.ejs +15 -0
- package/templates/base/resources/parameters/local/temporal.yaml.ejs +0 -3
- package/templates/base/resources/parameters/production/kafka.yaml.ejs +39 -8
- package/templates/base/resources/parameters/production/rabbitmq.yaml.ejs +32 -0
- package/templates/base/resources/parameters/production/temporal.yaml.ejs +0 -3
- package/templates/base/resources/parameters/test/kafka.yaml.ejs +12 -6
- package/templates/base/resources/parameters/test/rabbitmq.yaml.ejs +15 -0
- package/templates/base/resources/parameters/test/temporal.yaml.ejs +0 -3
- package/templates/base/root/AGENTS.md.ejs +1 -1
- package/templates/crud/DeleteCommandHandler.java.ejs +19 -1
- package/templates/crud/EndpointsController.java.ejs +1 -1
- package/templates/crud/ScaffoldCommand.java.ejs +5 -2
- package/templates/crud/ScaffoldCommandHandler.java.ejs +3 -1
- package/templates/crud/ScaffoldQuery.java.ejs +5 -2
- package/templates/crud/ScaffoldQueryHandler.java.ejs +3 -1
- package/templates/crud/SubEntityRemoveCommand.java.ejs +1 -1
- package/templates/crud/UpdateCommandHandler.java.ejs +53 -2
- package/templates/evaluate/report.html.ejs +1447 -90
- package/templates/kafka-event/KafkaConfigBean.java.ejs +1 -1
- package/templates/kafka-event/KafkaMessageBroker.java.ejs +3 -3
- package/templates/ports/PortAclMapper.java.ejs +35 -0
- package/templates/ports/PortFeignAdapter.java.ejs +7 -22
- package/templates/ports/PortFeignClient.java.ejs +4 -0
- package/templates/ports/PortResponseDto.java.ejs +1 -1
- package/templates/rabbitmq-event/RabbitConfigBean.java.ejs +33 -0
- package/templates/rabbitmq-event/RabbitConfigExchange.java.ejs +12 -0
- package/templates/rabbitmq-event/RabbitMessageBroker.java.ejs +35 -0
- package/templates/rabbitmq-event/RabbitMessageBrokerMethod.java.ejs +9 -0
- package/templates/rabbitmq-listener/RabbitConfigConsumerBean.java.ejs +33 -0
- package/templates/rabbitmq-listener/RabbitConfigConsumerExchange.java.ejs +12 -0
- package/templates/rabbitmq-listener/RabbitListenerClass.java.ejs +82 -0
- package/templates/rabbitmq-listener/RabbitListenerSimple.java.ejs +56 -0
- package/templates/read-model/ReadModelDomain.java.ejs +46 -0
- package/templates/read-model/ReadModelJpa.java.ejs +58 -0
- package/templates/read-model/ReadModelJpaRepository.java.ejs +13 -0
- package/templates/read-model/ReadModelKafkaListener.java.ejs +64 -0
- package/templates/read-model/ReadModelRabbitListener.java.ejs +71 -0
- package/templates/read-model/ReadModelRepository.java.ejs +42 -0
- package/templates/read-model/ReadModelRepositoryImpl.java.ejs +85 -0
- package/templates/read-model/ReadModelSyncHandler.java.ejs +54 -0
- package/templates/shared/configurations/kafkaConfig/KafkaConfig.java.ejs +18 -4
- package/templates/shared/configurations/rabbitmqConfig/RabbitMQConfig.java.ejs +100 -0
- package/templates/shared/configurations/temporalConfig/TemporalConfig.java.ejs +2 -64
- package/templates/shared/configurations/temporalConfig/TemporalWorkerFactoryLifecycle.java.ejs +41 -0
- package/templates/temporal-activity/ActivityImpl.java.ejs +68 -2
- package/templates/temporal-activity/ActivityInput.java.ejs +14 -0
- package/templates/temporal-activity/ActivityInterface.java.ejs +7 -1
- package/templates/temporal-activity/ActivityOutput.java.ejs +14 -0
- package/templates/temporal-activity/NestedType.java.ejs +12 -0
- package/templates/temporal-activity/SharedActivityInput.java.ejs +14 -0
- package/templates/temporal-activity/SharedActivityInterface.java.ejs +15 -0
- package/templates/temporal-activity/SharedActivityOutput.java.ejs +14 -0
- package/templates/temporal-activity/SharedNestedType.java.ejs +12 -0
- package/templates/temporal-flow/ModuleHeavyActivity.java.ejs +6 -0
- package/templates/temporal-flow/ModuleLightActivity.java.ejs +6 -0
- package/templates/temporal-flow/ModuleTemporalWorkerConfig.java.ejs +58 -0
- package/templates/temporal-flow/WorkFlowImpl.java.ejs +172 -12
- package/templates/temporal-flow/WorkFlowInput.java.ejs +11 -0
- package/templates/temporal-flow/WorkFlowInterface.java.ejs +5 -4
- package/templates/temporal-flow/WorkFlowService.java.ejs +42 -12
- package/COMMAND_EVALUATION.md +0 -911
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
# Temporal-Only Architecture: Análisis de Riesgos y Trade-offs
|
|
2
|
+
|
|
3
|
+
## 📐 Visión General
|
|
4
|
+
|
|
5
|
+
Este documento analiza los riesgos de usar **Temporal como único mecanismo de
|
|
6
|
+
comunicación** entre módulos, reemplazando Kafka (async) y Feign (sync).
|
|
7
|
+
|
|
8
|
+
Los archivos de diseño en esta carpeta (`docs/prototype/system/`) reflejan cómo
|
|
9
|
+
se vería la arquitectura `test-eva` bajo este enfoque.
|
|
10
|
+
|
|
11
|
+
**Actualización (v2):** Se eliminaron Read Models. Los datos cross-module se
|
|
12
|
+
obtienen on-demand via Remote Activities de lectura (`GetCustomerById`,
|
|
13
|
+
`GetProductsByIds`). Se eliminaron 4 workflows que solo sincronizaban datos
|
|
14
|
+
(`ProductUpdated/Deactivated`, `CustomerCreated/Updated`). Esto reduce
|
|
15
|
+
significativamente el acoplamiento productor→consumidor (R1).
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## 🔴 Riesgos Altos
|
|
20
|
+
|
|
21
|
+
### R1. Acoplamiento del productor a sus consumidores
|
|
22
|
+
|
|
23
|
+
**Kafka (actual):**
|
|
24
|
+
```
|
|
25
|
+
products publica ProductCreatedEvent → no sabe quién consume
|
|
26
|
+
├── inventory (se suscribe solo)
|
|
27
|
+
└── orders (se suscribe solo)
|
|
28
|
+
└── [futuro módulo X] (se suscribe sin tocar products)
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
**Temporal (prototipo v2):**
|
|
32
|
+
```
|
|
33
|
+
products.ProductCreatedWorkflow lista explícitamente:
|
|
34
|
+
└── step: InitializeStock → target: inventory
|
|
35
|
+
(solo 1 consumer — efecto de negocio real, no sync de datos)
|
|
36
|
+
|
|
37
|
+
Pero: cualquier workflow puede invocar GetProductById/GetProductsByIds
|
|
38
|
+
sin que products lo sepa → desacoplamiento por lectura on-demand.
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
**Impacto (v2 — REDUCIDO):**
|
|
42
|
+
Con la eliminación de read models, products ya no necesita saber que orders
|
|
43
|
+
consume sus datos. Solo mantiene `notifies: ProductCreatedWorkflow` porque
|
|
44
|
+
InitializeStock es un **efecto de negocio real** (crear stock), no sincronización.
|
|
45
|
+
|
|
46
|
+
`customers` queda **completamente desacoplado** — sus Domain Events son internos
|
|
47
|
+
y no notifican a nadie. Los workflows de orders obtienen datos del cliente via
|
|
48
|
+
`GetCustomerById` (Remote Activity) on-demand.
|
|
49
|
+
|
|
50
|
+
**Severidad:** 🟡 Media (reducida de 🔴 Alta) — el acoplamiento solo existe
|
|
51
|
+
para efectos de negocio reales (InitializeStock), no para sincronización de datos.
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
### R2. Pérdida de comunicación asíncrona real (fire-and-forget)
|
|
56
|
+
|
|
57
|
+
**Kafka:** El productor publica y continúa. Los consumidores procesan a su ritmo.
|
|
58
|
+
Si notifications tarda 30s en enviar un email, orders no se entera ni se bloquea.
|
|
59
|
+
|
|
60
|
+
**Temporal:** Aunque una activity se marque como `type: async` en el workflow, el
|
|
61
|
+
**workflow como instancia sigue vivo** hasta que todos los pasos terminen. Temporal
|
|
62
|
+
trackea el estado de cada step. Si NotifyOrderPlaced falla 3 veces, el workflow
|
|
63
|
+
completo queda en estado "running" indefinidamente.
|
|
64
|
+
|
|
65
|
+
**Mitigación:** Usar `Async.function()` para activities non-critical
|
|
66
|
+
(notificaciones) y NOT registrar compensación en la saga. Si falla, el workflow
|
|
67
|
+
no se bloquea — solo se pierde la notificación.
|
|
68
|
+
|
|
69
|
+
**Severidad:** 🔴 Alta — afecta resiliencia y operaciones.
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
### R3. Single Point of Failure: Temporal Server
|
|
74
|
+
|
|
75
|
+
**Kafka:** El broker tiene replicación nativa (particiones, ISR). Si un broker
|
|
76
|
+
cae, otro toma el liderazgo de la partición. Los productores y consumidores
|
|
77
|
+
tienen decenas de librerías y patrones maduros para manejar desconexiones.
|
|
78
|
+
|
|
79
|
+
**Temporal:** El Temporal Server (frontend + history + matching + worker services)
|
|
80
|
+
es el punto central. Si cae:
|
|
81
|
+
- Ningún workflow puede avanzar
|
|
82
|
+
- Ninguna activity puede ejecutarse
|
|
83
|
+
- La comunicación entre TODOS los módulos se detiene simultáneamente
|
|
84
|
+
|
|
85
|
+
Temporal sí soporta clusters multi-nodo, pero la complejidad operativa es mayor
|
|
86
|
+
que Kafka para este tipo de uso (Temporal no fue diseñado como message bus).
|
|
87
|
+
|
|
88
|
+
**Severidad:** 🔴 Alta — riesgo operativo en producción.
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## 🟡 Riesgos Medios
|
|
93
|
+
|
|
94
|
+
### R4. Complejidad operativa: N workers por módulo
|
|
95
|
+
|
|
96
|
+
Con Kafka, cada módulo tiene un consumer group. Escalar = agregar instancias.
|
|
97
|
+
|
|
98
|
+
Con Temporal y module-prefixed queues, cada módulo tiene su propio set de queues:
|
|
99
|
+
```
|
|
100
|
+
{MODULE}_WORKFLOW_QUEUE, {MODULE}_LIGHT_TASK_QUEUE, {MODULE}_HEAVY_TASK_QUEUE
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Esto permite **escalado selectivo** (ej: más workers para
|
|
104
|
+
`PAYMENT_HEAVY_TASK_QUEUE`), pero multiplica la cantidad de workers en deployment.
|
|
105
|
+
|
|
106
|
+
**Severidad:** 🟡 Media — manejable con module-scoped queues.
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
### R5. Latencia on-demand vs Read Models locales
|
|
111
|
+
|
|
112
|
+
Sin read models, cada workflow que necesita datos cross-module hace una llamada
|
|
113
|
+
on-demand via Remote Activity:
|
|
114
|
+
- `PlaceOrderWorkflow` → `GetCustomerById` (~5ms) + `GetProductsByIds` (~10ms)
|
|
115
|
+
- `CancelOrderWorkflow` → `GetCustomerById` (~5ms)
|
|
116
|
+
|
|
117
|
+
Con read models, esta data ya estaba en la BD local del módulo (~1ms).
|
|
118
|
+
|
|
119
|
+
**Impacto:** ~10-15ms extra por workflow. Aceptable para volúmenes bajos/medios.
|
|
120
|
+
Para alto volumen, se pueden agregar **caches con TTL** en las Activities sin
|
|
121
|
+
necesidad de read models persistentes.
|
|
122
|
+
|
|
123
|
+
**Severidad:** 🟡 Media — trade-off aceptable para la simplificación obtenida.
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
### R6. Servicios externos siguen necesitando HTTP
|
|
128
|
+
|
|
129
|
+
El módulo `payments` llama a un gateway externo de pagos
|
|
130
|
+
(`https://api.payments.example.com`). Ese servicio:
|
|
131
|
+
- No corre workers Temporal
|
|
132
|
+
- No entiende Activities
|
|
133
|
+
- Solo expone HTTP
|
|
134
|
+
|
|
135
|
+
Resultado: `ports[]` (Feign) **no puede eliminarse** para integraciones externas.
|
|
136
|
+
Temporal solo reemplaza comunicación **interna** entre módulos propios.
|
|
137
|
+
|
|
138
|
+
En la práctica, esto crea un modelo híbrido: Temporal entre módulos internos +
|
|
139
|
+
HTTP para servicios externos. La promesa de "un solo mecanismo" no se cumple.
|
|
140
|
+
|
|
141
|
+
**Severidad:** 🟡 Media — la arquitectura no es tan uniforme como parece.
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
### R7. Modelo de Datos del Diseño: Secciones Nuevas sin soporte
|
|
146
|
+
|
|
147
|
+
El prototipo introduce conceptos que eva4j no tiene:
|
|
148
|
+
|
|
149
|
+
| Concepto nuevo | Dónde aparece | Genera código? |
|
|
150
|
+
|---|---|---|
|
|
151
|
+
| `orchestration:` en system.yaml | Reemplaza `messaging:` | ❌ No |
|
|
152
|
+
| `workflows:` en system.yaml | Reemplaza `integrations:` | ❌ No |
|
|
153
|
+
| `activities:` en domain.yaml | Concepto nuevo | ❌ No |
|
|
154
|
+
| `notifies:` en events | Reemplaza `topic:` | ❌ No |
|
|
155
|
+
| `parallel:` en workflow steps | Indica `Async.function()` | ❌ No |
|
|
156
|
+
|
|
157
|
+
Implementar esto requiere nuevos generadores, templates, y validadores.
|
|
158
|
+
|
|
159
|
+
**Nota:** `saga:` y `readModels.syncedBy.activity` fueron eliminados en v2.
|
|
160
|
+
|
|
161
|
+
**Severidad:** 🟡 Media — alto esfuerzo de desarrollo en eva4j.
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## 🟢 Riesgos Bajos (Ventajas del enfoque)
|
|
166
|
+
|
|
167
|
+
### V1. Saga nativa con compensación
|
|
168
|
+
|
|
169
|
+
El flujo `CreateOrder → GetCustomer → GetProducts ∥ ReserveStock →
|
|
170
|
+
ProcessPayment → ConfirmOrder → NotifyOrderPlaced` es **sustancialmente mejor**
|
|
171
|
+
con Temporal. La saga tiene compensación automática, timeouts durables, y
|
|
172
|
+
visibilidad del estado en el Temporal UI.
|
|
173
|
+
|
|
174
|
+
Con Kafka, este mismo flujo requiere implementar manualmente:
|
|
175
|
+
- Correlation IDs
|
|
176
|
+
- Estado de la saga en base de datos
|
|
177
|
+
- Compensación manual con eventos de rollback
|
|
178
|
+
- Timeout checks con scheduled jobs
|
|
179
|
+
|
|
180
|
+
**Veredicto:** Para este patrón específico, Temporal es claramente superior.
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
### V2. Observabilidad centralizada
|
|
185
|
+
|
|
186
|
+
Cada workflow es visible en el Temporal UI con su historial completo: qué
|
|
187
|
+
activities se ejecutaron, cuáles fallaron, tiempos de ejecución, payloads.
|
|
188
|
+
|
|
189
|
+
Con Kafka, rastrear un flujo cross-module requiere correlación de logs,
|
|
190
|
+
distributed tracing (Zipkin/Jaeger), y mucha disciplina en los correlation IDs.
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
### V3. Retry con backoff nativo
|
|
195
|
+
|
|
196
|
+
Temporal reintenta activities automáticamente con backoff exponencial configurable.
|
|
197
|
+
Con Kafka, los retrys se implementan con DLQ (Dead Letter Queue), retry topics,
|
|
198
|
+
o lógica manual en el consumer.
|
|
199
|
+
|
|
200
|
+
---
|
|
201
|
+
|
|
202
|
+
### V4. Simplicidad sin Read Models
|
|
203
|
+
|
|
204
|
+
Eliminar read models simplifica significativamente la arquitectura:
|
|
205
|
+
- No hay tablas `rm_*` que mantener sincronizadas
|
|
206
|
+
- No hay workflows/listeners de sincronización
|
|
207
|
+
- No hay riesgo de datos stale en proyecciones
|
|
208
|
+
- Menos acoplamiento productor→consumidor (customers y products no notifican
|
|
209
|
+
a nadie para sync)
|
|
210
|
+
- Cada módulo es responsable solo de sus datos; otros acceden on-demand
|
|
211
|
+
|
|
212
|
+
**Trade-off:** Latencia on-demand (~10ms vs ~1ms local). Mitigable con caches.
|
|
213
|
+
|
|
214
|
+
---
|
|
215
|
+
|
|
216
|
+
## 📊 Matriz Comparativa
|
|
217
|
+
|
|
218
|
+
| Criterio | Kafka + Read Models | Temporal-only (v2, sin RM) |
|
|
219
|
+
|---|---|---|
|
|
220
|
+
| Acoplamiento productor→consumidor | ✅ Nulo | 🟡 Solo para efectos de negocio |
|
|
221
|
+
| Agregar nuevo consumidor | ✅ Solo en consumidor | 🟡 Agregar Activity de lectura |
|
|
222
|
+
| Fan-out 1→N | ✅ Nativo (topics) | ⚠️ Manual (N steps en workflow) |
|
|
223
|
+
| Saga con compensación | ❌ Manual | ✅ Nativo |
|
|
224
|
+
| Replay de eventos | ✅ Desde offset 0 | ❌ No disponible |
|
|
225
|
+
| Observabilidad de flujos | ⚠️ Tracing externo | ✅ Temporal UI |
|
|
226
|
+
| SPOF | ⚠️ Broker (mitigable) | 🔴 Temporal Server |
|
|
227
|
+
| Servicios externos (HTTP) | ✅ Feign/ports[] | ⚠️ Híbrido obligatorio |
|
|
228
|
+
| Complejidad operativa | ⚠️ Kafka + Zookeeper/KRaft | ⚠️ Temporal Server + Workers |
|
|
229
|
+
| Soporte en eva4j | ✅ Completo | ❌ No existe |
|
|
230
|
+
| Escalabilidad selectiva | ✅ Consumer groups | ✅ Module-prefixed queues |
|
|
231
|
+
| Simplicidad del esquema | ⚠️ Tablas rm_* + sync | ✅ Sin proyecciones |
|
|
232
|
+
| Latencia cross-module | ✅ Local (~1ms) | 🟡 On-demand (~10ms) |
|
|
233
|
+
| Event sourcing futuro | ✅ Natural | ❌ No aplica |
|
|
234
|
+
|
|
235
|
+
---
|
|
236
|
+
|
|
237
|
+
## 🎯 Recomendación
|
|
238
|
+
|
|
239
|
+
### Para este e-commerce: **Temporal-only es viable**
|
|
240
|
+
|
|
241
|
+
Con la eliminación de read models, la arquitectura Temporal-only se simplifica
|
|
242
|
+
considerablemente. Los riesgos principales se reducen:
|
|
243
|
+
|
|
244
|
+
```
|
|
245
|
+
R1 (acoplamiento): 🔴 → 🟡 (solo efectos de negocio, no sync de datos)
|
|
246
|
+
R5 (read models): 🟡 → ✅ (eliminado — reemplazado por latencia on-demand)
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
La arquitectura queda:
|
|
250
|
+
|
|
251
|
+
```
|
|
252
|
+
┌──────────────────────────────────────────────────────────────┐
|
|
253
|
+
│ Comunicación │
|
|
254
|
+
├──────────────────────────────┬───────────────────────────────┤
|
|
255
|
+
│ Temporal │ Feign (HTTP) │
|
|
256
|
+
│ │ │
|
|
257
|
+
│ • Saga de orden │ • Servicios externos │
|
|
258
|
+
│ (PlaceOrder, CancelOrder) │ (payment gateway) │
|
|
259
|
+
│ • Efectos de negocio │ │
|
|
260
|
+
│ (ProductCreated→InitStock) │ │
|
|
261
|
+
│ • Lectura on-demand │ │
|
|
262
|
+
│ (GetCustomer, GetProducts) │ │
|
|
263
|
+
│ • Notificaciones │ │
|
|
264
|
+
│ (Async.function, no-block) │ │
|
|
265
|
+
│ • Retry + compensación │ │
|
|
266
|
+
└──────────────────────────────┴───────────────────────────────┘
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
### Cuándo escalar a Kafka + Temporal complementarios
|
|
270
|
+
|
|
271
|
+
Si el sistema evoluciona a:
|
|
272
|
+
- **15+ módulos** con fan-out 1→N frecuente → Kafka para eventos de dominio
|
|
273
|
+
- **Alto volumen** donde ~10ms on-demand es inaceptable → Read Models + Kafka
|
|
274
|
+
- **Event sourcing** necesario → Kafka como log de eventos
|
|
275
|
+
|
|
276
|
+
Para un e-commerce de complejidad media-baja, Temporal-only cubre todos los
|
|
277
|
+
casos con menos infraestructura y menos complejidad operativa.
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# =============================================================================
|
|
2
|
+
# PROTOTYPE: customers.yaml — versión Temporal (sin Kafka)
|
|
3
|
+
# =============================================================================
|
|
4
|
+
|
|
5
|
+
aggregates:
|
|
6
|
+
- name: Customer
|
|
7
|
+
entities:
|
|
8
|
+
- name: customer
|
|
9
|
+
isRoot: true
|
|
10
|
+
tableName: customers
|
|
11
|
+
audit:
|
|
12
|
+
enabled: true
|
|
13
|
+
fields:
|
|
14
|
+
- name: id
|
|
15
|
+
type: String
|
|
16
|
+
- name: firstName
|
|
17
|
+
type: String
|
|
18
|
+
validations:
|
|
19
|
+
- type: NotBlank
|
|
20
|
+
message: "First name is required"
|
|
21
|
+
- name: lastName
|
|
22
|
+
type: String
|
|
23
|
+
validations:
|
|
24
|
+
- type: NotBlank
|
|
25
|
+
message: "Last name is required"
|
|
26
|
+
- name: email
|
|
27
|
+
type: String
|
|
28
|
+
validations:
|
|
29
|
+
- type: NotBlank
|
|
30
|
+
message: "Email is required"
|
|
31
|
+
- type: Email
|
|
32
|
+
message: "Invalid email format"
|
|
33
|
+
- name: phone
|
|
34
|
+
type: String
|
|
35
|
+
validations:
|
|
36
|
+
- type: NotBlank
|
|
37
|
+
message: "Phone number is required"
|
|
38
|
+
- name: address
|
|
39
|
+
type: Address
|
|
40
|
+
|
|
41
|
+
valueObjects:
|
|
42
|
+
- name: Address
|
|
43
|
+
fields:
|
|
44
|
+
- name: street
|
|
45
|
+
type: String
|
|
46
|
+
- name: city
|
|
47
|
+
type: String
|
|
48
|
+
- name: department
|
|
49
|
+
type: String
|
|
50
|
+
- name: zipCode
|
|
51
|
+
type: String
|
|
52
|
+
- name: neighborhood
|
|
53
|
+
type: String
|
|
54
|
+
methods:
|
|
55
|
+
- name: format
|
|
56
|
+
returnType: String
|
|
57
|
+
parameters: []
|
|
58
|
+
body: "return street + \", \" + neighborhood + \", \" + city + \" (\" + department + \") \" + zipCode;"
|
|
59
|
+
|
|
60
|
+
events:
|
|
61
|
+
- name: CustomerCreatedEvent
|
|
62
|
+
lifecycle: create
|
|
63
|
+
fields:
|
|
64
|
+
- name: customerId
|
|
65
|
+
type: String
|
|
66
|
+
- name: firstName
|
|
67
|
+
type: String
|
|
68
|
+
- name: lastName
|
|
69
|
+
type: String
|
|
70
|
+
- name: email
|
|
71
|
+
type: String
|
|
72
|
+
- name: phone
|
|
73
|
+
type: String
|
|
74
|
+
# SIN notifies → Domain Event interno.
|
|
75
|
+
# Sin read models, no hay workflows de sincronización.
|
|
76
|
+
# Los workflows que necesitan datos de cliente los obtienen
|
|
77
|
+
# on-demand via GetCustomerById (Remote Activity).
|
|
78
|
+
|
|
79
|
+
- name: CustomerUpdatedEvent
|
|
80
|
+
lifecycle: update
|
|
81
|
+
fields:
|
|
82
|
+
- name: customerId
|
|
83
|
+
type: String
|
|
84
|
+
- name: firstName
|
|
85
|
+
type: String
|
|
86
|
+
- name: lastName
|
|
87
|
+
type: String
|
|
88
|
+
- name: email
|
|
89
|
+
type: String
|
|
90
|
+
- name: phone
|
|
91
|
+
type: String
|
|
92
|
+
# SIN notifies → Domain Event interno.
|
|
93
|
+
|
|
94
|
+
# ─── Activities que este módulo EXPONE ───────────────────────────────────────
|
|
95
|
+
# Activity de lectura: permite a PlaceOrderWorkflow y CancelOrderWorkflow
|
|
96
|
+
# obtener datos del cliente on-demand (nombre, email para notificaciones).
|
|
97
|
+
activities:
|
|
98
|
+
- name: GetCustomerById
|
|
99
|
+
type: light
|
|
100
|
+
description: "Obtiene un cliente por su ID"
|
|
101
|
+
input:
|
|
102
|
+
- name: customerId
|
|
103
|
+
type: String
|
|
104
|
+
output:
|
|
105
|
+
- name: customerId
|
|
106
|
+
type: String
|
|
107
|
+
- name: firstName
|
|
108
|
+
type: String
|
|
109
|
+
- name: lastName
|
|
110
|
+
type: String
|
|
111
|
+
- name: email
|
|
112
|
+
type: String
|
|
113
|
+
- name: phone
|
|
114
|
+
type: String
|
|
115
|
+
timeout: 5s
|
|
116
|
+
|
|
117
|
+
endpoints:
|
|
118
|
+
basePath: /customers
|
|
119
|
+
versions:
|
|
120
|
+
- version: v1
|
|
121
|
+
operations:
|
|
122
|
+
- useCase: GetCustomer
|
|
123
|
+
method: GET
|
|
124
|
+
path: /{id}
|
|
125
|
+
- useCase: FindAllCustomers
|
|
126
|
+
method: GET
|
|
127
|
+
path: /
|
|
128
|
+
- useCase: CreateCustomer
|
|
129
|
+
method: POST
|
|
130
|
+
path: /
|
|
131
|
+
- useCase: UpdateCustomer
|
|
132
|
+
method: PUT
|
|
133
|
+
path: /{id}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# =============================================================================
|
|
2
|
+
# PROTOTYPE: inventory.yaml — versión Temporal (sin Kafka)
|
|
3
|
+
# =============================================================================
|
|
4
|
+
# Cambios vs original:
|
|
5
|
+
# - ELIMINA listeners[] — ya no consume ProductCreatedEvent por Kafka
|
|
6
|
+
# - AGREGA activities[] — expone Activities para que workflows las invoquen
|
|
7
|
+
# - endpoints, aggregates → SIN CAMBIOS
|
|
8
|
+
# =============================================================================
|
|
9
|
+
|
|
10
|
+
aggregates:
|
|
11
|
+
- name: Stock
|
|
12
|
+
entities:
|
|
13
|
+
- name: stock
|
|
14
|
+
isRoot: true
|
|
15
|
+
tableName: stock_items
|
|
16
|
+
audit:
|
|
17
|
+
enabled: true
|
|
18
|
+
fields:
|
|
19
|
+
- name: id
|
|
20
|
+
type: String
|
|
21
|
+
- name: productId
|
|
22
|
+
type: String
|
|
23
|
+
reference:
|
|
24
|
+
aggregate: Product
|
|
25
|
+
module: products
|
|
26
|
+
validations:
|
|
27
|
+
- type: NotBlank
|
|
28
|
+
message: "Product ID is required"
|
|
29
|
+
- name: quantity
|
|
30
|
+
type: Integer
|
|
31
|
+
validations:
|
|
32
|
+
- type: PositiveOrZero
|
|
33
|
+
message: "Quantity cannot be negative"
|
|
34
|
+
|
|
35
|
+
# ─── Activities que este módulo EXPONE ───────────────────────────────────────
|
|
36
|
+
activities:
|
|
37
|
+
- name: InitializeStock
|
|
38
|
+
type: light
|
|
39
|
+
description: "Crea un registro de stock inicial para un producto nuevo"
|
|
40
|
+
input:
|
|
41
|
+
- name: productId
|
|
42
|
+
type: String
|
|
43
|
+
- name: name
|
|
44
|
+
type: String
|
|
45
|
+
timeout: 10s
|
|
46
|
+
|
|
47
|
+
- name: ReserveStock
|
|
48
|
+
type: light
|
|
49
|
+
description: "Reserva stock para una orden"
|
|
50
|
+
input:
|
|
51
|
+
- name: orderId
|
|
52
|
+
type: String
|
|
53
|
+
- name: items
|
|
54
|
+
type: List<OrderItemDetail>
|
|
55
|
+
output:
|
|
56
|
+
- name: success
|
|
57
|
+
type: Boolean
|
|
58
|
+
- name: availableQuantity
|
|
59
|
+
type: Integer
|
|
60
|
+
externalTypes:
|
|
61
|
+
- name: OrderItemDetail
|
|
62
|
+
module: orders
|
|
63
|
+
timeout: 10s
|
|
64
|
+
compensation: ReleaseStock # Compensación explícita
|
|
65
|
+
|
|
66
|
+
- name: ReleaseStock
|
|
67
|
+
type: light
|
|
68
|
+
description: "Libera stock reservado (compensación)"
|
|
69
|
+
input:
|
|
70
|
+
- name: orderId
|
|
71
|
+
type: String
|
|
72
|
+
- name: items
|
|
73
|
+
type: List<OrderItemDetail>
|
|
74
|
+
externalTypes:
|
|
75
|
+
- name: OrderItemDetail
|
|
76
|
+
module: orders
|
|
77
|
+
timeout: 10s
|
|
78
|
+
|
|
79
|
+
- name: DeleteStock
|
|
80
|
+
type: light
|
|
81
|
+
description: "Elimina stock de un producto (compensación de InitializeStock)"
|
|
82
|
+
input:
|
|
83
|
+
- name: productId
|
|
84
|
+
type: String
|
|
85
|
+
timeout: 5s
|
|
86
|
+
|
|
87
|
+
# ─── ELIMINADOS: listeners[] ─────────────────────────────────────────────────
|
|
88
|
+
# ProductCreatedEvent → ya no se consume. InitializeStock es una Activity.
|
|
89
|
+
|
|
90
|
+
endpoints:
|
|
91
|
+
basePath: /inventory
|
|
92
|
+
versions:
|
|
93
|
+
- version: v1
|
|
94
|
+
operations:
|
|
95
|
+
- useCase: GetStock
|
|
96
|
+
method: GET
|
|
97
|
+
path: /{productId}
|
|
98
|
+
- useCase: FindAllStocks
|
|
99
|
+
method: GET
|
|
100
|
+
path: /
|
|
101
|
+
- useCase: AdjustStock
|
|
102
|
+
method: PUT
|
|
103
|
+
path: /{productId}/adjust
|
|
104
|
+
- useCase: ReserveStock
|
|
105
|
+
method: POST
|
|
106
|
+
path: /{productId}/reserve
|
|
107
|
+
- useCase: ReleaseStock
|
|
108
|
+
method: POST
|
|
109
|
+
path: /{productId}/release
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# =============================================================================
|
|
2
|
+
# PROTOTYPE: notifications.yaml — versión Temporal (sin Kafka, sin Read Models)
|
|
3
|
+
# =============================================================================
|
|
4
|
+
# Cambios vs original:
|
|
5
|
+
# - ELIMINA listeners[] (4 listeners de eventos Kafka)
|
|
6
|
+
# - ELIMINA readModels[] (CustomerReadModel) — datos de cliente llegan como
|
|
7
|
+
# input de las Activities, inyectados por el workflow caller
|
|
8
|
+
# - AGREGA activities[] — cada notificación es una Activity
|
|
9
|
+
# - Activities reciben datos del cliente directamente (no lookup local)
|
|
10
|
+
# =============================================================================
|
|
11
|
+
|
|
12
|
+
aggregates:
|
|
13
|
+
- name: Notification
|
|
14
|
+
entities:
|
|
15
|
+
- name: notification
|
|
16
|
+
isRoot: true
|
|
17
|
+
tableName: notifications
|
|
18
|
+
audit:
|
|
19
|
+
enabled: true
|
|
20
|
+
fields:
|
|
21
|
+
- name: id
|
|
22
|
+
type: String
|
|
23
|
+
- name: recipientEmail
|
|
24
|
+
type: String
|
|
25
|
+
- name: recipientPhone
|
|
26
|
+
type: String
|
|
27
|
+
- name: type
|
|
28
|
+
type: NotificationType
|
|
29
|
+
- name: channel
|
|
30
|
+
type: NotificationChannel
|
|
31
|
+
- name: status
|
|
32
|
+
type: NotificationStatus
|
|
33
|
+
readOnly: true
|
|
34
|
+
- name: subject
|
|
35
|
+
type: String
|
|
36
|
+
- name: body
|
|
37
|
+
type: String
|
|
38
|
+
- name: referenceId
|
|
39
|
+
type: String
|
|
40
|
+
- name: sentAt
|
|
41
|
+
type: LocalDateTime
|
|
42
|
+
readOnly: true
|
|
43
|
+
|
|
44
|
+
enums:
|
|
45
|
+
- name: NotificationType
|
|
46
|
+
values:
|
|
47
|
+
- ORDER_PLACED
|
|
48
|
+
- ORDER_CANCELLED
|
|
49
|
+
- PAYMENT_APPROVED
|
|
50
|
+
- PAYMENT_FAILED
|
|
51
|
+
|
|
52
|
+
- name: NotificationChannel
|
|
53
|
+
values:
|
|
54
|
+
- EMAIL
|
|
55
|
+
- SMS
|
|
56
|
+
|
|
57
|
+
- name: NotificationStatus
|
|
58
|
+
initialValue: PENDING
|
|
59
|
+
transitions:
|
|
60
|
+
- from: PENDING
|
|
61
|
+
to: SENT
|
|
62
|
+
method: markAsSent
|
|
63
|
+
- from: PENDING
|
|
64
|
+
to: FAILED
|
|
65
|
+
method: markAsFailed
|
|
66
|
+
values: [PENDING, SENT, FAILED]
|
|
67
|
+
|
|
68
|
+
# ─── Activities que este módulo EXPONE ───────────────────────────────────────
|
|
69
|
+
# Cada activity recibe TODA la data necesaria como input — zero lookups locales.
|
|
70
|
+
# El workflow caller (PlaceOrderWorkflow, CancelOrderWorkflow) obtiene datos
|
|
71
|
+
# del cliente via GetCustomerById y los pasa aquí directamente.
|
|
72
|
+
activities:
|
|
73
|
+
- name: NotifyOrderPlaced
|
|
74
|
+
type: light
|
|
75
|
+
description: "Envía notificación de orden colocada al cliente"
|
|
76
|
+
input:
|
|
77
|
+
- name: orderId
|
|
78
|
+
type: String
|
|
79
|
+
- name: email
|
|
80
|
+
type: String
|
|
81
|
+
- name: firstName
|
|
82
|
+
type: String
|
|
83
|
+
- name: totalAmount
|
|
84
|
+
type: BigDecimal
|
|
85
|
+
timeout: 15s
|
|
86
|
+
|
|
87
|
+
- name: NotifyOrderCancelled
|
|
88
|
+
type: light
|
|
89
|
+
description: "Envía notificación de orden cancelada al cliente"
|
|
90
|
+
input:
|
|
91
|
+
- name: orderId
|
|
92
|
+
type: String
|
|
93
|
+
- name: email
|
|
94
|
+
type: String
|
|
95
|
+
- name: firstName
|
|
96
|
+
type: String
|
|
97
|
+
timeout: 15s
|
|
98
|
+
|
|
99
|
+
- name: NotifyPaymentApproved
|
|
100
|
+
type: light
|
|
101
|
+
description: "Envía notificación de pago aprobado al cliente"
|
|
102
|
+
input:
|
|
103
|
+
- name: orderId
|
|
104
|
+
type: String
|
|
105
|
+
- name: customerEmail
|
|
106
|
+
type: String
|
|
107
|
+
- name: customerName
|
|
108
|
+
type: String
|
|
109
|
+
- name: amount
|
|
110
|
+
type: BigDecimal
|
|
111
|
+
timeout: 15s
|
|
112
|
+
|
|
113
|
+
- name: NotifyPaymentFailed
|
|
114
|
+
type: light
|
|
115
|
+
description: "Envía notificación de pago fallido al cliente"
|
|
116
|
+
input:
|
|
117
|
+
- name: orderId
|
|
118
|
+
type: String
|
|
119
|
+
- name: customerEmail
|
|
120
|
+
type: String
|
|
121
|
+
- name: customerName
|
|
122
|
+
type: String
|
|
123
|
+
- name: reason
|
|
124
|
+
type: String
|
|
125
|
+
timeout: 15s
|
|
126
|
+
|
|
127
|
+
# ─── ELIMINADOS: listeners[], readModels[], SyncCustomerReadModel ────────────
|
|
128
|
+
# CustomerReadModel ELIMINADO — datos del cliente llegan como input directo.
|
|
129
|
+
# SyncCustomerReadModel ELIMINADO — ya no hay proyección local que mantener.
|
|
130
|
+
# Consecuencia: notifications es un módulo stateless para datos cross-module.
|
|
131
|
+
# Solo persiste las notificaciones enviadas (entidad Notification).
|