Skip to content

Semantic Architecture as Code

The idea

Structurizr DSL proved that architecture models can live as text files in Git — reviewed in PRs, validated in CI/CD, generating views from a single model. That workflow is compelling.

RDF/Turtle gives you the same workflow with richer semantics. Turtle files are text. They live in Git. They diff cleanly. They can be validated in CI/CD (via SHACL). And they carry typed elements, typed relationships, cross-layer traceability, and formal validation — things Structurizr DSL can't express.

The trade-off is real: Turtle is more verbose than Structurizr DSL, the learning curve is steeper (you need Turtle + SPARQL basics), and diagram generation is less polished. For a 3-container system, Structurizr DSL is faster. For a system where you need cross-layer queries, governance validation, or AI agent access, Turtle pays for itself.


Side by side

Structurizr DSL:

workspace "Order Management" {
    model {
        customer = person "Customer"
        orderSystem = softwareSystem "Order Management" {
            api = container "Orders API" "Handles order lifecycle" "Spring Boot"
            db = container "Orders DB" "Stores orders" "PostgreSQL"
            worker = container "Orders Worker" "Processes events" "Spring Boot"
        }
        paymentSystem = softwareSystem "Payment System" "External payment provider"

        customer -> api "Places orders" "HTTPS"
        api -> db "Reads/writes" "JDBC"
        api -> worker "Sends events" "AMQP"
        api -> paymentSystem "Processes payments" "HTTPS"
    }
    views {
        container orderSystem { include * ; autolayout lr }
    }
}

Same system in Turtle (ArchiMate semantics):

@prefix am:   <https://meta.linked.archi/archimate3/onto#> .
@prefix arch: <https://meta.linked.archi/core#> .
@prefix bs:   <https://meta.linked.archi/backstage/onto#> .
@prefix skos: <http://www.w3.org/2004/02/skos/core#> .
@prefix ex:   <https://model.example.com/orders#> .

# Business context (Structurizr can't express this)
ex:OrderFulfillment a am:Capability ;
    skos:prefLabel "Order Fulfillment"@en .

ex:OrderPlacement a am:BusinessService ;
    skos:prefLabel "Order Placement"@en ;
    am:realizes ex:OrderFulfillment .

ex:Customer a am:BusinessActor ;
    skos:prefLabel "Customer"@en .

# Application layer
ex:OrderManagement a am:ApplicationComponent ;
    skos:prefLabel "Order Management"@en ;
    am:composedOf ex:OrdersAPI, ex:OrdersDB, ex:OrdersWorker .

ex:OrdersAPI a am:ApplicationComponent ;
    skos:prefLabel "Orders API"@en ;
    skos:definition "Handles order lifecycle"@en .

ex:OrdersDB a am:ApplicationComponent ;
    skos:prefLabel "Orders DB"@en .

ex:OrdersWorker a am:ApplicationComponent ;
    skos:prefLabel "Orders Worker"@en .

ex:Order a am:DataObject ;
    skos:prefLabel "Order"@en .

ex:OrdersAPI am:accesses ex:Order .
ex:OrdersAPI am:serves ex:OrdersWorker .
ex:OrdersAPI am:serves ex:PaymentSystem .

ex:PaymentSystem a am:ApplicationComponent ;
    skos:prefLabel "Payment System"@en .

# Technology (Structurizr captures as string annotations)
ex:SpringBoot a am:SystemSoftware ; skos:prefLabel "Spring Boot 3.2"@en .
ex:PostgreSQL a am:SystemSoftware ; skos:prefLabel "PostgreSQL 15"@en .
ex:OrdersAPI am:assignedTo ex:SpringBoot .
ex:OrdersDB am:assignedTo ex:PostgreSQL .

# Ownership (Structurizr can't express this)
ex:OrdersTeam a bs:Group ; skos:prefLabel "Orders Team"@en .
ex:OrderManagement bs:ownedBy ex:OrdersTeam .

The Turtle version is longer but captures: business capabilities, typed relationships, data objects, technology assignments, and ownership. The Structurizr version captures none of these — it has labeled arrows and string annotations.


Repository structure

flowchart TD
    subgraph repo["my-system/"]
        src["src/ — Application source code"]
        subgraph arch["architecture/"]
            subgraph model["model/"]
                om["orders-model.ttl — Architecture model"]
                od["orders-decisions.ttl — Architecture decisions"]
            end
            subgraph views["views/"]
                cs["context.sparql — System Context view query"]
                cont["containers.sparql — Container view query"]
                imp["impact.sparql — Impact analysis query"]
            end
            subgraph shapes["shapes/"]
                pr["project-rules.ttl — Organization-specific SHACL rules"]
            end
            subgraph gen["generated/"]
                docs["docs/ — Auto-generated Markdown"]
            end
        end
        subgraph gh[".github/workflows/"]
            yml["architecture.yml — CI/CD pipeline"]
        end
        mk["Makefile"]
    end

