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 {
team: Payments
interface Authorize
interface Capture
interface GetTransaction
interface PaymentEvents
}

Four interfaces on Payments. Each one has a kind (interface, the base kind) and a name. Other modules can invoke Payments.Authorize, query Payments.GetTransaction, subscribe to Payments.PaymentEvents.

This chapter uses the bare interface kind. The standard library (Chapter 11) introduces interface subtypes like command, query, event, and stream that add semantic distinctions and edge styling.

The bare interface kind

interface is the base interface kind. It carries no sync/async distinction; it doesn’t drive edge styling or imply subscribe semantics. 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 kinds add that distinction.

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

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 labels.
  • Wire a subscribes: (covered below).

You cannot put another interface inside an interface. Interfaces are leaves. If you want to group several related interfaces, use a facet (Chapter 6).

Subscriptions

Event-driven handlers wire themselves to event sources via subscribes:. The handler interface declares what triggers it:

module Shipping {
team: Fulfillment
interface CreateShipment {
"Create a shipment when an order is confirmed"
subscribes: Orders.OrderEvents
}
}

CreateShipment runs whenever Orders.OrderEvents fires. The subscription is wiring on the handler, not on the event source. The source (Orders.OrderEvents) doesn’t know who listens; the listener declares the connection.

This matters for diagrams. The renderer can draw an “Orders → Shipping.CreateShipment via OrderEvents” arrow joining the published event with the subscribing handler, without polluting either declaration with the other’s name.

Subscriptions are wiring, not process flow. A process that says Orders > Shipping.CreateShipment is asserting the same end-to-end fact — but the subscription form lets the dependency live with the handler, where it logically belongs.

For the bare interface kind, subscribes: is accepted but its semantic isn’t constrained — the language treats every interface uniformly. The stdlib event kind adds the constraint that only events can be subscribed to (Chapter 11).

Cross-references

Interface names are qualified by their module. From inside the same package you can reach any interface as Module.Interface (or Module.Facet.Interface if it’s inside a facet — 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. The resolver checks that every reference points at an actual leaf interface, not at a facet or module. If you write process P { X > Payments } (a module, not an interface), the validator rejects it.

No interface IDs

Modules carry stable IDs (#pay001); 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

  • An interface is a named operation declared by its provider.
  • The bare kind is interface. Stdlib subtypes (command, query, event, 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 facets to group them.
  • subscribes: on a handler interface wires it to an event source on another module.
  • No stable IDs on interfaces; identity is by dot-path.

What’s next

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