Writing a design

Every field of the design document — the envelope, requirements, groups, nodes, edges, and what makes a document structurally valid.

This is the complete reference for the design document. The JSON Schemas in schema/ are its machine-readable form; where the two disagree, one of them is a bug and neither is changed silently.

Conventions

These hold in every Zeema document, not just designs.

  • Quote every string value, including enum values and schemaVersion. Unquoted NO, yes, on and 1.0 parse as booleans or numbers in YAML, and will not mean what you typed.
  • Property names are camelCase. Ids and enum values are kebab-case.
  • No display text in any document except locales/. Names are looked up by id.
  • The first line points at the schema, so an editor validates the file with no other setup.

The envelope

Every design starts with the same three fields.

yaml
# yaml-language-server: $schema=https://zeema.dev/schema/design.schema.json
schemaVersion: "1"
type: "design"
id: "order-processing"
FieldRequiredMeaning
schemaVersionyesAlways the quoted string "1"
typeyes"design"
idyesMatches ^[a-z][a-z0-9-]*$ and is unique among designs

Then, in this order and all optional except nodes and edges: requirements, groups, nodes, edges.

requirements

What the whole design promises about itself. Rules compare nodes against these. Every path is optional, and an absent requirement never equals anything — a design that promises nothing is never judged against a promise it did not make.

yaml
requirements:
  availability: { multiRegion: true, sla: "99.95" }
  messaging:    { replay: true }
  latency:      { p95Ms: 300 }
PathTypeMeaning
availability.multiRegionbooleanMust survive the loss of one region
availability.slastringAvailability target, e.g. "99.95". Quoted
availability.zonesintegerZones every stateful node is expected to span
availability.regionsintegerRegions the system runs in
availability.discoveryRedundantbooleanDNS and coordination must have no single instance
availability.degradedModeAllowedbooleanFallback paths are acceptable
messaging.replaybooleanConsumers must be able to re-read past messages
messaging.exactlyOncebooleanMessage delivery must be exactly-once
messaging.orderingScopepartition, globalOrdering guarantee consumers rely on
latency.p95Msintegerp95 target for the primary request path
latency.p99Usintegerp99 target in microseconds (exchanges, matching)
latency.geoDistributedbooleanUsers on several continents with a latency target
durability.annualNinesintegerDesigned durability of stored objects, e.g. 11
durability.minReplicasAckedintegerReplicas that must acknowledge a write
consistency.modelap, cpWhich side of the partition trade the design takes
consistency.readAfterWritebooleanA write must be visible to the next read
consistency.quorumScoperegion, globalWhere quorums are formed
correctness.exactlyOnceEffectbooleanSide effects (charges, sends) must happen once
correctness.reproduciblebooleanAny past state must be rebuildable from a log
correctness.tamperEvidentbooleanUnauthorised change must be detectable
correctness.billingGradebooleanCounts are billed on
money.zeroSumbooleanEvery debit has a matching credit
money.pcibooleanCard data is in scope
data.reconciliationbooleanA batch job compares derived data to the source
data.lossDetectionbooleanPipelines must detect dropped records
data.ownershipExclusivebooleanOne service owns each datastore
data.immutableAuditbooleanAn append-only audit log is required
traffic.readWriteRatiointegerReads per write on the main path
traffic.skewuniform, heavy-tailWhether a few keys carry most traffic
traffic.peakMultiplierintegerPeak over average
isolation.perQueuebooleanEach queue needs its own worker pool
isolation.perEventTypebooleanEvent types must not share a pipeline
isolation.tenancyshared, cell, dedicatedBlast-radius model
freshness.maxSecondsintegerOldest acceptable derived data
security.certificateRotationAutomatedbooleanInternal certificates rotate without humans
security.credentialRotationDaysintegerDatastore credentials rotate at least this often
security.edgeAuthDeclaredbooleanEvery edge into data, secrets, queues, streams or an AI service states its auth
observability.alertingIndependentbooleanAlerting must not depend on the systems it watches

groups

Nested boundaries. A node belongs to at most one group; a group may have one parent.

yaml
groups:
  - { id: "eu-west-1", kind: "region" }
  - { id: "prod-vpc", kind: "vpc", parent: "eu-west-1" }
  - { id: "dmz", kind: "trust-zone", parent: "prod-vpc" }
