Skip to content

Grouping & membership

boundary groups nodes into frames for one view; facet marks which declared sets an element belongs to.

View the source on GitHub

System view
System view

Drawn with Group by: Boundary. Turn on the same toggle in the app to get this diagram. Read the spec

// Semantic clusters via `boundary` / `contains` (Issue #1822, P2b).
//
// A `boundary` block declares a named cluster of system-view nodes, drawn as the
// second "Group by" axis of the system view (the first is team ownership —
// see team-ownership.krs). With `groupBy: "boundary"` the renderer bands each
// boundary's members as a dependency-ordered group and frames it, exactly like
// the team axis — but grouped by the author's *semantic* clusters instead of by
// org chart. The two axes are independent and mutually exclusive.
//
// Here the "Payments" and "Catalog" boundaries cut across what could be a
// different team structure: a boundary groups by meaning ("everything payments"),
// not by who owns it. Un-clustered nodes (the shared Order DB) fall into a
// trailing band below the boundary frames.
//
// Members can live on drill-down levels too: grouping resolves per view,
// against the nodes rendered at the level being drawn (Issue #1983). The
// Payment domain nested inside Checkout is framed on Checkout's drill-down
// view, and its Authorize usecase on Payment's domain view — one boundary,
// disjoint frames on several levels. Keep "Group by: Boundary" on while you
// drill to see each level's frame.
//
// `boundary` is core notation from `.krs language v2.0` (docs/spec/syntax.md
// § Grouping the system view).
system Marketplace {
label "Marketplace"
service Checkout {
label "Checkout"
domain Cart { label "Cart" }
domain Payment {
label "Payment"
usecase Authorize { label "Authorize card" }
}
}
service Billing { label "Billing" }
service Wallet { label "Wallet" }
service Search { label "Search" }
service Inventory { label "Inventory" }
service Gateway { label "API Gateway" }
database OrderDB { label "Order DB" }
Gateway -> Search "route"
Gateway -> Checkout "route"
Checkout -> Billing "charge"
Billing -> Wallet "settle"
Checkout -> Inventory "reserve"
Search -> Inventory "read"
Checkout -> OrderDB "persist"
}
boundary payments {
label "Payments"
contains Checkout
contains Billing
contains Wallet
contains Payment
contains Authorize
}
boundary catalog {
label "Catalog"
contains Search
contains Inventory
}
System view
System view

Drawn with Group by: Boundary. The two frames overlap over the shared node; the node itself is drawn once. Read the spec

