Beliebte Suchanfragen
//

Architecture docs as code with Structurizr & Asciidoctor. Part 5: Generating documentation

20.12.2022 | 18 minutes of reading time

You are reading the final part of this article series about architecture documentation as code. In the previous articles a workflow was implemented that aims to reduce the efforts for maintaining long-living architecture documentation, keep it up to date, and ensure its consistency. Now, let's take this approach one step further by extracting documentation-relevant information from code and using this information to generate textual documentation as well as elements of our Structurizr model automatically.

This part and the shown examples depend on the previous articles of this series. If you missed one of them, be sure to check them out:

The foundation for this is that we can access the code we want to document. The easiest way to achieve this is to store the documentation next to the code and make the compiled application code available on the classpath of the documentation source set, as explained in part two of this series.

All code in this article is based on the previous articles of this series and can be found on GitHub.

Generating documentation

The benefit of generating documentation from code automatically is that maintenance efforts to keep the documentation up to date are further reduced, and it's ensured that the documentation reflects our current architecture. However, this automation comes with an initial effort. Therefore, it should be considered how often the relevant aspects of the architecture change and how complex it is to automate their documentation. Ask yourself:

How long does it probably take until the initial effort for automation will be paid back by maintenance tasks you don't have to do manually as the related parts of the architecture change over time?

Since the answer to this question depends on the software to document, general advice regarding what parts of the documentation should be generated from code can't be given. Instead, we will go through some examples demonstrating the capabilities, efforts and benefits of this approach, based on the example “Inventory Service” software system introduced in part three.

First, we will go through multiple examples demonstrating how we can generate elements of our Structurizr model as well as AsciiDoc documents from configuration files. Afterwards, we will learn how to generate Structurizr component diagrams from code using the Structurizr for Java extensions employing different strategies and comparing their results.

Generating documentation from configuration

Many important aspects of an application can be configured using textual configuration files. Not rarely, these configuration files are written in YAML. The following three examples demonstrate how we can extract the information contained in these YAML files and use this information to generate elements of our Structurizr model as well as AsciiDoc documents automatically. Especially when you have to maintain multiple applications using the same approach regarding their configuration, a reusable automation for documenting the contained information in these files can save plenty of efforts.

Generating Kafka topic containers from application.yml

Let's have a look back at the container diagram of the Inventory Service. In part three, we created all elements of this diagram manually.

The container diagram of the Inventory Service.

Let's assume the warehouse team decides to version their Kafka topics, e.g. to handle breaking changes, or the Inventory Provider consumes an entirely new topic. Each time such a change happens, we would need to change the related Kafka topic components in the Structurizr model.

Let's further assume the Inventory Service is a Spring Boot service using Spring Cloud Stream Binder for Kafka, as described by my colleagues in their article (German). The definition of the Kafka topics consumed by the Inventory Service would be done in an application.yml file, as shown in the following, incomplete listing.

1spring:
2  application:
3    name: inventory-service
4  cloud:
5    stream:
6      bindings:
7        processStock-in-0:
8          group: ${spring.application.name}-consumer
9          destination: warehouse.stock
10        processGoods-in-0:
11          group: ${spring.application.name}-consumer
12          destination: warehouse.goods
13    function:
14      definition: processStock;processGoods

Only a few lines of Kotlin code are needed to parse the Kafka topic names from this configuration, e.g. using the Jackson YAMLMapper:

1private val yamlParser: ObjectMapper = YAMLMapper()
2private val appYaml = object {}::class.java.classLoader
3    .getResource("application.yml")?.readText() ?: ""
4
5fun parseTopicDestinations(): List<String> = yamlParser
6    .readTree(appYaml)
7    .path("spring").path("cloud").path("stream").path("bindings")
8    .fields()
9    .asSequence()
10    .map { it.value.get("destination").textValue() }
11    .toList()

These names can then be used to generate the Kafka topic-related Structurizr containers, as shown in the following listing.

