Skip to content

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.

DDD ConceptIronflow Implementation
Command handlerFunction triggered by emit() or API call
Domain eventEvent emitted via emit() or appendToStream()
Event handler / ReactorFunction with triggers: [{ event: "..." }]
Integration eventEvent published via publish() to a topic

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 event
await 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!");
});
},
);

Ironflow distinguishes between two kinds of events, matching the DDD concept:

Domain EventsIntegration Events
Ironflow APIemit() or appendToStream()publish()
Trigger workflows?Yes (via triggers)No (by design)
ScopeWithin a bounded contextAcross contexts
SchemaRich, context-specificLean, stable
CouplingTightly coupled to domain modelLoosely coupled

Use emit() for domain events — events that represent facts within your domain and should trigger business logic:

// Domain event — triggers workflows, recorded as fact
await 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",
});

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.

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.

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:

AspectSeparate Buses (Pure DDD)Unified emit() (Ironflow)
InfrastructureTwo message types, two handlersOne message type, one handler
Type safetyCommands/events are distinct typesDistinguished by naming convention
Rejection semanticsBuilt into command busHandler throws error to “reject”
ComplexityHigher — more moving partsLower — simpler mental model
FlexibilityEnforced at type levelEnforced 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.

  • emit() = domain events — facts within your context that trigger business workflows.
  • publish() = integration events — lean notifications for cross-context communication.
  • Functions are event handlerstriggers define which events a function reacts to.
  • Name events in past tenseorder.placed, not placeOrder. 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.