Introduction
Changing software diagrams is hard. The simple act of adding a new box may require us to drag all the existing boxes and reorganize the diagram. This is one of the main reasons to why software diagrams are constantly left deprecated after the first stages of the development process.
In this post I’ll show how defining diagrams as code can help in designing and updating software diagrams, and how to automate the process of updating the documentation with those diagrams.
Why create diagrams as code?
- Easy to change: Just change the code and the elements of the diagram are rendered in a good position (sometimes it may need some tweeking);
- Reuse of code: Components, sprites, and functions can be defined and shared to be used in other diagrams. We can use loops and conditions to make those pieces of code even more reusable. Details here;
- History of changes: Because it is code, their versions can be tracked and compared with version control systems, like Git, for example;
- Single style in the whole diagram: Unless explicited, all the elements of the diagram will have the same style, no need to copy style from one element to another or having to resize all boxes after change the size of one;
- Inclusive: Everybody in the team can checkout the code and change it without fear because changes can be tracked and the style is the same for everyone.
PlantUML
PlantUML is a highly customizable open-source tool that allow us to create diagrams using code. Despite de name, it supports many types of diagrams besides UML diagrams. It has it’s own language and some extensions to other languages like AsciiMath, Creole and LaTeX.
PlantUML is a Java Command Line tool. We can run it from the command line, but the best experience is with a Visual Studio Code extension.
Rendering from Visual Studio Code
There is an extension that integrates PlantUML with Visual Studio Code.
It offers syntax hightlighting and a preview of the diagram on the side while editing, and options to export the current or all project diagrams, besides other features.
After installing the extension, open the command pallete and search for PlantUML
to see the available options.
Rendering from the command line
First, download the compiled JAR from the downloads page or from the GitHub releases page.
ℹ️ You may want to include the path to the plantuml.jar
file in the PATH
environment variable to be able to use it in any directory.
Then, to generate the diagram for one source file, just run the following command:
java -jar plantuml.jar Sequence.puml
We can also generate the diagrams for more than one file using glob patterns:
java -jar plantuml.jar *.puml
C4 Diagrams
The C4 model is a different approach to designing software architecture diagrams. I’ve talked about it in my previous post.
PlantUML has native support for C4 Diagrams. We just need to include the library and use its elements.
Here are some examples:
System Context diagram
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| @startuml C4_SystemContext
!include <C4/C4_Context>
left to right direction
Person(user, "User", "Company employee who has access to the HR system")
System(hrSystem, "HR System", "Allows users to manage personal data and contract of the company employees")
System_Ext(emailSystem, "E-mail System", "Responsible for queueing and sending e-mails")
Rel(user, hrSystem, "Create and change employee personal and contract information", "")
Rel(hrSystem, emailSystem, "Sends notification e-mails using", "")
@enduml
|
Containers diagram
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
| @startuml C4_HRSystem_Containers
!define DEVICONS https://raw.githubusercontent.com/tupadr3/plantuml-icon-font-sprites/master/devicons
!define AWSPuml https://raw.githubusercontent.com/awslabs/aws-icons-for-plantuml/v14.0/dist
!include <C4/C4_Container>
!include DEVICONS/msql_server.puml
!include DEVICONS/dotnet.puml
!include AWSPuml/AWSCommon.puml
!include AWSPuml/ApplicationIntegration/SimpleQueueServiceQueue.puml
left to right direction
Person(user, "User", "Company employee who has access to the HR system")
System_Boundary(hrSystem, "HR System") {
Container(webApp, "Web Application", "ASP.NET 7 Application", "Provides the system functionalities through the web browser", $sprite="dotnet")
Container(backgroundService, "E-mail service", ".NET 7 Application", "Background service that reads a queue for employee data changes and sends notification e-mails to the employees", $sprite="dotnet")
ContainerDb(database, "Database", "SQL Server 2022", "Holds employee and contract data", $sprite="msql_server")
ContainerQueue(emailQueue, "Queue", "AWS SQS", "Holds employee data changes", $sprite="SimpleQueueServiceQueue")
}
System_Ext(emailSystem, "E-mail System", "Responsible for queueing and sending e-mails")
Rel(user, webApp, "Create and change employee personal and contract information", "")
Rel(webApp, database, "Reads / Writes", "")
Rel(webApp, emailQueue, "Writes notifications to", "")
Rel(backgroundService, emailQueue, "Reads notifications from", "")
Rel(backgroundService, database, "Reads employee data from", "")
Rel(backgroundService, emailSystem, "Sends e-mails using", "")
@enduml
|
Components diagram
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
| @startuml C4_HRSystem_WebApp_Components
!define DEVICONS https://raw.githubusercontent.com/tupadr3/plantuml-icon-font-sprites/master/devicons
!define AWSPuml https://raw.githubusercontent.com/awslabs/aws-icons-for-plantuml/v14.0/dist
!include <C4/C4_Component>
!include DEVICONS/msql_server.puml
!include AWSPuml/AWSCommon.puml
!include AWSPuml/ApplicationIntegration/SimpleQueueServiceQueue.puml
left to right direction
Container(webApp, "Web Application", "ASP.NET 7 Application", "Provides the system functionalities through the web browser", $sprite="dotnet")
Container_Boundary(webApp, "Web Application") {
Component(employeesController, "Employees Controller", "Provides access to the employees related functionalities")
Component(registerEmployeesUseCase, "Register Employee Use Case", "Orchestrate the use case of registering a new employee")
Component(employeeDataQueueService, "Employee Data Queue Service", "Provides functionalities to communicate with the queue")
Component(employeeRepository, "Employee Repository", "Provides functionalities to communicate with the employee database table")
Component(loginController, "Login Controller", "ASP.NET Core Controller", "Allow users to authenticate in the web application")
Rel(employeesController, registerEmployeesUseCase, "Uses")
Rel(registerEmployeesUseCase, employeeDataQueueService, "Uses")
Rel(registerEmployeesUseCase, employeeRepository, "Uses")
}
ContainerDb(database, "Database", "SQL Server 2022", "Holds employee and contract data", $sprite="msql_server")
ContainerQueue(emailQueue, "Queue", "AWS SQS", "Holds employee data changes", $sprite="SimpleQueueServiceQueue")
Rel(employeeRepository, database, "Writes employee information", "")
Rel(employeeDataQueueService, emailQueue, "Writes notifications to", "")
@enduml
|
More examples
Here are more examples of what can be done with PlantUML.
Variables and colors
In this example, I created a sequence diagram and used variables for reusing the formatted HTTP verbs in the messages:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| @startuml SequenceDiagram
!$get_method = "<font color=lime><b>GET</b></font>"
!$post_method = "<font color=blue><b>POST</b></font>"
participant "Frontend" as Frontend
participant "BFF" as BFF
participant "PokéAPI" as PokeAPI
database "Cache" as Cache
Frontend -> BFF : $get_method /pokemondata/{name}
BFF -> Cache : $get_method Search for data in the cache
BFF <-- Cache : Cached data
alt data not found in cache
BFF -> PokeAPI : $get_method /pokemon/{name}
BFF <-- PokeAPI : Pokemon data
BFF -> Cache : $post_method Save data in the cache
end
Frontend <-- BFF : Return pokémon data
@enduml
|
Visualization of JSON data
One cool diagram that PlantUML can generate is the JSON diagram, showing the properties and data of a JSON. To generate a JSON diagram, just use the @startjson
and @endjson
symbols and paste the JSON content between them.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
| @startjson JSONDiagram
{
"abilities": [
{
"ability": {
"name": "blaze",
"url": "https://pokeapi.co/api/v2/ability/66/"
},
"is_hidden": false,
"slot": 1
},
{
"ability": {
"name": "solar-power",
"url": "https://pokeapi.co/api/v2/ability/94/"
},
"is_hidden": true,
"slot": 3
}
],
"base_experience": 267,
"forms": [
{
"name": "charizard",
"url": "https://pokeapi.co/api/v2/pokemon-form/6/"
}
],
"height": 17,
"held_items": [],
"id": 6,
"is_default": true,
"location_area_encounters": "https://pokeapi.co/api/v2/pokemon/6/encounters",
"name": "charizard",
"order": 7,
"past_types": [],
"species": {
"name": "charizard",
"url": "https://pokeapi.co/api/v2/pokemon-species/6/"
},
"types": [
{
"slot": 1,
"type": {
"name": "fire",
"url": "https://pokeapi.co/api/v2/type/10/"
}
},
{
"slot": 2,
"type": {
"name": "flying",
"url": "https://pokeapi.co/api/v2/type/3/"
}
}
],
"weight": 905
}
@endjson
|
Importing custom elements
PlantUML is extensible, so we can create or import custom elements to use in our diagrams. One example is the AWS Icons for PlantUML. It has elements to represent most of the main AWS Services.
To use it, we need to import the custom elements with the !import
command.
In this example, I used the !define
command to define a variable AWSPuml
with the base URL and used it in all imports. This helps when we need to update the version of the objects and also makes the code cleaner.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| @startuml InfrastructureDiagram
left to right direction
!define AWSPuml https://raw.githubusercontent.com/awslabs/aws-icons-for-plantuml/v14.0/dist
!include AWSPuml/AWSCommon.puml
!include AWSPuml/NetworkingContentDelivery/CloudFront.puml
!include AWSPuml/Compute/Lambda.puml
!include AWSPuml/Storage/SimpleStorageService.puml
!include AWSPuml/Database/ElastiCache.puml
actor "User" as User
CloudFront(CloudFront, "CloudFront", "")
SimpleStorageService(S3, "S3 Bucket", "Angular App")
Lambda(Bff, "BFF", "ASP.NET Core")
ElastiCache(Redis, "Cache", "Redis")
User --> CloudFront
CloudFront --> S3
S3 --> Bff
Bff --> Redis
@enduml
|
Themes
PlantUML support some themes by default. Just use the !theme
command followed by the theme name:
1
2
3
4
| @startuml
!theme materia
...
@enduml
|
Here is the previous sequence diagram with the Materia theme:
Here we can see a gallery with the available themes.
Automating the diagram publication
I’ve created a documentation site as an example of how to generate PlantUML diagrams in the CI/CD pipeline. When the diagrams are commited to the repository, the pipeline renders then as images and the documentation is automatically updated.
https://dgenezini.github.io/docs-sample/
It uses:
To render the diagrams as images and commit then to repository, configure a new job generate_plantuml
with the code below:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| jobs:
generate_plantuml:
runs-on: ubuntu-latest
name: plantuml
steps:
- name: checkout
uses: actions/checkout@v1
with:
fetch-depth: 1
- name: plantuml
id: plantuml
uses: grassedge/generate-plantuml-action@v1.5
with:
path: diagrams
message: "Render PlantUML files"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
Then, just reference the images in the documentation files:
![](/docs-sample/diagrams/2-containers.svg)
The full pipeline configuration can be seen here.
The source repository is here.
Diagram as code 2.0
Simon Brown, creator of the C4 model, has a very interesting concept called Diagram as code 2.0 in which one model code can generate multiple diagrams. More details in his blog post.
He built a tool called Structurizr for this.
References and Links