1private val topicConsumedByInventoryService =
2    parseTopicDestinations().map { topicName ->
3        warehouse.kafkaTopic(topicName, resolveDesciptionByTopicName(topicName))
4    }
5
6private fun SoftwareSystem.kafkaTopic(
7    name: String,
8    description: String
9): Container = this.container(
10    name = name,
11    description = description,
12    c4Type = C4Type.QUEUE,
13    technology = "Kafka",
14    icon = "kafka",
15    link = "https://examplecompany.akhq.org/$name"
16)

As a result, changes related to the Kafka topics the Inventory Provider consumes are reflected automatically in our Structurizr model and therefore in our container view. There is no need anymore to update these aspects of the architecture documentation manually.

Generating deployment related properties from Helm charts

Another source for documentation-relevant information can be deployments, as long as they are described in a parsable format. Therefore, let's assume the following deployment of the Inventory Service is configured via Helm

The deployment diagram of the Inventory Service.

and we want to automate the documentation of the Inventory POD-related properties which are configured in a Values file as shown in the following listing.

1resources:
2  requests_cpu: 2000m
3  requests_memory: 1500Mi
4  limits_cpu: 3000m
5  limits_memory: 1500Mi
6autoscaling:
7  min_replicas: 2
8  max_replicas: 10
9port: 8080
10imagePullPolicy: IfNotPresent

To automate the documentation of these properties, we need to parse the Values file and flatten the contained YAML properties. The following listing shows how this can be done using the Jackson YAMLMapper.

1private val yamlParser: ObjectMapper = YAMLMapper()
2private val valuesYaml = File("./helm/inventory-service/values.yml").readText()
3
4fun resolveHelmValues(): List<List<String>> {
5    val test = yamlParser.readValue(
6        valuesYaml,
7        object : TypeReference<Map<String, Any>>() {}
8    )
9    val properties = mutableListOf<List<String>>()
10    flattenProperties(test, properties)
11    return properties.sortedBy { it.first() }
12}
13
14private fun flattenProperties(
15    propertyMap: Map<String, Any>,
16    keys: MutableList<List<String>>,
17    parentPath: String = ""
18) {
19    propertyMap.entries
20        .forEach { (propertyName, propertyValue): Map.Entry<String, Any> ->
21            val canonicalName = if (parentPath.isNotBlank()) {
22                "$parentPath.$propertyName"
23            } else {
24                propertyName
25            }
26            if (propertyValue is Map<*, *>) {
27                flattenProperties(
28                    propertyValue as Map<String, Any>, 
29                    keys, 
30                    canonicalName
31                )
32            } else {
33                keys.add(
34                    listOf(canonicalName, propertyValue.toString())
35                )
36            }
37        }
38}

Afterwards, we can use the result as C4Properties for the inventoryPod:

1private val inventoryPod = eks.deploymentNode(
2    name = "Inventory POD",
3    properties = C4Properties(
4        values = resolveHelmValues()
5    )
6)

In this way, we automated another important aspect of our architecture documentation, and we don't have to care anymore about the documentation of changing deployment properties.

Generating AsciiDoc documents from Prometheus rules

Besides elements of the Structurizr model, we can also generate AsciiDoc documents from configurations. For example, let's assume we configured Prometheus alerting rules using the Prometheus Operator and created a PrometheusRule custom resource with the following content.

1apiVersion: monitoring.coreos.com/v1
2kind: PrometheusRule
3metadata:
4  name: {{ .Chart.Name }}
5  namespace: {{ .Release.Namespace }}
6  labels:
7    prometheus: example
8spec:
9  groups:
10    - name: example
11      rules:
12      - alert: InstanceDown
13        expr: up == 0
14        for: 5m
15        labels:
16          severity: critical
17        annotations:
18          summary: "Instance {{ $labels.instance }} down"
19          description: | 
20            {{ $labels.instance }} of job {{ $labels.job }} 
21            has been down for more than 5 minutes.
22      - alert: APIHighRequestLatency
23        expr: api_http_request_latencies_second{quantile="0.5"} > 1
24        for: 10m
25        labels:
26          severity: critical
27        annotations:
28          summary: "High request latency on {{ $labels.instance }}"
29          description: |
30            {{ $labels.instance }} has a median request latency 
31            > 1s (current value: {{ $value }}s)

