Series map
- The Gnome Village: processes as polite workers with private backpacks.
- Supervisors Are Managers: supervisors as calm managers who restart gnomes without drama.
- Gnomes, Domains, and Flows: why the boundaries matter.
- This post: how to carve domains so code and data stay together.
- Flows Keep Work Moving: data, message, process, and call flows. New today.
- Putting It Together: turning the checklist into a running system. New today.
- Next: process archetypes and flow choreography (coming soon).
Glossary: Resource owner = the process that owns mutable state for a domain object. Request owner = the process coordinating one user request end-to-end within a domain. Adapter = the boundary process that talks to an external system. Orchestration = the domain that coordinates multi-step work across other domains.

What “domain” means here
“Domain” already carries Domain-Driven Design baggage. I am using the same word, but a thinner slice. DDD talks about bounded contexts, aggregates, and ubiquitous language. Those tools are useful, but you do not need the full ceremony to keep code and state aligned. Here a domain is the smallest boundary where the data types, invariants, and transformations still belong together.
If you already speak DDD, translate this to a bounded context or even a single aggregate module. If you do not, the rule still works: put the structs and the logic that operate on them in the same OTP app or module, treat that unit as the owner, and force everything else to go through a public API or explicit message.
Carving domains on the BEAM
On the BEAM, domains map cleanly to modules and applications. A module defines the types and functions for one area. An OTP application groups related modules, supervision trees, and configuration. That gives you both the static boundary (code) and the runtime boundary (process tree).
Take a payments stack:
- The user domain owns user structs, validation rules, and identity checks.
- The order domain owns order records, pricing, and state transitions.
- The ledger domain owns balances, accounting entries, and append-only logs.
Keep the code and data for each domain in the same module or OTP app so you can see who owns what. Every process reads the scroll that belongs to the domain it needs, nothing more. When the ledger lives in its own app, you know which processes keep the balances, which module updates them, and where to add tracing or logging for auditors. It also prevents other teams from “borrowing” ledger state through ETS because that state simply does not live outside the boundary.
Each box in the diagram is one domain owning its data and behavior. Draw your own version for every system you build, then make sure the codebase mirrors it.
Spotting and fixing anti-patterns
The common failure is one module juggling multiple domains because “it was convenient.” You end up with user structs sitting next to payment calculations and helper processes that touch everything. The fix is mechanical:
- Draw boxes for each domain and list the types and transformations inside.
- Anything that does not fit cleanly in one box belongs in another.
- Move the code, give it a public API, and make callers use the new boundary.
That diagram also becomes your compliance evidence: it shows which module owns which data, who can mutate it, and how flows cross the boundary.
Contracts, not processes
Once domains are clean, the contracts become obvious. A process still runs whatever code you load and upgrade, but it calls into a domain with a stable API and types. Messages carry domain-typed payloads instead of anonymous maps, and you can trace mutations back to the module that owns them. Auditors do not care which PID touched the ledger; they care which domain exposes the write function.
A process is disposable; a domain API is durable. Optimize for the latter.
“Code and data live together” means domain modules control the surface area, while processes remain throwaway workers that call those modules. Keep that separation clear and the runtime can change shape without drifting away from the structure.
Evolution and rollouts
Domains change, but you can keep the blast radius small. Version schemas explicitly and run contract tests at every domain boundary. When an external API shifts, ship a blue/green adapter so you can flip traffic without downtime. For ledgers and other append-only stores, support dual-write plus read-switch migrations: write to both schemas, backfill, then flip readers once parity is proven. Small, well-defined domains make those rollouts boring.
Where to next
Now that the boundaries are clear, read Flows Keep Work Moving for the four flow checklists, then jump to Putting It Together to see the whole frame land in a payments example. If you need the origin story, start with Gnomes, Domains, and Flows.
Prev: Gnomes, Domains, and Flows | Next: Flows Keep Work Moving
