Skip to content

16. Defining Kinds

You’ve been consuming kinds from the stdlib since Chapter 4. Now you write your own. This chapter introduces three: a custom module kind, a custom facet kind, and a custom interface kind. Each one shows a different shape of type body.

A custom module kind

Suppose your team has a notion of an internal service — a service that’s only callable from inside your VPC, owned by your team, and tagged security.zone: Internal. Every instance is a service with those three things baked in.

The naive way: copy-paste the three lines into every internal service. The better way: define a kind.

type module internal_service {
required cascade team
labels {
security.zone: Internal
}
}

Now an instance:

internal_service Inventory {
team: Commerce
"Tracks stock counts in real time."
command CheckAvailability
command Reserve
}

Inventory gets a security.zone: Internal label automatically. It still has to fill in team because the type marked it required — that’s the next chapter.

Note that internal_service is declared with module as its parent kind. That makes it a generic module subtype — you could also subtype service if you wanted the visual treatment of a service plus the constraints of internal_service:

type service internal_service {
required cascade team
labels { security.zone: Internal }
}

Now internal_service instances render with the service widget by default (inherited from the stdlib service type), in addition to having the labels and required team.

The shape of a type declaration

type <stable_id?> <parent_kind> <name> { <body> }
  • <stable_id?> — optional #xyz slot. Formatter mints on save.
  • <parent_kind> — the type this one extends. Any registered kind. module, facet, interface are the three base kinds; everything else is a user-defined subtype somewhere up the chain.
  • <name> — the name your instances will use.
  • <body> — defaults, blanks, sub-declarations, and modifiers.

There’s no extends keyword. The parent is encoded by position. type service paymentsService { ... } means “paymentsService extends service.”

A custom facet kind

A facet for “an HTTP resource”:

type facet resource {
"A facet representing an HTTP resource. Path composes via append."
append base
}

append base says: instances of resource carry a base field that composes with descendants’ values (a parent’s /orders and a child’s /items resolve to /orders/items). The append modifier is the subject of Chapter 18 — for now, take it as “the way path components stack.”

Used as:

service Orders {
resource OrdersResource {
base: "/orders"
command Post
query Get
resource Items {
base: "/items" // appends to /orders → /orders/items
query List
}
}
}

A custom interface kind

For a webhook interface kind that takes a default protocol: webhook field:

type interface webhook {
protocol: webhook
"An HTTP webhook callback delivered by an external system."
}

Used as:

service OrderWebhooks {
webhook OrderCreated
webhook OrderCancelled
}

Every webhook instance inherits protocol: webhook. The instance can override or drop it.

What a type body can contain

Every shape we’ve already met as a body member can appear in a type body, with the addition of required markers and the cascade / append modifiers on fields:

In a type bodyMeaning
field: valueDefault value for instances
required fieldMandatory blank — instance must fill
cascade field: valueDefault value that cascades to descendants
append fieldField that composes with descendants’ values
required cascade fieldMandatory blank that, once filled, cascades
labels { x: y }Default label values
required labels.xMandatory blank label
command X { ... }Pre-filled interface — instance inherits
required command XMandatory blank interface — instance must refine
component Y { ... }Pre-filled sub-module
required component YMandatory blank sub-module

The combinations of required, cascade, and append are the language for designing your metamodel.

Mistake to avoid. required component logs { command Send } is a contradiction. required means “no value”; the brace block means “here’s a value.” The validator rejects this. Either mark it required (blank, to be filled) or provide content (no required).

Where types go

Types live in .arch files. Convention: keep them in a file named kinds.arch at the package root for visibility.

acme.shop/
├── package.archspace
├── kinds.arch # type module internal_service { ... }
├── payments.arch # internal_service Payments { ... }
└── orders.arch # internal_service Orders { ... }

To make a type visible to other packages, mark it export:

export type module internal_service {
required cascade team
labels { security.zone: Internal }
}

Without export, the type is internal to its package. Importers use the use mechanism from Chapter 12.

What can’t be a parent kind

Two restrictions:

  • process and view can’t be parent kinds. Their bodies aren’t stampable templates (they’re sequences and projections respectively), so subtyping them has no useful semantics yet. Reserved for future iterations.
  • Reserved keywords can’t be kind names. You can’t type module process { ... } because process is a keyword. Appendix B: Keywords lists the full set.

A worked metamodel slice

Here’s a small set of related types defining a payments-domain vocabulary:

kinds.arch
// A service that operates in PCI scope.
export type service pci_service {
required cascade team
labels {
security.zone: PCI
}
required ext.runbook_url
}
// A service that processes external card transactions.
export type pci_service card_processor {
required ext.processor_vendor
required ext.api_docs_url
"A card processor — talks to an external vendor and is in PCI scope."
}
// An interface for a callback delivered by an external system.
export type interface webhook {
protocol: webhook
"An HTTP webhook callback."
}

Then in instances:

pci_service Authorize {
team: Payments
ext.runbook_url: "https://wiki/auth-runbook"
command Authorize
}
card_processor StripeIntegration {
team: Payments
ext.runbook_url: "https://wiki/stripe-runbook"
ext.processor_vendor: "Stripe"
ext.api_docs_url: "https://stripe.com/docs"
command Charge
webhook PaymentSucceeded
webhook PaymentFailed
}

The metamodel encodes domain knowledge: every PCI service has a runbook URL; every card processor names its vendor and API docs. The validator enforces these requirements at parse time. The vocabulary (pci_service, card_processor, webhook) reads naturally in source.

Summary

  • A type extends a parent kind by position: type <parent> <name>. No extends keyword.
  • Type bodies contain defaults, mandatory blanks, pre-filled sub-declarations, and propagation modifiers.
  • module, facet, interface are the three base kinds; everything else is a subtype.
  • process and view can’t be parent kinds (yet).
  • Export types with export type ... to make them visible to importing packages.
  • Convention: keep types in kinds.arch at the package root.

What’s next

Chapter 17: Required Blanks → — the mandatory-decision mechanism that makes types more than syntactic sugar.