What Is Software Architecture? describes architecture as the shared understanding of a system and its decisions that are hard to change. Shared understanding does not survive on its own: people leave, teams grow, and six months later nobody remembers why the portal reads billing tables straight from the database. This page covers the lightweight techniques most teams use to write that understanding down. For general technical writing (tickets, runbooks, the Diátaxis framework) see Writing Tickets and Technical Documentation.

Views for different audiences

There is no single picture of an architecture. A developer wants to know which module calls which; an operations engineer wants to know what runs on which server; a manager wants to know which systems depend on a supplier. An architecture view is a description of the architecture from the perspective of one group of stakeholders, showing what answers their concerns and leaving out the rest. The standard on architecture descriptions, ISO/IEC/IEEE 42010 (2nd edition 2022), defines a view this way, and a viewpoint as the conventions for building one kind of view: which elements, which notation, which questions it answers.

Philippe Kruchten's "4+1" view model (1995) is a well-known set of viewpoints: a logical view (the main abstractions), a process view (what runs concurrently), a development view (modules and packages), a physical view (hardware and network), plus scenarios that tie the four together. You do not need a formal framework to use the idea. Ask who will read the document and what they need to decide, then draw that, and say in the title which view it is.

Views are easiest to keep consistent when they come from one model. This script holds Kestrel Energy's metering and billing containers once and prints two views of them: where each one runs, for the operations team, and who owns it, for developers looking for someone to ask.

# One model, two views: what runs where (operations) and who owns what (developers)
containers = [
    # name,              host,                       owner
    ("Customer portal",  "web01, web02",             "Portal team (Ravi Kumar)"),
    ("meter-api",        "api01",                    "Portal team (Ravi Kumar)"),
    ("KBS",              "bill-app-01, bill-app-02", "Billing (Joost Bakker)"),
    ("db01 PostgreSQL",  "db01",                     "DBA (Hamid Rahimi)"),
]

def view(title, key_index):
    print(title)
    groups = {}
    for row in containers:
        groups.setdefault(row[key_index], []).append(row[0])
    for key in sorted(groups):
        print(f"  {key:26} {', '.join(groups[key])}")

view("Deployment view (for operations): host -> containers", 1)
view("Ownership view (for developers): owner -> containers", 2)
Deployment view (for operations): host -> containers
  api01                      meter-api
  bill-app-01, bill-app-02   KBS
  db01                       db01 PostgreSQL
  web01, web02               Customer portal
Ownership view (for developers): owner -> containers
  Billing (Joost Bakker)     KBS
  DBA (Hamid Rahimi)         db01 PostgreSQL
  Portal team (Ravi Kumar)   Customer portal, meter-api

KBS (Kestrel Billing System) is the billing application; its HTTP interface is the billing-api service on the bill-app servers. The two views say different things about the same four containers, and neither is complete. That is the point: a diagram that tries to answer every question answers none of them well.

The C4 model

Many architecture diagrams are boxes and lines that make sense only to the person who drew them: what is a box, a server or a program? Does an arrow mean "calls", "sends data to" or "depends on"? The C4 model, created by Simon Brown and published at c4model.com, is "an easy to learn, developer friendly approach to software architecture diagramming". It fixes the problem with a small set of element types and four levels of zoom, like a map that goes from country to street:

Level Shows Typical audience
1. System context The software system as one box, the people who use it and the other systems it talks to Everyone, including non-technical people
2. Container The applications and data stores inside the system, their technology and how they communicate Developers, operations, architects
3. Component The main building blocks inside one container, such as the modules of an application Developers of that container
4. Code Classes or tables inside one component, often a UML class diagram or an entity-relationship diagram Developers, usually generated by tools if at all

Three supplementary diagram types reuse the same elements: a system landscape (all systems of an organization), a dynamic diagram (the order of calls in one scenario) and a deployment diagram (which container runs on which infrastructure). The C4 site is explicit that "you don't need to use all 4 levels of diagram; only those that add value." For most systems the context and container diagrams carry almost all the value; level 4 goes out of date with every commit.

The system context diagram

A system context diagram shows one software system as a single box in the middle, with the people who use it and the external systems it exchanges data with around it. It contains no technology details, so anyone can read it: it is the diagram to show a new colleague, an auditor or the chief financial officer. Here is Kestrel's metering and billing system at that level:

