Le langage DSL

Le Structurizr DSL est un langage textuel dont le seul rôle est de décrire un workspace : les éléments, leurs relations, et les vues à en tirer. Sa syntaxe tient en une page, et c’est délibéré.

L’ossature

Trois blocs, dont le troisième est optionnel.

workspace "Billetterie" "Plateforme de réservation de spectacles" {

    model {
        // Les éléments et leurs relations
    }

    views {
        // Les vues à produire, et les styles
    }

    configuration {
        // Portée, visibilité, droits — surtout utile en version hébergée
    }
}

Les commentaires s’écrivent //, #, ou /* ... */. Les mots-clés sont insensibles à la casse — softwareSystem et softwaresystem sont équivalents — mais la convention en camelCase se respecte par lisibilité.

Warning

Le DSL est orienté ligne, et cela a deux conséquences qui surprennent :

  • une accolade ouvrante doit terminer sa ligne, et la fermante être seule sur la sienne. model { } ou model { /* ... */ } sur une seule ligne ne compilent pas — seuls les blocs styles échappent à la règle ;
  • les mots d’un commentaire en fin de ligne comptent comme des arguments. Une déclaration container acceptant au plus quatre arguments, db = container "Base" // un deux trois quatre échoue sur un « Too many tokens » parfaitement déroutant, alors que le même commentaire en deux mots passe.

La règle sûre est donc de placer les commentaires sur leur propre ligne. Les expressions à argument unique — include, exclude — n’ont pas ce problème.

Un exemple minimal complet

Le plus court fichier qui produise quelque chose d’utile :

workspace "Billetterie" {

    model {
        client = person "Client" "Réserve des places en ligne"

        billetterie = softwareSystem "Billetterie" "Vente et placement" {
            web = container "Application web" "Interface de réservation" "React"
            api = container "API" "Règles de placement et de tarification" "Spring Boot"
            db = container "Base de données" "Séances, réservations" "PostgreSQL" {
                tags "Base de données"
            }
        }

        paiement = softwareSystem "Prestataire de paiement" "Encaissement par carte" {
            tags "Externe"
        }

        client -> web "Réserve des places"
        web -> api "Appelle" "JSON / HTTPS"
        api -> db "Lit et écrit" "JDBC"
        api -> paiement "Débite la carte" "HTTPS"
    }

    views {
        systemContext billetterie "Contexte" {
            include *
            autolayout lr
        }

        container billetterie "Conteneurs" {
            include *
            autolayout lr
        }

        theme default
    }
}

Quinze lignes de modèle produisent deux diagrammes cohérents. Ajouter un troisième — déploiement, dynamique — ne demande que quelques lignes dans views, sans retoucher au modèle.

Les éléments du modèle

Chaque type d’élément suit la même forme : un mot-clé, puis des arguments positionnels, tous optionnels sauf le nom.

ÉlémentSignature
personperson <nom> [description] [étiquettes]
softwareSystemsoftwareSystem <nom> [description] [étiquettes]
containercontainer <nom> [description] [technologie] [étiquettes]
componentcomponent <nom> [description] [technologie] [étiquettes]
groupgroup <nom> { ... } — un regroupement visuel

L’imbrication reproduit celle du modèle C4 : les container se déclarent dans un softwareSystem, les component dans un container.

api = container "API" "Cœur métier" "Spring Boot" {
    controleurs = component "Contrôleurs REST" "Traduit HTTP" "Spring MVC"
    tarification = component "Moteur de tarification" "Règles de prix" "Java"
    depots = component "Dépôts" "Accès aux données" "Spring Data JPA"

    controleurs -> tarification "Calcule un tarif"
    tarification -> depots "Lit les grilles"
}

L’affectation à un identifiant — api = ... — est nécessaire dès qu’on veut référencer l’élément dans une relation ou une vue. Un élément jamais référencé peut être déclaré sans identifiant.

Les groupes

Un group rassemble visuellement des éléments sans créer de niveau d’abstraction — pratique pour matérialiser une équipe, un contexte délimité ou une zone réseau.

