From Structurizr
Structurizr DSL and ArchCode describe the same thing — a C4 model — and most of a workspace carries over line for line. What does not carry over is deliberate: styles, per-view layout and dynamic views are not part of an architecture, so ArchCode has no place for them. This page is the map; a converter is on its way soon.
The shape of both
Structurizr keeps model and views in one workspace; so does ArchCode, but the catalogue is the file and a view only selects from it. There is no workspace { } wrapper, no model { } — a declaration at the top level is in the model.
| Structurizr DSL | ArchCode | note |
|---|---|---|
workspace "Name" { … } | arch name "Name" { … } or nothing | the file is the workspace |
model { … } | (top level) | |
person "Customer" | actor customer "Customer" | ids are yours to choose, and they are stable |
softwareSystem "Orders" | system orders "Orders" | |
softwareSystem "Stripe" { tags "External" } | external stripe "Stripe" | a kind, not a tag |
container "API" "…" "Go" | service api "API" tech Go — or container api "API" tech Go until you decide | see containers below |
container "DB" "…" "PostgreSQL" { tags "Database" } | datastore db "DB" tech PostgreSQL | the kind says what the tag used to |
component "Auth" "…" "Spring" | component auth "Auth" tech Spring | inside its container's block |
group "Sales" { … } | nesting: system sales { … } | a group is a boundary with a name |
a -> b "Places order" "HTTPS" | a calls b over HTTPS "Places order" | see relationships below |
tags "Core,PII" | tags core, pii | |
properties { "cost" "12" } | x-cost 12 | unknown keys are kept; x- says they are yours |
description "…" | description "…" | |
url "…" | spec openapi://… on a relation, exposes http openapi://… on an object | a pointer, not a copy |
deploymentEnvironment "Prod" | env prod "Prod" { … } | |
deploymentNode "AWS" { deploymentNode "EC2" { … } } | env prod { segment aws { node ec2 { … } } } | cluster for Kubernetes, managed for RDS-style services |
containerInstance api | run api | inside the node; run api count 3 cpu 1 mem 2Gi sizes it |
infrastructureNode "Load balancer" | gateway lb "Load balancer" { transit } in the model, run lb on its node | infrastructure that carries traffic is a transit node |
!docs, !adrs | decision d "…" { … } reserved · v0.3 | kept by the parser today, read in v0.3 |
!include, !constant, !identifiers | — | one file; several files are §11 partial |
Containers: the tag becomes the kind
Structurizr has one container and tells them apart by tags and styles. ArchCode has kinds, because a linter cannot reason about a tag: service, webapp, app, gateway, broker, datastore, cache, function, job. Pick by what the thing is, not by what it is drawn as — the renderer draws a cylinder for a datastore on its own. Not sure yet? container is the generic kind: it draws as a plain card, and you refine it later.
container "Web app" "…" "React" { tags "Web Browser" }
container "API" "…" "Go"
container "Database" "…" "PostgreSQL" { tags "Database" }
container "Events" "…" "Kafka" { tags "Message Bus" }
webapp web "Web app" tech React service api "API" tech Go datastore db "Database" tech PostgreSQL owned_by api broker events "Events" { transit tech Kafka }
archcode: "0.2"
objects:
- id: web
kind: webapp
name: "Web app"
attrs:
tech: [React]
- id: api
kind: service
name: API
attrs:
tech: [Go]
- id: db
kind: datastore
name: Database
attrs:
tech: [PostgreSQL]
owned_by: [api]
- id: events
kind: broker
name: Events
attrs:
transit: []
tech: [Kafka]
relations: []
placements: []
{
"archcode": "0.2",
"objects": [
{
"id": "web",
"kind": "webapp",
"name": "Web app",
"attrs": {
"tech": [
"React"
]
}
},
{
"id": "api",
"kind": "service",
"name": "API",
"attrs": {
"tech": [
"Go"
]
}
},
{
"id": "db",
"kind": "datastore",
"name": "Database",
"attrs": {
"tech": [
"PostgreSQL"
],
"owned_by": [
"api"
]
}
},
{
"id": "events",
"kind": "broker",
"name": "Events",
"attrs": {
"transit": [],
"tech": [
"Kafka"
]
}
}
],
"relations": [],
"placements": []
}
owned_by says which service owns the store — Structurizr has no word for it, and it is the single most useful fact on a container diagram. transit says the broker is passed through, not called: api publishes order.placed via events.
Relationships: the verb carries the meaning
A Structurizr relationship is an arrow with a description. ArchCode's is a verb, and the verb is what makes rules possible ("a service writes a store it does not own"):
web -> api "Makes API calls to" "JSON/HTTPS" api -> db "Reads from and writes to" "SQL" api -> events "Publishes order events" mail -> events "Subscribes to order events"
web calls api over HTTPS "Makes API calls" api reads db over SQL api writes db over SQL api publishes order.placed via events mail subscribes order.placed via events
Verbs: calls uses reads writes publishes subscribes streams depends_on (and emits / listens as spellings of the last two). "Reads from and writes to" is two lines, because it is two facts. The technology goes to over, the description stays as the label.
Views: select, do not redeclare
| Structurizr | ArchCode |
|---|---|
systemContext orders { include * } | the model with collapse orders (every system as one box) — or Logical lens, top level |
container orders { include * } | the same picture with orders opened; view c2 { … } pins it |
component api { include * } | expand api |
deployment * "Prod" | kind deployment |
dynamic … { a -> b "1. …" } | not in v0.2 — sequences are a separate notation (soon kind sequence) |
autoLayout lr | nothing — the layout engine is always on; layout rows / columns steer it |
styles { element "Database" { shape Cylinder } } | nothing — a datastore is a cylinder |
themes …, colours, icons | nothing — layout and looks are not architecture |
manual positions in workspace.json | view main { at api 120 40 } — only when a person pinned them |
A view block in ArchCode holds pins, routes, the lens and the phase — the things a person decided about this picture. Everything else is computed, so two people describing the same system get the same file.
What you lose, and why
- Styles and themes. On purpose. A picture that depends on a colour scheme is a picture, not a model. Kinds carry the semantics a style used to hint at.
- Dynamic views. Sequences are a different notation; they will get their own view kind rather than being squeezed into a container diagram.
- Per-view include/exclude filters. Views select by lens, phase and collapse/expand, not by lists of elements — a filter list goes stale, a lens does not.
!includeacross files. One file per system today; cross-file references are §11 and partial.
What you gain
- A validator. Unresolved references, ambiguous ids, a
runoutside a host, a service writing a store it does not own — reported with line and column. - Round trip. Drag a card in the Playground and the file changes by one line; the file stays reviewable in a pull request.
- Placement with numbers.
run api count 3 cpu 1 mem 2Gion a node,at 2027-01 count 6— and a resources table that sums it per environment and moment. - Pointers, not copies.
exposes http openapi://api.yaml @9f3c— the contract stays where it lives; the model holds the fact, the address and the revision.
Doing it by hand today
- One
.archfile per Structurizr workspace;arch <id> "<workspace name>"at the top if you want the frame. - Every
person→actor; everysoftwareSystem→system(orexternal); go inside each and translate its containers by kind. - Relationships: one line per fact, verb first, technology to
over, description as the label; relationships into a store becomereads/writes. - Deployment:
envper environment, nestsegment/cluster/node,runthe containers where their instances were. - Open the file in the Playground — the diagnostics bar tells you what did not resolve;
Deploymentshows where things landed. - Pin nothing until the picture is right; then
⌘Sonce.