Back to blog

Architecture as Code: ADR, C4 Diagrams and CI Quality Gates

|
| architecture, adr, c4-model, documentation, devops

We drew beautiful diagrams; six months later nobody could say if they were still true. “Why did we switch to microservices?” A new architect asked me this question 3 years ago. I couldn’t answer. The person who made that decision was no longer on the team, and all we found in Confluence was an outdated diagram from 2019.

That moment was humbling. Here I was, a senior engineer who’d been on the project for two years, unable to explain one of its most fundamental architectural choices. The decision had been made in a meeting I didn’t attend, captured in an email thread that got lost when we migrated to a new email system. The original architect had moved to another company. The knowledge simply evaporated.

Since then, I have a rule: every architectural decision must be written down and versioned. After 20+ years in software development, I’ve seen this problem too often—decisions get lost in emails, Slack conversations, or the heads of people who leave. It’s not just inconvenient; it’s dangerous. Teams make contradictory decisions because they don’t know what was decided before. New developers spend months understanding what could be explained in a few well-written documents.

My experience: I’ve implemented ADR in 5 different projects. I generate C4 diagrams from code using Structurizr. CI quality gates run in both GitHub Actions and GitLab CI.

In this article, I’ll show you a system that works: ADR (Architecture Decision Records) + C4 diagrams + CI automation. Not theory, but concrete templates, workflows, and quality gates you can implement tomorrow.

Why Documentation Fails

Most teams know these problems:

  • Documentation rots - written once, never updated
  • Decisions get lost - “why did we even do it this way?”
  • Onboarding hurts - new people need weeks to understand the system
  • Architect = bottleneck - one person holds all knowledge in their head

The solution isn’t to write more documentation. The solution is to write the right documentation and automate its maintenance.

The Root Cause of Documentation Decay

I’ve seen this pattern repeatedly: a team decides to “improve documentation” and spends a sprint writing comprehensive docs. Three months later, the documentation is outdated and nobody trusts it. What went wrong?

The problem is treating documentation as a separate activity from development. When docs live in a wiki, disconnected from the code they describe, there’s no natural forcing function to keep them updated. A developer changes how authentication works but doesn’t even think about the wiki page describing authentication. Why would they? It’s not in their workflow.

The solution is twofold. First, documentation must live with the code—in the same repository, subject to the same review process. Second, the CI pipeline must enforce documentation updates. When the tests fail, developers fix them. When the documentation checks fail, developers update the docs. It’s that simple, but it requires treating documentation as a first-class citizen of your codebase.

Core Building Blocks

Architecture Decision Records (ADR)

An ADR is a short document that captures one architectural decision. Not a novel, but a structured record with required sections that force you to think through the decision properly.

The beauty of ADRs is their simplicity. Each record captures a single decision at a specific point in time. When that decision is later superseded, you don’t delete the old ADR—you create a new one that references the original. This preserves the historical context. You can trace the evolution of your architecture through the chain of decisions, understanding not just what you decided but why you changed course.

Here’s the template I use:

# ADR-001: Use PostgreSQL as Primary Database

## Status
Accepted

## Context
We need a relational database for core business data.
We have experience with both PostgreSQL and MySQL on the team.

## Decision
We will use PostgreSQL 16 with TimescaleDB extension for time-series data.

## Alternatives Considered
1. MySQL 8 - less advanced JSON features
2. MongoDB - we don't need schema flexibility, we need ACID
3. CockroachDB - overhead for our scale

## Consequences
### Positive
- Strong JSON support, full-text search, GIS
- Better support for analytics workloads

### Negative
- Higher ops complexity compared to managed MySQL
- Fewer candidates with deep PostgreSQL knowledge

### Risks
- TimescaleDB vendor lock-in for time-series part

Anti-patterns to avoid:

  • ADR as a novel (max 1-2 pages)
  • Missing alternatives (always list at least 2-3)
  • No consequences (this is the most important part!)

The consequences section deserves special attention. I’ve seen teams write elaborate ADRs with detailed context and decision rationale, only to leave consequences as an afterthought. This is backwards. The consequences section is where you demonstrate that you’ve actually thought through the implications. Both positive and negative consequences force intellectual honesty. If you can’t articulate any negative consequences, you probably haven’t thought hard enough about the decision.

The C4 Model

The C4 model defines 4 levels of abstraction for architecture diagrams:

  1. Context - the system and its surroundings (users, external systems)
  2. Container - applications, databases, message brokers
  3. Component - internal structure of one container
  4. Code - classes, modules (usually generated from code)

The genius of C4 is that it provides a common vocabulary for discussing architecture at different levels of abstraction. Before C4, teams would create diagrams at random levels of detail. One diagram would mix high-level system boundaries with low-level class relationships. The result was confusion—nobody knew what they were looking at.

C4 solves this by being explicit about the zoom level. When you look at a Context diagram, you know you’re seeing the 30,000-foot view. Container diagrams show you what’s inside the system boundary. Component diagrams zoom into a single container. This hierarchy creates a natural drill-down path from the big picture to implementation details.

