Architecture as code.

A typed language for modules, interfaces, and processes. One infinite canvas zooms from the whole system down to a single interface.

Early preview, under active development. Experiment with it; not ready for production yet. See readiness Give feedback

Zoom in, pan around — drag the board to explore the model.
Loading editor…
Oh yes, my system architecture is modelled in

Process when, why, who

Behavior over time. A process drives interfaces between modules — and this is where edges come from. You describe the flow; the relationships fall out of it.

process Checkout {
    Customer > Orders.placeOrder
    Orders   > Payments.authorize
    Payments > Ledger.record
}

Every edge carries a cause: which process, which step, why.

Module what exists

The parts of the system. Nest them without limit and give each the type and fields you define — a service, a database, a third-party vendor.

service Payments {
    aspect team: "Risk"
    module Gateway
    module Ledger
}

C4's boxes — with infinite nesting and types of your own.

Interface the contracts

How parts are allowed to talk: named, typed surfaces a module exposes. Nothing crosses a boundary except through one, and each interface declares its own protocol — sync or async, never guessed from transport.

service Payments {
    rest_create charge {
        aspect path: "/v1/charges"
        rate_limit: "100/s"
        openapi: "https://api.payments.acme/openapi.json"
    }
}

The vocabulary of interaction.

Projection how you see it

A view is a slice of the one model — pick a process, a team, a subtree, a question. The model stays the single truth; each diagram is one projection of it.

view SecurityZones { group by @@security.zone }
view NetworkMap    { group by @@network.segment }

Change the question, not the model.

Downloads

Viewer

Drop-in custom element. Live diagrams in any HTML page.

Open viewer

Scratch

A blank sketch surface. Type a module, watch it draw, share the link.

Open scratch

VS Code

Rename, hover, autocomplete, format-on-save, inline errors.

Install for VS Code

IntelliJ Platform coming soon

Works in IDEA, WebStorm, GoLand, Rider, PyCharm. Same features as VS Code.

Install for IntelliJ

CLI

Validate, format, generate diffs. Fail CI on broken architecture.

Install via npm

Claude Code skills

Author, review, and sync .arch files with Claude Code. Five skills, one install.

Install via npx

Studio coming soon

Hosted multi-workspace with branch-based diff review.

Join wait-list

Features

Architecture as code, diffable in version control