// A node in more than one `boundary` (Issue #2161).
//
// Ledger is listed in BOTH `payments` and `pci` below. Both memberships are
// kept in the model, and with "Group by: Boundary" on you can see both: the
// `pci` frame reaches up out of its own band to wrap Ledger where it sits at
// the bottom of `payments`, so the two frames OVERLAP over that one card.
//
// Ledger is still drawn exactly ONCE. What overlaps is the frames, not the
// node — a banded layout gives every node one row, and multi-membership is
// expressed by letting a frame leave its band, never by copying the card.
//
// Each boundary has its own colour, cycled by declaration order, and fills it
// faintly. That is not decoration: in one shared colour, two dashed outlines
// around the same card read as one frame NESTED in the other. The colours (and
// the third tone where the two fills composite) are what make it read as a
// crossing instead. The frame titles take the same colour, so you can tell
// which outline belongs to which boundary.
//
// Two things decide whether you see the overlap at all:
//
// 1. Boundaries that share a member are banded next to each other where the
// dependency flow allows it, and the shared node is seated on the row that
// TOUCHES the other band. Here Ledger sits at the bottom of `payments`,
// against `pci`. Without that seam there is nothing to reach across.
// 2. A frame is widened only when the corridor to the card holds no card that
// is NOT its member. A frame must never enclose a non-member, and that
// rule wins over showing the containment.
//
// To watch rule 2 refuse — and see what it falls back to — add an edge
// `Ledger -> Wallet "settle"`. That pins Ledger above Wallet inside `payments`,
// so the corridor from `pci` up to Ledger now runs through Wallet, which is not
// in `pci`. The frame stays where it is and Ledger gets a dashed `◇ PCI scope`
// tab on its bottom edge instead, plus an `info` diagnostic saying the frame
// could not reach it. Expect the tab to be the common outcome rather than the
// exception: whether the corridor is clear depends on where the dependency flow
// put the cards, which is not something you chose.
//
// The diagnostics panel also reports Ledger as an `info` for the model fact —
// it belongs to more than one boundary. A fact worth seeing, not an error, the
// same register as a node owned by two teams. That one is about the MODEL; the
// reach diagnostic above is about THIS DRAWING.
//
// There is one case where a shared node moves frames: a boundary whose members
// are ALL claimed by earlier ones has nothing to band, so rather than vanish it
// takes one of its shared members — as long as the boundary it takes from keeps
// another member. Not the case here: `pci` bands Card vault on its own, so it
// needs nothing from `payments`.
//
// Boundaries group by meaning, and meanings overlap: Ledger is part of the
// payments flow AND inside the PCI audit scope. Listing it in both is a normal
// thing to write, not a mistake (see boundary-clusters.krs for the basics of
// the axis). The whole membership survives a multi-file model too — a
// `boundary` in an imported file and one here both count, rather than the
// first one winning.
//
// `boundary` is core notation from `.krs language v2.0` (docs/spec/syntax.md
// § Grouping the system view).
system Payments {
label "Payments platform"
service Checkout { label "Checkout" }
service Ledger { label "Ledger" }
service Wallet { label "Wallet" }
service CardVault { label "Card vault" }
service Reporting { label "Reporting" }
database LedgerDB { label "Ledger DB" }
Checkout -> CardVault "tokenize"
Checkout -> Ledger "record"
Checkout -> Wallet "debit"
Ledger -> LedgerDB "persist"
Reporting -> LedgerDB "read"
}
boundary payments {
label "Payments"
contains Checkout
contains Ledger
contains Wallet
}
boundary pci {
label "PCI scope"
// Ledger is already in `payments` above. Both memberships are kept, and both
// frames enclose it — this one reaches up to the row Ledger sits on.
contains Ledger
contains CardVault
}

Scoped declaration (boundary inside a node block)

Section titled “Scoped declaration (boundary inside a node block)”
System view
System view

Drawn with Group by: Boundary. This is the root view; the boundary declared inside Checkout frames only Checkout’s drill-down view. Read the spec

// Scoped boundary — a `boundary` declared INSIDE a node block (#2036).
//
// Written inside a node block, a boundary is that layer's own concern: its
// members are the declaring node's DIRECT CHILDREN (bare ids — siblings are
// already unique), and its frame appears on the declaring node's canvas only.
// Contrast boundary-clusters.krs, where a top-level boundary gathers nodes by
// reference from anywhere and fragments across drill levels.
//
// Turn "Group by: Boundary" on and drill to see each declaration frame its own
// canvas:
// - `edge` (declared in the system block) frames Web + Mobile on the root view.
// - `core` (declared inside Checkout) frames Ledger + Cart on Checkout's
// drill-down view — and nowhere else. Reporting stays outside the frame.
//
// A same-named boundary in another scope is a DIFFERENT boundary (identity =
// declaring scope + id): its frame, label, and collapse state are independent.
//
// `boundary` is core notation from `.krs language v2.0` (docs/spec/syntax.md
// § Grouping the system view).
system Shop {
label "Shop"
boundary edge {
label "Edge services"
contains Web
contains Mobile
}
service Web { label "Web BFF" }
service Mobile { label "Mobile BFF" }
service Checkout {
label "Checkout"
boundary core {
label "Core domains"
contains Ledger
contains Cart
}
domain Ledger { label "Ledger" }
domain Cart { label "Cart" }
domain Reporting { label "Reporting (unclustered)" }
}
Web -> Checkout "order"
Mobile -> Checkout "order"
}

tag / annotation / facet / boundary: which register says what

Section titled “tag / annotation / facet / boundary: which register says what”
System view
System view

Drawn with the pci facet selected. Select the same facet from Facets in the app to get this diagram. Read the spec

