This is the first of six worked designs in Part VI. Each chapter takes a concrete system shape and models it from scratch — modules, interfaces, processes, views, and (where relevant) types. The goal isn’t to teach features that were covered in earlier chapters; it’s to show what a complete model looks like in practice and what decisions you’ll face when you build your own.
The system this chapter: a small SaaS backend. Authentication, order management, payments, notifications. Mostly sync, with an async event spine. Six modules, four processes, two views. Roughly the size of an early-stage startup’s product backend.
Two domains: commerce (Orders, Inventory, Payments, Ledger) and platform (Auth, Notifications). One async hop — order events on Orders fan out to a handler on Notifications.
command Capture { "Capture an authorized payment." }
command Refund { "Issue a refund." }
}
service Ledger {
team: Finance
labels {
domain: Finance
security.zone: Internal
}
"Immutable financial record of every transaction."
command Record { "Record a financial event." }
}
Six modules. Note the team distribution: Platform, Commerce, Payments, Finance. Note the labels — domain and security.zone cascade to every interface inside; we’ll lean on that in the views.
Mistake to avoid. Don’t try to model “auth” by adding a requiresAuth: true field to every command. Authentication is a concern of the call site (the actor or the caller in the process), not of the receiver. The model captures who calls whom via processes; whether that call carries a session token is implementation, not architecture.
The processes
processes.arch:
process#flow01 SessionStart {
Customer > Auth.Login
Auth > Auth.SessionEvents
}
process#flow02 Checkout {
Customer > Auth.GetSession : "validate session"
Customer > Orders.CreateOrder
Orders > Inventory.Reserve
Orders > Payments.Authorize
switch"payment outcome" {
authorized {
Orders > Payments.Capture
Payments > Ledger.Record
Orders > Orders.OrderEvents : "OrderPaid"
}
declined {
Orders > Inventory.Release
Orders > Orders.OrderEvents : "OrderFailed"
}
}
}
process#flow03 Cancellation {
Customer > Auth.GetSession
Customer > Orders.CancelOrder
Orders > Inventory.Release
Orders > Payments.Refund
Payments > Ledger.Record
Orders > Orders.OrderEvents : "OrderCancelled"
}
Checkout is the central business flow. switch "payment outcome" captures the two-way branch on authorization — each case-label (authorized, declined) names a branch. The labels after the colon (: "validate session", : "OrderPaid") annotate steps for diagram readability without changing semantics.
The notification fan-out is wired declaratively — Notifications.SendEmail carries subscribes: Orders.OrderEvents, which lives on the interface declaration in platform.arch. No explicit process is needed; the diagram renderer joins the event with its subscriber.
Alternative: explicit-process style.
If you’d rather see the fan-out as a process for review-discoverability, drop the subscribes: and add:
Same architectural fact, different surface. The choice is taste — explicit processes are more discoverable; subscribes: is less duplicative. The validator and renderer accept both.
"End-to-end customer journey. Used in onboarding and architecture reviews."
}
view PCIScope {
focus security.zone: PCI
groupby team
layout dagre
"Every service in PCI scope. For compliance review."
}
Two views. The first is the all-up customer journey grouped by team — what you’d show in onboarding. The second isolates the PCI-scoped subset for compliance reviews; with the labels in place it’s literally focus security.zone: PCI and the renderer does the work.
Decisions you’ll face when modeling your own
Module granularity. Should Orders be one module or several? Rule: if it has one team and one boundary, one module. The team can break it up internally with component sub-modules later (see the component stdlib kind in Chapter 11) without changing the external surface.
Sync vs async. Use command and query for sync; use event (with subscribes: on the handler) for async. Mixing them in one interface is wrong — the kind conveys the call shape.
Where to put labels. Labels go on whatever container best describes the classification’s scope. domain and security.zone belong on modules. criticality might belong on individual interfaces (a service can have one critical command and twenty advisory queries). When in doubt, put it on the broader scope and override down.
Process granularity. One process per business flow. If a flow has a clean conditional split, use if or switch. If it has independent parallel work, use parallel. Use try / catch for explicit error paths. Don’t try to encode every error path; capture the happy path and the major alternatives — exhaustiveness is the validator’s job, not the model’s.
External actors.Customer here is an actor from the stdlib. For B2B integrations or third-party callers, declare them as external_system or actor and put them on the left of process steps. They never appear on the right (because they don’t expose interfaces to your model).
What this gives you
After validation:
A diagram with six service nodes and the actor, edges derived from four processes, grouping by team.
A second diagram filtered to PCI scope.
Validation that every process step references a real interface.
A diff engine that recognizes renames if you, say, rename Orders to OrderService later.
Markdown-rich descriptions in hover tooltips, with [[Notifications]] cross-links and @security.zone interpolation if you add it.
Total source: roughly 120 lines of .arch across four files. That’s the entire shape of the system, in text, in version control.
Summary
Model your domain by writing modules first, then drawing processes between them. Arrows in the diagram follow.
Group modules by team and by domain via labels; views slice the model along those axes.
Use command/query for sync, event+subscribes: for async; pick explicit-process or subscribe-wiring style based on what’s clearer in your codebase.
Three-to-six modules per file, splitting by domain, is a comfortable density.
The PCI view is automatic once security.zone: PCI is on the relevant modules — labels do the work.
What’s next
Chapter 26: An Event-Driven Pipeline → — a different topology: queues, streams, fan-out fan-in, and subscribe wiring as the primary integration mechanism.