Skip to content

C4 architecture with Structurizr

py-doc reads a Structurizr workspace, writes Markdown chapters from it, draws its views, joins the result with hand-written chapters, and opens the model in a live editor.

Model, pictures and text

  • Model: structurizr-cli export -format json (in the tool container, or the program named by c4.cli) resolves the DSL, so py-doc has no DSL parser of its own. A workspace.json is read directly and needs no Java. Exports are cached in .py-doc/c4 by the hash of the DSL and every file it includes (!include, !docs, !adrs). The versions tested are listed in py_doc/c4/structurizr.py. A field the format has dropped is named in the error.
  • Pictures: views are drawn as Structurizr draws them, in a browser. py-doc starts Structurizr Local on a private Docker network, opens it with headless Chromium in the tool container, writes <view key>.png and .svg, and stops Local if it started it. A failing view, an empty view, a missing picture or a missing engine is an error with a non-zero exit status that names the view. Nothing is ever written as a stand-in picture.
  • Text: one Markdown page per level: context, containers, components, relationships, deployment, dynamic views, and an element catalogue with a glossary, then the workspace’s !docs and !adrs. Output is sorted and has no times. Each page says that it is generated and from which file.
c4:
  cli: structurizr-cli          # optional: use this program instead of the one in the tool container
  image: structurizr/structurizr@sha256:...   # optional: pin Structurizr Local
  port: 8080

documents:
  architecture:
    title: Sensor Node Architecture
    sources:
      - file: intro.md
      - structurizr: architecture/workspace.dsl      # or workspace.json
        levels: [context, containers, components, deployment]
        systems: [Sensor Node]
        heading_offset: 1
        diagrams: {engine: browser, format: png}     # or {engine: source, format: mermaid}
      - file: decisions.md
    outputs:
      docx: {}
      pptx: {split_level: 2}

With engine: source, the views are put in the text as plantuml or mermaid blocks under a line that says what they are, and the diagram step draws them. Generated pages are rebuilt only when the workspace or these options change. For PowerPoint, every view is a level 2 heading with its picture, so split_level: 2 gives one slide per view.

Commands

Command Does
py-doc c4 check PATH Reads each workspace and checks the model: C4001 element without description, C4002 container without technology, C4003 relationship without description, C4004 element with no relationships. Severities are set under lint.rules. --strict fails on warnings
py-doc c4 export PATH -o DIR -f FORMAT... -f (--formats) takes png, svg, plantuml, c4plantuml, mermaid, dot, static and markdown. --views KEY... limits the pictures, --levels the pages, --dry-run lists the files and writes nothing, --url uses a Local that is already running (reachable from the container)
py-doc c4 lock [--tag TAG] Pulls the Structurizr Local image and records its digest in c4-images.lock, so builds use the same image everywhere. Without a lock file, py-doc warns and uses the tag
py-doc c4 up PATH Opens the workspace in Structurizr Local to view it and arrange its views, until Ctrl+C. See Live editing
py-doc c4 down, status Stops a detached Local, or says whether one is running

PATH is a .dsl or workspace.json file, or a folder: workspaces below it are found in sorted order, skipping hidden and vendor folders and files that are only included. In a folder that has both, the .dsl is the workspace and the workspace.json beside it holds its layouts.

py-doc lint also runs the model rules for each structurizr: source in py-doc.yml. The export cache lives in .py-doc/c4/json at the project root, whatever the working folder. py-c4-exporter still works: it prints a rename note and runs py-doc c4 export with the same flags.

Live editing

py-doc c4 up architecture/workspace.dsl            # until Ctrl+C
py-doc c4 up architecture/workspace.dsl --detach   # leave it running; py-doc c4 down stops it

c4 up starts Structurizr Local with the project folder itself, opens http://localhost:8080/workspace/1/diagrams in your browser (--no-browser does not), and keeps running until Ctrl+C, when it stops Local.

  • Changes to the DSL show when the page is reloaded.
  • Layouts you arrange in the diagram editor (the pencil button, then the save button) are saved to workspace.json beside the .dsl. Structurizr keeps manual positions there because the DSL has no syntax for them. Commit workspace.json with the .dsl: when Local reads the DSL again, it takes the positions from the JSON, so a renamed element keeps its place. A view with autoLayout is laid out again on every load, so remove autoLayout from the views you arrange by hand.
  • Builds use the saved layouts: when a document draws a workspace.dsl, its workspace.json goes with it.
  • The project is never changed by a build: drawing works on a copy of the workspace folder, so only c4 up writes workspace.json. Local also keeps a search index and logs in .structurizr/ beside the workspace, which py-doc init adds to .gitignore.
  • Local runs as your user, so the files it writes are yours. Local opens only workspace.dsl or workspace.json, so a workspace with another name is shown from a copy, and its layouts are not kept (a warning says so).
  • On Windows, WSL would stop Docker soon after c4 up returned. py-doc keeps the distro running for as long as Local runs, and c4 down ends that too.

If Local does not answer, check that Docker is running and that the port is free (c4.port in py-doc.yml changes it), then run py-doc c4 down and try again.