3.5 KiB
@n8n/engine — structure & modularity intent
The blueprint we're following
We structure this package after the Durable Scheduler modularity blueprint
(Notion,
worked example: packages/@n8n/scheduler, enforced by its
src/__tests__/dependency-purity.test.ts). The core idea: a pure core that
decides, with every effect handed in as a port. Dependency arrows point one
way — consumers depend on the engine; the engine core reaches for nothing.
Reality now: loose, but seam-aware
We are deliberately loose about composition at this stage. This one package currently holds the engine core and its serving/persistence infrastructure. We expect things to shuffle, so we're not paying the full ports-and-adapters tax yet. What we do commit to is keeping the internal boundaries clean, so serving infra and persistence can later be lifted into their own packages (e.g. a deployable engine worker) without touching core logic.
Layers (today, all in this package)
- Core (decides) —
graph/,execution/,admittance/. Pure orchestration/policy. Must not open connections, bind an HTTP server, or read the environment. Everything external arrives via constructor args / a deps bag (thecreateScheduler(deps)pattern). - Ports — interfaces the core depends on, each defined in its own core
module beside a default/reference use:
AdmittanceService(admittance/),WorkQueue(queue/),ExecutionStore(execution/). Adapters implement them; the core never imports the port from an adapter. Handed in at construction. - Adapters (do) — effectful implementations:
database/(TypeORM entities, migrations, the PostgresDataSource, andTypeOrmExecutionStore),queue/(in-memory default). The Postgres/ORM coupling lives here only. - Serving infra —
server/(express),serve.ts(standalone entrypoint), Dockerfile. First candidate to be extracted later. - Composition roots —
serve.ts(standalone) and, in integrated mode,packages/cli. These construct the concrete adapters (theDataSource, etc.) and hand them in. Construction lives here, not in the core.
Rules that keep the seams extractable
- Core modules (
graph,execution,admittance) don't importexpress,pg, or@n8n/typeorm, don't construct aDataSource, and never import fromdatabase/. Persistence, queue, and HTTP are injected. The dependency arrow is one-way:database/imports the port + domain types from the core, not the reverse. @n8n/typeorm/pgstay confined todatabase/.@n8n/config+@n8n/diare used only at the serving/composition layer (forEngineConfig), never in core logic. (The blueprint flags@n8n/configas debatable precisely because it pulls the DI runtime in — keep it out of core.)- We go one step stricter than the scheduler's allowlist: no
n8n-workflowdependency at all, not even type-only (per the Engine 2.0 design — the core must stay free of v1 concepts). Shared JSON types are redefined locally incommon/. - Arrows point inward:
cli/servedepend on the engine; the engine never importscli. - When serving infra is extracted, add a
dependency-purity.test.ts(as@n8n/schedulerdoes) to enforce the allowlist. Until then, this doc is the intent.
Known deviations — the seams to clean up on decomposition
DataSourceconstruction currently lives indatabase/; it is really a composition-root concern.