Commands, Events & Reactions
Ironflow’s emit() and event-triggered functions implement the command-event-handler pattern from DDD. Events are the facts of your system — commands are the intentions that produce them.
The Mapping
Section titled “The Mapping”| DDD Concept | Ironflow Implementation |
|---|---|
| Command handler | Function triggered by emit() or API call |
| Domain event | Event emitted via emit() or appendToStream() |
| Event handler / Reactor | Function with triggers: [{ event: "..." }] |
| Integration event | Event published via publish() to a topic |
Command → Event → Reaction
Section titled “Command → Event → Reaction”In Ironflow, the command-event-reaction flow looks like this:
1. A command arrives — something emits an event that triggers a function:
// External system or API handler emits a command-like eventawait ironflow.emit("order.placed", { orderId: "order-123", customerId: "cust-456", items: [{ sku: "WIDGET-1", qty: 2 }], total: 59.98,});2. A function reacts — the event triggers a workflow that processes the command:
import { ironflow } from "@ironflow/node";import { NonRetryableError } from "@ironflow/core";
export const processOrder = ironflow.createFunction( { id: "process-order", triggers: [{ event: "order.placed" }], }, async ({ event, step }) => { // Validate the command const inventory = await step.run("check-inventory", async () => { return await inventoryService.check(event.data.items); });
if (!inventory.available) { throw new NonRetryableError("Items out of stock"); }
// Record the domain event in the aggregate await step.run("record-order", async () => { await ironflow.streams.append(`order-${event.data.orderId}`, { entityType: "order", name: "order.confirmed", data: { ...event.data, confirmedAt: new Date().toISOString() }, }); }); },);3. Other functions react — downstream functions can trigger on the entity event:
export const sendConfirmation = ironflow.createFunction( { id: "send-confirmation", triggers: [{ event: "order.confirmed" }], }, async ({ event, step }) => { await step.run("send-email", async () => { await emailService.send(event.data.customerId, "Order confirmed!"); }); },);Domain Events vs Integration Events
Section titled “Domain Events vs Integration Events”Ironflow distinguishes between two kinds of events, matching the DDD concept:
| Domain Events | Integration Events | |
|---|---|---|
| Ironflow API | emit() or appendToStream() | publish() |
| Trigger workflows? | Yes (via triggers) | No (by design) |
| Scope | Within a bounded context | Across contexts |
| Schema | Rich, context-specific | Lean, stable |
| Coupling | Tightly coupled to domain model | Loosely coupled |
Use emit() for domain events — events that represent facts within your domain and should trigger business logic:
// Domain event — triggers workflows, recorded as factawait ironflow.emit("order.placed", { orderId: "123", total: 59.98 });Use publish() for integration events — events meant for external consumers that shouldn’t trigger workflows:
// Integration event — notifies other contexts, no workflow trigger// Outside a workflow:await ironflow.publish("notifications.order", { orderId: "123", status: "confirmed",});
// Inside a workflow (durable, memoized):await step.publish("notifications.order", { orderId: "123", status: "confirmed",});Event Versioning
Section titled “Event Versioning”Domain events evolve over time as your model matures. Ironflow supports schema evolution through upcasters — functions that transform old event versions to new ones at read time. This means consumers always see the latest schema, even when reading historical events.
For details on defining upcasters and managing event versions, see the Event Versioning guide.
Commands and Events in Ironflow
Section titled “Commands and Events in Ironflow”Ironflow uses emit() for all messages — the infrastructure doesn’t distinguish between commands and events. The distinction is in naming convention and how you use them:
- Commands use imperative, dot-separated form:
create.order,fulfill.order— they express intent and trigger a handler that may accept or reject them - Events use past tense:
order.placed,order.shipped— they record facts in entity streams and trigger downstream reactions - Both flow through the same
emit()→ function trigger pipeline
This mirrors how messaging systems work in general: NATS doesn’t care if a message is a command or an event — your code gives it meaning through naming and handling patterns.
Design Tradeoff: Unified Message Pipeline
Section titled “Design Tradeoff: Unified Message Pipeline”Pure DDD implementations often use separate buses for commands and events — a command bus with validation/rejection semantics, and an event bus for immutable facts. Ironflow intentionally unifies them for pragmatic reasons:
| Aspect | Separate Buses (Pure DDD) | Unified emit() (Ironflow) |
|---|---|---|
| Infrastructure | Two message types, two handlers | One message type, one handler |
| Type safety | Commands/events are distinct types | Distinguished by naming convention |
| Rejection semantics | Built into command bus | Handler throws error to “reject” |
| Complexity | Higher — more moving parts | Lower — simpler mental model |
| Flexibility | Enforced at type level | Enforced by convention |
The tradeoff favors simplicity and flexibility. You still get the benefits of command/event separation — just enforced through naming conventions and handler logic rather than infrastructure. If a “command” fails validation, the handler throws an error and no event is recorded. If it succeeds, the handler records the domain event in the aggregate.
For a working example that demonstrates this distinction, see the DDD Order Management example.
Key Takeaways
Section titled “Key Takeaways”emit()= domain events — facts within your context that trigger business workflows.publish()= integration events — lean notifications for cross-context communication.- Functions are event handlers —
triggersdefine which events a function reacts to. - Name events in past tense —
order.placed, notplaceOrder. Events are facts. Commands use imperative form:create.order.
For event types, namespaces, and pattern matching, see the Event Types guide. For topic-based publishing, see the Topics guide.