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
dataat 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.