Vertical slices
We recommend organizing an Arc application by feature, not by technical layer. A feature, “show the ticker” or “register an author”, is one behavior that spans the stack: a command or query on the server, the proxy the browser calls it with, and the page that shows it. Arc does not require this; it finds your commands and read models wherever they live. This page shows how to make the layout cohesive.
This is the same advice as Arc on .NET, and the layout is the same: open one folder and see the whole feature.
What a slice looks like
Section titled “What a slice looks like”The showcase sample’s ticker is a slice:
lib/samples/ticker/├── ticker.ex # the read model├── queries.ex # the observable query├── clock.ex # the process that makes it tick├── Ticker.ts # generated: the read model proxy├── Observe.ts # generated: the query proxy├── index.ts # generated: re-exports└── TickerPage.tsx # the React page, written by handThe page imports the proxy that sits beside it:
import { Observe } from '.';When the query’s parameters or the read model change, the proxy next to it changes on the next compile and the page stops compiling where it no longer fits.
Turn it on
Section titled “Turn it on”Generate the proxies beside the Elixir they come from with layout: :colocated. The output is then the root that holds both your Elixir and your React code:
compilers: Mix.compilers() ++ [:arc], arc: [application: Samples.Arc, output: "lib", layout: :colocated],Each proxy goes in the folder of the .ex file that defines its module, relative to output. Two modules in one file share a folder. A module whose source file is outside output (from a dependency, say) is generated where the default layout would put it: under output, in a folder that mirrors its namespace. Imports between proxies are relative paths, written for you.
mix arc.gen.proxies --layout colocated does the same from the command line.
Make the build see both
Section titled “Make the build see both”Elixir compiles .ex files only, so a .ts or .tsx file in lib is ignored by Mix and by Phoenix’s code reloader. The JavaScript tooling needs two things:
- Put
package.jsonandnode_modulesat the project root, abovelib. A file inlib/samples/tickerfinds@cratis/arcby looking upward from its own folder, sonode_modulesmust be an ancestor of every slice. - Point the TypeScript configuration and the bundler at both the app shell and
lib:
"include": ["frontend/src", "lib/**/*.ts", "lib/**/*.tsx", "vite.config.ts"]The app shell, the index.html, the providers and the page that picks which slice to show, can stay in its own folder (frontend in the showcase). It is the only code that knows every slice.
Commit the generated files
Section titled “Commit the generated files”A generated file starts with // @generated by Cratis. Source: <module>. Hash: <sha256>, and the output is deterministic, so committing it is safe. It lets a pull request show the contract change beside the Elixir change that caused it, and mix arc.gen.proxies --check in CI fails when a proxy is out of date. A file without the marker is never overwritten or deleted.
Kinds of slice
Section titled “Kinds of slice”Every slice does one of four jobs, as in an event model:
| Kind | Does | In Elixir |
|---|---|---|
| State change | Changes state | A command with handle/2 |
| State view | Serves state | An Arc.ReadModel with queries, observable when the data changes |
| Automation | Reacts and does side effects | A Chronicle reactor or a Watch source |
| Translation | Adapts one slice’s output into another’s command | A reactor that executes a command |
The nuance
Section titled “The nuance”Co-location is about a behavior, not about copying. What several slices genuinely share, a concept such as an author id or a read model that more than one feature reads, belongs in a shared folder that the slices depend on. A slice reaches another slice only through a command, a query or a shared contract, never by importing its internals.