Skip to content

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.

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.

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 lowerCamelCase and 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.

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.

An interface body is optional. If you don’t need to add anything, omit it:

interface authorize

When 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.

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.

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.

  • 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 no subscribes: or separate event construct.
  • No stable IDs on interfaces; identity is by dot-path.

Chapter 6: Surfaces → — how to group interfaces inside a module’s surface.