4. Modules
A module is an architectural unit with a single accountable maintainer, a clear boundary, and a public surface composed of interfaces. It is the primitive everything else attaches to.
module Payments { aspect team: "Payments" "Authorizes and captures card payments."
interface authorize interface capture interface refund}That’s a module. The type is module; the name Payments is UpperCamelCase; the aspect team says who owns it; the description and the three lowerCamelCase interfaces flesh out what it does.
This chapter and the next few use only the language’s bare types — module, surface, interface. The standard library adds richer types like service, database, command, event (see Chapter 11). Everything in this chapter applies to those too; they are subtypes of module.
The rule
Section titled “The rule”Rule. If it has an owner and a boundary, it’s a module.
That’s the entire test. Services are modules. Databases are modules. External systems are modules. People and external clients (actors) are modules. Subsystems containing other modules are modules. A “library” sitting inside one team’s repo, with its own contract surface, is a module.
If you find yourself asking “is this thing a module or something else?” — and it has an owner and a boundary — it’s a module.
How deep to go
Section titled “How deep to go”You can nest modules forever — a module per class if you wanted. You shouldn’t. Code is fluid, so a deeply-modeled architecture goes stale fast, and every level of detail is something you have to come back and update.
Rule. Match modeling depth to the scale of the system, and no deeper.
A microservice is a perfectly good root-level module — often it’s better to describe it well than to split it into trivial submodules. A monolith earns several nested modules; a service-oriented system, a few per service. The recommended floor is one module per lowest-level domain unit (a feature) — stop there. Module-per-class maps programming structure onto architecture, and that’s the wrong default: architecture is not programming. When a submodule would be too granular to earn its keep, capture the detail in a description instead.
The bare module type
Section titled “The bare module type”The base type is module. It carries no required fields, no default interfaces, and no special widget. It is the most generic possible module declaration:
module Orders { interface createOrder interface cancelOrder}That’s a complete module. Two interfaces, no team, no description, no aspects. The validator accepts it. The diagram renderer draws it as a labeled box.
Most architectures don’t use bare module directly. They use a stdlib type like service or a project-defined type like payment_service, both of which are subtypes of module that add team requirements, widgets, and conventions. But the bare form is always available and is what every richer type reduces to.
Nesting
Section titled “Nesting”Modules can contain other modules. Use either form:
Nested directly:
module Platform { aspect team: "Platform Engineering"
module AuthService { interface authenticate }
module UserService { interface getUser }}Flat with in:
module Platform { aspect team: "Platform Engineering"}
in Platform module AuthService { interface authenticate}
in Platform module UserService { interface getUser}Both produce identical structure. The in form lets you keep a parent’s declaration in one file and let teams add child modules from their own files without all editing the same place — that distributed authorship is the point.
The viewer renders nested modules as containers. A module Platform becomes a frame; AuthService and UserService become nodes inside it.
Nesting means domain nesting
Section titled “Nesting means domain nesting”Putting B inside A is a claim: B belongs to A’s domain — it’s a sub-part of that thing, not a peer it talks to. So nest by domain, not by who-calls-whom. AuthService lives inside Platform because it’s part of the platform domain, not because something calls it.
This is what makes single-owner resources easy. A datastore used by exactly one module is a technical detail of that module’s domain, so it nests inside:
module Orders { aspect team: "Commerce" interface createOrder
module OrderStore { // Orders' own datastore — encapsulated interface read interface write }}Rule. A resource owned by exactly one module nests inside it. A resource shared by several modules is a peer — declare it as a sibling at the same level.
Don’t draw the textbook picture of a row of services each wired to its own database box. A single-owner database is encapsulated by its owner; only a shared database is a sibling node. (Where the datastore is hosted — the DBMS, the server — is a different plane again, attached by an aspect, not by nesting; Chapter 9 covers that.)
Ownership follows the same nesting. A team aspect set on a parent propagates to its children unless a child overrides it, so you mark ownership only where it actually changes — Orders carries aspect team: "Commerce", and OrderStore inherits it.
Unnamed modules: name from type
Section titled “Unnamed modules: name from type”When a nested module’s type already says everything its name would, you can leave the name off. A custom-typed nested module written with no name (and no ID) auto-takes its type’s name, converted from lower_snake_case to UpperCamelCase:
service SomeService { database // unnamed → a module named "Database" payment_processor // unnamed → a module named "PaymentProcessor" cache Redis // still nameable when you want a real name}This is the “no point naming the obvious” shortcut — especially for uses a library: write the library’s type, skip the name. (database, cache, payment_processor are custom/stdlib types here — Chapter 11 and Chapter 16 — not the bare module type.)
The rules are tight enough that the derived name is always unambiguous:
- Custom types only. The base
moduletype has no type name to borrow, so a baremodulemust always be named. - One unnamed per type, per parent. A type may be left unnamed only if it’s used exactly once in the parent. Two
database-typed children can’t both be unnamed — name both. But adatabasechild and acachechild may each be unnamed side by side, since their applied types differ (and a subtype counts as its own type). - Modules only. Interfaces and surfaces don’t take a name from their type.
- No ID until promoted. An unnamed module stays ID-less; naming it later (or the formatter promoting it) is when it earns a stable ID.
Reference an unnamed child by its derived name — that is its name: Orders > Database.write.
Rule. An unnamed-by-type module is not a
TODO. It is fully named and fully resolved — a naming shortcut, not draft debt. (Contrast an anonymous process or interface, which areTODOs — Chapter 7.)
Fields
Section titled “Fields”The body of a module is mostly fields. A field is key: value:
module Payments { aspect team: "Payments" repo.url: "https://github.com/acme/payments" version: v2 ext.cmdb.ci: "CI28304858" "Core payments service"}Field keys can be dotted (repo.url, ext.cmdb.ci). Values are identifiers (Payments, v2), quoted strings, numbers, or booleans. That’s the entire value language — no nested objects, no per-field types. Chapter 9 covers fields in depth.
The bare string "Core payments service" is a special field: the description. Module bodies may have multiple descriptions; they concatenate. Chapter 10 is dedicated to them.
Interfaces
Section titled “Interfaces”Interfaces are how modules expose themselves to other modules. Inside a module body, interface declarations sit alongside fields:
module Orders { aspect team: "Commerce"
interface createOrder interface cancelOrder interface getOrder interface orderEvents}The keyword interface is the base interface type. The stdlib defines subtypes like command, query, and event that carry semantic information (synchronous vs async, read vs write). For now, every interface is just interface. Chapter 5 covers interfaces in detail; Chapter 11 introduces the stdlib interface types.
Interfaces are always leaves: an interface does not contain another interface. To group them, use surfaces — Chapter 6.
Stable IDs
Section titled “Stable IDs”After you save a .arch file through the formatter, each module gains a stable ID:
module #m4k29p Payments { aspect team: "Payments"}The #m4k29p prefix anchors the module’s identity across renames. Rename Payments to PaymentsService and the ID stays the same — tools and diffs know it’s the same module. The ID is intentionally opaque: it encodes no name or meaning, so it never needs to change. Chapter 13 returns to this in depth. For now: don’t write IDs by hand; let the formatter mint them.
Empty bodies
Section titled “Empty bodies”If a module has nothing to add, the body can be omitted entirely:
module NotificationsThat’s valid. The module exists with no fields and no interfaces. Useful as a placeholder or when the module’s identity is the entire architectural fact.
Summary
Section titled “Summary”- A module is anything with an owner and a boundary.
- The bare type is
module. Every richer type (service, database, user, …) is a subtype. - Bodies contain fields, descriptions, interfaces, surfaces, and nested modules — in any order.
- Modules can nest directly or attach to a declared parent via
in Parent. - Stable IDs (
#xyz) anchor identity across renames; the formatter mints them on save.
What’s next
Section titled “What’s next”Chapter 5: Interfaces → — how modules expose themselves to each other.