Skip to content

6. Surfaces

A module with twenty interfaces in a flat list is unreadable. A surface is a named grouping of interfaces (and nested surfaces) inside a module’s body. It’s organizational only — surfaces have no deployment semantics, no stable IDs, no presence outside the module they live in.

module Orders {
aspect team: "Commerce"
surface ordersResource {
"Order CRUD operations"
base: "/orders"
interface create
interface get
interface update
interface delete
surface products {
base: "/products"
interface add
interface remove
}
}
surface webhooks {
interface subscribe
interface unsubscribe
}
}

Two top-level surfaces on Orders: ordersResource (with its own nested products surface) and webhooks. Each groups its interfaces under a name; each can carry fields, aspects, and a description. Surface names are lowerCamelCase, like the interfaces they group.

Without surfaces, every interface lives directly in the module body. That works for small modules. For modules with a real public surface — an HTTP service exposing twenty endpoints, a domain service grouping operations by aggregate — the flat layout becomes noise.

A surface is an interface domain — the same domain-thinking that drives how you split files and nest modules, applied one level down to a module’s API surface. A userInteraction surface holds the user-interaction operations; a webhooks surface holds the webhook operations.

Surfaces give you a place to:

  • Group conceptually related interfaces (/orders operations together).
  • Attach shared fields like a base path that should apply to everything below.
  • Apply a type template (Chapter 16) so you can stamp a reusable crud surface that gets a fixed set of operations for free — the same standard shape applied across modules instead of re-declaring it each time.

They aren’t about deployment, ownership, or addressing. Those belong on modules.

The grouping test is the file test, scaled down: imagine writing each interface binding out. Bindings you’d happily keep in one file belong in the same surface; bindings you’d rather split into separate files belong in different surfaces. And an interface that doesn’t fit any group — unrelated to the rest — shouldn’t be forced into one. Leave it directly on the module, or give it its own surface. A standalone operation lives fine on the module body; reach for a surface when a real group exists.

Rule. Modules contain surfaces; surfaces do not contain modules.

A surface can contain other surfaces and interfaces. It cannot contain a module. If you want a sub-module inside a service, declare it as a nested module (Chapter 4), not as a surface:

module Orders {
surface api {
interface create // ✅ interfaces inside a surface
}
module Worker { // ✅ nested module — NOT inside the surface
interface runJob
}
// ❌ This would be a parse error:
// surface Bad {
// module Inside { ... }
// }
}

The reason is identity. Modules carry stable IDs and represent architectural elements; surfaces are organizational. Allowing modules inside surfaces would muddle the two.

Process steps and cross-references resolve through surfaces via dot-paths:

process Buy {
Customer > Orders.ordersResource.create
Customer > Orders.ordersResource.products.add
Customer > Orders.webhooks.subscribe
}

The full path is Module.Surface.[Surface.]Interface. Surfaces are transparent to the resolver — they organize the source but don’t introduce a separate namespace you have to navigate around.

surface is the only surface type the stdlib defines. You can declare custom surface types when domain-specific vocabulary is preferred:

type surface resource {
"An HTTP resource — appends to a base path"
append base
}
module Orders {
resource ordersResource {
base: "/orders"
interface post
interface get
}
}

resource is now a user-defined surface type that behaves like surface plus an append base field. (Chapter 16 covers defining types; Chapter 18 covers append.)

Common conventions you’ll see in real projects:

  • resource — HTTP-resource style, appends paths.
  • capability — capability-style grouping, no path semantics.
  • endpoint_group — grouping of related interfaces under a shared aspect.

None of these ship with the stdlib; teams define them as needed.

Surface bodies carry the same field-and-aspect content as modules:

surface ordersResource {
"Order CRUD operations"
base: "/orders"
version: v2
aspect {
domain: "Orders"
}
interface create
interface get
}

Aspects declared on a surface cascade to its contained interfaces and nested surfaces — that’s how an aspect { domain: "Orders" } on the surface propagates to every operation inside without repetition. Chapter 18 covers cascade in full.

Surfaces, like interfaces, don’t carry stable IDs. Their identity is the dot-path inside the enclosing module. Rename detection uses structural heuristics, same as interfaces (Chapter 13).

A surface with nothing to add over its type’s template can omit the body:

module Orders {
resource ordersResource // surface using a 'resource' type, no instance additions
}

That declares an ordersResource surface whose contents come entirely from the resource type. We’ll meet types in Chapter 15.

  • A surface groups interfaces (and nested surfaces) inside a module body.
  • Surfaces are organizational: no stable IDs, no deployment semantics.
  • Containment is one-way — modules contain surfaces, never the reverse.
  • Process steps reach interfaces inside surfaces by dot-path (Module.Surface.Interface).
  • Custom surface types (resource, capability) let you align with domain vocabulary.

Chapter 7: Processes → — how behavior is described, and where dependency arrows actually come from.