billetterie = softwareSystem "Billetterie" {
    group "Contexte Réservation" {
        api = container "API Réservation" "" "Spring Boot"
        db = container "Base réservations" "" "PostgreSQL"
    }
    group "Contexte Fidélité" {
        fidelite = container "API Fidélité" "" "Spring Boot"
        dbFid = container "Base fidélité" "" "PostgreSQL"
    }
}

Les relations

Une flèche, une description, et de préférence une technologie.

client -> web "Réserve des places"
web -> api "Appelle" "JSON / HTTPS"
api -> paiement "Débite la carte" "HTTPS" "Synchrone"

Les arguments sont, dans l’ordre : description, technologie, étiquettes. La description est ce qui distingue un diagramme lisible d’un plat de spaghettis : une flèche sans texte n’apprend rien.

Une relation peut aussi se déclarer dans un bloc, ce qui permet de lui donner des propriétés :

api -> db "Lit et écrit" "JDBC" {
    tags "Chemin critique"
    properties {
        "latence cible" "< 10 ms"
    }
}
Note

Structurizr crée automatiquement les relations implicites entre parents. Une relation web -> api entre deux conteneurs induit une relation entre leurs systèmes respectifs si ceux-ci diffèrent — ce qui fait qu’une vue de niveau 1 se remplit sans qu’on ait déclaré quoi que ce soit à ce niveau. C’est exactement le bénéfice du modèle unique, et cela se règle par la directive !impliedRelationships false si le comportement gêne.

Les identifiants

Par défaut, les identifiants sont plats : tous vivent dans le même espace de noms, et deux conteneurs nommés db dans deux systèmes différents entrent en collision. Sur un modèle qui grossit, cela impose des noms comme billetterieApiDb.

La directive !identifiers hierarchical corrige cela : les identifiants deviennent relatifs à leur parent, et se référencent par chemin.

workspace "Billetterie" {

    !identifiers hierarchical

    model {
        client = person "Client"

        billetterie = softwareSystem "Billetterie" {
            web = container "Application web"
            api = container "API" {
                controleurs = component "Contrôleurs"
            }
            db = container "Base de données"
        }

        fidelite = softwareSystem "Fidélité" {
            // Aucune collision avec billetterie.db
            db = container "Base de données"
        }

        client -> billetterie.web "Réserve"
        billetterie.api -> billetterie.db "Lit et écrit"
        billetterie.api -> fidelite "Crédite des points"
    }
}

C’est le mode à activer systématiquement dès que le modèle dépasse un système. Le coût est nul, et l’inverse — migrer un modèle plat devenu grand — est fastidieux.

Les étiquettes

Les étiquettes (tags) sont le mécanisme central de Structurizr : elles servent à styler, à filtrer les vues, et à porter de la sémantique. Chaque élément en reçoit d’office selon son type — Element, Person, Software System, Container, Component — et l’on ajoute les siennes.

db = container "Base de données" "" "PostgreSQL" {
    tags "Base de données" "Donnée personnelle"
}

paiement = softwareSystem "Prestataire de paiement" {
    tags "Externe" "SaaS"
}

Il vaut la peine d’y réfléchir un peu en amont : une étiquette Externe, Hérité, Déprécié ou Donnée personnelle permet ensuite de produire des vues thématiques — les systèmes tiers, la trajectoire de décommissionnement, la cartographie RGPD — sans toucher au modèle.

Le déploiement

Les nœuds de déploiement forment une hiérarchie parallèle, dans laquelle on instancie les conteneurs déjà déclarés.

production = deploymentEnvironment "Production" {

    deploymentNode "AWS" "" "Amazon Web Services" {
        deploymentNode "eu-west-3" "Région Paris" {

            alb = infrastructureNode "Load balancer" "" "Application Load Balancer"

            deploymentNode "Amazon ECS" "" "Fargate" {
                deploymentNode "Tâche API" "" "Docker" 3 {
                    apiInstance = containerInstance billetterie.api
                }
            }

            deploymentNode "Amazon RDS" "" "PostgreSQL 16" {
                deploymentNode "Instance primaire" {
                    containerInstance billetterie.db
                }
                deploymentNode "Réplique de lecture" {
                    containerInstance billetterie.db
                }
            }
        }
    }

    alb -> apiInstance "Répartit le trafic" "HTTPS"
}
Mot-cléRôle
deploymentEnvironment <nom>Un environnement : production, recette, développement
deploymentNode <nom> [desc] [techno] [étiquettes] [instances]Un nœud : région, cluster, machine, conteneur d’exécution
containerInstance <identifiant>Une instance d’un conteneur du modèle
softwareSystemInstance <identifiant>Une instance d’un système entier
infrastructureNode <nom> [desc] [techno]Un élément d’infrastructure sans code applicatif : répartiteur, pare-feu, DNS