If something goes wrong, these alarms are triggered based on the expressions (expr) defined in the configuration. Therefore, it might make sense to document these alerts in order to facilitate operational tasks.

Usually there are more conditions under which an alarm should be triggered and thus, more alarming rules. To avoid documenting all of them manually we can parse the alarm configuration and map it to a list of type Alert as shown in the following listing.

1private val yamlParser: ObjectMapper = YAMLMapper()
2    .registerModule(KotlinModule.Builder().build())
3    .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
4
5// matches variables and stores reference $variablename in group $1
6private val variablesRegex = """\{\{\s*(.*?)\s*\}\}""".toRegex()
7
8private fun parseAlertingRules(): List<Alert> {
9    val rulesYaml = File(
10        "./helm/inventory-service/templates/prometheus-rules.yaml"
11    ).readText().replace(variablesRegex, "$1")
12    val yamlAlerts = yamlParser.readTree(rulesYaml).findPath("rules")
13    return yamlParser.readValue(
14        yamlAlerts.toPrettyString(),
15        object : TypeReference<List<Alert>>() {}
16    )
17}
18
19data class Alert(
20    val alert: String,
21    val expr: String,
22    val `for`: String,
23    val annotations: Annotations,
24    val labels: Labels
25) {
26    companion object {
27        const val ASCII_TABLE_HEADER = 
28            "|Name |Severity |Summary |Description |Expression |Time range"
29    }
30
31    private val invalidSubstrings = listOf("\n", "|")
32
33    fun toAsciiTable(): String {
34        return """
35           || ${alert.sanitize()}
36           || ${labels.severity.sanitize()}
37           || ${annotations.summary.sanitize()}
38           || ${annotations.description.sanitize()}
39           ||`${expr.sanitize()}`
40           || ${`for`.sanitize()}
41        """.trimIndent()
42    }
43
44    private fun String?.sanitize(): String {
45        var replaced = this
46        invalidSubstrings.forEach { replaced = replaced?.replace(it, "") }
47        return replaced ?: " "
48    }
49
50    data class Labels(val severity: String)
51
52    data class Annotations(val summary: String, val description: String?)
53}

The Alert list can then be converted to AsciiDoc, as shown in the following listing.

1fun createAsciiAlertTable(): String {
2    val alertTableRows = parseAlertingRules()
3        .joinToString(separator = "\n\n") { it.toAsciiTable() }
4    return """
5        ||===
6        |${Alert.ASCII_TABLE_HEADER}
7        
8        $alertTableRows
9        
10        ||===
11    """.trimMargin()
12}

This results in the following AsciiDoc table, which should be stored in a separate file and regenerated each time we generate our documentation.

1|===
2|Name |Severity |Summary |Description |Expression |Time range
3
4| InstanceDown
5| critical
6| Instance $labels.instance down
7| $labels.instance of job $labels.job has been down for more than 5 minutes.
8|`up == 0`
9| 5m
10
11| APIHighRequestLatency
12| critical
13| High request | latency on $labels.instance
14| $labels.instance has a median request latency > 1s (current value: $values)
15|`api_http_request_latencies_second{quantile="0.5"} > 1`
16| 10m
17|===

After including this file in our documentation and converting it to HTML, we get the following documentation of our alerting rules.

The generated alert table in AsciiDoc.

As a result, colleagues responsible for operational tasks always have an up-to-date documentation of the alarming rules they can use to interpret triggered alerts.

Generating component diagrams using Structurizr analysis

