# Project layout

A Bounda app is a tree of small modules. Each file is one concept and exports the functions that
concept needs; the folder it sits in and its name say what it is. `bounda generate` reads the tree
and writes the registry and the types, so there is no central file to keep in sync. Only `app/domain` and
`app/read` are read; anything else in `app/`, such as the routes of a web framework, is ignored.

- app/
  - domain/
    - order/ an aggregate
      - state.ts optional: initialState and aggregateId
      - order-placed.ts an event: payload, and begin or evolve
      - order-placed.upcast.ts optional: how older payloads become today's
      - order-paid.ts
      - inventory.ts a port: the interface of what the handlers call outside
      - money.ts any other module: a value object, a domain service
      - commands/
        - pay-order.ts a command: payload and handler
        - place-order.ts
      - policies/
        - issue-invoice-on-order-paid.ts reacts to OrderPaid
        - notify-on-order-placed.ts
      - processes/
        - order-payment/ a process
          - index.ts config and state
          - on-order-placed.ts handler for OrderPlaced
          - on-order-paid.ts handler for OrderPaid
          - at-next-reminder.ts handler for the deadline nextReminder
          - at-timeout.ts handler for the timeout
      - infrastructure/
        - inventory/ the implementations of the port inventory.ts
          - http.ts an implementation
          - fake.ts another; bounda.config.ts picks one
  - read/
    - order-summary/ a read model
      - view.ts fields
      - rates.ts a port, for its queries
      - projections/
        - order/ events of the order aggregate
          - order-placed.ts reacts to order's OrderPlaced
      - queries/
        - get-order.ts payload, repository, handler
      - infrastructure/
        - rates/ the implementations of the port rates.ts
          - ecb.ts
## Names

File and directory names are kebab-case: lower-case letters, digits and dashes. The generator turns
them into the names your code sees:

| File | Registry key | Type name |
| --- | --- | --- |
| `order-placed.ts` | `orderPlaced` | `OrderPlaced` |
| `place-order.ts` | `placeOrder` | `PlaceOrder` |
| `issue-invoice-on-order-paid.ts` | `issueInvoiceOnOrderPaid` | reacts to `OrderPaid` |
| `on-order-paid.ts` | handler for `orderPaid` | |
| `audit-log.ts` with `infrastructure/audit-log/` | port `auditLog` | `AuditLog`, exported by `audit-log.ts` |
| `infrastructure/audit-log/in-memory.ts` | implementation `"in-memory"`, as the config names it | |
| `policies/payment/refund-on-payment-failed.ts` | `paymentRefundOnPaymentFailed` | reacts to payment's `PaymentFailed` |
| `order-summary/` | `orderSummary` | `OrderSummaryRow` |

Files that start with `_` or `.`, tests (`*.test.ts`, `*.test-d.ts`), declarations (`*.d.ts`) and
`+types` directories are ignored.

An event belongs to its aggregate: `order`'s `Cancelled` and `subscription`'s `Cancelled` are two
events, and a handler that reacts to one never sees the other. A file refers to the events of the
aggregate it sits in; a folder named after another aggregate holds what reacts to that
aggregate's events. A read model has no aggregate of its own, so its projections always sit in one
of those folders.

## Aggregates: `app/domain/<aggregate>/`

A module at the root of the aggregate is an event when it exports `payload`, `begin` or `evolve`,
and nothing else at run time: those names are an event's alone. It is a port when
`infrastructure/` holds a directory named after it. Anything else there, files and directories
alike, is yours: a value object, a domain service, a helper the handlers import, which the
generator leaves alone. `state.ts` and `<event>.upcast.ts` are the only other names it reads.