When to use which level:

AudienceLevelExample
Management, stakeholdersContext”How our system communicates with the bank”
Developers, opsContainer”What services we have and how they talk”
Service teamComponent”Internal structure of order-service”
Code reviewCodeGenerated from code, not manual

Why Text-Based Diagrams Matter

I strongly recommend using text-based diagram tools like PlantUML or Mermaid rather than visual editors like Visio or Lucidchart. The reason is version control. Text-based diagrams can be reviewed in pull requests just like code. You can see exactly what changed between versions. You can use git blame to find out who added a particular component and why.

Visual editors produce binary files that can’t be meaningfully diffed. When someone updates a Visio diagram, reviewers see only that “diagram.vsdx changed.” They have to open both versions side by side to spot differences. This friction means diagrams don’t get reviewed properly, and incorrect diagrams slip through.

Repository Structure

/docs
  /adr
    /0001-use-postgresql.md
    /0002-event-driven-architecture.md
    /0003-kubernetes-deployment.md
    /index.md              # auto-generated list
  /c4
    /context.puml          # or .mmd for Mermaid
    /containers.puml
    /components
      /order-service.puml
      /payment-service.puml
  /rfcs                    # for larger changes before implementation
    /0001-migrate-to-grpc.md

Naming conventions:

  • ADR: NNNN-kebab-case-title.md
  • Number sequentially, never renumber
  • Mark superseded ADRs in status, but don’t delete

The sequential numbering is important. When someone references “ADR-047,” everyone on the team knows exactly which decision they mean. Don’t be tempted to organize ADRs into subdirectories by topic—the flat list with sequential numbers is intentional. It creates a timeline of decisions that’s easy to scan and navigate.

Linking ADR and C4

Each ADR should reference relevant C4 diagrams:

## Context
We need to solve asynchronous communication between services.
See [Container diagram](/docs/c4/containers.puml) - Message Broker section.

## Decision
We will implement event-driven architecture with Apache Kafka.

And vice versa, C4 diagrams can reference ADRs:

' Ref: ADR-002 Event-Driven Architecture
Container(kafka, "Apache Kafka", "Event streaming platform")

This bidirectional linking is crucial. When someone reads an ADR about adopting Kafka, they should be able to immediately see where Kafka fits in the overall architecture. When someone looks at the container diagram and wonders why you chose Kafka over RabbitMQ, the ADR reference tells them exactly where to find that explanation.

I’ve found that maintaining these links is one of the most valuable aspects of docs-as-code. The links create a web of knowledge that’s far more navigable than traditional documentation silos.

Docs-as-Code Workflow

PR Review Checklist

For every PR that changes architecture:

  • Does an ADR exist or is one created for this decision?
  • Is the ADR approved (Status: Accepted)?
  • Are relevant C4 diagrams updated?
  • Are links between ADR and C4 correct?
  • Is the ADR linked from code (comment, README)?

Ownership Without Bottleneck

Architect is NOT the gatekeeper. The architect is:

  • Facilitator - helps the team write ADRs
  • Reviewer - gives feedback, not veto
  • Curator - maintains consistency

Rule: Anyone can propose an ADR. Approval requires review from 2 seniors + architect.

This distributed ownership model is essential for scaling. In my experience, teams with a single architect bottleneck produce fewer ADRs—not because there are fewer decisions, but because getting the architect’s time becomes too difficult. Important decisions go undocumented because “it’s not worth the hassle.”

By empowering anyone to propose ADRs and requiring only review (not approval) from the architect, you get more decisions documented while maintaining quality through the review process.

CI Automation (Quality Gates)

Here’s the power of this approach. The CI pipeline automatically validates your documentation, ensuring it stays in sync with reality.

1. Markdown Linting

# .github/workflows/docs.yml
- name: Lint ADRs
  uses: DavidAnson/markdownlint-cli2-action@v14
  with:
    globs: 'docs/adr/*.md'

Markdown linting catches formatting inconsistencies before they accumulate. It might seem pedantic, but consistent formatting makes ADRs easier to scan and read. When every ADR uses the same heading levels and list styles, readers can focus on content rather than deciphering structure.

2. Required Sections Check

