Skip to content
@tde.io/plexis  ·  npm install @tde.io/plexis  ·  npm  ·  GitHub

The Two-Layer Model

Plexis models business logic as two distinct things: long-lived flows that outlast a single operation, and finite workflows that complete and are done. Keeping these separate is the central design decision the library is built around.

A Domain is a flow that persists. It holds the current phase, context, and event history for the lifetime of the object. Each call to domain.follow() moves it from one phase to another and returns the new context. The domain is still there after the event, ready to handle the next one.

import { defineDomain, when, on, target, terminal } from '@tde.io/plexis';
const order = defineDomain('order', () => {
when('pending', () => {
on('submit', target('processing'));
});
when('processing', () => {
on('fulfill', target('fulfilled'));
on('cancel', target('cancelled'));
});
when('fulfilled', terminal());
when('cancelled', terminal());
return { context: { orderId: 'ord_1' }, initial: 'pending' };
});
// Later — same domain instance, multiple events over time
await order.follow('submit'); // pending → processing
await order.follow('fulfill'); // processing → fulfilled

The domain retains its phase between operations. order.phase is 'fulfilled' after both calls, and order.history() records the full path.

A Pipeline is a workflow that executes start-to-finish in a single invocation. It begins at the initial node, evaluates fork conditions to decide which node comes next, runs node actions, and stops at a terminal node (or when no fork matches). After pipeline.run() returns, the pipeline itself is unchanged — you can call run() again with different context.

import { definePipeline, node, action, fork, target, terminal } from '@tde.io/plexis';
const validatePayment = definePipeline('validate-payment', () => {
node('check-card', () => {
action(async (ctx) => ({ checked: true }));
fork('valid', target('charge'), (ctx) => ctx.cardValid);
fork('invalid', target('decline'), (ctx) => !ctx.cardValid);
});
node('charge', terminal());
node('decline', terminal());
return { initial: 'check-card' };
});
const result = await validatePayment.run({ cardValid: true });
// result.finalNode === 'charge'
// result.status === 'completed'

Each run() is isolated. There is no “current phase” on the pipeline — context flows in, a result comes out.

QuestionDomainPipeline
Does this persist between operations?YesNo
Does it have phases an entity moves through over time?YesNo
Is it a single, finite execution path?NoYes
Can it be run again from scratch?No (restore needed)Yes, any time

Use a Domain when the entity exists across time and different events drive it forward — an order, a subscription, a user session.

Use a Pipeline when you need a defined execution path from start to finish — a payment flow, a validation sequence, a multi-step transformation.

Combine them when a domain event triggers a workflow: attach a Pipeline to an event or a phase entry. The Domain manages which phase the entity is in; the Pipeline handles the work of getting there.

import { defineDomain, definePipeline, when, on, pipeline, node, action, fork, target, terminal } from '@tde.io/plexis';
const chargeCard = definePipeline('charge-card', () => {
node('charge', () => {
action(async (ctx) => ({ charged: true }));
fork('next', target('done'));
});
node('done', terminal());
return { initial: 'charge' };
});
const checkout = defineDomain('checkout', () => {
when('cart', () => {
on('checkout', () => { pipeline(chargeCard); return target('processing'); });
});
when('processing', terminal());
return { context: {}, initial: 'cart' };
});

For signatures and full API details, see the Domain reference and Pipeline reference.