Branches are future states, commits are decisions, pull-request review is architecture review. Stable IDs (#xyz) anchor each node's identity through any rename — cross-references between modules never break.

orders.arch
service #q7f2 Orders {
    aspect team: "Commerce"
    "Owns the order lifecycle.
    Publishes events consumed by [[#m4k9]]."

    kafka orderPlaced
}

service #m4k9 Shipping {
    aspect team: "Fulfillment"
    rest_create createShipment
}
Rename Orders → SalesOrders. The [[#m4k9]] reference still resolves to Shipping. git diff shows a rename, not a broken link.

From a napkin to an enterprise

One syntax, both ends. A single arrow — Orders > Payments — is a complete model; the same language scales to thousands of services with owners, domains, events, and stores. No rewrite, no second tool, no upgrade cliff. Benchmarked to 100,000 nodes.

scale.arch
// the floor — a complete model on its own
Orders > Payments

// …the same language, fully detailed
service Orders {
    aspect team: "Commerce"
    rest_create place
    kafka orderPlaced
}
One axis from the first arrow to a modelled platform — the demo tabs above walk the whole ramp.

Behavior drives dependencies

Write the flow, get the graph. Processes are the source of truth for who calls whom. Call graph, blast radius, and critical path are all derived from behavior, not drawn as arrows.

checkout.arch
actor Customer
service Orders   { aspect team: "Commerce"; rest_create placeOrder }
service Payments { aspect team: "Risk";     rest_create authorize }
service Ledger   { aspect team: "Finance";  kafka record }

process Checkout {
    Customer > Orders.placeOrder
    Orders   > Payments.authorize
    Payments > Ledger.record
}
Change one interface, see every process that breaks. Sync vs async is declared on the interface type (rest_create here, kafka there), never inferred from transport.

Partial models stay valid

Incompleteness is typed, not fatal. The required keyword marks mandatory blanks; instances either fulfill them or explicitly drop them. Sketch Stage 1, ship, fill the rest later. The validator points at the exact gaps.

payments.arch
// a type that demands a compliance aspect
type service audited {
    required aspect compliance
}

audited Payments {
    aspect team: "Risk"
    rest_create charge
    // aspect compliance unset: the validator flags this
    // exact gap, yet the model still renders and commits
}
Nothing prevents rendering or committing. Branch by branch, the gaps close.

One model, many views

Tag each module with aspects — security zone, network segment, team — then define a view per lens: grouped diagrams, BPMN swimlanes, latency-annotated flows. Every view reads the same source, so a security diagram and a network diagram can never disagree.

payments.arch
service Payments {
    aspect {
        team:            "Risk"
        security.zone:   "PCI"
        network.segment: "DMZ"
    }
    rest_create authorize { latency: 200ms }
}

view SecurityZones { group by @@security.zone }
view NetworkMap    { group by @@network.segment }
view CheckoutOps   { flow Checkout bpmn { lane by @@team } }
Add a lens — a group-by, a BPMN swimlane, a latency badge — without copying the model. No more 'which Lucid tab is the real one'.

An open metamodel

Types aren't a fixed schema — you define your own. Start from a stdlib type, refine it for your domain, add required fields, and cascade pushes shared values down the tree. Instances inherit the whole chain; override or drop anything you don't want. Turn any query into a policy and `archlang check` enforces it project-wide.

types.arch
// stdlib defines an optional `aspect team`; mandatory ownership is
// the opt-in arch.policy OwnershipCoverage policy.
type service pci_service {
    required aspect compliance
    aspect { security.zone: "PCI" }
}

// instance inherits the whole chain
pci_service Payments {
    aspect {
        team:       "Risk"
        compliance: "PCI-DSS v4"
    }
}

// enforce it project-wide — archlang check fails the build on a gap
policy OwnershipCoverage {
    forbid service and where (not @@team)
}
Refine, override, or drop inherited declarations with no extra keyword. The same query vocabulary that groups a view also forbids a gap — `archlang check` fails the build on it.

Bring your own rendering

Module, interface, process, view are the only fixed primitives — even how a node draws is yours. Register a custom element on a type, or drop an inline HTML template on a single node, no JS required.

package.archspace
// registers your custom elements
widgets: "./widgets.js"
platform.arch
// a type the core never shipped, drawn by your <arch-queue>
type module queue {
    cascade widget: arch-queue
}

queue OrderBus { kafka orderPlaced }

// no JS: style one node inline; {{...}} reads the body
service Payments {
    aspect team: "Risk"
    widget: "<b class='text-arch-300'>{{name}}</b> · {{aspects.team}}"
}
Widgets cascade and override like any field. The bundled stdlib already ships them for the common types, so reach for this only when you want something bespoke.

Batteries included

You don't start from an empty meta-model. The stdlib ships ready-to-use types across every layer: base primitives, modules and devices, the full backend stack, diagram primitives, and cloud catalogs grouped by provider, each with sensible defaults and a widget. Import what you need, subtype the rest. It's all optional and extendable: drop the bundled library and ship your own types, icons, and widgets.

c4

person system container component

extras

actor system client browser smartphone tablet desktop server

backend

service database cache queue broker gateway function container

diagrams

table class enum state

data

pipeline stream warehouse lakehouse bi catalog

ai

model llm gateway features registry

org

team department guild

policy

OwnershipCoverage ExternalContract DomainCoverage ZoneIsolation ClassificationFlow GatewayNoBypass

cloud

aws
lambda rds cloudfront
gcp
cloud_run bigquery pubsub
azure
aks cosmos_db event_hubs

and more

How it compares

The question isn't which tool draws the nicest boxes. It's what your source of truth is, and where the connections come from. Most tools make you draw the edges. ArchLang derives them from behavior.

Drawing tools

Lucid, Miro, Excalidraw, diagrams.net

A drawing isn't data. You can't query it, compute a blast radius, or enforce a rule like “every service has an owner”. And each view is a separate artifact kept in sync by hand.

Where they win: draw anything, not just software, with zero syntax, a freeform canvas, and real-time multiplayer editing.

Diagram-as-code

Mermaid, PlantUML, D2

It's text, so it diffs, but it's still a picture, not a model. The script says how to draw, not what's true: no types, no queries, no rules, and the same system is redrawn from scratch in every diagram.

Where they win: ubiquitous and zero-setup. A fenced block renders inline in GitHub, GitLab, and most wikis, with no tooling to install.

Architecture models

C4 / Structurizr, ArchiMate

Closest in spirit: many views from one source. But relationships are asserted by hand, not derived from behavior, so the graph is only as honest as someone remembered to keep it. And the metamodel is fixed, with no defining your own types, required fields, or hierarchy.

Where they win: mature and widely adopted, with established notation, a deep ecosystem, and integrations ArchLang doesn't have yet.

ArchLang

one typed model

One versioned, typed source. Edges are derived from processes, not drawn. Every view, check, and branch-to-branch diff comes from that single model.

The trade-offs: software architecture oriented; a typed language to learn, not drag-and-drop; and edges need a modeled process, so quick sketches are harder.

Lucid / MiroMermaid / PlantUMLC4 / StructurizrArchLang
Lives as text in git ✗ ✓ ✓ ✓
Typed model with validation ✗ ✗ ~ ✓
Edges derived from behavior ✗ ✗ ✗ ✓
Many views from one source ✗ ✗ ✓ ✓
Governance rules enforced automatically (policies) ✗ ✗ ✗ ✓
Architectural (not just text) diff ✗ ✗ ~ ✓
Renders inline in GitHub & wikis ✗ ✓ ✗ ~
Draw freely, no syntax to learn ✓ ✗ ✗ ✗

Readiness

A preview under active development. Surfaces and behaviour can still change between versions.

Give feedback

FAQ

Do I have to model the whole system before it's useful?

No. Partial models are valid by design: sketch a few modules, commit, render, fill the gaps later. The required keyword marks mandatory blanks and the validator points at the exact gaps without blocking rendering or commits.

Does this replace Mermaid, PlantUML, or diagrams-as-code?

Different layer. Those render one diagram from one script. ArchLang is a typed model: one versioned source yields many derived views, checks, and diffs. The diagram is an output, not the artifact you hand-maintain.

How is it different from drawing tools like Lucidchart or Miro?

Drawings drift the moment code changes, and nobody can diff them. Here architecture is versioned text in git, so branches, PR review, blame, and real before/after diffs all apply.

Can I import an existing C4, Structurizr, or EA model?

By hand, not by import. Those tools draw connections directly; ArchLang derives them from processes, so there's no faithful mechanical mapping, because the behavior that produces the edges isn't present in the source diagram. The metamodel maps cleanly onto C4 levels, and the docs walk through migrating from EA by hand.

What if the standard library doesn't fit my domain?

Subtype any type to refine it, or drop the bundled stdlib entirely and ship your own types, icons, and widgets. Only the four core primitives are fixed: module, interface, process, view.

Is it ready for production?

Not yet. It's an early preview under active development, and surfaces can still change between versions. Experiment with it and send feedback; the readiness matrix above lists per-component status.