Notation cookbook — idioms for modeling with karasu
English(this file) · 日本語
docs/spec/syntax.md is the grammar — every keyword and
rule. This cookbook is the missing companion: a compact idiom catalog that
answers “how do I express X” for patterns that are reachable from the grammar
but not obvious from it. Each entry is a worked snippet you can copy, adapt, and
learn from.
It is deliberately short so you can feed it to an LLM alongside syntax.md
when reverse-engineering a project (see
Reverse-engineering with an LLM), and it doubles
as human onboarding. Like any generated model, treat the output as a map, not a
spec — verify against the code.
How to use this
Section titled “How to use this”- With an LLM: paste this file after
syntax.md. It shows the model idioms, not just grammar, so it picks the karasu-idiomatic shape instead of inventing one. - As a human: skim for the pattern you need; each entry stands alone.
Every entry follows the same shape: When you reach for it, the Pattern
(the rule in one line), a minimal .krs, and Why it’s modeled this way.
1. Key/value store (Redis, etcd) — a leaf-less database
Section titled “1. Key/value store (Redis, etcd) — a leaf-less database”When — you have a store with no meaningful table/collection structure to model: a session cache, a KV/config store, a lock service.
Pattern — declare a database with no leaves, connect a service to it with
a node-level edge (Service --> Store), and name the concrete engine in the
physical layer with a store unit. Do not freeze a @kv annotation or a new
kv kind — a KV store is a database you simply don’t decompose.
system Web { service ApiGateway { label "API Gateway" }
database SessionStore [cache] { // leaf-less: no `table` inside label "Session store" }
ApiGateway --> SessionStore "Reads/writes session tokens" // node-level edge}
deploy "production" { store "session-kv" { type "Redis 7" // the concrete engine lives here realizes SessionStore }}Why
- Reference at node granularity with an edge, not a
resourcedot-path. Aresource <Db>.<Leaf>reference needs a declared leaf; a KV store has no leaves to name, so writingresource SessionStoreleaves it unassigned (a warning, and a node drawn only inside its own usecase, never reaching the system view where the store you meant actually lives). The idiomatic connection is a directService --> SessionStoreedge — the same way thehatoexample wires its leaf-lessD1/R2/Tasksstores. - Engine in the physical layer. “Redis 7” is a technology choice. The logical
layer stays technology-agnostic; the
store { type … }unit records which engine realizes the logical store, so swapping Redis for etcd never disturbs the logical model. - No new vocabulary. Annotations (
@…) are lifecycle markers (deprecated, experimental), not kinds —@kvis rejected on purpose. A leaf-lessdatabasealready expresses “a store with no interesting sub-structure”. [cache]says it is not the system of record. Losing the session store logs everyone out; it does not lose business data, which is exactly the test the tag encodes. Tag the role, never the technology —[kv]is rejected for the same reason@kvis (see store role tags).
2. A store that is not the system of record — [index] / [cache] / [analytics]
Section titled “2. A store that is not the system of record — [index] / [cache] / [analytics]”When — you have a search / vector index (ElasticSearch, OpenSearch, pgvector) that is derived from a system of record, not the source of truth itself.
Pattern — tag the database with [index]; keep the concrete engine in the
physical layer.
database SearchIndex [index] { table documents}
// physical layerdeploy "production" { store "search" { type "ElasticSearch 8" realizes SearchIndex }}Why — [index] marks a role (a secondary index over the SoR), not a
technology, and adds an index badge. A vector DB or ElasticSearch that is itself
the system of record stays a plain database (no [index]); a single Postgres that
is both SoR and index also stays plain. This is the same “role via tag, technology in
the physical layer” discipline as idiom #1 — it avoids minting a vector-store /
search kind for every engine. See
ADR-1718.
[index] has two siblings on the same axis, both usable on storage as well as
database: [cache] for a store you could rebuild (a session store, a CDN
origin cache — if losing it loses business data, it is the system of record and
takes no tag) and [analytics] for a warehouse / data lake ingested for
analysis. No tag means the store is the system of record. See
store role tags.
3. Something outside the boundary — the [external] tag
Section titled “3. Something outside the boundary — the [external] tag”When — a third-party API, a managed SaaS, or a store your system depends on but does not own.
Pattern — suffix the node id with [external]. It applies to service and to
the infra kinds database / queue / storage.
service PaymentGateway [external] { label "Payment gateway"}
database AnalyticsDB [external] { // a managed third-party store label "Vendor analytics DB"}Why — [external] draws the node with a dashed, gray-toned border so a reader
instantly sees the system boundary. External stores are also excluded from the
shared-infra-fan-in diagnostic (idiom #4) — sharing a vendor API across services
is expected, not a design smell.
4. Shared infrastructure (fan-in)
Section titled “4. Shared infrastructure (fan-in)”When — several services read/write the same datastore.
Pattern — declare the store once; each service’s usecase references the shared
leaf with a resource <Db>.<Leaf> dot-path. The resolver aggregates these into
service → database edges automatically.
database ArticleDB { table articles}
service ArticleDelivery { domain Delivery { usecase "Fetch an article" { resource ArticleDB.articles } }}
service Authoring { domain Publishing { usecase "Publish an article" { resource ArticleDB.articles } }}Why — two services fanning into one ArticleDB raises the shared-infra-fan-in
info diagnostic (never an error — karasu surfaces the coupling without prescribing
against it). Note this uses the resource dot-path because the store has a
modeled leaf (articles); contrast idiom #1, where a leaf-less store uses a
node-level edge instead. See diagnostics.
5. Cross-domain and cross-system references
Section titled “5. Cross-domain and cross-system references”When — one domain depends on something owned by another domain, service, or system.
Pattern — declare the edge inside the source block; the origin-scope rule
binds the source, not the target, so a block may depend on things it does not own.
For another system, use System.Node dot-notation (the referenced system
renders as a ghost).
system Shop { service BillingService { domain Billing { label "Billing" Billing -> Contract "Created from a contract" // Contract lives in another service } }
// cross-system: PaymentGateway is a separate `system` (imported elsewhere) OrderService -> PaymentGateway.PaymentService "Request payment"}Why — cross-service domain edges are auto-derived into implicit service-level
edges on the system view, so the high-level picture stays readable while the detail
lives where you wrote it. An endpoint you never modeled is kept and reported as
unresolved-edge-endpoint rather than dropped.
6. Split a model across files
Section titled “6. Split a model across files”When — one system grows too large for a single file, or different teams own different slices.
Pattern — split by facet (per-service files + a shared infra.krs), and
stitch them with import. Same-id system / deploy / organization blocks merge
(reopen). Import a whole file with import "x.krs", or a single node with
import { Node } from "x.krs".
// index.krs — the entry pointimport "infra.krs" // shared database / queue / storageimport "reader.krs" // one service per fileimport "editor.krs"
system Blog { label "Blog Platform Demo"}Why — the file is not a grouping unit in the model: splitting is purely for
authoring ergonomics, and the merged result renders identically to a single file.
Keep shared stores in one infra.krs that each slice imports, so every slice also
renders standalone. See the
multi-file-system
example.
7. Cloudflare Workers — from wrangler.toml
Section titled “7. Cloudflare Workers — from wrangler.toml”When — a serverless Cloudflare Workers app whose physical layer lives in a
wrangler.toml. There is no compose / k8s file, so hand-modeling the bindings
risks leaking the concrete tech (“D1 (SQLite)”) into logical labels.
Pattern — let karasu translate --from wrangler extract it deterministically.
The adapter emits a logical system (engine-neutral infra + the Worker service
- edges) and a physical
deploywhere the concrete Cloudflare technology lands instore { type ... }, never in a logical label:
system Hato { service Hato { label "hato" }
database DB { } // D1 storage EXPORTS { } // R2 queue TASKS { } // Queues database SEARCH [index] { } // Vectorize — a derived vector index (idiom #2) database CACHE [cache] { } // KV — a store you can rebuild (idiom #2) service AI [external] { } // Workers AI — an external model service (idiom #3) service SessionActor [external] { } // Durable Object — opaque stateful actor
Hato --> DB // owned infra uses --> Hato -> AI // external / other Workers use -> Hato -> AuthWorker // service binding = Worker→Worker RPC edge}
deploy "hato" { function "hato" { runtime "cloudflare-workers"; realizes Hato } store DBStore { type "Cloudflare D1"; realizes DB } store SEARCHStore { type "Cloudflare Vectorize"; realizes SEARCH }}Why — the binding→karasu mapping reuses existing idioms rather than minting
new syntax: Vectorize → database [index] (idiom #2, a derived index), Workers
AI and Durable Objects → service [external] (idiom #3, opaque to this adapter),
and a service binding → a -> communication edge, and KV → database [cache]
(idiom #2’s sibling role — a store the Worker can rebuild).
Unknown binding kinds are skipped with a warning — never guessed. Run
karasu translate --from wrangler wrangler.toml > index.krs.
8. Cross-cutting concerns (PCI, PII, auth) — pick the right register
Section titled “8. Cross-cutting concerns (PCI, PII, auth) — pick the right register”When — you want to mark elements with a compliance scope, a data-classification,
or a policy (“these services are in PCI scope”, “this entity is PII”, “this usecase
requires login”) and are tempted to invent a tag ([pci]) or an annotation
(@requires_auth).
Pattern — labels live in four registers; membership in an externally defined set is not a tag and not an annotation:
| You are saying… | Register | Construct |
|---|---|---|
| what the element is (architecturally) | archetype | builtin tag — [external], [index] |
| what development state it is in | lifecycle | builtin annotation — @deprecated, @new |
| how peers group in a view | view grouping | boundary |
| which externally defined set it belongs to | membership | facet (#2065 Part B, upcoming) — until it lands, description + link |
.krs — PCI / auth today (prose + link), with rule content never modelled:
system Shop { service Checkout { description "In PCI DSS scope — see assessment" link "https://wiki.example.com/pci/scope" "PCI scope doc" domain Ordering { usecase PlaceOrder { description "Requires authentication; who may call it is defined by the IAM policy" link "https://wiki.example.com/policies/iam" "IAM policy" } } }}Why — tag / annotation vocabularies are tool-owned: a non-builtin name draws a
tag-not-builtin / annotation-not-builtin deprecation warning in v1.x, and
syntax v2.0 accepts tool vocabulary only. Membership also semantically misfits
tags: a database is a database whether or not it is in PCI scope, and a
nine-out-of-ten [pci] diagram reads as a false audit guarantee. The rule
content (roles, plans, conditions) stays prose + link permanently
(ADR-832); only the scope becomes
first-class when facet lands. If what you actually need is a missing archetype,
request a builtin tag addition instead — see
tags-annotations.md.
See also
Section titled “See also”docs/spec/syntax.md— the precise.krsgrammar (feed this first)docs/spec/tags-annotations.md— the full tag / annotation list- Reverse-engineering with an LLM — feed this cookbook alongside the grammar
- Onboarding guide — reading an existing system down into diagrams
© 2026 Hiroki Kondo · Licensed underApache-2.0