5. Interfaces
An interface is a named operation a module exposes to other modules. It’s the only way one module can interact with another — processes (Chapter 7) reference interfaces, never modules directly.
module Payments { aspect team: "Payments"
interface authorize interface capture interface getTransaction interface paymentEvents}Four interfaces on Payments. Each one has a type (interface, the base type) and a name. Other modules can invoke Payments.authorize, query Payments.getTransaction, or consume Payments.paymentEvents.
This chapter uses the bare interface type. The standard library (Chapter 11) introduces interface subtypes like rest_create, rest_read, kafka, and grpc_server_stream that add semantic distinctions and edge styling.
The bare interface type
Section titled “The bare interface type”interface is the base interface type. It carries no sync/async distinction; it doesn’t drive edge styling. It just declares “this module exposes an operation named X.”
module Orders { interface createOrder interface getOrder interface orderEvents}The validator accepts the declaration; the diagram renderer draws an edge for each invocation a process makes against Orders.createOrder, etc. Whether the call is sync RPC, async event delivery, or a function call is opaque to the bare language. The stdlib interface types add that distinction.
Name an interface after its real protocol
Section titled “Name an interface after its real protocol”An interface models the actual operation a module exposes, so name it the way the real protocol names it. Don’t invent abstract names — reflect what the module truly offers.
- REST →
<action><Resource>:getOrder,createInvoice,listShipments. - RPC → the call verb:
chargeCard,reserveSeat,cancelOrder.
Rule. Interface names are
lowerCamelCaseand mirror the protocol — a REST action-plus-resource, or an RPC verb.
This keeps the model legible to anyone who has touched the real system: the name on the diagram is the name in the code, the OpenAPI spec, or the RPC definition. A vague handleStuff tells a reviewer nothing; getOrder tells them exactly which endpoint the edge represents.
Direction is unambiguous
Section titled “Direction is unambiguous”An interface is always declared by its provider. The provider is the module that runs it. A process step is always Caller > Callee.Interface — the right side names the interface (and therefore the provider); the left side names whoever called.
process Checkout { Customer > Payments.authorize // Customer calls Payments Payments > Ledger.record // Payments calls Ledger}You never declare “the interface that Customer uses to talk to Payments.” Interfaces exist on the provider. Callers reach them by qualified name (ModuleName.InterfaceName).
This rules out a common modeling mistake: drawing a “Customer→Payments” arrow without naming what’s being called. In ArchLang, you can’t. The interface has to exist on Payments first; only then can a process step invoke it.
Bodies
Section titled “Bodies”An interface body is optional. If you don’t need to add anything, omit it:
interface authorizeWhen you do need a body, the most common content is a description and fields:
interface authorize { "Authorize a payment hold. Returns a token used by capture." timeout: "5s" idempotent: true}Inside the body you can:
- Declare a description (a bare string).
- Set fields (
timeout: "5s",protocol: rest). - Add aspects.
You cannot put another interface inside an interface. Interfaces are leaves. If you want to group several related interfaces, use a surface (Chapter 6).
Events are async interfaces reached by a process edge
Section titled “Events are async interfaces reached by a process edge”There is no separate event or subscription construct in ArchLang. An event is just an async-kind interface — a kafka, amqp, webhook, or sse interface from the stdlib (Chapter 11) — reached by an ordinary > process edge. The edge is the subscription:
module Orders { aspect team: "Commerce" interface orderEvents // an event: an async interface (kafka in the stdlib)}
module Shipping { aspect team: "Fulfillment" interface createShipment}
process Fulfilment { Orders > Shipping.createShipment // Orders' behavior triggers Shipping's handler}The same arrow > carries sync RPC and async event delivery alike; which one it is depends on the interface type on the callee, not on a separate operator. There is no standing “subscription” declared on an interface — the dependency is drawn only where a process actually uses it. A latent subscription with no flow is the mirror of an unused outbound client: capability the model deliberately doesn’t draw.
Two modeling conventions are both valid; pick whichever keeps the arrow pointing the way the dependency really runs (the language stays out of it):
- Producer-owned event. The event interface lives on the producer (
Orders.orderEvents); a consumer edges to it:Shipping > Orders.orderEvents. Adding a consumer never touches the producer’s file — exactly how pub/sub behaves. - Consumer-owned handler. The handler lives on the consumer (
Shipping.createShipment); the producer’s flow calls it:Orders > Shipping.createShipment.
Either way, the connection is the process step, and Chapter 26 works a full event-driven pipeline end to end.
Cross-references
Section titled “Cross-references”Interface names are qualified by their module. From inside the same package you can reach any interface as Module.Interface (or Module.Surface.Interface if it’s inside a surface — see Chapter 6). Namespaced modules use the same dot-path: Personal.Banking.Payments.authorize.
In descriptions (Chapter 10) and in process steps you reach interfaces by qualified name. In the mature form the resolver expects a leaf interface. A step whose callee resolves to a module (X > Payments) is not an error: it synthesizes an anonymous interface on that module and tracks it as a TODO — a valid draft to be filled in later (Chapter 7). The merge gate keeps those out of a finished model.
No interface IDs
Section titled “No interface IDs”Modules carry stable IDs (#m4k29p); interfaces do not. Their identity is the dot-path inside the enclosing module (Payments.authorize). Renames are detected by structural and contract-shape heuristics at diff time — see Chapter 13 for the trade-off.
The practical consequence: don’t write #xyz prefixes on interfaces. The formatter won’t add them, and there’s no grammatical slot for one — the parser will reject it.
Summary
Section titled “Summary”- An interface is a named operation declared by its provider.
- The bare type is
interface. Stdlib subtypes (rest_create,rest_read,kafka,grpc_server_stream, …) add semantic distinctions — see Chapter 11. - Process direction is always
Caller > Callee.Interface; the interface lives on the callee. - Interfaces are leaves — they don’t contain other interfaces. Use surfaces to group them.
- An event is an async-kind interface reached by an ordinary
>process edge — there’s nosubscribes:or separate event construct. - No stable IDs on interfaces; identity is by dot-path.
What’s next
Section titled “What’s next”Chapter 6: Surfaces → — how to group interfaces inside a module’s surface.