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,740 @@
|
|
|
1
|
+
# Metodología de Diseño con Temporal como Broker
|
|
2
|
+
|
|
3
|
+
## 📋 Propósito
|
|
4
|
+
|
|
5
|
+
Guía paso a paso para diseñar un sistema de microservicios usando **Temporal como único mecanismo de comunicación inter-módulo**, reemplazando Kafka (async) y Feign (sync). Define qué va en `system.yaml`, qué va en cada `{domain}.yaml`, y las mejores prácticas para tomar decisiones de diseño.
|
|
6
|
+
|
|
7
|
+
**Documentos complementarios:**
|
|
8
|
+
- [TEMPORAL_COMMUNICATION_PATTERNS.md](TEMPORAL_COMMUNICATION_PATTERNS.md) — Patrones de comunicación (Remote Activity, Child Workflow, Signal, Async.function)
|
|
9
|
+
- [system/RISKS.md](system/RISKS.md) — Análisis de riesgos y trade-offs
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## 🗺️ Visión General de la Arquitectura
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
17
|
+
│ system.yaml │
|
|
18
|
+
│ ┌──────────┐ ┌──────────┐ ┌─────────────────────────────┐ │
|
|
19
|
+
│ │ system: │ │ modules: │ │ workflows: │ │
|
|
20
|
+
│ │ │ │ │ │ PlaceOrderWorkflow │ │
|
|
21
|
+
│ │ name │ │ products │ │ CancelOrderWorkflow │ │
|
|
22
|
+
│ │ database │ │ orders │ │ ProductCreatedWorkflow │ │
|
|
23
|
+
│ │ temporal │ │ payments │ │ (orquestación cross-module) │ │
|
|
24
|
+
│ └──────────┘ └──────────┘ └─────────────────────────────┘ │
|
|
25
|
+
└─────────────────────────────────────────────────────────────────┘
|
|
26
|
+
↕ ↕ ↕
|
|
27
|
+
┌──────────────┐ ┌──────────────┐ ┌──────────────────────────────┐
|
|
28
|
+
│ products.yaml│ │ orders.yaml │ │ payments.yaml │
|
|
29
|
+
│ │ │ │ │ │
|
|
30
|
+
│ aggregates: │ │ aggregates: │ │ aggregates: │
|
|
31
|
+
│ Product │ │ Order │ │ Payment │
|
|
32
|
+
│ │ │ │ │ │
|
|
33
|
+
│ events: │ │ events: │ │ events: │
|
|
34
|
+
│ (internos) │ │ (→notifies) │ │ (internos) │
|
|
35
|
+
│ │ │ │ │ │
|
|
36
|
+
│ activities: │ │ activities: │ │ activities: │
|
|
37
|
+
│ GetProduct │ │ ConfirmOrder│ │ ProcessPayment │
|
|
38
|
+
│ (cross-mod) │ │ (local) │ │ RefundPayment (cross-mod) │
|
|
39
|
+
│ │ │ │ │ RetryCharge (interna) │
|
|
40
|
+
│ endpoints: │ │ endpoints: │ │ │
|
|
41
|
+
│ REST API │ │ REST API │ │ workflows: (single-module) │
|
|
42
|
+
│ │ │ │ │ RetryChargeWorkflow │
|
|
43
|
+
│ │ │ workflows: │ │ │
|
|
44
|
+
│ │ │ ExpireOrder │ │ ports: (servicios EXTERNOS) │
|
|
45
|
+
│ │ │ (interno) │ │ PaymentGateway (HTTP) │
|
|
46
|
+
└──────────────┘ └──────────────┘ └──────────────────────────────┘
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## 📐 Qué va en Cada Archivo
|
|
52
|
+
|
|
53
|
+
### `system.yaml` — Vista de Pájaro (Orquestación)
|
|
54
|
+
|
|
55
|
+
El system.yaml es el **mapa de navegación** del sistema. Contiene todo lo que cruza fronteras de módulo.
|
|
56
|
+
|
|
57
|
+
| Sección | Contenido | Ejemplo |
|
|
58
|
+
|---------|-----------|---------|
|
|
59
|
+
| `system:` | Metadata del proyecto | nombre, database, Spring Boot version |
|
|
60
|
+
| `orchestration:` | Configuración de Temporal | target, namespace |
|
|
61
|
+
| `modules:` | Lista de módulos con endpoints REST | products, orders, payments |
|
|
62
|
+
| `workflows:` | **Flujos cross-module** | PlaceOrderWorkflow, CancelOrderWorkflow |
|
|
63
|
+
|
|
64
|
+
**Regla cardinal:** Si un flujo toca **2+ módulos**, se define en `system.yaml`.
|
|
65
|
+
|
|
66
|
+
```yaml
|
|
67
|
+
# system.yaml — SOLO orquestación cross-module
|
|
68
|
+
workflows:
|
|
69
|
+
- name: PlaceOrderWorkflow
|
|
70
|
+
trigger:
|
|
71
|
+
module: orders
|
|
72
|
+
on: create
|
|
73
|
+
taskQueue: ORDER_WORKFLOW_QUEUE
|
|
74
|
+
saga: true
|
|
75
|
+
steps:
|
|
76
|
+
- activity: GetCustomerById # → customers
|
|
77
|
+
- activity: GetProductsByIds # → products ⎫ parallel
|
|
78
|
+
- activity: ReserveStock # → inventory ⎭
|
|
79
|
+
- activity: ProcessOrderPayment # → payments
|
|
80
|
+
- activity: ConfirmOrder # → orders (local)
|
|
81
|
+
- activity: NotifyOrderPlaced # → notifications (async)
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### `{domain}.yaml` — Vista Interna del Módulo
|
|
85
|
+
|
|
86
|
+
Cada domain.yaml define **lo que el módulo ES y lo que OFRECE**.
|
|
87
|
+
|
|
88
|
+
| Sección | Contenido | Ejemplo |
|
|
89
|
+
|---------|-----------|---------|
|
|
90
|
+
| `aggregates:` | Modelo de dominio (entities, VOs, enums) | Product, Order, Payment |
|
|
91
|
+
| `events:` | Domain Events internos (con o sin `notifies`) | ProductCreatedEvent |
|
|
92
|
+
| `activities:` | **Capacidades del módulo** (cross-module e internas) | GetProductById, ReserveStock, ConfirmOrder |
|
|
93
|
+
| `workflows:` | **Flujos internos** del módulo (single-module) | RetryChargeWorkflow, ExpireOrderWorkflow |
|
|
94
|
+
| `endpoints:` | API REST del módulo | GET /products, POST /orders |
|
|
95
|
+
| `ports:` | Servicios **EXTERNOS** (no-Temporal) | PaymentGateway HTTP |
|
|
96
|
+
|
|
97
|
+
**Regla cardinal:** El domain.yaml declara lo que el módulo **sabe hacer**. NO declara cómo otros lo usan.
|
|
98
|
+
|
|
99
|
+
```yaml
|
|
100
|
+
# inventory.yaml — lo que inventory OFRECE
|
|
101
|
+
activities:
|
|
102
|
+
- name: ReserveStock # Capacidad de escritura
|
|
103
|
+
type: light
|
|
104
|
+
input: [orderId, items]
|
|
105
|
+
output: [success]
|
|
106
|
+
compensation: ReleaseStock # Su propia compensación
|
|
107
|
+
|
|
108
|
+
- name: ReleaseStock # Capacidad de compensación
|
|
109
|
+
type: light
|
|
110
|
+
input: [orderId, items]
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Workflows en `system.yaml` vs `{domain}.yaml`
|
|
114
|
+
|
|
115
|
+
**Regla de separación:**
|
|
116
|
+
|
|
117
|
+
| Criterio | Ubicación | Ejemplo |
|
|
118
|
+
|----------|-----------|--------|
|
|
119
|
+
| Flujo cruza **2+ módulos** | `system.yaml` | PlaceOrderWorkflow (orders→inventory→payments→notifications) |
|
|
120
|
+
| Flujo es **interno** a 1 módulo | `{domain}.yaml` | RetryChargeWorkflow (payments→payments) |
|
|
121
|
+
|
|
122
|
+
Un workflow single-module es un proceso interno del bounded context que no involucra activities de otros módulos. Se declara en el `{domain}.yaml` del módulo porque es una **capacidad interna**, no una decisión de sistema.
|
|
123
|
+
|
|
124
|
+
```yaml
|
|
125
|
+
# payments.yaml — workflow INTERNO del módulo
|
|
126
|
+
workflows:
|
|
127
|
+
- name: RetryChargeWorkflow
|
|
128
|
+
trigger:
|
|
129
|
+
on: paymentFailed
|
|
130
|
+
taskQueue: PAYMENT_WORKFLOW_QUEUE
|
|
131
|
+
steps:
|
|
132
|
+
- activity: RetryCharge # activity local de payments
|
|
133
|
+
retryPolicy:
|
|
134
|
+
maxAttempts: 3
|
|
135
|
+
backoff: exponential
|
|
136
|
+
- activity: NotifyChargeResult # activity local de payments
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
**Ejemplos de workflows single-module:**
|
|
140
|
+
|
|
141
|
+
| Módulo | Workflow | Propósito |
|
|
142
|
+
|--------|----------|---------|
|
|
143
|
+
| payments | `RetryChargeWorkflow` | Reintentar cobro fallido con backoff exponencial |
|
|
144
|
+
| inventory | `ReplenishStockWorkflow` | Reabastecer cuando el stock cae bajo mínimo |
|
|
145
|
+
| customers | `VerifyEmailWorkflow` | Enviar código, esperar verificación con timeout |
|
|
146
|
+
| orders | `ExpireOrderWorkflow` | Cancelar orden pendiente tras X minutos sin pago |
|
|
147
|
+
|
|
148
|
+
**Señales de que un workflow es single-module:**
|
|
149
|
+
- Todas sus activities pertenecen al mismo módulo
|
|
150
|
+
- No necesita datos de otros bounded contexts
|
|
151
|
+
- Es un proceso interno (retry, timeout, scheduling, verificación)
|
|
152
|
+
- Otros módulos no necesitan saber que existe
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## 🔄 Proceso de Diseño (8 Pasos)
|
|
157
|
+
|
|
158
|
+
### Paso 1: Identificar los Bounded Contexts (Módulos)
|
|
159
|
+
|
|
160
|
+
Listar los módulos del sistema con su responsabilidad principal.
|
|
161
|
+
|
|
162
|
+
```yaml
|
|
163
|
+
modules:
|
|
164
|
+
- name: products # "Qué vendemos"
|
|
165
|
+
- name: customers # "A quién le vendemos"
|
|
166
|
+
- name: orders # "Qué nos piden"
|
|
167
|
+
- name: payments # "Cómo nos pagan"
|
|
168
|
+
- name: inventory # "Cuánto tenemos"
|
|
169
|
+
- name: notifications # "A quién avisamos"
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
**Pregunta clave:** ¿Este módulo tiene su propia base de datos / tabla? Si sí, es un módulo.
|
|
173
|
+
|
|
174
|
+
### Paso 2: Modelar los Agregados (domain.yaml)
|
|
175
|
+
|
|
176
|
+
Para cada módulo, definir el modelo de dominio puro. Este paso es **idéntico** con o sin Temporal.
|
|
177
|
+
|
|
178
|
+
```yaml
|
|
179
|
+
# orders.yaml
|
|
180
|
+
aggregates:
|
|
181
|
+
- name: Order
|
|
182
|
+
entities:
|
|
183
|
+
- name: order
|
|
184
|
+
isRoot: true
|
|
185
|
+
fields: [id, customerId, status, totalAmount, ...]
|
|
186
|
+
enums:
|
|
187
|
+
- name: OrderStatus
|
|
188
|
+
transitions: [...]
|
|
189
|
+
valueObjects:
|
|
190
|
+
- name: ShippingAddress
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
**No pensar en workflows todavía.** Solo el dominio puro.
|
|
194
|
+
|
|
195
|
+
### Paso 3: Identificar los Domain Events
|
|
196
|
+
|
|
197
|
+
Para cada agregado, listar los hechos de negocio relevantes:
|
|
198
|
+
|
|
199
|
+
```yaml
|
|
200
|
+
events:
|
|
201
|
+
- name: OrderPlacedEvent # Se colocó una orden
|
|
202
|
+
- name: OrderCancelledEvent # Se canceló una orden
|
|
203
|
+
- name: ProductCreatedEvent # Se creó un producto nuevo
|
|
204
|
+
- name: PaymentApprovedEvent # Se aprobó un pago
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
**Pregunta clave para cada evento:** ¿Algo DEBE ocurrir en OTRO módulo cuando esto pasa?
|
|
208
|
+
|
|
209
|
+
| Evento | ¿Efecto cross-module? | Resultado |
|
|
210
|
+
|--------|----------------------|-----------|
|
|
211
|
+
| OrderPlacedEvent | SÍ → reservar stock, cobrar, notificar | **Workflow** |
|
|
212
|
+
| OrderCancelledEvent | SÍ → liberar stock, reembolsar, notificar | **Workflow** |
|
|
213
|
+
| ProductCreatedEvent | SÍ → inicializar stock en inventory | **Workflow** |
|
|
214
|
+
| ProductUpdatedEvent | NO → nadie necesita reaccionar | **Domain Event interno** |
|
|
215
|
+
| CustomerCreatedEvent | NO → nadie necesita reaccionar | **Domain Event interno** |
|
|
216
|
+
| PaymentApprovedEvent | NO → ya está dentro del workflow de orden | **Domain Event interno** |
|
|
217
|
+
|
|
218
|
+
### Paso 4: Diseñar los Workflows (system.yaml)
|
|
219
|
+
|
|
220
|
+
Para cada evento que **SÍ tiene efecto cross-module**, diseñar el workflow.
|
|
221
|
+
|
|
222
|
+
**Árbol de decisión para cada step:**
|
|
223
|
+
|
|
224
|
+
```
|
|
225
|
+
¿Este step modifica datos?
|
|
226
|
+
│
|
|
227
|
+
├─ SÍ → ¿En otro módulo?
|
|
228
|
+
│ │
|
|
229
|
+
│ ├─ SÍ → ¿Necesita compensación si un paso posterior falla?
|
|
230
|
+
│ │ │
|
|
231
|
+
│ │ ├─ SÍ → Activity con compensation: (dentro de saga: true)
|
|
232
|
+
│ │ │ Ej: ReserveStock + compensation: ReleaseStock
|
|
233
|
+
│ │ │
|
|
234
|
+
│ │ └─ NO → Activity sin compensación
|
|
235
|
+
│ │ Ej: paso final de la saga, nada que deshacer
|
|
236
|
+
│ │
|
|
237
|
+
│ └─ NO → Activity LOCAL del propio módulo
|
|
238
|
+
│ Ej: ConfirmOrder (orders actualiza su propia orden)
|
|
239
|
+
│
|
|
240
|
+
└─ NO → ¿Lee datos de otro módulo?
|
|
241
|
+
│
|
|
242
|
+
├─ SÍ → Activity de lectura (Remote Activity)
|
|
243
|
+
│ Ej: GetCustomerById → customers
|
|
244
|
+
│
|
|
245
|
+
└─ NO → ¿Notifica sin esperar respuesta?
|
|
246
|
+
│
|
|
247
|
+
├─ SÍ → Activity async (type: async / Async.function)
|
|
248
|
+
│ Ej: NotifyOrderPlaced → notifications
|
|
249
|
+
│
|
|
250
|
+
└─ NO → No necesita step
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
**¿Steps paralelos?** Si 2+ steps son **independientes entre sí**, marcarlos con `parallel: true`:
|
|
254
|
+
|
|
255
|
+
```yaml
|
|
256
|
+
# GetProductsByIds y ReserveStock no dependen uno del otro
|
|
257
|
+
- activity: GetProductsByIds
|
|
258
|
+
parallel: true # Async.function()
|
|
259
|
+
- activity: ReserveStock
|
|
260
|
+
parallel: true # Async.function()
|
|
261
|
+
# → Promise.allOf(productPromise, stockPromise).get()
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
### Paso 5: Definir las Activities de cada Módulo (domain.yaml)
|
|
265
|
+
|
|
266
|
+
Cada módulo declara las **capacidades** que ofrece — tanto a workflows cross-module como a sus propios workflows internos.
|
|
267
|
+
|
|
268
|
+
**4 tipos de activities:**
|
|
269
|
+
|
|
270
|
+
| Tipo | Ámbito | Propósito | Ejemplo | Task Queue |
|
|
271
|
+
|------|--------|-----------|---------|------------|
|
|
272
|
+
| **Lectura** | Cross-module | Consultar datos del módulo | `GetCustomerById` | `{MOD}_LIGHT_TASK_QUEUE` |
|
|
273
|
+
| **Escritura** | Cross-module | Ejecutar una operación de negocio | `ReserveStock`, `ProcessPayment` | `{MOD}_LIGHT_TASK_QUEUE` o `{MOD}_HEAVY_TASK_QUEUE` |
|
|
274
|
+
| **Compensación** | Cross-module | Deshacer una operación | `ReleaseStock`, `RefundPayment` | misma que su operación |
|
|
275
|
+
| **Local** | Interno | Operación invocada por workflows del propio módulo | `ConfirmOrder`, `RetryCharge` | `{MOD}_LIGHT_TASK_QUEUE` |
|
|
276
|
+
|
|
277
|
+
> **Nota:** Una activity **local** y una **cross-module** se declaran igual en `activities[]` — la diferencia es solo quién la invoca. Si un workflow de `system.yaml` referencia una activity, es cross-module. Si solo la usan workflows del propio `{domain}.yaml`, es local. No hay distinción sintáctica.
|
|
278
|
+
|
|
279
|
+
**Regla de aislamiento de datos:** Cada activity accede **SOLO** a la base de datos de su propio módulo. Datos de otro bounded context deben llegar como input del workflow — la activity nunca consulta repositorios ajenos.
|
|
280
|
+
|
|
281
|
+
```yaml
|
|
282
|
+
# ❌ INCORRECTO — la activity consulta la BD de otro módulo
|
|
283
|
+
activities:
|
|
284
|
+
- name: NotifyOrderPlaced
|
|
285
|
+
input: [orderId, customerId]
|
|
286
|
+
# Internamente: customerRepo.findById(customerId) ← ACOPLAMIENTO CROSS-MODULE
|
|
287
|
+
|
|
288
|
+
# ✅ CORRECTO — el workflow ensambla la data y se la pasa
|
|
289
|
+
activities:
|
|
290
|
+
- name: NotifyOrderPlaced
|
|
291
|
+
input: [orderId, customerEmail, customerName, totalAmount]
|
|
292
|
+
# No necesita saber nada de customers
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
> **Aclaración:** Una activity SÍ consulta su propia BD. `ReserveStock` lee el stock actual de su repositorio de inventory — eso es correcto. Lo prohibido es que una activity de inventory consulte la tabla de customers.
|
|
296
|
+
|
|
297
|
+
**Regla de compensación explícita:** Si una activity tiene compensación, declararla en el mismo módulo.
|
|
298
|
+
|
|
299
|
+
```yaml
|
|
300
|
+
activities:
|
|
301
|
+
- name: ReserveStock
|
|
302
|
+
compensation: ReleaseStock # ← declarado aquí mismo
|
|
303
|
+
|
|
304
|
+
- name: ReleaseStock # ← la compensación es otra activity
|
|
305
|
+
input: [orderId, items]
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
### Paso 6: Conectar Events con Workflows (notifies)
|
|
309
|
+
|
|
310
|
+
Solo los eventos que disparan workflows llevan `notifies:`.
|
|
311
|
+
|
|
312
|
+
```yaml
|
|
313
|
+
# orders.yaml — el evento SABE qué workflow lanza
|
|
314
|
+
events:
|
|
315
|
+
- name: OrderPlacedEvent
|
|
316
|
+
notifies:
|
|
317
|
+
- workflow: PlaceOrderWorkflow # definido en system.yaml
|
|
318
|
+
|
|
319
|
+
- name: OrderCancelledEvent
|
|
320
|
+
notifies:
|
|
321
|
+
- workflow: CancelOrderWorkflow
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
```yaml
|
|
325
|
+
# customers.yaml — el evento NO lanza ningún workflow
|
|
326
|
+
events:
|
|
327
|
+
- name: CustomerCreatedEvent
|
|
328
|
+
# SIN notifies → Domain Event interno puro
|
|
329
|
+
# Otros módulos obtienen datos de customer via GetCustomerById
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
**Regla de desacoplamiento:** Si un evento solo sirve para que **otro módulo se entere** de datos actualizados (sincronización), **NO necesita notifies**. El workflow que necesite esos datos los obtiene on-demand via Remote Activity de lectura.
|
|
333
|
+
|
|
334
|
+
### Paso 7: Diseñar Workflows Internos (domain.yaml)
|
|
335
|
+
|
|
336
|
+
Para cada módulo, identificar si existen **procesos internos que necesitan durabilidad**: reintentos con backoff, timeouts, scheduling, verificaciones con espera.
|
|
337
|
+
|
|
338
|
+
**Pregunta clave:** ¿Este proceso interno necesita sobrevivir a reinicios del servicio, tiene reintentos complejos, o espera eventos con timeout?
|
|
339
|
+
|
|
340
|
+
| SÍ → | NO → |
|
|
341
|
+
|-------|-------|
|
|
342
|
+
| Workflow single-module en `{domain}.yaml` | Lógica síncrona directa (service method, @Scheduled) |
|
|
343
|
+
|
|
344
|
+
```yaml
|
|
345
|
+
# orders.yaml — workflow INTERNO
|
|
346
|
+
workflows:
|
|
347
|
+
- name: ExpireOrderWorkflow
|
|
348
|
+
trigger:
|
|
349
|
+
on: orderCreated
|
|
350
|
+
taskQueue: ORDER_WORKFLOW_QUEUE
|
|
351
|
+
steps:
|
|
352
|
+
- activity: WaitForPayment # Workflow.sleep() o Signal + Await
|
|
353
|
+
timeout: 30m
|
|
354
|
+
- activity: CancelExpiredOrder # activity local de orders
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
```yaml
|
|
358
|
+
# payments.yaml — workflow INTERNO
|
|
359
|
+
workflows:
|
|
360
|
+
- name: RetryChargeWorkflow
|
|
361
|
+
trigger:
|
|
362
|
+
on: paymentFailed
|
|
363
|
+
taskQueue: PAYMENT_WORKFLOW_QUEUE
|
|
364
|
+
steps:
|
|
365
|
+
- activity: RetryCharge # activity local de payments
|
|
366
|
+
retryPolicy:
|
|
367
|
+
maxAttempts: 3
|
|
368
|
+
backoff: exponential
|
|
369
|
+
- activity: NotifyChargeResult # activity local de payments
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
**Regla:** Los workflows single-module solo componen activities del propio módulo. Si descubres que un step necesita datos o acciones de otro módulo, el workflow debe subir a `system.yaml`.
|
|
373
|
+
|
|
374
|
+
### Paso 8: Declarar Servicios Externos (ports)
|
|
375
|
+
|
|
376
|
+
`ports[]` solo se usa para servicios que **NO corren workers Temporal**: APIs de terceros, payment gateways, servicios SaaS externos.
|
|
377
|
+
|
|
378
|
+
```yaml
|
|
379
|
+
# payments.yaml — el gateway externo es HTTP puro
|
|
380
|
+
ports:
|
|
381
|
+
- name: processCharge
|
|
382
|
+
service: PaymentGatewayService
|
|
383
|
+
target: payment-gateway
|
|
384
|
+
baseUrl: https://api.payments.example.com
|
|
385
|
+
http: POST /charges
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
**Regla:** Si el servicio está **dentro** de tu sistema (tiene domain.yaml), usa Activities. Si es **externo**, usa `ports[]`.
|
|
389
|
+
|
|
390
|
+
---
|
|
391
|
+
|
|
392
|
+
## ✅ Checklist por Tipo de Módulo
|
|
393
|
+
|
|
394
|
+
> **Nota:** Los roles no son categorías excluyentes — un módulo puede combinar características de varios tipos. Por ejemplo, `orders` es principalmente Orquestador (sus eventos disparan sagas), pero también declara activities locales (`ConfirmOrder`) y workflows internos (`ExpireOrderWorkflow`). Los checklists son guías por **rol principal**, no restricciones absolutas.
|
|
395
|
+
|
|
396
|
+
### Módulo Orquestador (ej: orders)
|
|
397
|
+
|
|
398
|
+
Es el módulo que **inicia** los workflows cross-module. Sus eventos disparan sagas.
|
|
399
|
+
|
|
400
|
+
- [ ] `events[].notifies` → apunta a workflows en system.yaml
|
|
401
|
+
- [ ] Puede tener `activities[]` **locales** invocadas por workflows que corren en su queue (ej: `ConfirmOrder`)
|
|
402
|
+
- [ ] Puede tener `workflows[]` **single-module** para procesos internos (ej: `ExpireOrderWorkflow`)
|
|
403
|
+
- [ ] **NO tiene** `listeners[]`, `readModels[]`
|
|
404
|
+
- [ ] **NO** declara sagas cross-module — la orquestación va en system.yaml
|
|
405
|
+
|
|
406
|
+
```yaml
|
|
407
|
+
# orders.yaml — orquestador
|
|
408
|
+
events:
|
|
409
|
+
- name: OrderPlacedEvent
|
|
410
|
+
notifies:
|
|
411
|
+
- workflow: PlaceOrderWorkflow
|
|
412
|
+
|
|
413
|
+
activities:
|
|
414
|
+
- name: ConfirmOrder # local: invocada por PlaceOrderWorkflow
|
|
415
|
+
type: light
|
|
416
|
+
|
|
417
|
+
workflows:
|
|
418
|
+
- name: ExpireOrderWorkflow # interno: cancelar orden si no paga
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
### Módulo Proveedor de Datos (ej: customers, products)
|
|
422
|
+
|
|
423
|
+
Sus datos son consumidos **on-demand** por workflows de otros módulos.
|
|
424
|
+
|
|
425
|
+
- [ ] `activities[]` → al menos una activity de **lectura** (GetXById)
|
|
426
|
+
- [ ] `events[]` → Domain Events internos, **SIN notifies** (a menos que haya un efecto de negocio real)
|
|
427
|
+
- [ ] Considerar activities **batch** si se consultan múltiples registros (GetProductsByIds)
|
|
428
|
+
|
|
429
|
+
```yaml
|
|
430
|
+
# customers.yaml — proveedor de datos
|
|
431
|
+
activities:
|
|
432
|
+
- name: GetCustomerById # ← lectura on-demand
|
|
433
|
+
type: light
|
|
434
|
+
input: [customerId]
|
|
435
|
+
output: [id, firstName, lastName, email, phone]
|
|
436
|
+
|
|
437
|
+
events:
|
|
438
|
+
- name: CustomerCreatedEvent # ← interno, SIN notifies
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
### Módulo Ejecutor (ej: inventory, payments)
|
|
442
|
+
|
|
443
|
+
Ofrece **operaciones de negocio** que los workflows invocan.
|
|
444
|
+
|
|
445
|
+
- [ ] `activities[]` → operaciones de escritura + sus compensaciones
|
|
446
|
+
- [ ] `compensation:` → declarada explícitamente en cada activity reversible
|
|
447
|
+
- [ ] `timeout:` y `retryPolicy:` → configurados por activity
|
|
448
|
+
- [ ] `ports[]` → solo si llama a servicios EXTERNOS (ej: payment gateway)
|
|
449
|
+
|
|
450
|
+
```yaml
|
|
451
|
+
# inventory.yaml — ejecutor
|
|
452
|
+
activities:
|
|
453
|
+
- name: ReserveStock
|
|
454
|
+
type: light
|
|
455
|
+
input: [orderId, items]
|
|
456
|
+
compensation: ReleaseStock
|
|
457
|
+
|
|
458
|
+
- name: ReleaseStock
|
|
459
|
+
type: light
|
|
460
|
+
input: [orderId, items]
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
### Módulo Reactor (ej: notifications)
|
|
464
|
+
|
|
465
|
+
Ejecuta efectos secundarios (emails, SMS, webhooks) invocados por workflows.
|
|
466
|
+
|
|
467
|
+
- [ ] `activities[]` → reciben **TODA** la data como input (zero lookups)
|
|
468
|
+
- [ ] Invocados como `type: async` en el workflow (non-blocking)
|
|
469
|
+
- [ ] **NO** tienen `readModels[]` — son stateless para datos cross-module
|
|
470
|
+
- [ ] Solo persisten sus propias entidades (ej: registro de notificación enviada)
|
|
471
|
+
|
|
472
|
+
```yaml
|
|
473
|
+
# notifications.yaml — reactor
|
|
474
|
+
activities:
|
|
475
|
+
- name: NotifyOrderPlaced
|
|
476
|
+
type: light
|
|
477
|
+
input: [orderId, customerEmail, customerName, totalAmount]
|
|
478
|
+
# Toda la data viene del workflow caller
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
---
|
|
482
|
+
|
|
483
|
+
## 🏗️ Patrones de Diseño de Workflows
|
|
484
|
+
|
|
485
|
+
### Patrón 1: Saga con Compensación
|
|
486
|
+
|
|
487
|
+
Flujo multi-step donde el fallo de un paso deshace los anteriores.
|
|
488
|
+
|
|
489
|
+
```yaml
|
|
490
|
+
workflows:
|
|
491
|
+
- name: PlaceOrderWorkflow
|
|
492
|
+
saga: true
|
|
493
|
+
steps:
|
|
494
|
+
- activity: ReserveStock
|
|
495
|
+
compensation: ReleaseStock # ← se ejecuta si paso posterior falla
|
|
496
|
+
- activity: ProcessOrderPayment
|
|
497
|
+
compensation: RefundPayment
|
|
498
|
+
- activity: ConfirmOrder # último paso, sin compensación
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
**Cuándo:** Operaciones que involucran escritura en múltiples módulos y necesitan consistencia eventual.
|
|
502
|
+
|
|
503
|
+
### Patrón 2: Enriquecimiento + Acción
|
|
504
|
+
|
|
505
|
+
Obtener datos de lectura antes de ejecutar la acción principal.
|
|
506
|
+
|
|
507
|
+
```yaml
|
|
508
|
+
steps:
|
|
509
|
+
# 1. Enriquecimiento (lectura on-demand)
|
|
510
|
+
- activity: GetCustomerById
|
|
511
|
+
output: [firstName, email]
|
|
512
|
+
|
|
513
|
+
# 2. Acción (usa los datos obtenidos)
|
|
514
|
+
- activity: ProcessOrderPayment
|
|
515
|
+
input: [orderId, customerId, totalAmount]
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
**Cuándo:** El workflow necesita datos de otro módulo para tomar decisiones o pasarlos a steps posteriores.
|
|
519
|
+
|
|
520
|
+
### Patrón 3: Steps Paralelos (Async.function)
|
|
521
|
+
|
|
522
|
+
Ejecutar steps independientes en paralelo para reducir latencia.
|
|
523
|
+
|
|
524
|
+
```yaml
|
|
525
|
+
steps:
|
|
526
|
+
- activity: GetProductsByIds
|
|
527
|
+
parallel: true # ⎫ ejecutar juntos
|
|
528
|
+
- activity: ReserveStock # ⎭
|
|
529
|
+
parallel: true
|
|
530
|
+
# → Promise.allOf().get()
|
|
531
|
+
- activity: ProcessOrderPayment # después de ambos
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
**Cuándo:** 2+ steps son independientes entre sí (no usan el output del otro).
|
|
535
|
+
|
|
536
|
+
### Patrón 4: Efecto de Negocio Puntual
|
|
537
|
+
|
|
538
|
+
Un evento dispara una única acción en otro módulo.
|
|
539
|
+
|
|
540
|
+
```yaml
|
|
541
|
+
workflows:
|
|
542
|
+
- name: ProductCreatedWorkflow
|
|
543
|
+
trigger:
|
|
544
|
+
module: products
|
|
545
|
+
on: create
|
|
546
|
+
steps:
|
|
547
|
+
- activity: InitializeStock # solo 1 step
|
|
548
|
+
compensation: DeleteStock
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
**Cuándo:** Efecto colateral simple, no una saga compleja.
|
|
552
|
+
|
|
553
|
+
### Patrón 5: Notificación Non-Blocking
|
|
554
|
+
|
|
555
|
+
Último step del workflow, no afecta el resultado de la saga.
|
|
556
|
+
|
|
557
|
+
```yaml
|
|
558
|
+
steps:
|
|
559
|
+
# ... pasos críticos de la saga ...
|
|
560
|
+
- activity: NotifyOrderPlaced
|
|
561
|
+
type: async # fire-and-forget
|
|
562
|
+
# NO tiene compensation — si falla, no revierte la saga
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
**Cuándo:** Efectos secundarios no-críticos (emails, logs, analytics).
|
|
566
|
+
|
|
567
|
+
---
|
|
568
|
+
|
|
569
|
+
## ⚠️ Errores Comunes
|
|
570
|
+
|
|
571
|
+
### ❌ Crear workflows de sincronización de datos
|
|
572
|
+
|
|
573
|
+
```yaml
|
|
574
|
+
# ❌ NO HACER — workflow que solo sincroniza datos
|
|
575
|
+
- name: CustomerUpdatedWorkflow
|
|
576
|
+
steps:
|
|
577
|
+
- activity: SyncCustomerReadModel
|
|
578
|
+
target: orders
|
|
579
|
+
- activity: SyncCustomerReadModel
|
|
580
|
+
target: notifications
|
|
581
|
+
|
|
582
|
+
# ✅ SÍ HACER — lectura on-demand en el workflow que necesita el dato
|
|
583
|
+
# PlaceOrderWorkflow → GetCustomerById → customers
|
|
584
|
+
```
|
|
585
|
+
|
|
586
|
+
**Por qué:** Los workflows de sync acoplan al productor con todos sus consumidores. La lectura on-demand invierte la dependencia.
|
|
587
|
+
|
|
588
|
+
### ❌ Activities que hacen lookups cross-module
|
|
589
|
+
|
|
590
|
+
```yaml
|
|
591
|
+
# ❌ NO HACER — la activity de notifications busca datos de customers
|
|
592
|
+
activities:
|
|
593
|
+
- name: NotifyOrderPlaced
|
|
594
|
+
input: [orderId, customerId]
|
|
595
|
+
# Internamente: fetch customer data from DB/service
|
|
596
|
+
|
|
597
|
+
# ✅ SÍ HACER — el workflow obtiene los datos y los pasa
|
|
598
|
+
activities:
|
|
599
|
+
- name: NotifyOrderPlaced
|
|
600
|
+
input: [orderId, customerEmail, customerName, totalAmount]
|
|
601
|
+
# La activity tiene todo lo que necesita
|
|
602
|
+
```
|
|
603
|
+
|
|
604
|
+
**Por qué:** Si notifications busca datos de customers internamente, crea una dependencia oculta. El workflow debe ser el orquestador que ensambla la data.
|
|
605
|
+
|
|
606
|
+
### ❌ Poner orquestación **cross-module** en el domain.yaml
|
|
607
|
+
|
|
608
|
+
```yaml
|
|
609
|
+
# ❌ NO HACER en orders.yaml
|
|
610
|
+
saga:
|
|
611
|
+
workflow: PlaceOrderWorkflow
|
|
612
|
+
steps: [...]
|
|
613
|
+
|
|
614
|
+
# ✅ SÍ HACER — la orquestación cross-module va en system.yaml
|
|
615
|
+
# El domain.yaml solo declara el evento con notifies:
|
|
616
|
+
events:
|
|
617
|
+
- name: OrderPlacedEvent
|
|
618
|
+
notifies:
|
|
619
|
+
- workflow: PlaceOrderWorkflow
|
|
620
|
+
```
|
|
621
|
+
|
|
622
|
+
**Por qué:** La orquestación cross-module es una **decisión de sistema**, no del módulo individual.
|
|
623
|
+
|
|
624
|
+
> **Excepción:** Los workflows **single-module** SÍ se declaran en `{domain}.yaml` porque son procesos internos del bounded context (ej: `RetryChargeWorkflow`, `ExpireOrderWorkflow`). La regla aplica solo a sagas que tocan 2+ módulos.
|
|
625
|
+
|
|
626
|
+
### ❌ Usar `notifies:` para eventos que no tienen efecto cross-module
|
|
627
|
+
|
|
628
|
+
```yaml
|
|
629
|
+
# ❌ NO HACER — evento de sync disfrazado de workflow
|
|
630
|
+
events:
|
|
631
|
+
- name: CustomerUpdatedEvent
|
|
632
|
+
notifies:
|
|
633
|
+
- workflow: CustomerUpdatedWorkflow # solo sincroniza datos
|
|
634
|
+
|
|
635
|
+
# ✅ SÍ HACER — Domain Event interno
|
|
636
|
+
events:
|
|
637
|
+
- name: CustomerUpdatedEvent
|
|
638
|
+
# SIN notifies — otros módulos obtienen la data on-demand
|
|
639
|
+
```
|
|
640
|
+
|
|
641
|
+
### ❌ Olvidar `compensation:` en activities dentro de sagas
|
|
642
|
+
|
|
643
|
+
```yaml
|
|
644
|
+
# ❌ NO HACER — sin compensación, la saga no puede deshacer
|
|
645
|
+
steps:
|
|
646
|
+
- activity: ReserveStock # ¿qué pasa si el pago falla?
|
|
647
|
+
|
|
648
|
+
# ✅ SÍ HACER
|
|
649
|
+
steps:
|
|
650
|
+
- activity: ReserveStock
|
|
651
|
+
compensation: ReleaseStock # deshace si falla un paso posterior
|
|
652
|
+
```
|
|
653
|
+
|
|
654
|
+
---
|
|
655
|
+
|
|
656
|
+
## 📊 Matriz de Decisión: ¿Es un Workflow o un Domain Event Interno?
|
|
657
|
+
|
|
658
|
+
| Pregunta | SÍ → | NO → |
|
|
659
|
+
|----------|------|------|
|
|
660
|
+
| ¿Algo DEBE ocurrir en otro módulo? | Workflow cross-module (system.yaml) | ↓ siguiente pregunta |
|
|
661
|
+
| ¿Es un proceso interno con durabilidad (retry, timeout, scheduling)? | Workflow single-module (domain.yaml) | Domain Event interno |
|
|
662
|
+
| ¿Son múltiples pasos coordinados cross-module? | Saga (workflow) | Activity puntual |
|
|
663
|
+
| ¿Necesita consistencia (compensación)? | Saga con compensación | Activity async |
|
|
664
|
+
| ¿Solo otro módulo necesita enterarse de datos nuevos? | **NI workflow NI notifies** — lectura on-demand | — |
|
|
665
|
+
|
|
666
|
+
---
|
|
667
|
+
|
|
668
|
+
## 📊 Matriz de Decisión: ¿Qué Tipo de Activity?
|
|
669
|
+
|
|
670
|
+
| Pregunta | Tipo |
|
|
671
|
+
|----------|------|
|
|
672
|
+
| ¿Solo lee datos de este módulo? | **Lectura** (`GetXById`, `GetXsByIds`) |
|
|
673
|
+
| ¿Modifica datos y puede deshacerse? | **Escritura** + `compensation:` |
|
|
674
|
+
| ¿Modifica datos irreversiblemente? | **Escritura** sin compensación |
|
|
675
|
+
| ¿Es el reverso de otra activity? | **Compensación** (referenciada en `compensation:`) |
|
|
676
|
+
| ¿Es un efecto secundario no-crítico? | **Reactor** (invocado como `type: async`) |
|
|
677
|
+
| ¿Es una operación interna del módulo? | **Local** (invocada por workflows del propio `{domain}.yaml` o por un workflow cross-module que corre en el queue del módulo) |
|
|
678
|
+
|
|
679
|
+
---
|
|
680
|
+
|
|
681
|
+
## 🔧 Convenciones de Naming
|
|
682
|
+
|
|
683
|
+
### Task Queues (Module-Prefixed)
|
|
684
|
+
|
|
685
|
+
```
|
|
686
|
+
{MODULE_SCREAMING_SNAKE}_WORKFLOW_QUEUE → ORDER_WORKFLOW_QUEUE
|
|
687
|
+
{MODULE_SCREAMING_SNAKE}_LIGHT_TASK_QUEUE → ORDER_LIGHT_TASK_QUEUE
|
|
688
|
+
{MODULE_SCREAMING_SNAKE}_HEAVY_TASK_QUEUE → PAYMENT_HEAVY_TASK_QUEUE
|
|
689
|
+
```
|
|
690
|
+
|
|
691
|
+
### Activities
|
|
692
|
+
|
|
693
|
+
| Tipo | Patrón | Ejemplo |
|
|
694
|
+
|------|--------|---------|
|
|
695
|
+
| Lectura singular | `Get{Entity}ById` | `GetCustomerById` |
|
|
696
|
+
| Lectura batch | `Get{Entities}ByIds` | `GetProductsByIds` |
|
|
697
|
+
| Escritura | `{Verbo}{Sustantivo}` | `ReserveStock`, `ProcessOrderPayment` |
|
|
698
|
+
| Compensación | `{Verbo inverso}{Sustantivo}` | `ReleaseStock`, `RefundPayment` |
|
|
699
|
+
| Reactor | `Notify{Evento}` | `NotifyOrderPlaced`, `NotifyOrderCancelled` |
|
|
700
|
+
| Local | `{Verbo}{Sustantivo}` (mismo patrón que escritura) | `ConfirmOrder`, `RetryCharge` |
|
|
701
|
+
|
|
702
|
+
### Workflows
|
|
703
|
+
|
|
704
|
+
```
|
|
705
|
+
{Verbo}{Entidad}Workflow → PlaceOrderWorkflow
|
|
706
|
+
{Entidad}{Evento}Workflow → ProductCreatedWorkflow
|
|
707
|
+
{Verbo}{Entidad}Workflow → CancelOrderWorkflow
|
|
708
|
+
```
|
|
709
|
+
|
|
710
|
+
---
|
|
711
|
+
|
|
712
|
+
## 📦 Resumen: Distribución de Responsabilidades
|
|
713
|
+
|
|
714
|
+
```
|
|
715
|
+
system.yaml
|
|
716
|
+
├── system: → Metadata del proyecto
|
|
717
|
+
├── orchestration: → Configuración de Temporal (target, namespace)
|
|
718
|
+
├── modules: → Lista de módulos + endpoints REST
|
|
719
|
+
└── workflows: → Flujos cross-module (sagas, efectos de negocio)
|
|
720
|
+
¿QUIÉN orquesta QUÉ?
|
|
721
|
+
|
|
722
|
+
{domain}.yaml
|
|
723
|
+
├── aggregates: → Modelo de dominio (entities, VOs, enums, transitions)
|
|
724
|
+
├── events: → Domain Events (con o sin notifies: → workflow)
|
|
725
|
+
├── activities: → Capacidades del módulo (cross-module + internas)
|
|
726
|
+
├── workflows: → Flujos INTERNOS del módulo (single-module, no cross-module)
|
|
727
|
+
├── endpoints: → API REST del módulo
|
|
728
|
+
└── ports: → Solo servicios EXTERNOS no-Temporal (HTTP)
|
|
729
|
+
¿QUÉ sabe hacer este módulo?
|
|
730
|
+
```
|
|
731
|
+
|
|
732
|
+
**Principio fundamental:**
|
|
733
|
+
> `system.yaml` define **la coreografía** (quién habla con quién).
|
|
734
|
+
> `{domain}.yaml` define **las capacidades** (qué puede hacer cada uno).
|
|
735
|
+
> Los workflows cross-module **ensamblan** las capacidades en flujos de negocio.
|
|
736
|
+
> Los workflows single-module son **procesos internos** del bounded context.
|
|
737
|
+
|
|
738
|
+
---
|
|
739
|
+
|
|
740
|
+
**Última actualización:** 2026-04-07
|