// The four vocabulary registers, side by side (Issue #2177; `facet` and
// `boundary` are core from `.krs language v2.0`).
//
// karasu has four ways to say something extra about an element, and they are
// not interchangeable. Picking the wrong one is the most common modelling
// mistake, so this file puts all four on one diagram:
//
// tag [external] [index] [web] what the element IS — its archetype
// annotation @deprecated @new where it is in its LIFECYCLE
// facet facets pci, pii what externally-defined SET it is in
// boundary boundary … contains how you want the VIEW grouped
//
// The test that separates them: could the element change without the model
// changing? A database is a database whether or not PCI scope covers it — PCI
// is imposed from outside, so it is a facet. `@deprecated` is a fact about this
// model's timeline. `[index]` says what the store is for. And `boundary` is not
// about the element at all; it is about how you want to look at the picture.
//
// Things worth trying, in this order:
//
// 1. Select `pci` in the "Facets" selector. The members ring; everything else
// dims. Notice what the tags did NOT do — `[external]` never grouped
// anything, because it was never a membership. The Checkout → terminal
// edge lights up on its own account: it carries card data, which is a
// fact about the hop rather than about either end of it.
// 2. Open "Membership overview" at the bottom of that menu. The list of
// what is in `pci` is DERIVED from the `facets` lines below; nothing in
// this file authors it, so it cannot be out of date.
// 3. Switch "Group by" to `boundary` while `pci` is still selected. The
// frames and the rings coexist — one is a layout, the other is a paint.
// 4. Look at `tag-facet-registers.krs.style`. `[facets=pci]` styles by
// membership, which is what an arbitrary `[pci]` tag selector used to be
// abused for. In `.krs language v2.0` that tag rule matches nothing; this is the
// replacement.
@import "tag-facet-registers.krs.style"
// ── facet: externally-defined sets ────────────────────────────────
// The declaration carries the concern's metadata and nothing else. There is no
// `contains` list — membership is written on the elements, so a rename never
// means editing a distant list. Deliberately no predicates, ever: a facet says
// WHICH things a policy covers, never what the policy says (ADR-832).
facet pci {
label "Cardholder data"
description "In scope for the annual PCI DSS assessment"
link "https://example.com/policies/pci" "PCI policy"
}
facet pii {
label "Personal data"
description "Holds or transits data identifying a natural person"
}
// ── boundary: a grouping you draw over the view ───────────────────
// Not a property of the elements. Two people can group the same system
// differently and both be right, which is why this is by reference and why it
// only draws under "Group by: boundary".
boundary money {
label "Money path"
contains Checkout
contains Ledger
}
boundary discovery {
label "Discovery"
contains Catalogue
contains Search
}
system Shop {
label "Online shop"
user Customer [human] {
label "Customer"
}
// `[web]` is an archetype: it says what kind of client this is. It is not a
// set anyone outside the architecture defined.
client WebApp [web] {
label "Web storefront"
facets pii
}
service Checkout {
label "Checkout"
description "Takes payment — sees both the buyer and the card"
// Two facets at once. Multi-membership is a normal state, not a conflict:
// an element can be in PCI scope and hold personal data.
facets pci, pii
}
service Accounts {
label "Accounts"
description "Profiles, addresses, consent records"
facets pii
}
service Catalogue {
label "Catalogue"
description "Products and prices. Holds nothing about anyone."
}
// All three registers on one element, each saying a different kind of thing.
service Search @new {
label "Search"
description "Query service over the catalogue"
}
// `@deprecated` is lifecycle; `[external]` is archetype. Neither is
// membership — the card processor is not "in" PCI scope, it IS the outside.
service LegacyCardTerminal [external] @deprecated {
label "In-store card terminal"
description "Being retired once the new gateway lands"
}
// `[index]` is an archetype tag: what the store is for. `facets pci` is the
// regulation that happens to cover it. Same element, two different registers.
database Ledger [index] {
label "Payment ledger"
facets pci
}
database ProfileStore {
label "Profile store"
facets pii
}
storage ProductImages {
label "Product images"
}
Customer -> WebApp "browses"
WebApp -> Accounts "sign in"
WebApp -> Checkout "pay"
WebApp -> Catalogue "browse"
Catalogue -> Search "index"
Catalogue -> ProductImages "read"
Accounts -> ProfileStore "read/write"
Checkout -> Ledger "record"
// An edge can be in a facet too, and here it has to be. The card number
// leaves the building on this hop, so PCI covers the FLOW — the terminal
// outside is not in scope, and Checkout being in scope does not say that
// this particular hop carries card data. Membership belongs on the edge.
Checkout --> LegacyCardTerminal {
label "settle"
facets pci
}
}

© 2026 Hiroki Kondo · Licensed underApache-2.0

Built with Cloudflare