In part three we created a context and container diagram manually but skipped the third core diagram type of the C4 model, the component diagram. We skipped component diagrams because they give a detailed, technical view on the components inside a container. Therefore, component diagrams can become obsolete fast and thus need a lot of maintenance effort to keep them up to date manually. Instead, it is recommended to generate them from code automatically if they provide value to the reader.

For this purpose, the Structurizr for Java extensions provides a Structurizr analysis library containing a ComponentFinder which allows for the extraction of components from the compiled application bytecode based on one or more strategies. These strategies define the search criteria the ComponentFinder is using to extract the components. The Structurizr for Java extensions provides multiple of those strategies. We will use and compare them in the following sections. Additionally to these predefined strategies, custom strategies can be implemented.

The general usage of the ComponentFinder is independent of the chosen strategies. First, we need to add the Structurizr analysis dependency to the docsImplementation source set:

1"docsImplementation"("com.structurizr:structurizr-analysis:1.3.5")

Afterwards, we create a new Kotlin object called Components. Inside the init method of this object, we create, configure and execute the ComponentFinder as shown in the following listing.

1object Components {
2
3    init {
4        val strategy = createComponentFinderStrategy()
5
6        strategy.duplicateComponentStrategy = IgnoreDuplicateComponentStrategy()
7
8        val componentFinder = ComponentFinder(
9            inventoryProvider,
10            "example.webshop.inventory",
11            strategy
12        )
13
14        componentFinder.urlClassLoader = URLClassLoader(
15            ClasspathHelper
16                .forPackage("example.webshop.inventory")
17                .toTypedArray()
18        )
19
20        componentFinder.findComponents()
21    }
22}

We create a ComponentFinderStrategy by calling the method createComponentFinderStrategy. We will implement this method in the next sections using the different strategies.

The ComponentFinder traverses the compiled bytecode in order to find components using one or more strategies. Therefore, the same component can be found multiple times resulting in duplicate components, which are not allowed by the Structurizr model. To handle duplicate components, we need to define a duplicateComponentStrategy. The IgnoreDuplicateComponentStrategy we use in the listing always takes the first found component and ignores all later found duplicates.

Now that we have initialized our strategy and configured how we handle duplicate components, we can initialize the ComponentFinder. We want to find components for the inventoryProvider container we created in part three of this series. The root package where the components to be found are located is "example.webshop.inventory" and we want to use the just defined strategy to find the components.

The strategies we will use in the following sections are employing reflection on the compiled bytecode to find components. Therefore, a reflection library is used which has an issue when executed from a JAR file. To fix this issue, we have to set the urlClassLoader for the ComponentFinder manually, providing an array of the relevant classpath URLs.

Afterwards, we can execute the findComponents method which will add the found components to the Structurizr model.

To create a component diagram showing the found components for the inventoryProvider, we implement the method createComponentView inside the Component Kotlin object. This method adds a component view for the inventoryProvider to the InventoryWorkspace views as shown in the following listing.

