Skip to content

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 {
team: Payments
"Authorizes and captures card payments."
interface Authorize
interface Capture
interface Refund
}

That’s a module. The kind is module; the name is Payments; the field team says who owns it; the description and the three interfaces flesh out what it does.

This chapter and the next few use only the language’s bare kindsmodule, facet, interface. The standard library adds richer kinds like service, database, command, event (see Chapter 11). Everything in this chapter applies to those too; they are subtypes of module.

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.

The bare module kind

The base kind 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 labels. 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 kind like service or a project-defined kind 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 kind reduces to.

Nesting

Modules can contain other modules. Use either form:

Nested directly:

module Platform {
team: "Platform Engineering"
module AuthService {
interface Authenticate
}
module UserService {
interface GetUser
}
}

Flat with in:

module Platform {
team: "Platform Engineering"
}
module AuthService in Platform {
interface Authenticate
}
module UserService in Platform {
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.

The viewer renders nested modules as containers. A module Platform becomes a frame; AuthService and UserService become nodes inside it.

Fields

The body of a module is mostly fields. A field is key: value:

module Payments {
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

Interfaces are how modules expose themselves to other modules. Inside a module body, interface declarations sit alongside fields:

module Orders {
team: Commerce
interface CreateOrder
interface CancelOrder
interface GetOrder
interface OrderEvents
}

The keyword interface is the base interface kind. 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 kinds.

Interfaces are always leaves: an interface does not contain another interface. To group them, use facets — Chapter 6.

Stable IDs

After you save a .arch file through the formatter, each module gains a stable ID:

module #pay001 Payments {
team: Payments
}

The #pay001 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. Chapter 13 returns to this in depth. For now: don’t write IDs by hand; let the formatter mint them.

Empty bodies

If a module has nothing to add, the body can be omitted entirely:

module Notifications

That’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

  • A module is anything with an owner and a boundary.
  • The bare kind is module. Every richer kind (service, database, actor, …) is a subtype.
  • Bodies contain fields, descriptions, interfaces, facets, 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

Chapter 5: Interfaces → — how modules expose themselves to each other.