Guards vs Forks
Guards and forks both involve a boolean condition, but they live at different layers and have different effects. A guard stops a domain event; a fork routes a pipeline to a branch. Choosing the right one is mostly about which layer owns the decision.
Guards — blocking a domain event
Section titled “Guards — blocking a domain event”A guard lives on an event (OnDef.guard) and is evaluated before any side effects run. If it returns false, the event is blocked: no exit, no action, no pipeline, no phase change, no history entry. The domain stays exactly where it was.
import { defineDomain, when, on, terminal } from '@tde.io/plexis';
type Ctx = { balance: number; amount: number };
const account = defineDomain<Ctx>('account', () => { when('open', () => { on('withdraw', () => { // Guard: block the event entirely if there are insufficient funds guard(async (ctx) => ctx.balance >= ctx.amount); action(async (ctx) => ({ balance: ctx.balance - ctx.amount })); return target('open'); }); }); return { context: { balance: 100, amount: 0 }, initial: 'open' };});
const result = await account.follow('withdraw');// result.status === 'blocked' — guard returned false, nothing ranThe DomainFollowResult.status will be 'blocked' when a guard rejects the event.
Forks — routing pipeline flow
Section titled “Forks — routing pipeline flow”A fork lives on a pipeline node’s forks array. Unlike a guard, forks do not block execution — they route it. The pipeline always continues; the fork decides which direction it takes.
import { definePipeline, node, action, fork, target, terminal } from '@tde.io/plexis';
type Ctx = { amount: number; fundsSufficient: boolean };
const process = definePipeline<Ctx>('process', () => { node('check-funds', () => { action(async (ctx) => ({ fundsSufficient: ctx.amount <= 100 })); fork('charge', target('charge'), (ctx) => ctx.fundsSufficient); fork('decline', target('decline'), (ctx) => !ctx.fundsSufficient); }); node('charge', terminal()); node('decline', terminal()); return { initial: 'check-funds' };});
const result = await process.run({ amount: 150, fundsSufficient: false });// Pipeline always runs to a terminal — it routes to 'decline', not blocked// result.finalNode === 'decline'// result.status === 'completed'Choosing between them
Section titled “Choosing between them”| Situation | Use |
|---|---|
| Block a domain event before any side effects run | Guard |
| Route a pipeline to a success or failure branch | Fork |
| The decision is about “can this event happen at all?” | Guard |
| The decision is about “what should happen next in this workflow?” | Fork |
| The entity stays in the same phase when denied | Guard |
| The workflow always ends at some terminal | Fork |
Guards protect domain integrity. They are the right place for invariants that must hold before a phase change is allowed — balance checks, role checks, status pre-conditions.
Forks encode workflow logic. They are the right place for conditional routing that should be captured in the audit trail, traced, and part of the pipeline’s declared graph.
Can I use both?
Section titled “Can I use both?”Yes. A domain event can have a guard and a pipeline. If the guard passes, the pipeline runs as part of the event. Inside that pipeline, forks route the work.
import { defineDomain, definePipeline, when, on, target, guard, pipeline, enter, exit, node, action, fork, terminal } from '@tde.io/plexis';
type Ctx = { balance: number; amount: number; charged: boolean };
const charge = definePipeline<Ctx>('charge', () => { node('attempt', () => { action(async (ctx) => ({ charged: true })); fork('next', target('done')); }); node('done', terminal()); return { initial: 'attempt' };});
const account = defineDomain<Ctx>('account', () => { when('open', () => { on('withdraw', () => { guard(async (ctx) => ctx.balance >= ctx.amount); // blocks if insufficient pipeline(charge); // runs if guard passes action(async (ctx) => ({ balance: ctx.balance - ctx.amount })); return target('open'); }); }); return { context: { balance: 100, amount: 50, charged: false }, initial: 'open', };});For full type signatures, see OnDef in the Registration Helpers reference and PipelineForkDef in the Registration Helpers reference.