1fun createComponentView() {
2    val componentView = InventoryWorkspace.views.componentView(
3        container = inventoryProvider,
4        key = "inventory_components",
5        description = 
6            "Component diagram for the ${inventoryProvider.name} service",
7    )
8    componentView.addAllComponents()

Finally, we call this method inside our main method located in the WriteDiagrams file created in part three of this series.

1Components.createComponentView()

Thus, our basic algorithm to create the component diagram is ready, and we can implement the strategies.

Extracting components using the SpringComponentFinderStrategy

If your application is a Spring application, the easiest way to generate component diagrams from code is based on the Spring annotations. The Structurizr for Java extensions provides dedicated strategies to extract components based on the Spring annotations @Controller, @RestController, @Component, @Service, @Repository, and those that extend JpaRepository. Furthermore, there is a strategy which combines all these strategies named SpringComponentFinderStrategy which we will use in this section.

Since the Spring-related strategies are not part of the default strategies provided by the Structurizr analysis library, we need to add the following additional dependency for the docsImplementation source set:

1"docsImplementation"("com.structurizr:structurizr-spring:1.3.5")

Furthermore, to use the SpringComponentFinderStrategy, we need to initialize it in the createComponentFinderStrategy method we defined in the previous section, as shown in the following listing.

1private fun createComponentFinderStrategy(): AbstractComponentFinderStrategy =
2    StructurizrAnnotationsComponentFinderStrategy()

Let's assume that the Inventory Provider consists of the Spring-annotated types shown in the following code listing.

1package example.webshop.inventory
2
3abstract class AbstractQuery
4
5@Controller
6class InventoryQuery(
7    private val inventoryReadService: InventoryReadService
8): AbstractQuery()
9
10interface Consumer
11
12@Component
13class GoodsConsumer(
14    private val inventoryWriteService: InventoryWriteService
15): Consumer
16
17@Component
18class StockConsumer(
19    private val inventoryWriteService: InventoryWriteService
20): Consumer
21
22@Component
23class OrderClient
24
25@Service
26class InventoryReadService(
27    private val inventoryRepository: InventoryRepository
28)
29
30@Service
31class InventoryWriteService(
32    private val inventoryRepository: InventoryRepository,
33    private val orderClient: OrderClient
34)
35
36@Repository
37class InventoryRepository

After executing our main method, we get the diagram shown in the following listing.

The component diagram of the Inventory Provider created using the SpringComponentFinderStrategy.

If we compare the code listing with the resulting diagram, we will notice that the GoodsConsumer and the StockConsumer implementation classes are missing. Instead, the Consumer interface is part of the diagram. The reason for this is that the SpringComponentFinderStrategy prefers interfaces to implementation classes. This behavior only applies for interfaces. The abstract class AbstractQuery is instead missing from the diagram. Details regarding this behavior can be found in the documentation of the Structurizr for Java extensions.

Furthermore, only the annotation types are added for the components, but any descriptions and additional information is missing. This is expected, since the SpringComponentFinderStrategy can't add information which is not present in the code. Nonetheless, without any descriptions, the diagram likely does not provides much value to readers.

To add more information to the components, we either need to add some custom logic after the components were extracted as shown in the following listing,

1inventoryProvider.components
2    .filter {
3        it.technology.equals(SpringComponentFinderStrategy.SPRING_REPOSITORY)
4    }.map {
5        it.description = "Spring JDBC Repository"
6     }
7   componentView.addAllComponents()

or we need to use a different strategy such as the TypeMatcherComponentFinderStrategy.

Extracting components using the TypeMatcherComponentFinderStrategy

Compared to the SpringComponentFinderStrategy, the TypeMatcherComponentFinderStrategy gives us more control over the components and information added to the resulting diagrams. Furthermore, it is included in the Structurizr analysis library. Thus, no additional dependencies are needed. However, we have to configure the strategy by defining type matchers.

The TypeMatcherComponentFinderStrategy supports the following list of type matchers, which can be extended by custom implementation:

  • AnnotationTypeMatcher
  • ExtendsClassTypeMatcher
  • ImplementsInterfaceTypeMatcher
  • NameSuffixTypeMatcher
  • RegexTypeMatcher

The following listing shows an implementation of the createComponentFinderStrategy using the TypeMatcherComponentFinderStrategy.

1private fun createComponentFinderStrategy(): AbstractComponentFinderStrategy =
2    TypeMatcherComponentFinderStrategy(
3        NameSuffixTypeMatcher(
4            "WriteService", 
5            "Writes data to database and triggers orders if goods run out of stock", 
6            "Spring Service"
7        ),
8        NameSuffixTypeMatcher(
9            "ReadService", 
10            "Reads data from database", 
11            "Spring Service"
12        ),
13        AnnotationTypeMatcher(
14            Repository::class.java, 
15            "Spring JDBC Repository", 
16            "Spring JDBC"
17        ),
18        ImplementsInterfaceTypeMatcher(
19            Consumer::class.java, 
20            "Consumes Kafka topic", 
21            "Spring Cloud Stream Binder Kafka"
22        ),
23        ExtendsClassTypeMatcher(
24            AbstractQuery::class.java, 
25            "GraphQL query", 
26            "Spring Controller"
27        ),
28        RegexTypeMatcher(
29            ".*\\.OrderClient",
30            "Sends orders via POST to order service if goods run out of stock",
31            "Spring RestTemplate"
32        )
33    )

The resulting component diagram is shown in the following figure.

The component diagram of the Inventory Provider created using the TypeMatcherComponentFinderStrategy.

Different from the diagram created with the SpringComponentFinderStrategy, all components we expect are part of the diagram. Maybe even too much.

Let's assume we won't extract the AbstractQuery class and the Consumer interface. To achieve this, we could adapt the type matchers. Another way is to define excludes for the ComponentFinder, which work independently of the chosen strategy. The excludes work with regular expressions which are matched against the full canonical name of the component. If we define the following excludes for the ComponentFinder before we call the findComponents method,

1componentFinder.exclude(".*\\.Abstract.*", ".*\\.Consumer")

any component with a simple name starting with "Abstract" or with the name "Consumer" will be excluded and not added to the Structurizr model. As a result, the following diagram is created.

The Component diagram created using the TypeMatcherComponentFinderStrategy without the AbstractQuery and the Consumer interface.

What the diagram is still missing is descriptions of the relationships between the components. To add them, we can again add some custom logic after the components were extracted or use the StructurizrAnnotationsComponentFinderStrategy.

Extracting Components using the StructurizrAnnotationsComponentFinderStrategy

The StructurizrAnnotationsComponentFinderStrategy extracts components based on custom annotations which need to be added to the code we want to analyze. To use these annotations, we have to add the Structurizr annotations dependency for the main source set.

1implementation("com.structurizr:structurizr-annotations:1.3.5")

The Structurizr annotations library provides the following annotations:

  • @Component
  • @UsesComponent
  • @UsedByPerson
  • @UsedBySoftwareSystem
  • @UsesSoftwareSystem
  • @UsedByContainer
  • @UsesContainer

How these annotations can be used with our Inventory Provider example is shown in the following listing. In the listing, the Spring annotations were removed to avoid name clashes between the Structurizr @Component annotation and the Spring @Component annotation. For real world applications, this can easily be avoided by introducing an import alias.

1package example.webshop.inventory
2
3abstract class AbstractQuery
4
5@Component(
6    description = "Query providing inventory data to clients", 
7    technology = "GraphQL"
8)
9@UsesContainer(
10    name = "Container://GraphQL Federation.subgraph-inventory", 
11    description = "Is part of"
12)
13class InventoryQuery(
14    @UsesComponent(description = "Reads inventory data using")
15    private val inventoryReadService: InventoryReadService
16)
17
18interface Consumer
19
20@Component(description = "Consumes goods Kafka topic")
21@UsesContainer(
22    name = "Container://Warehouse.warehousegoods", 
23    description = "Consumes", technology = "Kafka"
24)
25class GoodsConsumer(
26    @UsesComponent(description = "Writes received goods via")
27    private val inventoryWriteService: InventoryWriteService
28) : Consumer
29
30@Component(description = "Consumes stock Kafka topic")
31@UsesContainer(
32    name = "Container://Warehouse.warehousestock", 
33    description = "Consumes", 
34    technology = "Kafka"
35)
36class StockConsumer(
37    @UsesComponent(description = "Writes received stock data")
38    private val inventoryWriteService: InventoryWriteService
39) : Consumer
40
41@Component(description = "Sends orders via POST to Order service")
42@UsesSoftwareSystem(
43    name = "Order service", 
44    description = "Sends orders via POST requests to", 
45    technology = "REST"
46)
47class OrderClient
48
49@Component(description = "Service reading inventory data")
50class InventoryReadService(
51    @UsesComponent(description = "Reads inventory data using")
52    private val inventoryRepository: InventoryRepository
53)
54
55@Component(description = "Writes Inventory data to database " +
56    "and triggers orders if goods run out of stock"
57)
58class InventoryWriteService(
59    @UsesComponent(description = "Writes Inventory data to")
60    private val inventoryRepository: InventoryRepository,
61    @UsesComponent(description = "Uses to order new goods")
62    private val orderClient: OrderClient
63)
64
65@Component(description = "JDBC repository reading/writing inventory data")
66@UsesContainer(
67    name = "Inventory Database", 
68    description = "Reads/writes Inventory data", 
69    technology = "JDBC"
70)
71class InventoryRepository

As the listing shows, for each annotation we can set a description and a technology. Furthermore, the listing shows how we can annotate relationships to components, software systems and containers using their name. For relationships to components and software systems, there simple name can be used to reference them, e.g. "Order service". For containers, it depends on if the container is part of the same software system as the component. If that's the case, the simple name of the container can be used. In our example that's the case for the "Inventory Database" which is used by the InventoryRepository. Both belong to the Software system Inventory Service. In contrast, the warehouse.goods container referenced by the GoodsConsumer is part of the Software system Warehouse. Therefore, we have to reference it using the full canonical name of the container "Container://Warehouse.warehousegoods".

To extract components based on the Structurizr annotations, we have to initialize the StructurizrAnnotationsComponentFinderStrategy in the createComponentFinderStrategy method.

1private fun createComponentFinderStrategy(): AbstractComponentFinderStrategy = 
2    StructurizrAnnotationsComponentFinderStrategy()

To visualize the software systems and containers in the component diagram, we need to adapt the createComponentView method. We add the related elements to the component diagram by adding all nearest neighbors of the inventoryProvider components. Furthermore, we add a custom C4PlantUmlLayout to lay out the dependencies. The adapted createComponentView method is shown in the following listing.

1fun createComponentView() {
2    val componentView = InventoryWorkspace.views.componentView(
3        container = inventoryProvider,
4        key = "inventory_components",
5        description = "Component diagram for the ${inventoryProvider.name} service",
6        layout = C4PlantUmlLayout(
7            dependencyConfigurations = listOf(
8                DependencyConfiguration(
9                    filter = { 
10                        it.destination.parent == Systems.warehouse 
11                        || it.destination == subGraphInventory 
12                    },
13                    direction = Direction.Up
14                )
15            )
16        )
17    )
18    componentView.addAllComponents()
19    inventoryProvider.components.forEach {
20        componentView.addNearestNeighbours(it)
21    }
22}

As a result, the following component diagram is created.

The Component diagram of the Inventory Provider created using the StructurizrAnnotationsComponentFinderStrategy.

As the diagram shows, the StructurizrAnnotationsComponentFinderStrategy provides the most detailed and informative diagrams. The downside of this strategy is that additional annotations need to be added to the implementation code, which can have an impact on the readability.

Summary

In this article, we learned how to further automate the architecture documentation approach that was implemented in the previous articles by generating documentation from code. The benefit of this is that maintenance efforts for keeping the documentation up to date are further reduced, and it's ensured that the documentation reflects our current architecture.

We went through multiple examples demonstrating how we can generate elements of our Structurizr model as well as AsciiDoc documents from YAML configuration files. Afterwards, we learned how to generate component diagrams from code using the different strategies provided by the Structurizr for Java extensions and compared the results.

Since this part marks the end of this article series, I am interested in your opinion. Did you find it useful? How do you document your architecture? Leave me a comment and let me know.

share post

//

More articles in this subject area

Discover exciting further topics and let the codecentric world inspire you.

//

Gemeinsam bessere Projekte umsetzen.

Wir helfen deinem Unternehmen.

Du stehst vor einer großen IT-Herausforderung? Wir sorgen für eine maßgeschneiderte Unterstützung. Informiere dich jetzt.

Hilf uns, noch besser zu werden.

Wir sind immer auf der Suche nach neuen Talenten. Auch für dich ist die passende Stelle dabei.