Skip to content

Semantic Architecture as Code — Turtle Files in Git with SHACL CI/CD

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. But Structurizr DSL hits a wall when you need typed relationships, cross-layer queries, or governance validation.

RDF/Turtle gives you the same workflow with richer semantics. This guide shows the approach, compares it side-by-side with Structurizr DSL, and provides a working CI/CD pipeline you can adopt.


Side-by-Side — Structurizr DSL vs Turtle (ArchiMate Semantics)

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 labelled 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 Instead of DSL View Declarations

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 — Custom Rules in CI/CD

Beyond the ArchiMate relationship validity shapes (which validate the metamodel rules), you add project-specific governance. Paste these into a shapes file to enforce ownership and service traceability:

@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 — Structurizr DSL + Turtle Together

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 — Honest Comparison

  • 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