System context diagram of Kestrel metering and billing. In the middle the software system Metering and billing (portal, meter-api and KBS). On the left two people: the Customer, who views usage and pays invoices, and the Billing clerk, who corrects readings and runs billing. On the right three external systems: Smart meters send readings every 15 minutes over HTTPS to the system; the system sends online payments to the Payment provider and a nightly direct-debit file to the Bank. A legend explains that solid boxes are Kestrel people and systems, dashed boxes are external systems, and every arrow is a labeled one-way relationship. System context · Kestrel metering and billing Customer [Person] Billing clerk [Person] Metering and billing [Software system] portal, meter-api, KBS Smart meters [External system] Payment provider [External system] Bank [External system] views usage, pays invoices fixes readings, runs billing readings every 15 min [HTTPS] online payments direct-debit file, nightly Kestrel person or system external system one-way relationship
Level 1: one box for the whole system. The diagram has a title, a legend, a type on every element and a label on every arrow, as the C4 notation rules ask.

The C4 site lists a short set of notation rules that make a diagram readable without its author: every diagram has a title that states its type and scope, and a key or legend; every element has its type stated and a short description; every container and component has its technology specified; and "every line should represent a unidirectional relationship" and "every line should be labelled". Those rules are cheap to follow and remove most of the guessing.

Containers: things that have to run

Zoom into the middle box and you get the container diagram. A container, in the C4 model, is an application or a data store that must be running for the system to work: a web application, an API, a batch job, a database, a message broker. The C4 site puts it bluntly: "Not Docker!" A C4 container may well be deployed in a Docker container, but it may also be a database server or a program on a virtual machine. Kestrel's metering and billing system has four running containers today, plus the broker that ADR-0007 (below) plans to add:

Container diagram of Kestrel metering and billing. Inside the system boundary: the Customer portal (web application on web01 and web02) reads usage and billing tables from db01 over SQL; meter-api (Python HTTP API) writes readings to db01 over SQL; KBS (the billing monolith) reads and writes billing tables on db01 over SQL. db01 is a PostgreSQL database shared by all three. A planned message broker, drawn dashed, would receive ReadingReceived events from meter-api and deliver them to KBS. Container diagram · Kestrel metering and billing (today, plus planned broker) Metering and billing [Software system] Customer portal [Container: web app] web01, web02 meter-api [Container: Python, HTTP API] KBS [Container: billing monolith] db01 [Container: PostgreSQL] Message broker [Container: planned] reads usage, billing tables [SQL] writes readings [SQL] ReadingReceived [planned] delivers events reads and writes billing tables [SQL] container shared data store planned (dashed)
Level 2: the containers inside the system and how they talk. People and external systems are left out here to save space; a full container diagram repeats them from the context diagram.

The container diagram shows something the context diagram hides: three containers read and write the same database. During INC-22047 a connection leak in meter-api slowed the portal down too, and the diagram makes it obvious why. Good diagrams do not only describe a system; they make its risks visible.

Architecture decision records

Diagrams show what the structure is. They rarely show why, and the why is what a new developer needs before changing something. An architecture decision record (ADR) is a short text document that records one architecturally significant decision: the situation that called for it, what was decided, and what follows from it. Michael Nygard described the format in a blog post on 15 November 2011, "Documenting Architecture Decisions". His ADR has five parts:

  • Title: a short noun phrase with a number.
  • Context: the forces at play, technical, political and organizational, written neutrally.
  • Decision: the response to those forces, in full sentences and active voice ("We will…").
  • Status: proposed, accepted, deprecated or superseded.
  • Consequences: everything that follows, the good and the bad.

Nygard's other rules are just as important. ADRs are numbered "sequentially and monotonically", and numbers are never reused. An ADR is one or two pages, not a design document. And when a decision is reversed, you do not edit or delete the old record: you write a new one and mark the old one as superseded, with a link both ways. The collection becomes a log of how the architecture got where it is. Thoughtworks put lightweight ADRs in the "Adopt" ring of its Technology Radar in November 2017: "For most projects, we see no reason why you wouldn't want to use this technique."

A worked ADR: ADR-0007

After INC-22047, Ravi Kumar (portal tech lead) and Joost Bakker (the billing engineer who owns KBS) proposed to stop KBS from reading meter readings out of shared tables. Kestrel keeps its ADRs as Markdown in docs/adr/, using the MADR template (described below). The record as proposed, shortened; it is still under review:

---
status: proposed
date: 2026-10-06
decision-makers: Ravi Kumar, Joost Bakker
consulted: Hamid Rahimi, Yara Haddad
---
# Use a message broker between meter-api and KBS

## Context and Problem Statement

meter-api writes about 444 readings per second on average, in bursts at
:00, :15, :30 and :45, into tables on db01 that KBS also reads. During
INC-22047 a connection leak in meter-api slowed down every client of db01.
Any change to the readings tables needs agreement from three teams.

## Considered Options

* Keep the shared tables, add per-client connection limits
* KBS calls meter-api over HTTP when it needs readings
* meter-api publishes ReadingReceived events through a message broker

## Decision Outcome

Chosen option: "message broker", because KBS no longer depends on
meter-api's tables or on meter-api being up when billing runs.
We will write each event to an outbox table in the same transaction
as the reading, and a relay will publish it (transactional outbox).

### Consequences

* Good, because meter-api and KBS can change their tables independently.
* Bad, because billing data can be up to 30 minutes behind.
* Bad, because the broker is a new component to run and monitor.
* KBS must handle duplicate events (at-least-once delivery).

Note what the ADR does not contain: the broker product, its configuration or the event schema. Those are design details for other documents, and they can change without a new ADR. The record explains a choice that would be expensive to reverse, and the alternatives the team rejected. A newcomer who wonders "why not just call meter-api over HTTP?" finds the answer here. The mechanics of the outbox are on Event-Driven Architecture.

Templates: Nygard, MADR and Y-statements

Nygard's five headings are the minimum. MADR (Markdown Architectural Decision Records, version 4.0.0 of September 2024 is still the current one as of October 2026) adds headings that make the trade-off explicit: Context and Problem Statement, Decision Drivers, Considered Options, Decision Outcome (with Consequences and Confirmation), Pros and Cons of the Options, and More Information. Its optional front matter records status, date, decision-makers, consulted and informed. The short form is the Y-statement, one sentence with fixed slots:

In the context of meter-api handing readings to KBS, facing a shared database where one client's connection leak slowed down everyone, we decided for a message broker with a transactional outbox and neglected direct HTTP calls and per-client connection limits, to achieve independent change and failure of the two services, accepting billing data up to 30 minutes behind and one more component to run, because readings arrive in bursts that billing does not need to process immediately.

Because ADRs are files, ordinary tools work on them. Here is the docs/adr folder today, with ADR-0007 still under review; grep lists every status, and grep -L lists the records that do not name anyone who was consulted:

mkdir -p docs/adr
cat > docs/adr/0001-record-architecture-decisions.md <<'ADR'
---
status: accepted
date: 2026-09-14
decision-makers: Ravi Kumar, Joost Bakker
---
# Record architecture decisions
ADR
cat > docs/adr/0007-use-a-message-broker-between-meter-api-and-kbs.md <<'ADR'
---
status: proposed
date: 2026-10-06
decision-makers: Ravi Kumar, Joost Bakker
consulted: Hamid Rahimi, Yara Haddad
---
# Use a message broker between meter-api and KBS
ADR
ls docs/adr
grep -H '^status:' docs/adr/*.md
grep -L '^consulted:' docs/adr/*.md
0001-record-architecture-decisions.md
0007-use-a-message-broker-between-meter-api-and-kbs.md
docs/adr/0001-record-architecture-decisions.md:status: accepted
docs/adr/0007-use-a-message-broker-between-meter-api-and-kbs.md:status: proposed
docs/adr/0001-record-architecture-decisions.md

ADR-0001 is traditionally "Record architecture decisions": the decision to keep ADRs at all, where they live and which template they use. A small script can keep an index up to date and work out the next number. This one reads three of the records (as strings, so the example is self-contained) and prepares the file name for the next decision, moving annual statement PDF generation out of KBS (see Evolving Architecture):

import re

adr_files = {
    "0001-record-architecture-decisions.md":
        "# 1. Record architecture decisions\n\n## Status\n\nAccepted\n",
    "0003-turn-kbs-into-a-modular-monolith.md":
        "# 3. Turn KBS into a modular monolith\n\n## Status\n\nAccepted\n",
    "0007-use-a-message-broker-between-meter-api-and-kbs.md":
        "# 7. Use a message broker between meter-api and KBS\n\n"
        "## Status\n\nProposed\n",
}

def parse(text):
    title = text.splitlines()[0].lstrip("# ").split(". ", 1)[1]
    status = re.search(r"## Status\n\n(.+)", text).group(1)
    return title, status

def slug(title):
    return re.sub(r"[^a-z0-9]+", "-", title.lower()).strip("-")

print("| ADR | Title | Status |")
print("|-----|-------|--------|")
for name in sorted(adr_files):
    title, status = parse(adr_files[name])
    print(f"| {name[:4]} | [{title}]({name}) | {status} |")

new_title = "Move annual statement PDF generation out of KBS"
number = max(int(name[:4]) for name in adr_files) + 1
print()
print(f"next file: {number:04d}-{slug(new_title)}.md")
| ADR | Title | Status |
|-----|-------|--------|
| 0001 | [Record architecture decisions](0001-record-architecture-decisions.md) | Accepted |
| 0003 | [Turn KBS into a modular monolith](0003-turn-kbs-into-a-modular-monolith.md) | Accepted |
| 0007 | [Use a message broker between meter-api and KBS](0007-use-a-message-broker-between-meter-api-and-kbs.md) | Proposed |

next file: 0008-move-annual-statement-pdf-generation-out-of-kbs.md

These three records use Nygard's headings rather than MADR's front matter; real repositories often contain both, from before and after a template change, and a tool that reads them has to cope.

arc42: a template for the whole document

ADRs record decisions one at a time; sometimes you need a complete architecture document, for a handover, an audit or a new team. arc42 is a free template for such a document, by Peter Hruschka and Gernot Starke, published under the CC BY-SA 4.0 license (the English template is at version 9.0 of July 2025, as of October 2026). It has twelve sections:

Section What goes in it Links to
1 Introduction and Goals Main requirements, top quality goals, stakeholders
2 Architecture Constraints Fixed choices, such as "must run in the EU" or "PostgreSQL"
3 Context and Scope The system and its neighbors C4 system context diagram
4 Solution Strategy The few fundamental decisions, in short
5 Building Block View The static structure, zoomed in step by step C4 container and component diagrams
6 Runtime View Important scenarios as sequences of steps C4 dynamic diagram
7 Deployment View Infrastructure and what runs where C4 deployment diagram
8 Cross-cutting Concepts Logging, security, error handling, persistence
9 Architecture Decisions Important decisions and their reasons ADRs
10 Quality Requirements Quality goals made concrete quality scenarios
11 Risks and Technical Debts Known problems, ordered by severity debt register
12 Glossary The domain and technical terms the team uses

arc42 and C4 fit together: arc42 gives the chapters, C4 gives the diagrams inside them, and section 9 can simply link to the ADR folder. Treat the template as a checklist of questions rather than a form. An empty section with the note "not relevant for this system" is fine; padding every section with text nobody reads is not.

Diagrams as code

A diagram drawn in a drawing tool lives in a binary file that nobody reviews, and it goes stale the first time someone changes the system without opening the tool. Diagrams as code means describing diagrams in a text language, keeping the text in the same repository as the code, and letting a tool render the picture. Changes to the diagram then show up in a diff, go through code review and are versioned with the system they describe. It is the diagram side of docs-as-code. Common tools:

  • Structurizr, by the author of the C4 model: "models as code". You describe the model once (people, systems, containers, relationships) and define several views of it, so the context and container diagrams can never disagree.
  • PlantUML, an older text format for UML and other diagrams, with a C4 extension (C4-PlantUML).
  • Mermaid, which GitHub and GitLab render directly inside Markdown (see Mermaid diagrams). It has C4 support, which its documentation calls "an experimental diagram for now" (as of October 2026); flowcharts are the stable choice.

The context and container diagrams above, simplified, as a Structurizr DSL workspace. One model, two views:

workspace "Kestrel metering and billing" {
    model {
        customer = person "Customer" "Views usage, pays invoices"
        clerk = person "Billing clerk" "Corrects readings, runs billing"
        meters = softwareSystem "Smart meters" "Send a reading every 15 minutes" "External"

        billing = softwareSystem "Metering and billing" {
            portal = container "Customer portal" "Usage and invoices for customers" "Web application"
            meterApi = container "meter-api" "Receives smart-meter readings" "Python, HTTP API"
            kbs = container "KBS" "Tariffs, invoices, payments" "Billing monolith"
            db01 = container "db01" "Readings and billing data" "PostgreSQL" "Database"
        }

        customer -> portal "Views usage, pays invoices" "HTTPS"
        clerk -> kbs "Corrects readings, runs billing"
        meters -> meterApi "Send readings" "HTTPS"
        portal -> db01 "Reads usage and billing tables" "SQL"
        meterApi -> db01 "Writes readings" "SQL"
        kbs -> db01 "Reads and writes billing tables" "SQL"
    }
    views {
        systemContext billing "Context" {
            include *
            autolayout lr
        }
        container billing "Containers" {
            include *
            autolayout lr
        }
    }
}

Text also means you can check a diagram before anyone looks at it. This script keeps a container model as data, applies two of the C4 notation rules (technology on every container, a label on every relationship), and then generates Mermaid text from the same model:

# A tiny C4 container model of Kestrel metering and billing, kept as code
elements = {
    "portal":   {"name": "Customer portal", "type": "Container",
                 "tech": "Web application"},
    "meterapi": {"name": "meter-api", "type": "Container",
                 "tech": "Python, HTTP API"},
    "kbs":      {"name": "KBS", "type": "Container",
                 "tech": ""},                      # forgotten
    "db01":     {"name": "db01", "type": "Container",
                 "tech": "PostgreSQL database"},
}
relationships = [
    ("portal", "db01", "Reads usage and invoices [SQL]"),
    ("meterapi", "db01", "Writes readings [SQL]"),
    ("kbs", "db01", ""),                           # forgotten
]

# Check two of the C4 notation rules before anything is drawn
problems = [f"{key}: no technology" for key, e in elements.items()
            if not e["tech"]]
problems += [f"{src} -> {dst}: no label" for src, dst, label in relationships
             if not label]
for problem in problems:
    print("PROBLEM", problem)

# Generate Mermaid flowchart text from the same model
print("flowchart LR")
for key, e in elements.items():
    print(f'    {key}["{e["name"]}<br>[{e["type"]}: {e["tech"] or "?"}]"]')
for src, dst, label in relationships:
    print(f'    {src} -->|"{label or "?"}"| {dst}')
PROBLEM kbs: no technology
PROBLEM kbs -> db01: no label
flowchart LR
    portal["Customer portal<br>[Container: Web application]"]
    meterapi["meter-api<br>[Container: Python, HTTP API]"]
    kbs["KBS<br>[Container: ?]"]
    db01["db01<br>[Container: PostgreSQL database]"]
    portal -->|"Reads usage and invoices [SQL]"| db01
    meterapi -->|"Writes readings [SQL]"| db01
    kbs -->|"?"| db01

In a pipeline, the same check would fail the build, the way a linter fails on badly formatted code. The model is the source of truth; pictures are output.

Keeping documentation alive

Architecture documentation fails in two ways: nobody writes it, or somebody writes a lot of it once and nobody updates it. A few habits keep it small and current:

  • Keep it next to the code. A docs/ folder with Markdown, ADRs and diagram sources is updated in the same pull request as the change it describes. A wiki page three clicks away is not. A documentation site can publish the folder for readers who do not use Git.
  • Document decisions and structure, not details. The context diagram, the container diagram, the ADRs and the quality goals change rarely and are expensive to rediscover. Class diagrams and lists of endpoints change weekly; generate them or link to the code.
  • Date every diagram and name its owner. A diagram without a date cannot be trusted, because nobody knows whether it still matches the system.
  • Make the change checklist ask. "Does this change need an ADR or a diagram update?" in the pull request template or the Definition of Done costs one line and catches most misses.
  • Use the documents. Show the container diagram in onboarding and in incident reviews; link the ADR in the change request. Documentation that is read gets corrected.

Common mistakes

  • Boxes and lines without a legend. Nobody knows whether a box is a server, a program or a team, or which way the arrows point. Give every diagram a title, a legend, element types and labeled one-way arrows.
  • One diagram for everything. Servers, modules, teams and data flows on one page is unreadable. Draw separate views and say in each title which one it is.
  • Editing an ADR when the decision changes. The history disappears. Write a new ADR that supersedes the old one, and set the old one's status to "Superseded by ADR-nnnn".
  • ADRs for trivial choices, or for nothing. Record decisions that are hard to reverse or that people will ask about; skip the library for date formatting.
  • Treating C4 or arc42 as a standard to comply with. Both are lightweight, optional aids. C4 is not a replacement for UML (which is still useful for detailed design) and arc42 is not a form that must be completely filled in.
  • Documents nobody can find. Link the architecture folder from the repository's README and from the service's entry in the service catalog or configuration management database.