A mistake that leaves the layout valid, such as an event that lost its `evolve` export or a
directory named `policy` instead of `policies`, gets a warning instead of a failure; the full list
is in [Conventions](/reference/conventions/#what-the-generator-warns-about).

```ts
// app/domain/order/order-placed.ts
import type { Event } from "./+types/order-placed";

export const payload = ({ z }: Event.PayloadArgs) =>
  z.object({ customerId: z.string(), total: z.number().positive() });

export const begin = ({ event }: Event.BeginArgs) => ({
  status: "placed" as const,
  customerId: event.payload.customerId,
  total: event.payload.total,
});
```

```ts
// app/domain/order/order-paid.ts
import type { Event } from "./+types/order-paid";

export const evolve = ({ event }: Event.EvolveArgs) => ({
  status: "paid" as const,
  paidAt: event.timestamp,
});
```

`payload` is optional; an event without one has an empty payload. Its `PayloadArgs` carry
`personal` and `person` besides `z`, for the fields that hold
[personal data](/guides/personal-data/). An event returns the state
fields it sets, merged over the state one level deep: a field returned as `undefined` keeps its
value, so clear one with `null`. `begin` opens the aggregate and gets the event alone, since
there is no state before it; `evolve` folds a later event and gets the state too. An event that can
do both exports both.

Once one of an aggregate's events exports `begin`, the aggregate starts with such an event. A
command that would start it with another event, or put an event that only exports `begin` on an
aggregate that exists, throws `CreationOrderError` and stores nothing: a bug in the handler, which
a reaction does not retry.
When a payload changes shape after events are stored, `<event>.upcast.ts` next to it brings the
old ones up to date as they are read: see [Changing an event's shape](/guides/changing-events/).

### State

Without `state.ts`, the generator infers the aggregate's state from what its events return, each
field typed as the union of what the events assign. With the two events above, a command handler
sees either an order that does not exist yet, every field `undefined`, or one where the fields
`begin` sets are always there: `status: "placed" | "paid"`, `customerId: string` and
`total: number`, with `paidAt?: string`. Any field `begin` sets tells the two apart:

```ts
if (state.status === undefined) return reject("NotPlaced");
state.total; // number
```

`evolve` only runs on an aggregate that exists, so its `state` is the created one. Without any
`begin`, every field is optional, since any event could come first.

Add `state.ts` when you want an initial value before the first event, or when a field's type is
not visible from outside its module:

```ts
// app/domain/order/state.ts
export const initialState = {
  status: "new" as "new" | "placed" | "paid",
  customerId: "",
  total: 0,
};
export const aggregateId = "orderId";
```

`aggregateId` names the payload field that identifies the aggregate. It defaults to
`<aggregate>Id`, so `orderId` for `order`.

### Ports: `<port>.ts`

A port is what the aggregate's handlers call outside the app: an inventory service, a mailer, a
payment provider. A read model has ports too, for its queries
([read models](#read-models-appreadread-model)). It is a module at the root of the aggregate that
exports its interface, named after the file in PascalCase (a type alias of that name works too),
and every handler of the aggregate receives it under that name: its commands, its policies and
every handler of its processes. Its implementations live in `infrastructure/<port>/`, one file
each, with a default export or with a `create` function.

```ts
// app/domain/order/inventory.ts
export interface AvailableArgs {
  readonly skus: readonly string[];
}

export interface Inventory {
  available(args: AvailableArgs): Promise<boolean>;
}
```

```ts
// app/domain/order/infrastructure/inventory/fake.ts
import type { Inventory } from "../../inventory.ts";

export default {
  available: async () => true,
} satisfies Inventory;
```

An implementation is plain TypeScript: it imports the port and fulfils it, and the `satisfies`
gives completion and an error in place. The generated registry checks every implementation against
the interface anyway, so one that does not fulfil it fails `tsc` either way. Each operation takes
one object, `XxxArgs`, as every handler of Bounda does: a field can be added without breaking a
call, and `idempotencyKey` travels as one more. A port with one operation can be callable instead,
`export interface Notifier { (args: NotifierArgs): Promise<void> }`, and the handler then calls
`notifier({ ... })`.

An implementation with state, such as a client, a connection pool or a secret, exports `create`
instead of a default, never both, typed as `CreateImplementation` of its port. The app calls it
once when it is created, and every handler receives what it returns; it may be async.

```ts
// app/domain/order/infrastructure/inventory/http.ts
import type { CreateImplementation } from "@bounda-dev/core";
import type { Inventory } from "../../inventory.ts";

export const create: CreateImplementation<Inventory> = ({ env, logger }) => {
  const url = env.INVENTORY_URL;
  if (url === undefined) throw new Error("INVENTORY_URL is not set");
  return {
    available: async ({ skus }) => {
      const response = await fetch(`${url}/available`, {
        method: "POST",
        body: JSON.stringify(skus),
      });
      logger.debug("inventory checked", { status: response.status });
      return response.ok;
    },
  };
};
```

`create` receives the host's environment as `env` (`process.env` after `.env` is loaded under
`boot()`, the Durable Object's `env` on Cloudflare, what a test passes to `createTestApp`), the
name of the store the app serves as `tenant` (the Durable Object's name on Cloudflare, what a test
passes, none under `boot()`: see
[an account per tenant](/guides/calling-the-outside-world/#an-account-per-tenant)), the app's
`logger` and its `clock`. It runs once per app: once per process under `boot()`, once per Durable
Object, once per `createTestApp`. When what it returns has `[Symbol.asyncDispose]`,
`app.stop()` calls it after closing the storage, the last one built first; one that fails to
close is logged and the rest still close. A default export is never closed, since every app in
the process shares the module.

Open no connection and read no environment at the top of an implementation module: the registry
imports every implementation, also those the configuration does not choose. Only the chosen one's
`create` runs. Keeping SDKs and clients under `infrastructure/` also keeps the rest of the
aggregate free of them, which a lint rule can hold by path.

`bounda.config.ts` picks one implementation per port, by aggregate or read model and port in camelCase, with
the implementation's file name as the value. The generator emits the type of that section, so a
name that does not exist does not compile, and a port with several implementations must be
named: no implementation is a default. A port with one may be left out.

```ts
export default defineConfig({
  storage: sqlite({ path: "./data/app.db" }),
  ports: {
    order: { inventory: process.env.INVENTORY === "fake" ? "fake" : "http" },
  },
});
```

The value has to be one of the names, so a choice made from the environment says both branches,
as above; a plain `process.env.INVENTORY ?? "http"` is a `string` and does not compile.

A port cannot be named after an event of the aggregate, nor after an argument a handler already
receives ([reserved names](/reference/conventions/#what-the-generator-refuses)); the generator says
which. Domain logic the handlers share but
that has no implementations to choose from is not a port: it is a module of the aggregate like any
other, which the handlers import. A provider two aggregates use is two ports, one in each, with the
contract each aggregate needs; the client they share lives outside `app/domain`, in
`app/lib/stripe.ts` for instance, and each implementation imports it.

The runtime chooses once, when the app is created, and a config that names a port, an
implementation, an aggregate or a read model that does not exist fails at boot as well.

Tests do not read this section: `createTestApp` takes its own `ports`, a double or a file
name per port, and gives a port it leaves out no implementation at all
([Testing](/guides/testing/#doubles)).

### Commands: `commands/`

A command is `commands/<name>.ts`.

```ts
// app/domain/order/commands/place-order.ts
import type { Command } from "./+types/place-order";

export const payload = ({ z }: Command.PayloadArgs) =>
  z.object({ orderId: z.uuid(), customerId: z.string(), skus: z.array(z.string()).min(1) });

export const rejections = ({ command }: Command.RejectionsArgs) => ({
  AlreadyPlaced: `Order ${command.aggregateId} was already placed`,
  OutOfStock: "Some of the items are out of stock",
});

export const handler = async ({ command, state, events, inventory, reject }: Command.HandlerArgs) => {
  if (state.status !== undefined) return reject("AlreadyPlaced");
  if (!(await inventory.available({ skus: command.payload.skus }))) return reject("OutOfStock");
  return [events.orderPlaced({ customerId: command.payload.customerId, skus: command.payload.skus })];
};
```

The handler receives the aggregate's ports next to `command`, `state` and `events`. In a
command they are what the handler reads from outside to decide: stock, prices, a feature flag.
Effects on the world (a charge, an email) belong in the policy or process that reacts to the
event, after it is stored. A handler can run more than once for one command and has a time
limit, so it also receives `idempotencyKey` and `signal` for those calls; see
[calling the outside world](/guides/calling-the-outside-world/#deciding-then-acting).

#### Rejections

A command that may say no declares how in `rejections`: each key is a code, named after the reason
(`NotOpen`, never as a fact like `OrderClosed`), and each value its message. The runtime calls it
when the handler rejects, with the `command` and the `state` the handler saw, so the message can
tell why. Only a module that exports `rejections` gets `reject`, and `reject` only takes its codes.

`reject(code)` returns the `DomainError` the caller gets, with the code in `rejected`;
`return reject(code)` and `throw reject(code)` do the same, the second from a helper the handler
calls. Pass a second argument to give that rejection another message. A `DomainError` is never
built with `new`. If `rejections` throws, or has no message for the code, the rejection still
stands, with the code as its message, and the runtime logs a warning.

A rejection stores nothing and is not retried. `app.commands` throws it to the caller; a policy or
a process gets it as a value instead: `commands.<name>()` resolves with `rejected` set to the code,
or `false` when the aggregate decided (see
[reacting to events](/guides/reacting-to-events/#what-a-command-answers)).

Repeating what is already done is not a rejection: return `[]`, so a retry, a webhook that arrives
twice or a reaction that runs again changes nothing. Before an aggregate exists `state.status` is
`undefined`, so a message reads it with a fallback, `state.status ?? "new"`. A "no" the business
remembers, or that someone else listens to, is an event instead: see
[rejection or event](/guides/sagas/#two-ways-to-fail-no-hook-for-either).

### Policies: `policies/`

A policy reacts to an event with commands. `<action>-on-<event>.ts` names the event: the longest
event the name ends with, so `put-on-hold-on-payment-failed.ts` reacts to `PaymentFailed`. A
policy whose name ends with no event exports `on` with the event type names it reacts to, and
`bounda generate` warns about one that has `-on-` but names no event, a misspelling most likely.

```ts
// app/domain/order/policies/issue-invoice-on-order-paid.ts
import type { Policy } from "./+types/issue-invoice-on-order-paid";

export const handler = async ({ event, commands }: Policy.HandlerArgs) => {
  await commands.issueInvoice({ orderId: event.aggregateId, method: event.payload.method });
};
```

A policy that reacts to another aggregate's event sits in a folder named after that aggregate:
`app/domain/order/policies/payment/cancel-order-on-payment-failed.ts` is an `order` policy that
reacts to `payment`'s `PaymentFailed`. Its key carries the aggregate, `paymentCancelOrderOnPaymentFailed`,
so it never collides with an `order` policy of the same name. The policies and processes of an
aggregate need distinct keys, since the key is what the runtime records a handled event under,
and a policy cannot be named after an aggregate
([what the generator refuses](/reference/conventions/#what-the-generator-refuses)).

Policies dispatch through `commands`, the typed facade of every command in the app. A command can
be delayed: `commands.sendReminder({ orderId }, { delay: "24h" })`. The call's type follows: one
with `delay` resolves with `scheduled: true` and `executeAt`, one without with what the command
decided (`eventTypes`, and `position` from `app.commands`), with no scheduled case to rule out
first; see [delaying a command](/guides/reacting-to-events/#delaying-a-command).

A policy receives the aggregate's ports next to `event` and `commands`: an `order` policy
gets `order`'s, also when it reacts to another aggregate's event.

```ts
// app/domain/order/policies/notify-on-order-placed.ts
import type { Policy } from "./+types/notify-on-order-placed";

export const handler = async ({ event, mailer, idempotencyKey }: Policy.HandlerArgs) => {
  await mailer.send({
    to: event.payload.customerId,
    text: `Order ${event.aggregateId} placed`,
    idempotencyKey,
  });
};
```

A policy that exports `delay` (`export const delay = "1m"`, or `asDuration(...)` for a value
from the environment) runs that long after the event instead of as soon as it is read; see
[delaying a policy](/guides/reacting-to-events/#delaying-a-policy).

A policy runs after the events it reacts to are stored, and at least once, with an inbox
([what the runtime promises](/guides/reacting-to-events/#what-the-runtime-promises)): a call it
makes must be safe to repeat.

### Processes: `processes/<name>/`

A process follows an instance of its aggregate over time. `index.ts` says which events start and
complete it and how long it may stay open; `on-<event>.ts` handles an event while it is open;
`at-<deadline>.ts` runs when a deadline of its state comes due, and `at-timeout.ts` when the time
is up. `config` receives every event of the app by aggregate, `events.order.OrderPlaced`, so a
process can start, continue or finish on another aggregate's events. A handler returns only the
fields of the state that change, or nothing.

```ts
// app/domain/order/processes/order-payment/index.ts
import type { Process } from "./+types/index";

export const config = ({ events }: Process.ConfigArgs) => ({
  startedBy: [events.order.OrderPlaced],
  completedBy: [events.order.OrderPaid, events.order.OrderCancelled],
  timeout: "48h",
});

export const state = ({ z, deadline, instant }: Process.StateArgs) =>
  z.object({ reminders: z.int().default(0), nextReminder: deadline(), paidAt: instant() });
```

A field of the state declared with `deadline()` is a deadline, and `at-<field>.ts` is its handler:
`nextReminder` runs `at-next-reminder.ts`, which receives `Process.DeadlineArgs`. `instant()`
declares a moment the process only records, and runs nothing. The name `timeout` is kept for
`config.timeout`, whose handler is `at-timeout.ts`, and the two must come in pairs
([what boot refuses](/reference/conventions/#what-boot-refuses)). How a handler sets, moves and cancels a
deadline, and when each runs, is in [deadlines](/guides/reacting-to-events/#deadlines).

```ts
// app/domain/order/processes/order-payment/at-next-reminder.ts
import type { Process } from "./+types/at-next-reminder";

export const handler = async ({ state, aggregateId, commands, after }: Process.DeadlineArgs) => {
  await commands.sendReminder({ orderId: aggregateId });
  return {
    reminders: state.reminders + 1,
    nextReminder: state.reminders < 2 ? after("24h") : null,
  };
};
```

A handler for another aggregate's event sits in a folder named after that aggregate,
`processes/order-payment/payment/on-payment-failed.ts`. Such an event carries that aggregate's id,
not the order's. When its payload declares the order's id field, `orderId` (or the `aggregateId`
of the order's `state.ts`), it belongs to the instance that field names, and to none when it is
`null`: nothing else to write. Boot reads the field from a plain `z.object`; behind a
`.transform()` what is stored cannot be read, so such an event needs `correlate`. Otherwise `index.ts` says which instance it belongs to in
`correlate`, which gets `from` and returns `from.<aggregate>.<Event>(…)` for each such event: a
function from the event to the id of the process's own aggregate, or `null` to ignore it. It
also overrides the id field. Boot refuses an event of another aggregate the process listens to
with neither.

```ts
export const correlate = ({ from }: Process.CorrelateArgs) => [
  from.shipping.ParcelLost((event) => event.payload.reference),
];
```

What happens to an event no open instance takes, and how the state a handler returns is checked,
is in [which instance an event reaches](/guides/reacting-to-events/#which-instance-an-event-reaches).

Every handler of the process, the `at-` ones included, receives the aggregate's ports,
as its commands and policies do.

## Read models: `app/read/<read-model>/`

A read model is `view.ts` (its table), `projections/<aggregate>/<event>.ts` (what fills it) and
`queries/<name>.ts` (what reads it), with ports of its own for its queries.
[Read models](/guides/read-models/) shows each one.

## Generated files

`bounda generate` writes `.bounda/` and a `+types/` directory next to each module. They are
derived from your code: keep them out of git, include `.bounda/**/*` in `tsconfig.json` and run
the generator before anything type-checks. [Conventions](/reference/conventions/#generated-files)
says what each file holds and how to set it up; `create-bounda` does it for you.