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.
Why surfaces exist
Section titled “Why surfaces exist”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 (
/ordersoperations 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
crudsurface 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.
Containment is one-way
Section titled “Containment is one-way”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.
Dot-paths
Section titled “Dot-paths”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 types
Section titled “Surface types”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.
Fields, aspects, descriptions
Section titled “Fields, aspects, descriptions”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.
No stable IDs
Section titled “No stable IDs”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).
Empty bodies
Section titled “Empty bodies”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.
Summary
Section titled “Summary”- 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.
What’s next
Section titled “What’s next”Chapter 7: Processes → — how behavior is described, and where dependency arrows actually come from.