Skip to content

Architecture ​

flowyd is organized into four strict layers. Imports flow in one direction only — downward. No layer may import from a layer above it.

The four layers ​

types/ ​

Pure TypeScript interfaces, enums, and discriminated unions. Contains zero logic and imports nothing from the rest of the codebase. Everything else derives its type contracts from this layer.

Key exports: IState, IForkState, IJoinState, IWaitState, IGuard, GuardContext, TransitionDefinition, InstanceSnapshot, DispatchResult, WorkflowDefinition, StateKind, StateStatus, JoinMode.

states/ and guards/ ​

Concrete implementations of IState and IGuard. Import only from types/. Neither layer is aware of the engine, the builder, or each other.

  • states/ — BaseState, StepState, ForkState, JoinState, WaitState
  • guards/ — FnGuard, InjectedGuard, AndGuard, OrGuard, NotGuard, StateCompletedGuard, StateActiveGuard, AlwaysGuard, NeverGuard, Guard (factory namespace)

core/ ​

The engine, builder, instance, and state/guard registries. Imports from types/, states/, and guards/. The main files (plus utils.ts helpers and an index.ts barrel):

FileResponsibility
builder.tsFluent WorkflowBuilder — accumulates the graph definition
workflow.tsImmutable Workflow factory — createInstance, restoreInstance, getDefinition
instance.tsMutable WorkflowInstance — holds snapshot, exposes dispatch, canExecute, etc.
engine.tsPure, stateless WorkflowEngine — computes the next snapshot from a current snapshot + action
registry.tsStateRegistry and GuardRegistry — lookup maps with explicit error messages

visualization/ ​

Stateless exporters. Import only from types/ (they need WorkflowDefinition and InstanceSnapshot shapes — nothing else). The core/ layer has zero knowledge that this layer exists.

This is enforced by keeping visualization as a separate package entry point (flowyd/visualization). Tree-shakers strip it when unused.

Why one-directional imports? ​

Bidirectional imports create invisible coupling — e.g. if core/engine.ts imported MermaidExporter, a visualization bug could corrupt the engine. Wanting to import "upward" signals a wrong abstraction boundary: extract the shared concept into types/ instead.

No exceptions — every cross-layer dependency goes through a types/ interface.

File map ​

src/
├── types/
│   ├── state.ts         ← StateKind, StateStatus, IState, IForkState, IJoinState, IWaitState, JoinMode
│   ├── guard.ts         ← IGuard, GuardFn, GuardContext
│   ├── transition.ts    ← TransitionDefinition
│   ├── instance.ts      ← ReadonlyInstanceState, InstanceSnapshot, DispatchResult, HistoryEntry, HookContext/HookFn/StateHooks
│   ├── workflow.ts      ← WorkflowDefinition, ActionPayloadMap
│   └── index.ts         ← barrel re-export
│
├── states/
│   ├── base.ts
│   ├── step-state.ts
│   ├── fork-state.ts
│   ├── join-state.ts
│   ├── wait-state.ts
│   └── index.ts
│
├── guards/
│   ├── constant-guards.ts ← AlwaysGuard, NeverGuard
│   ├── fn-guard.ts        ← FnGuard
│   ├── inject-guard.ts    ← InjectedGuard
│   ├── and-guard.ts
│   ├── or-guard.ts
│   ├── not-guard.ts
│   ├── state-guard.ts     ← StateCompletedGuard, StateActiveGuard
│   ├── factory.ts         ← Guard namespace
│   └── index.ts
│
├── core/
│   ├── builder.ts
│   ├── workflow.ts
│   ├── instance.ts
│   ├── engine.ts
│   ├── registry.ts
│   ├── utils.ts
│   └── index.ts
│
└── visualization/
    ├── exporter.ts
    ├── mermaid.ts
    ├── json-graph.ts
    └── index.ts

Released under the MIT License.