CI/CD validation

The workflow on every PR that touches architecture files:

flowchart TD
    A[".ttl file edited"] --> B["Syntax Check<br/>(Turtle parser)"]
    B -->|pass| C["SHACL Validate"]
    B -->|fail| X1["❌ Block merge"]
    C -->|pass| D["SPARQL Views"]
    C -->|violations| X2["⚠️ Report / block"]
    D --> E["Generated views<br/>as build artifacts"]

    subgraph SHACL["SHACL Validate"]
        C1["Relationship validity"]
        C2["Element constraints"]
        C3["Custom governance"]
    end

The tools are CI-platform-agnostic — validate.sh runs the Java-based Turtle syntax checker and SHACL validator, arq (Apache Jena) executes SPARQL queries. Wire them into GitHub Actions, GitLab CI, or whatever your team uses.

The key point: architecture governance runs on every commit, not quarterly in a review board. The review board still handles judgment calls — trade-offs, exceptions, strategic direction. The mechanical checks (ownership, relationship validity, required properties) are automated.


SPARQL views

In Structurizr, views are DSL declarations (container orderSystem { include * }). In this approach, views are SPARQL queries — more verbose but able to express things Structurizr can't.

Impact analysis (no Structurizr equivalent):

# What is affected if the Orders data object becomes unavailable?
PREFIX am:   <https://meta.linked.archi/archimate3/onto#>
PREFIX skos: <http://www.w3.org/2004/02/skos/core#>
PREFIX ex:   <https://model.example.com/orders#>

SELECT ?level ?affected ?label WHERE {
    {
        ?affected am:accesses ex:Order ;
                  skos:prefLabel ?label .
        BIND("1-component" AS ?level)
    } UNION {
        ?comp am:accesses ex:Order .
        ?comp am:assignedTo ?func .
        ?func am:realizes ?affected .
        ?affected a am:ApplicationService ;
                  skos:prefLabel ?label .
        BIND("2-service" AS ?level)
    } UNION {
        ?comp am:accesses ex:Order .
        ?comp am:assignedTo ?func .
        ?func am:realizes ?svc .
        ?svc am:realizes ?affected .
        ?affected a am:BusinessService ;
                  skos:prefLabel ?label .
        BIND("3-business-service" AS ?level)
    }
}
ORDER BY ?level

This traces from a data object through application components, through services, to business services — three layers of impact in one query.


SHACL governance-as-code

Beyond the ArchiMate relationship validity shapes (which validate the metamodel rules), you add project-specific governance:

@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix am: <https://meta.linked.archi/archimate3/onto#> .
@prefix bs: <https://meta.linked.archi/backstage/onto#> .

# Every Application Component must have an owner
[] a sh:NodeShape ;
    sh:targetClass am:ApplicationComponent ;
    sh:property [
        sh:path bs:ownedBy ;
        sh:minCount 1 ;
        sh:message "Application Component has no owner."@en ;
    ] .

# Every Application Service must realize a Business Service
[] a sh:NodeShape ;
    sh:targetClass am:ApplicationService ;
    sh:property [
        sh:path am:realizes ;
        sh:minCount 1 ;
        sh:class am:BusinessService ;
        sh:message "Application Service must realize at least one Business Service."@en ;
    ] .

Architecture governance runs on every commit. The review board handles judgment calls; the mechanical checks are automated.


The hybrid approach

You don't have to choose between Structurizr and Turtle. In practice:

  1. Developers maintain Structurizr DSL for day-to-day C4 diagrams (fast, familiar, great diagram output)
  2. The PlantUML/Structurizr converter extracts C4 elements into the RDF graph
  3. Enterprise architects maintain ArchiMate models in Archi (or write Turtle directly)
  4. The ArchiMate converter extracts into the same graph
  5. The unified graph connects C4 Software Systems to ArchiMate Application Components via shared identifiers
  6. SPARQL queries traverse across both — "for this C4 Container, what business capability does it support?"

Developers keep their workflow. Enterprise architects keep their precision. The graph connects them.


What you trade

  • Diagram quality. Structurizr generates polished, interactive diagrams natively. Generating equivalent visual quality from RDF requires the static navigator or custom generators — functional but less polished.
  • Terseness. Structurizr DSL is more concise for simple models. For a 3-container system, it's faster to write and easier to read.
  • Learning curve. Structurizr DSL is learnable in hours. Turtle + SPARQL basics take a day or two.
  • Ecosystem maturity. Structurizr has a focused, mature ecosystem (DSL, Lite, Cloud, CLI). The Linked.Archi tooling is newer and more modular.

What you gain: typed elements, typed relationships, cross-layer traceability, SHACL validation, SPARQL queryability, AI agent access, and composition with the full ontology ecosystem (decisions, quality attributes, TIME assessments, financial architecture).


References