Le dernier argument de deploymentNode est le nombre d’instances, affiché sur le diagramme. Les relations entre conteneurs sont reprises automatiquement entre leurs instances ; celles qui impliquent un infrastructureNode se déclarent à la main.

Tip

Le même modèle peut porter plusieurs environnements — production, recette, local — et donner autant de vues de déploiement. C’est un moyen très économique de documenter les écarts entre environnements, qui sont d’ordinaire décrits nulle part.

Modularité et directives

Les directives commencent par !. Ce sont elles qui permettent à un modèle de rester lisible au-delà de deux cents lignes.

DirectiveRôle
!identifiers hierarchicalIdentifiants relatifs au parent
!include <fichier|dossier|url>Insère un fragment de DSL
!const <nom> <valeur>Définit une constante, réutilisée par ${nom}
!docs <chemin>Rattache une documentation Markdown ou AsciiDoc
!adrs <chemin>Rattache un répertoire d’Architecture Decision Records
!extends <fichier|url>Étend un workspace existant au lieu d’en créer un
!impliedRelationships <true|false>Active ou coupe les relations implicites
!script <fichier>Exécute un script sur le modèle — Groovy, Kotlin, Ruby, JS
!plugin <classe>Branche une extension Java

Découper un gros modèle

L’organisation qui tient à l’usage :

architecture/
  workspace.dsl              workspace, includes, views
  model/
    personnes.dsl
    billetterie.dsl
    fidelite.dsl
    deploiement.dsl
  views/
    contexte.dsl
    conteneurs.dsl
  styles.dsl
  docs/
    01-introduction.md
    02-conteneurs.md
  adrs/
    0001-choix-de-postgresql.md
workspace "Billetterie" "Plateforme de réservation" {

    !identifiers hierarchical
    !const ORG "Kaizen"

    model {
        !include model/personnes.dsl
        !include model/billetterie.dsl
        !include model/fidelite.dsl
        !include model/deploiement.dsl
    }

    views {
        !include views/contexte.dsl
        !include views/conteneurs.dsl
        !include styles.dsl
    }

    !docs docs
    !adrs adrs
}

!include accepte un dossier, dont il inclut alors tous les .dsl par ordre alphabétique — pratique, mais l’inclusion explicite fichier par fichier documente mieux l’ordre, qui compte puisqu’un élément doit être déclaré avant d’être référencé.

Le squelette à réutiliser

Un point de départ complet, prêt à adapter :

workspace "Nom du système" "Une phrase de description" {

    !identifiers hierarchical

    model {
        utilisateur = person "Utilisateur" "Rôle principal"

        systeme = softwareSystem "Nom du système" "Ce qu'il fait" {
            web = container "Application web" "Interface" "React"
            api = container "API" "Règles métier" "Spring Boot"
            db = container "Base de données" "Persistance" "PostgreSQL" {
                tags "Base de données"
            }
        }

        tiers = softwareSystem "Système tiers" "Ce qu'il apporte" {
            tags "Externe"
        }

        utilisateur -> systeme.web "Utilise"
        systeme.web -> systeme.api "Appelle" "JSON / HTTPS"
        systeme.api -> systeme.db "Lit et écrit" "JDBC"
        systeme.api -> tiers "Interroge" "HTTPS"
    }

    views {
        systemContext systeme "Contexte" {
            include *
            autolayout lr
        }

        container systeme "Conteneurs" {
            include *
            autolayout lr
        }

        styles {
            element "Externe" {
                background #999999
                color #ffffff
            }
            element "Base de données" {
                shape cylinder
            }
        }

        theme default
    }
}

Les vues, les expressions de filtrage et les styles font l’objet de la page suivante.

Références