FieldRequiredMeaning
idyesUnique within the design
kindyesOne of the group kinds below
parentnoThe id of another group in this design
labelnoThe group's name as drawn in the source. Read by renderers, never by rules
countnoAn integer of 2 or more: the group stands for that many identical copies of everything inside it, drawn as a stack with a ×N chip. Never read by rules
Group kindWhat it is
regionA cloud region or a data centre
availability-zoneA failure domain inside a region
vpcA private network
trust-zoneA security boundary
systemA logical system or service boundary
cellA failure-isolated slice serving a subset of tenants
point-of-presenceAn edge site
namespaceA logical partition inside a cluster or platform (a Kubernetes namespace, an OpenShift project). It scopes names and policy, not the network or failures

Groups carry no properties in v1 beyond label and count. They exist for rules — inGroup, crossesGroup — and for renderers, which draw them as nested boxes.

nodes

yaml
nodes:
  - { id: "api", component: "aws-api-gateway" }
  - id: "orders-queue"
    component: "sqs"
    label: "Orders queue"
    group: "prod-vpc"
    props: { deadLetterQueue: true, retentionDays: 14 }
    nfr:   { rps: 500, latencyP95Ms: 40, consistency: "strong" }
FieldRequiredMeaning
idyesUnique within the design
componentyesThe id of a component in the catalogue
labelnoDisplay name from the source, kept verbatim by importers. Never read by rules
groupnoThe id of a group in this design
propsnoProperty values; see the resolution order below
nfrnorps (integer), latencyP95Ms (integer), consistency (strong or eventual)
refnoAnother public design this node stands for: { design: "owner/slug", version }, where version is a release number (1 or more) or "head". On a zeema-design node (kind subsystem) it is what the box is; on any other node, an annotation. Rules never read it
modulesnoThe modules of the code the node runs, each { id, label? }, ids unique within the node. Only on a node whose kind hosts code: app-server, worker, client

Modules inside a node

A service deployed as one unit can still say what it is made of. List its modules, and point an edge at one as <nodeId>/<moduleId>:

yaml
nodes:
  - id: backend
    component: container-service
    modules:
      - { id: api, label: "API" }
      - { id: otp, label: "OTP" }
edges:
  - { from: backend/otp, to: sms, mode: sync, protocol: https }
  - { from: backend/api, to: backend/otp }

Rules read a module end as its node, so backend/otp calling sms counts exactly as backend calling it. An edge with both ends inside one node is in-process: it is not a network hop, takes no mode or protocol, and rules ignore it. The box is drawn collapsed with an "N modules" chip.

Property resolution order

The effective value of a property on a node is the first of these that exists:

  1. The node's props
  2. The component's defaults
  3. The property definition's default — on the kind, or on the component for a component-specific property
  4. Absent

A node may only set properties that exist on its component's kind or on the component itself. Anything else is a structural error, not a warning.

edges

Directed. from and to are node ids in this design, or a group id for an edge to the group as a unit, drawn once to its frame. Rules read a group end as every node inside the group, nested groups included, as if the edge had been drawn to each: hasEdge on a member counts it, and an edge rule fires on the group edge (once) when any member pair would trip it.

yaml
edges:
  - { from: "api", to: "orders-queue", protocol: "https", mode: "async", delivery: "at-least-once" }
  - from: "checkout-service"
    to: "psp"
    protocol: "https"
    mode: "sync"
    timeoutMs: 2000
    retryPolicy: "exponential-jitter"
    idempotent: true
    circuitBreaker: true
    fallback: "degraded"
FieldRequiredValues
from, toyesNode or group ids in this design, or <nodeId>/<moduleId> for a module
protocolyes, except in-processFree string in v1: http, https, grpc, kafka, postgres, redis, …
modeyes, except in-processsync (the caller waits) or async (the caller continues)
deliverynoat-most-once, at-least-once, exactly-once. Meaningful on async edges
labelnoFree text for renderers. Never read by rules
timeoutMsnoCaller timeout. Rules read it on sync edges
retryPolicynonone, fixed, exponential, exponential-jitter
idempotentnoThe call can safely be repeated
circuitBreakernoThe caller stops calling after repeated failures
fallbacknonone, static, cached, degraded
batchednoCalls are coalesced
authnomanaged-identity, iam-role, service-account, workload-identity, api-key, oauth, mtls, basic, none

