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 DSLArchCodenote
workspace "Name" { … }arch name "Name" { … } or nothingthe 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 decidesee containers below
container "DB" "…" "PostgreSQL" { tags "Database" }datastore db "DB" tech PostgreSQLthe kind says what the tag used to
component "Auth" "…" "Spring"component auth "Auth" tech Springinside 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 12unknown keys are kept; x- says they are yours
description "…"description "…"
url "…"spec openapi://… on a relation, exposes http openapi://… on an objecta 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 apirun apiinside 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 nodeinfrastructure that carries traffic is a transit node
!docs, !adrsdecision d "…" { … } reserved · v0.3kept 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 }

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

StructurizrArchCode
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 lrnothing — the layout engine is always on; layout rows / columns steer it
styles { element "Database" { shape Cylinder } }nothing — a datastore is a cylinder
themes …, colours, iconsnothing — layout and looks are not architecture
manual positions in workspace.jsonview 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.
  • !include across files. One file per system today; cross-file references are §11 and partial.

What you gain

  • A validator. Unresolved references, ambiguous ids, a run outside 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 2Gi on 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

  1. One .arch file per Structurizr workspace; arch <id> "<workspace name>" at the top if you want the frame.
  2. Every person → actor; every softwareSystem → system (or external); go inside each and translate its containers by kind.
  3. Relationships: one line per fact, verb first, technology to over, description as the label; relationships into a store become reads/writes.
  4. Deployment: env per environment, nest segment/cluster/node, run the containers where their instances were.
  5. Open the file in the Playground — the diagnostics bar tells you what did not resolve; Deployment shows where things landed.
  6. Pin nothing until the picture is right; then ⌘S once.