How to Create Software Architecture Diagrams Using the C4 Model

Alex PliutauAlex Pliutau
8 min read

As a developer, you'll likely work on a complex project at some point where deciphering the codebase feels like reading a whole novel. Engineers are code wizards, but even the best get lost in sprawling code.

The challenge is that architecture diagrams – if they even exist – are often outdated relics from a bygone era.

This is why creating and maintaining effective and clear diagrams should be effortless. Up-to-date visuals ensure everyone stays on the same page, eliminating confusion and wasted time.

Table of Contents

What is the C4 Model?

The C4 model was created as a way to help software development teams describe and communicate software architecture.

C4 stands for “Context, Containers, Components, and Code”. Those are the four levels that should be enough to describe a complex system.

The best way to explain the concept is to think about how we use Google Maps. When we are exploring an area in Google Maps, we will often start zoomed out to help us get context. Once we find the rough area we are interested in we can zoom in to get a little more detail.

Level 1: Context

This level is the most zoomed out. It's a bird’s eye view of the system in the greater context of the world. The diagram concentrates on actors and systems.

For the examples below, we will use a simple Task Management Software System to demonstrate all these 4 levels.

Diagram showing the context Level

This diagram portrays the Task Management Software System's interactions with external systems and the different user groups that utilize it. We can see that the Task Management software relies on two external systems: Email and Calendar, and two types of actors (users) use it: Customer and Admin User.

Level 2: Containers

The containers level is a more detailed view of your system (don’t confuse C4 containers with Docker containers).

It reveals how various functional units like applications and databases work together and distribute responsibilities.

This diagram also highlights the key technologies employed and showcases the communication flow between these containers. It presents a simplified, technology-centric view of the system's core components and their interactions.

If you have Microservice architecture, then each Microservice would be a container.

Examples of containers are:

  • Single page application

  • Web server

  • Serverless function

  • Database

  • API

  • Message buses

And so on.

Diagram showing the Containers Level

This level delves into the internal composition of the Task Management Software System. It showcases that our Task Management software system consists of containers such as User Web UI, Admin Web UI, API and a Database. API is also the container that is connected to external systems, for example to send emails or create events in calendar.

Level 3: Components

The next level of zoom is components. This shows the major structural building blocks of your application, and is often a conceptual view of the application. The term component is loose here. It could represent a controller or a service containing business logic.

Diagram showing the Components Level

This diagram focuses on the internal structure of the API container within the Task Management Software System. It reveals that the API container houses crucial functionalities like CRUD operations (Create, Read, Update, Delete) for data manipulation and user authentication mechanisms. The CRUD components is the one that talks to the database.

Level 4: Code

The deepest level of zoom is the code diagram. Although this diagram exists, it is often not used as the code paints a very similar picture. However, in highly regulated environments and complex legacy projects this level can help to paint a better picture of inner intricacies of the software.

Supplementary Diagrams

Besides the 4 diagrams above, there are a couple more worth mentioning:

  • Deployment diagram

  • Dynamic diagram: to describe the process or a flow

Login Flow

On this diagram we show a Login Flow, which is not a container or component, but rather a software process that happens in our software system. It shows that Web/Admin UIs use JWT-based authentication for communication with the API and the JWT token is stored in local storage on a client side.

Diagrams as Code

The power of C4 comes with a diagram-as-code approach. This means treating your diagrams just like your codebase:

  • Version control: Store them in a source control system (like Git) for easy tracking and collaboration.

  • Collaboration: Work together on diagrams using pull requests, similar to code reviews.

  • Automation: Integrate them into your build pipelines for automatic rendering with your preferred tools.

Helpful Tool: Structurizr

There are few tools to help with modeling and diagramming, but the most popular nowadays is Structurizr with their custom DSL (Domain Specific Language).

All you need is to get familiar with the DSL syntax, which is pretty simple. As long as you get used to it you will be able to create or update diagrams in no time.

Below you can see the DSL for our Task Management Software System.