Those seven optional fields are the only edge properties in v1, and each exists because a real rule reads it. Anything else about an edge goes in label.

auth says how the caller authenticates on that hop. A managed identity, an IAM role or a service account is not a box between every caller and callee; it is how the edge authenticates:

yaml
edges:
  - { from: "orders-api", to: "orders-db", protocol: "tds", mode: "sync", auth: "managed-identity" }

The canvas and the SVG export draw it as a small glyph on the line by the source end: the cloud's own mark for managed-identity (Azure managed identity), iam-role (AWS IAM) and service-account (Google Cloud IAM), a shield for workload-identity, a key for api-key and basic, a lock for mtls and oauth, nothing for none. Any other value is a structural error.

What makes a document structurally valid

These are checked before any rule runs, and every one of them blocks a save. The wording below is the wording the linter uses.

CheckMessage
Node ids are uniqueduplicate node ids
Group ids are uniqueduplicate group ids
A group's parent existsgroup <id> parent missing
A node's component is in the cataloguenode <id> unknown component <component>
A node's group existsnode <id> unknown group
A node only sets known propertiesnode <id> unknown prop <name>
An enum property's value is in the enumnode <id> prop <name>=<value> not in enum
A boolean property gets a booleannode <id> prop <name> not boolean
An integer property gets an integernode <id> prop <name> not integer
A string property gets a stringnode <id> prop <name> not string
A ref property names an existing noderef <name> missing
…of the right kindref <name> wrong kind
Every required property resolvesnode <id> missing required <name>
Both ends of every edge existedge from <id> missing: no node or group, edge to <id> missing: no node or group
A module end names a node and one of its modulesedge from <a>/<b> missing: no node <a>, edge from <a>/<b> missing: node <a> has no module <b>
Only a code host lists modulesnode <id> lists modules, but kind <kind> does not host code (…)
Module ids are unique within a nodenode <id> duplicate module ids
An in-process edge has no mode or protocoledge <from> -> <to> is in-process (inside node <id>) and takes no <field>

On top of that, the document is checked against schema/design.schema.json: unknown fields, missing required fields and wrong types are reported with the path that carried them.

A complete design

Everything above, in one file.

yaml
schemaVersion: "1"
type: "design"
id: "checkout"

requirements:
  availability: { multiRegion: false }
  messaging: { replay: false }
  correctness: { exactlyOnceEffect: true }

groups:
  - { id: "eu-west-1", kind: "region" }
  - { id: "prod-vpc", kind: "vpc", parent: "eu-west-1" }
  - { id: "dmz", kind: "trust-zone", parent: "prod-vpc" }

nodes:
  - id: "web-client"
    component: "web-browser"
    props: { platform: "web" }

  - id: "api"
    component: "aws-api-gateway"
    group: "dmz"
    props: { idempotencyKeys: true }
    nfr: { rps: 300 }

  - id: "checkout-service"
    component: "container-service"
    group: "prod-vpc"

  - id: "orders-queue"
    component: "sqs"
    group: "prod-vpc"
    props: { deadLetterQueue: true, retentionDays: 14 }

  - id: "orders-db"
    component: "postgres"
    group: "prod-vpc"
    props: { backupEnabled: true, backupRetentionDays: 30, encryptionAtRest: true }

edges:
  - { from: "web-client", to: "api", protocol: "https", mode: "sync", timeoutMs: 3000 }
  - { from: "api", to: "checkout-service", protocol: "http", mode: "sync", timeoutMs: 2000 }
  - { from: "checkout-service", to: "orders-db", protocol: "postgres", mode: "sync" }
  - { from: "checkout-service", to: "orders-queue", protocol: "https", mode: "async", delivery: "at-least-once" }

What v1 deliberately does not have

  • Kind-to-kind inheritance
  • Object or list property types
  • Properties on groups, or on edges beyond the six optional fields above
  • Expressions in rules
  • Layout hints of any sort
  • Any reference to a file outside the repository

Each is a candidate for v2 once a real design proves it necessary, and the evidence is recorded rather than argued.

On this page