Skip to content
hekla

Project layout

How a directory becomes a program, which three directories are enforced, and why file order never matters.

A hekla project is a directory. Every .hk file under it is compiled together as one program, so a command can name an event declared three directories away without saying so.

There is no manifest, no import statement, no header item and no build step. A module is a file, not a namespace.

What discovery reads

Everything under the project root ending in .hk, except three things:

  • anything beginning with .
  • a directory named target
  • a directory named data at the project root only, because that is where the runtime keeps its own state

A data/ directory deeper in the tree is read normally, which matters if you happen to have a domain concept called that.

The three enforced directories

Only three directory names change what the runtime does, so only three are enforced.

Directory Holds Enforced
commands/ command declarations yes
commands/internal/ commands not exposed over HTTP a prefix of commands/
projectors/ projector declarations yes
effects/ effect declarations yes
events/ event declarations convention
tests/ test declarations convention
lib/ shared const, fn, refusal, guard convention

The test each one passes is whether putting a declaration somewhere else would change behaviour. A command outside commands/ would have no route, so that is an error naming the declaration. An event in lib/ behaves identically to one in events/, so that is your business.

commands/internal/ is a literal prefix. A command at commands/billing/internal/x.hk stays public: only the path beginning commands/internal/ marks one internal.

A typical project:

orders/
  hekla.toml            optional; defaults apply without it
  events/order.hk
  commands/place-order.hk
  commands/internal/record-confirmation.hk
  projectors/customer-orders.hk
  effects/notify-customer.hk
  lib/config.hk
  tests/place-order.hk
  data/                 created on first run, never yours to edit

Order does not matter

Declarations are read in six passes, so a command may name a guard declared below it, a projector may use an enum from another file, and a const may refer to another const written later. Sorting your files differently changes nothing, and neither does renaming them.

The one thing a file name never does is name anything. A route comes from the declared name:

command PlaceOrder(

That is served at POST /commands/PlaceOrder whatever the file is called.

Six name spaces

These six kinds each get their own name space, so a command and a projector may share a name without colliding:

command, projector, effect, fn, guard, refusal

Three of them are reachable by name from inside a program: invoke Name, guard Name, and reject Name.

Events are not in that list because an event is named by a path (@order.placed) rather than by an identifier, and paths are global.

Where configuration goes

hekla.toml sits at the project root and is optional. A missing file means every default applies. Every key is validated on load and an unknown one is an error rather than a warning, so a misspelling is caught rather than silently ignored.

Running a project lists every section and default.

Two tools, two rule sets

hek check checks the language. hekla check runs the same compiler and then adds what only the runtime knows: that a declaration sits in the directory its kind requires, and that a read model can be keyed and indexed the way the read API needs.

A program can pass the first and fail the second, so hekla check is the one to gate on.