workspace {
    model {
        # Actors
        customer = person "Customer" "" "person"
        admin = person "Admin User" "" "person"

        # External systems
        emailSystem = softwareSystem "Email System" "Mailgun" "external"
        calendarSystem = softwareSystem "Calendar System" "Calendly" "external"

        # Task Management System
        taskManagementSystem  = softwareSystem "Task Management System"{
            webContainer = container "User Web UI" "" "" "frontend"
            adminContainer = container "Admin Web UI" "" "" "frontend"
            dbContainer = container "Database" "PostgreSQL" "" "database"
            apiContainer = container "API" "Go" {
                authComp = component "Authentication"
                crudComp = component "CRUD"
            }
        }

        # Relationships (Actors & Systems)
        customer -> webContainer "Manages tasks"
        admin -> adminContainer "Manages users"
        apiContainer -> emailSystem "Sends emails"
        apiContainer -> calendarSystem "Creates tasks in Calendar"

        # Relationships (Containers)
        webContainer -> apiContainer "Uses"
        adminContainer -> apiContainer "Uses"
        apiContainer -> dbContainer "Persists data"

        # Relationships (Components & Containers)
        crudComp -> dbContainer "Reads from and writes to"
        webContainer -> authComp "Authenticates using"
        adminContainer -> authComp "Authenticates using"
    }
}

Let's dive into the most important parts:

workspace [name] [description] {
    model {
    }
}

Here we define our workspace which should have at least one model. A workspace can optionally be given a name and description.

customer = person "Customer" "" "person"
admin = person "Admin User" "" "person"

In this section we define our persons (for example, a user, actor, role, or persona) in the following format: person <name> [description] [tags].

You can use a similar format (name, description, tags) to identify the external systems:

        emailSystem = softwareSystem "Email System" "Mailgun" "external"
        calendarSystem = softwareSystem "Calendar System" "Calendly" "external"

To describe the internal software system we need to write a block that also shows its containers and components:

taskManagementSystem  = softwareSystem "Task Management System"{
    webContainer = container "User Web UI" "" "" "frontend"
    adminContainer = container "Admin Web UI" "" "" "frontend"
    dbContainer = container "Database" "PostgreSQL" "" "database"
    apiContainer = container "API" "Go" {
        authComp = component "Authentication"
        crudComp = component "CRUD"
    }
}
  • Container format: container <name> [description] [technology] [tags]

  • Component format: component <name> [description] [technology] [tags]

The rest of the model is the most interesting part where we define the relationships between all parts (systems, containers, components):

apiContainer -> emailSystem "Sends emails"

The following format is used: <identifier> -> <identifier> [description] [technology] [tags].

There are other features available in Structurizr DSL, such as styling, themes, visibility, etc. You can find find them here.

Automate Rendering in Your CI

Since you can host your models on GitHub, it is very easy to automate the pipeline for rendering the diagrams in the tools of your choice.

In our case, Structurizr has a GitHub Action that allows you to run structurizr-cli, a command line utility for Structurizr that lets you create software architecture models based upon the C4 model using a textual domain specific language (DSL).

This sample repository contains a workflow that simply generates a static page and publishes it to GitHub Pages.

name: Deploy static content to Github Pages

on:
  push:
    branches: ["main"]

permissions:
  contents: read
  pages: write
  id-token: write

concurrency:
  group: "pages"
  cancel-in-progress: false

jobs:
  build:
    runs-on: ubuntu-latest
    container:
      image: ghcr.io/avisi-cloud/structurizr-site-generatr
      options: --user root
    steps:
      - name: Checkout
        uses: actions/checkout@v3
      - name: Create site
        run: |
          /opt/structurizr-site-generatr/bin/structurizr-site-generatr generate-site -w diagram.dsl
      - uses: actions/upload-artifact@v3
        with:
          name: website
          path: build/site
  deploy:
    needs: build
    environment:
      name: github-pages
      url: ${{ steps.deployment.outputs.page_url }}
    runs-on: ubuntu-latest
    steps:
      - uses: actions/download-artifact@v3
        with:
          name: website
          path: build/site
      - name: Setup Pages
        uses: actions/configure-pages@v3
      - name: Upload artifact
        uses: actions/upload-pages-artifact@v1
        with:
          path: "build/site"
      - name: Deploy to GitHub Pages
        id: deployment
        uses: actions/deploy-pages@v2

This Github Action uses Structurizr CLI action to compile our DSL file as HTML and publish it to Github Pages.

Conclusion

I believe that creating and maintaining effective and clear diagrams should be effortless. Up-to-date visuals ensure everyone stays on the same page, eliminating confusion and wasted time.

The C4 model and a bit of automation with Structurizr DSL can help make this process faster and keep diagrams close to the codebase. The whole process can now be automated as well into your SDLC.

Resources

0
Subscribe to my newsletter

Read articles from Alex Pliutau directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Alex Pliutau
Alex Pliutau

Staff Software Engineer @BINARLY | Docker Captain | Backend, Go, Cloud, DevOps | Writing packagemain.tech Newsletter | 🇩🇪