#!/bin/bash
# scripts/validate-adr.sh
for file in docs/adr/*.md; do
  for section in "## Status" "## Context" "## Decision" "## Consequences"; do
    if ! grep -q "$section" "$file"; then
      echo "ERROR: $file missing $section"
      exit 1
    fi
  done
done

This script ensures every ADR has the required sections. It’s remarkable how often people forget the Consequences section when they’re in a hurry. The CI check makes this impossible—incomplete ADRs simply can’t be merged.

- name: Check links
  uses: lycheeverse/lychee-action@v1
  with:
    args: --verbose docs/

Broken links are the enemy of documentation trust. Once readers encounter a few broken links, they stop trusting the documentation entirely. Automated link checking catches broken references before they reach production.

4. Diagram Generation

- name: Generate C4 diagrams
  run: |
    plantuml -tpng docs/c4/*.puml
    # or for Mermaid:
    # mmdc -i docs/c4/*.mmd -o docs/c4/output/

Generating diagrams in CI ensures that the rendered output matches the source. It also catches syntax errors in diagram definitions before merge.

5. Documentation Publishing

- name: Deploy docs
  uses: peaceiris/actions-gh-pages@v3
  with:
    publish_dir: ./docs

Definition of Done for Architectural Changes

When an ADR MUST be created:

  • Adding a new service/container
  • Changing database technology
  • New integration pattern (sync → async)
  • Security decisions (authentication, authorization)
  • Significant API contract changes

When a C4 update IS ENOUGH:

  • Adding a new endpoint to an existing service
  • Refactoring without changing external behavior
  • Performance optimizations

The distinction matters. Not every change deserves an ADR—that would create noise and dilute the value of the records. ADRs capture decisions that are hard to reverse or have significant consequences. Routine changes just need diagram updates to keep the architecture visualization current.

How to Implement Without Revolution

Rolling out architecture-as-code doesn’t require a big-bang adoption. The incremental approach works better:

Week 1-2: Pilot

  1. Select 5 most pressing decisions from the last year
  2. Write ADRs for them (retrospective is OK)
  3. Create Context and Container C4 diagrams

Starting with retrospective ADRs has a dual benefit. First, you’re documenting decisions that already exist, so there’s no pressure to make new decisions. Second, the exercise reveals how much context has been lost—a powerful motivator for doing this properly going forward.

Week 3-4: Feedback

  1. Present to the team
  2. Collect feedback on templates
  3. Adjust as needed

Don’t skip the feedback phase. Every team has different needs and conventions. The templates I’ve provided are starting points; adjust them to fit your team’s vocabulary and concerns.

Week 5+: Roll-out

  1. Add to Definition of Done
  2. Enable CI checks (warning first, then error)
  3. Iterate

Starting with CI warnings rather than errors gives people time to adjust. After a week or two of warnings, switch to errors. By then, the team has internalized the new workflow.

Success Metrics

How to know it’s working:

  • Onboarding time - new developer productive in X days (target: -30%)
  • Incidents from misunderstanding - “I didn’t know it worked that way” (target: -50%)
  • Lead time for change - from decision to deploy (target: stable or lower)
  • ADR coverage - % of major components with ADR (target: 80%+)

Track these metrics before and after adoption. The onboarding improvement is usually the most visible—new developers consistently report that ADRs are the most valuable part of documentation because they explain the “why” behind the architecture.

Templates to Download

I’ve prepared for you:

Conclusion

Living documentation isn’t about writing more documentation. It’s about writing the right documentation in the right place with automatic validation.

The key insight is that documentation fails when it’s separated from the code it describes. By treating documentation as code—versioned, reviewed, and CI-validated—you create a system that stays in sync with reality.

Start with a small step: write 5 ADRs today for the most important decisions in your project. Tomorrow add a CI check. In a week, you’ll have a system that saves you hours on every onboarding and incident.

Your next step: Identify 5 architectural decisions from the last year and write ADRs for them. Use the template above. The exercise of writing retrospective ADRs will teach you more about your own system than you expect.

Frequently Asked Questions (FAQ)

How many ADRs should a project have?

It depends on project size. For a typical microservices project, expect 20-50 ADRs after 2 years. Don’t write ADRs for every decision—only for those that are hard to reverse or have significant impact. A good rule of thumb: if reversing the decision would require more than a day of work, it deserves an ADR.

Who should write ADRs?

Anyone making an architectural decision. It’s not just the architect’s job. A senior developer proposing a new integration pattern should write the ADR themselves. The act of writing forces clarity of thought—if you can’t articulate the decision clearly, you probably haven’t thought it through well enough.

What if a decision turns out to be wrong?

Never delete old ADRs. Instead, write a new ADR with a reference to the original and explain why you’re changing the decision. Change the original ADR’s status to “Superseded by ADR-XXX”. This history is valuable—it shows that your team learns from experience and improves over time.

What tool should I use for C4 diagrams?

PlantUML or Mermaid are the best choices—they’re text-based so you can version them in Git. Structurizr is a commercial alternative with better UI and diagram extraction from code. Choose based on your team’s preferences and whether you need the advanced features of Structurizr.

How do ADRs relate to RFCs?

ADRs capture decisions; RFCs propose changes before implementation. For small decisions, an ADR is sufficient. For large changes that need broader input—like migrating to a new framework or redesigning a core component—write an RFC first, gather feedback, and then record the decision in an ADR. The RFC explains the proposal and discussion; the ADR captures the final decision.


Related posts

Cite this article

If you reference this post, please link to the original URL and credit the author.

Michal Drozd. "Architecture as Code: ADR, C4 Diagrams and CI Quality Gates". https://www.michal-drozd.com/en/blog/architecture-as-code/ (Published October 31, 2025).