Architecture documentation in modern engineering organizations suffers from a chronic failure mode: “Whiteboard Rot.”
It begins during an architecture kickoff. A team of engineers and architects designs a system on a physical or virtual canvas—Miro, Lucidchart, or a conference room whiteboard. Boxes are sketched, arrows are drawn, photos are snapped, and decisions are celebrated.
Three sprints later, the database schema has evolved, two microservices have merged to reduce network hops, and an asynchronous message bus has replaced a synchronous HTTP call. The diagram, marooned in an unversioned wiki page, is never updated. Six months later, a senior hire references the diagram to debug a critical production incident, only to be told: “Oh, don’t look at that page. That’s how we thought it would work before we built it. Ask Dave how it actually works.”
Tribal knowledge becomes the primary runtime engine of the engineering organization.
The root cause of this breakdown is structural: we treat architecture diagrams as visual illustrations rather than version-controlled technical artifacts.
To build maintainable, scalable, and resilient systems, our architectural representations must follow the same engineering discipline as our source code. Simon Brown’s C4 model bridges high-level strategic intent and low-level code implementation. When paired with a Diagrams-as-Code (DaC) toolchain, it allows you to build living, zoomable, and automated architectural documentation.
1. The Core Problem: Ambiguity in Visual Software Architecture
Most engineering teams do not suffer from a lack of diagrams; they suffer from a lack of rigorous visual syntax.
Traditional software diagrams fall into two primary anti-patterns:
The “Boxes and Lines” Anti-Pattern
Consider a typical box-and-line diagram found in an engineering RFC:
This diagram fails to answer fundamental architectural questions:
- What do the boxes represent? Is the “Web App” a single-page application (SPA) running in a client browser, an edge-rendered Next.js server, or a mobile client?
- What do the arrows communicate? Data flow? Compile-time dependency?
- A synchronous network request?
- What protocol runs over those lines? HTTPS/JSON? gRPC over HTTP/2? Plain TCP? AMQP? * What do the differing arrow weights and line styles denote?
- Is
<=======>a bidirectional websocket connection or simply an inconsistent graphic style?
The Granularity Trap
Unstructured diagrams inevitably conflate levels of abstraction. You will frequently see an AWS CloudFront distribution positioned next to a React UI component, pointing directly to a specific SQL table, which then points to a generic third-party SaaS provider.
[ Non-Technical Stakeholders ] <--- Lost in implementation minutiae ^ | (Mismatched Abstraction Levels) v[ Software Engineers ] <--- Starved of actionable technical contractsThis lack of distinct boundaries renders the artifact useless to both groups: non-technical stakeholders (Product Managers, Compliance Officers) are overwhelmed by low-level plumbing, while engineers are starved of the exact network topologies and execution contexts needed to build and operate the system safely.
The Solution: Simon Brown’s C4 Model
Created by software architect Simon Brown, the C4 model brings hierarchical, map-like navigation to software systems. Rather than cramming every detail into a single diagram, C4 structures visual architecture into four distinct zoom levels:
The Paradigm Shift: Model vs. View
The foundational principle of C4—and the reason it integrates cleanly with code—is the strict separation between the underlying architecture model and the rendered views.
The Model: A semantic, graph-based data structure defining elements (Actors, Systems, Containers, Components) and their directed relationships (Source, Target, Description, Protocol).
The View: A visual query or projection executed against the model. A “Container View” does not redraw nodes; it simply queries the model: “Render the
E-Commerce Systemcontainer boundaries, the external systems that interface with its containers, and all edges between them.”
By treating the architecture as an underlying relational model, we eliminate visual drift across views.
2. Architecture & Conceptual Design: Deconstructing C4
To use the C4 model effectively, you must understand the operational scope, target audience, and engineering value of each level.
Level 1: System Context Diagram
Scope: The system under design, represented as a single block in the center, surrounded by the human users (personas) who interact with it and the external systems it depends on.
Audience: Everyone. Product Owners, Business Analysts, Engineering Leads, Security/Compliance Auditors, and junior engineers during onboarding.
Core Question Answered: “What business problem does this system solve, who interacts with it, and what external dependencies sit outside our organizational boundary?”
Rule: Internal technologies, protocols, and deployment models are omitted here. Focus on business value and boundary definition.
Level 2: Container Diagram
Scope: Decomposes the central software system into its high-level runtime units.
Crucial Disambiguation: A Container in C4 is not a Docker container. A container is any independently deployable unit of compute or data storage that executes code or retains state. Examples include:
A client-side Single Page Application (e.g., React, Vue).
A mobile application (iOS/Android).
A backend API server (e.g., Go binary, Node.js process).
A relational database (e.g., PostgreSQL).
An event streaming platform (e.g., Apache Kafka).
An Amazon S3 bucket used for public asset storage.
Audience: Software Architects, Developers, Site Reliability Engineers (SREs), and DevOps teams.
Core Question Answered: “What is the runtime topology, what responsibilities are assigned to each runtime, and how do they communicate across the network?”
Rule: Every relationship line at this level must explicitly document the intent and the transfer protocol (e.g.,
"Submits orders over HTTPS/REST","Publishes payment events to Kafka over SASL/SSL").
Level 3: Component Diagram
Scope: Decomposes a single container to expose its internal structural building blocks.
Definition: A component is an encapsulated group of related functionality bundled behind a well-defined interface (e.g., a NestJS Controller, a Spring Boot Service, a Domain Repository layer).
Audience: The specific engineering team building, testing, and maintaining that individual container.
Core Question Answered: “How is the application structured internally, where does new business logic belong, and how are boundaries enforced within the codebase?”
Rule: Do not map every individual utility file or class to a component. Focus on coarse-grained functional groupings.
Level 4: Code Diagram
Scope: Fine-grained implementation detail: UML Class diagrams, Entity-Relationship (ER) diagrams, or state machines.
Audience: The developer actively implementing a feature.
The Rule of Pruning: Never hand-craft Level 4 diagrams. Hand-drawn code-level diagrams become obsolete the instant a pull request merges. Reserve Level 4 representations exclusively for automated reflection (e.g., an IDE generating a class diagram on the fly) or for exceptionally complex state machines that warrant living alongside the code.
Supplementary C4 Views for Production Systems
Beyond the standard 1–4 levels, production systems require two additional projections:
Dynamic Diagrams: Instead of showing static wiring, a dynamic diagram models a specific runtime scenario or request-response lifecycle (e.g., “Customer initiates payment and receives an asynchronous receipt”). It numbers steps sequentially ($1.0, 1.1, 2.0$) to clarify execution order across containers or components.
Deployment Diagrams: Maps the logical containers directly to physical or cloud infrastructure. It shows which Kubernetes Pods run within which AWS EKS node groups, how containers map to managed services (e.g., Amazon RDS Aurora Multi-AZ), and where CDN/edge termination occurs.
3. Step-by-Step Code Snippets and Production Deployment Instructions
To implement C4 cleanly, we treat our architectural model as an engineering codebase: 1. Define the model using declarative code. 2. Validate the model in our CI/CD pipelines. 3. Automatically render target views (SVG, Mermaid, PNG). 4. Publish the views directly to developer portals and documentation hubs.
Tooling Strategy: The Structurizr Ecosystem
The enterprise standard for Diagrams-as-Code with C4 is Structurizr DSL, designed by Simon Brown. The Structurizr ecosystem decouples the system definition from the presentation layer:
Step 1: Modeling a Production Example in Structurizr DSL
Below is a complete, syntactically valid workspace.dsl modeling an E-Commerce Order Fulfillment Platform. It defines the elements, connects their relationships with explicit protocols, and organizes them into views.
workspace "E-Commerce Fulfillment Platform" "Production architecture for high-throughput order processing."
!identifiers hierarchical
model {
// Actors / Personas
customer = person "Customer" "A registered user who places orders online." "Customer"
supportAgent = person "Support Agent" "Internal operations personnel resolving customer issues." "InternalStaff"
// External Software Systems
paymentGateway = softwareSystem "Stripe Payment Gateway" "External credit card authorization and settlement provider." "ExternalSystem"
logisticsProvider = softwareSystem "Third-Party Logistics (3PL)" "External warehouse and distribution carrier APIs." "ExternalSystem"
// Primary Enterprise System Boundary
fulfillmentSystem = softwareSystem "Order Fulfillment Platform" "Processes, orchestrates, and monitors customer orders." {
// Containers (Level 2)
singlePageApp = container "Web Storefront" "Delivers the user purchasing experience via the browser." "TypeScript / Next.js" "WebBrowser"
apiGateway = container "API Gateway" "Handles authentication, rate limiting, and request routing." "Envoy / Traefik" "Infrastructure"
orderService = container "Order Lifecycle Service" "Manages order state machines, business validation, and workflows." "Go 1.22" {
// Components (Level 3 inside Order Lifecycle Service)
orderController = component "Order REST Controller" "Exposes external HTTP API endpoints for order mutations." "Go / Chi"
workflowEngine = component "Fulfillment Workflow Engine" "Executes state transitions and business invariant validations." "Go / Temporal Worker"
paymentClient = component "Payment Adapter" "Encapsulates network integration with external payment processors." "Go Client"
orderRepository = component "Order Persistence Layer" "Executes queries and transactional updates against the database." "Go / pgx"
}
eventBus = container "Event Bus" "Decouples transaction execution from asynchronous fan-out." "Apache Kafka" "Broker"
orderDatabase = container "Order Database" "Primary transactional storage for orders, line items, and audit records." "PostgreSQL 16" "Database"
}
// --- RELATIONSHIPS ---
// Level 1: System Context Interactions
customer -> fulfillmentSystem.singlePageApp "Browses catalog and submits orders using" "HTTPS/TLS"
supportAgent -> fulfillmentSystem.apiGateway "Queries order health and resolves stalls using" "HTTPS/JSON"
fulfillmentSystem.orderService -> paymentGateway "Authorizes charges and captures funds via" "HTTPS/REST (TLS 1.3)"
fulfillmentSystem.orderService -> logisticsProvider "Dispatches shipping manifests via" "HTTPS/REST (Mutual TLS)"
// Level 2: Container Wiring
fulfillmentSystem.singlePageApp -> fulfillmentSystem.apiGateway "Makes API calls to" "JSON/HTTPS"
fulfillmentSystem.apiGateway -> fulfillmentSystem.orderService.orderController "Forwards order requests to" "gRPC over HTTP/2"
fulfillmentSystem.orderService.workflowEngine -> fulfillmentSystem.eventBus "Emits 'OrderPlaced' and 'OrderShipped' events to" "Kafka Protocol (SASL/SCRAM)"
fulfillmentSystem.orderService.orderRepository -> fulfillmentSystem.orderDatabase "Reads and writes order data to" "TCP 5432 (TLS)"
// Level 3: Internal Component Relationships
fulfillmentSystem.orderService.orderController -> fulfillmentSystem.orderService.workflowEngine "Invokes fulfillment workflow via" "Internal Go Call"
fulfillmentSystem.orderService.workflowEngine -> fulfillmentSystem.orderService.paymentClient "Delegates payment execution to" "Internal Go Call"
fulfillmentSystem.orderService.workflowEngine -> fulfillmentSystem.orderService.orderRepository "Persists updated order states through" "Internal Go Call"
fulfillmentSystem.orderService.paymentClient -> paymentGateway "Dispatches payment intent requests to" "HTTPS/REST (TLS 1.3)"
}
views {
// View 1: System Context
systemContext fulfillmentSystem "ContextView" {
include *
autoLayout lr
}
// View 2: Container View
container fulfillmentSystem "ContainerView" {
include *
autoLayout lr
}
// View 3: Component View for Order Lifecycle Service
component fulfillmentSystem.orderService "OrderServiceComponentView" {
include *
autoLayout tb
}
// Visual Styling Engine
styles {
element "Person" {
shape Person
background #08427B
color #ffffff
}
element "SoftwareSystem" {
background #1168BD
color #ffffff
}
element "ExternalSystem" {
background #999999
color #ffffff
}
element "Container" {
background #438DD5
color #ffffff
}
element "WebBrowser" {
shape WebBrowser
}
element "Database" {
shape Cylinder
background #2A629A
color #ffffff
}
element "Broker" {
shape Pipe
background #2F5D62
color #ffffff
}
element "Component" {
background #85BBF0
color #000000
}
}
}Step 2: Alternative In-Repository Rendering via Mermaid.js
If your team is not ready to deploy a dedicated Structurizr pipeline, you can embed C4 diagrams directly into GitHub or GitLab Markdown using native Mermaid.js C4 syntax.
Here is our System Context diagram rendered natively for PR descriptions or README.md files:
```mermaid
C4Context
title System Context for E-Commerce Order Fulfillment Platform
Person(customer, "Customer", "A registered user who places orders online.")
Person(supportAgent, "Support Agent", "Internal operations personnel resolving customer issues.")
Enterprise_Boundary(b0, "Order Processing Domain") {
System(fulfillmentSystem, "Order Fulfillment Platform", "Processes, orchestrates, and monitors customer orders.")
}
System_Ext(paymentGateway, "Stripe Payment Gateway", "External credit card authorization and settlement provider.")
System_Ext(logisticsProvider, "Third-Party Logistics (3PL)", "External warehouse and distribution carrier APIs.")
Rel(customer, fulfillmentSystem, "Browses catalog and submits orders", "HTTPS/TLS")
Rel(supportAgent, fulfillmentSystem, "Queries order health and resolves stalls", "HTTPS/JSON")
Rel(fulfillmentSystem, paymentGateway, "Authorizes charges and captures funds", "HTTPS/REST (TLS 1.3)")
Rel(fulfillmentSystem, logisticsProvider, "Dispatches shipping manifests", "HTTPS/REST (Mutual TLS)")
UpdateLayoutConfig($c4ShapeInRow="3", $c4BoundaryInRow="1")
#### Rendered Mermaid Output:
```mermaidC4Context title System Context for E-Commerce Order Fulfillment Platform
Person(customer, "Customer", "A registered user who places orders online.") Person(supportAgent, "Support Agent", "Internal operations personnel resolving customer issues.")
Enterprise_Boundary(b0, "Order Processing Domain") { System(fulfillmentSystem, "Order Fulfillment Platform", "Processes, orchestrates, and monitors customer orders.") }
System_Ext(paymentGateway, "Stripe Payment Gateway", "External credit card authorization and settlement provider.") System_Ext(logisticsProvider, "Third-Party Logistics (3PL)", "External warehouse and distribution carrier APIs.")
Rel(customer, fulfillmentSystem, "Browses catalog and submits orders", "HTTPS/TLS") Rel(supportAgent, fulfillmentSystem, "Queries order health and resolves stalls", "HTTPS/JSON") Rel(fulfillmentSystem, paymentGateway, "Authorizes charges and captures funds", "HTTPS/REST (TLS 1.3)") Rel(fulfillmentSystem, logisticsProvider, "Dispatches shipping manifests", "HTTPS/REST (Mutual TLS)")
UpdateLayoutConfig($c4ShapeInRow="3", $c4BoundaryInRow="1")Step 3: Setting Up Structurizr CLI for Automated Validation and Export
The Structurizr CLI compiles, inspects, and converts your DSL workspaces into output targets like PlantUML, Mermaid, or JSON.
Run the CLI locally using Homebrew, a binary release, or Docker:
# 1. Validate the syntax of your architecture workspace
structurizr-cli validate -w docs/architecture/workspace.dsl
# 2. Export the views directly to Mermaid format for Git rendering
structurizr-cli export -w docs/architecture/workspace.dsl -format mermaid -output docs/architecture/generated/
# 3. Export to PlantUML if your pipeline uses the PlantUML rendering engine
structurizr-cli export -w docs/architecture/workspace.dsl -format plantuml -output docs/architecture/generated/
If validation fails, the CLI returns a non-zero exit code along with the exact line number of the parsing error:
Unexpected token "databse" on line 29 of docs/architecture/workspace.dsl. Expected: container, component, ...
This diagnostic output lets you use architectural validation as a gating step in your CI pipelines.
Step 4: Building the Continuous Architecture CI/CD Pipeline
To ensure the architecture model does not rot, your CI pipeline must validate every change. The following GitHub Actions workflow compiles the DSL, generates Mermaid diagrams, and commits the updated visual artifacts back to the repository.
Create .github/workflows/architecture-docs.yml:
name: "Continuous Architecture: Validate and Compile C4 Diagrams"
on: push: branches: - main paths: - "docs/architecture/**" - ".github/workflows/architecture-docs.yml" pull_request: paths: - "docs/architecture/**"
permissions: contents: write
jobs: validate-and-export: name: "Validate DSL & Compile Views" runs-on: ubuntu-latest
steps: - name: "Checkout Repository" uses: actions/checkout@v4 with: token: ${{ secrets.GITHUB_TOKEN }} fetch-depth: 0
- name: "Pull Structurizr CLI Docker Image" run: | docker pull structurizr/cli:latest
- name: "Lint and Validate Workspace DSL" run: | docker run --rm \ -v ${{ github.workspace }}/docs/architecture:/workspace \ structurizr/cli:latest \ validate -w /workspace/workspace.dsl
- name: "Compile Views to Mermaid Markdown Artifacts" run: | mkdir -p docs/architecture/generated docker run --rm \ -v ${{ github.workspace }}/docs/architecture:/workspace \ structurizr/cli:latest \ export -w /workspace/workspace.dsl -format mermaid -o /workspace/generated
- name: "Verify Git Status for Pull Requests" if: github.event_name == 'pull_request' run: | if [ -n "$(git status --porcelain docs/architecture/generated)" ]; then echo "ERROR: Generated architecture diagrams are out of sync with workspace.dsl." echo "Please run 'structurizr-cli export -w workspace.dsl -format mermaid' locally and commit the result." git status --porcelain docs/architecture/generated exit 1 fi
- name: "Commit and Push Updated Diagrams (Main Branch Only)" if: github.event_name == 'push' && github.ref == 'refs/heads/main' run: | git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" git add docs/architecture/generated/ if git diff --staged --quiet; then echo "No architectural changes detected. Clean working tree." else git commit -m "chore(docs): compile updated C4 architecture diagrams [skip ci]" git push origin main fiStep 5: Production Deployment and Hosting Options
For local iteration or team-wide interactive navigation, you can run Structurizr Lite via Docker. This lightweight server parses a directory containing workspace.dsl, runs an interactive UI, and watches the filesystem for changes:
docker run -it --rm \ -p 8080:8080 \ -v $(pwd)/docs/architecture:/usr/local/structurizr \ structurizr/lite
Navigate to http://localhost:8080 to interactively explore, search, and zoom through the architecture hierarchy.
Integrating with Spotify Backstage
For organizations using Spotify Backstage as an internal developer portal, you can link the C4 model directly to your services via the TechDocs plugin:
Use Structurizr CLI in your build script to export diagrams to SVG or PNG format.
In your MkDocs configuration (
mkdocs.yml), embed the generated images using standard Markdown.Reference the central model inside your Backstage
catalog-info.yamlentity descriptors:
apiVersion: backstage.io/v1alpha1kind: Componentmetadata: name: order-lifecycle-service description: Manages order state machines and fulfillment workflows. annotations: backstage.io/techdocs-ref: dir:.spec: type: service lifecycle: production owner: fulfillment-core-team system: order-fulfillment-platform dependsOn: - resource:default/order-database - component:default/event-busThis bridges your live service catalog with your declarative C4 models, creating a single source of truth across the engineering organization.
4. Best Practices and Lessons Learned
Architectural Best Practices
1. Treat Diagrams as Code
Place your .dsl files directly in the monorepo root or in a dedicated systems-architecture repository. Any pull request that introduces a new microservice, provisions a new cloud database, or switches synchronous calls to an event bus must include the corresponding diff to workspace.dsl. If the architecture diff is missing, the PR is incomplete.
2. The Level 4 Pruning Rule
Never manually draft Level 4 (Code) diagrams. If an algorithm or component is so complex that a class diagram is required, generate it on demand using IDE tools or reflection libraries. If an artifact cannot be generated automatically, its maintenance cost will rapidly outstrip its architectural value.
3. Enforce Unambiguous Relationship Labels
An arrow without an explicit semantic label is tech debt. Every relationship edge must clearly define three properties: * The Action/Intent: What business operation is occurring? (e.g., Publishes payment events) * The Protocol: Over what transport layer does this travel? (e.g., Kafka / SASL SSL, gRPC / HTTP/2) * The Communication Style: Is this call synchronous or asynchronous?
// Poor: AmbiguousorderService -> orderDatabase "Uses"
// Good: Action, protocol, and runtime parameters explicitorderService -> orderDatabase "Reads/Writes order entities" "TCP 5432 (TLS, connection pooled)"Common Anti-Patterns to Avoid
Anti-Pattern | Manifestation | Corrective Action |
|---|---|---|
The “Kitchen Sink” Diagram | Merging Context, Containers, and internal components onto a single, massive canvas. | Decompose into distinct C4 views. Use the |
Conflating Runtimes with Physical Nodes | Drawing an AWS EC2 instance or a Kubernetes Node inside a Container view. | Keep infrastructure topologies in the Deployment View. Keep the Container View focused on runtime software processes. |
Stale Static Exports | Committing manually exported | Delete checked-in binary images. Use Structurizr CLI in CI to compile SVGs, or rely on Mermaid.js text embeddings. |
Bi-directional Functional Arrows | Drawing arrows with arrowheads on both ends to show a request-response flow. | Directed arrows must only show dependency or initiation direction. A request-response interaction is initiated by the client; the return payload is implied. |
Lessons Learned from Enterprise Rollouts
Shift Left for Architecture: ADRs + C4
Combine Architecture Decision Records (ADRs) with C4 diagram diffs in the same pull request. When an engineer proposes changing a synchronization model via an ADR (e.g., ADR-0024: Migrate from REST to Kafka for Order Events), the PR must include both the Markdown ADR and the Structurizr DSL diff:
- orderService -> logisticsProvider "Dispatches shipping manifests via" "HTTPS/REST"+ orderService -> eventBus "Emits 'OrderReadyForShipment' event to" "Kafka Protocol"+ logisticsWorker -> eventBus "Subscribes to 'OrderReadyForShipment' from" "Kafka Protocol"+ logisticsWorker -> logisticsProvider "Dispatches shipping manifests via" "HTTPS/REST"This makes architectural reviews fast and objective. Reviewers can easily inspect structural changes in the pull request diff.
Accelerating Developer Onboarding
In benchmark rollouts across distributed engineering teams, access to clean, hierarchical C4 documentation reduced new engineer time-to-first-commit by 35% to 50%. Instead of parsing hundreds of thousands of lines of application code to understand service boundaries, engineers navigate systematically:
System Context (Day 1) --> Container Topology (Day 2) --> Component Details (Day 3)This gives engineers a clear mental model of where their feature changes belong before they write their first line of code.
5. Conclusion & The Future of Living Architecture
The traditional software architecture diagram is dead. Hand-drawn visual canvases in disconnected SaaS tools cannot keep pace with modern, rapidly deploying engineering teams.
Simon Brown’s C4 model brings structural discipline, bounded contexts, and hierarchical zoom levels to visual software architecture. When combined with Structurizr DSL and a continuous automated toolchain, architecture documentation transforms from a decaying afterthought into a living engineering artifact.

The Future of C4: Telemetry-Infused Models
We are now seeing the convergence of declarative architecture models and live operational telemetry: * Telemetry Overlays: By joining container identifiers in a C4 workspace with OpenTelemetry (OTel) service names, modern systems can project live metrics directly onto the diagram. Architects can view a Container Diagram where edges are dynamically colored by p99 latency, error rates, or cross-zone network transfer costs. * AI-Driven Model Extraction: Modern static analysis tools leverage LLMs and Abstract Syntax Trees (ASTs) to inspect application codebases, discover external interfaces, and propose updates to workspace.dsl files via automated pull requests. * Unified Developer Platforms: Portals like Spotify Backstage are increasingly adopting C4-native standards to unify documentation, ownership models, service health, and runtime dependencies under a single visual model.
Take the Challenge
Take one undocumented or outdated system in your organization this week. Avoid the temptation to open a visual whiteboard tool.
Instead:
1. Write a 40-line workspace.dsl that models the system’s Context and Containers.
2. Commit that file alongside the system’s application code.
3. Wire up the validation step in your CI pipeline.
Transform your architecture documentation from a decaying visual artifact into a durable, version-controlled engineering asset.
.png)
.png)


.png)



