Architecture Documentation
The Core Problem
Why architecture docs fail:
| Problem | Why it happens |
|---|---|
| Too much too early | Spec written before code — outdated by first sprint |
| No time during development | Devs skip it under pressure, never catch up |
| No ownership | Nobody is explicitly responsible → everyone assumes someone else does it |
| Unclear scope | “What actually goes in there?” — no shared answer |
Solution framing: Don’t ask “how do we document everything?” — ask “what and how do we document?” Less is more. Document decisions and context, not implementation details the code already tells you.
Docs-as-Code
Architecture docs live in the source repository, versioned with the code.
Why:
- Docs stay in sync with the code they describe
- Same review process (PRs, diffs) as code
- No separate wiki that rots
- Diagrams are text → diffable, branchable, mergeable
Tooling for textual diagrams:
- PlantUML — UML diagrams from text (sequence, class, component)
- ContextMapper — DDD-specific diagrams from CML DSL (see below)
- Structurizr — C4 model diagrams from DSL (see below)
Template: Arc42
Site: https://arc42.org/overview
Structured template with 12 sections. Use what fits — skip the rest.
| # | Section | What goes here |
|---|---|---|
| 1 | Introduction & Goals | Top 3–5 quality goals, stakeholder table with expectations |
| 2 | Constraints | Legal, organizational, technical limits that restrict design choices |
| 3 | Context & Scope | System boundary diagram — what’s inside, what’s outside, who talks to what |
| 4 | Solution Strategy | Key architectural decisions in 1 page — technology choices, patterns selected |
| 5 | Building Block View | Static decomposition — white/black box hierarchy of components |
| 6 | Runtime View | How blocks interact at runtime — sequence diagrams for key scenarios |
| 7 | Deployment View | Which software runs where — servers, containers, cloud regions |
| 8 | Crosscutting Concepts | Things that apply everywhere: logging, auth, error handling patterns |
| 9 | Architectural Decisions | Links to ADRs (see below) |
| 10 | Quality Requirements | Non-functional requirements as scenarios — “system serves 1000 RPS with <100ms p99” |
| 11 | Risks & Technical Debt | Known problems, shortcuts taken, things that might bite you |
| 12 | Glossary | Domain terms and acronyms — especially where business and tech language differ |
Practical tip: Section 3 (context diagram) and Section 4 (solution strategy) deliver the most value for the least effort. Start there.
ADRs – Architecture Decision Records
Source: https://www.heise.de/hintergrund/Gut-dokumentiert-Architecture-Decision-Records-4664988.html
Concept by Michael Nygard (2011). One short document per significant architectural decision. Captures why something was decided — not just what.
Structure
# ADR-0042: Use PostgreSQL for primary storage
## Status
Accepted
## Context
We need a relational store. Team has strong SQL expertise.
MongoDB was evaluated but schema flexibility not needed here.
## Decision
Use PostgreSQL 15. No ORM — plain SQL via jOOQ.
## Consequences
+ ACID guarantees, mature tooling, team knows it
+ jOOQ gives type-safe queries without Hibernate magic
- Schema migrations need careful management (Flyway)
- No easy horizontal write scaling if we hit 10M+ rows/day
The 5 questions a good ADR answers
| Question | Maps to |
|---|---|
| When? | Date / Status history |
| Who? | Author / team |
| Why? | Context section |
| What for? | Consequences |
| What? | Decision section |
Status lifecycle
proposed → accepted → deprecated → superseded by ADR-XXXX
Where to store ADRs
docs/
adr/
0001-use-postgresql.md
0002-no-orm-use-jooq.md
0003-event-sourcing-for-orders.md
Tooling: adr-tools (Bash) handles numbering and status updates automatically. But plain numbered Markdown files work fine.
Key insight: One decision = one ADR. If an ADR contains multiple choices, split it.
ContextMapper
Site: https://contextmapper.org/
DSL for Strategic DDD — models Bounded Contexts and their relationships in code (CML = Context Mapping Language, Xtext-based).
What it generates from a CML model
- Graphical Context Maps
- PlantUML component/class diagrams
- MDSL (micro-)service contracts
- Spring Boot / JHipster project stubs (via Freemarker templates)
Example CML
ContextMap InsuranceContextMap {
type = SYSTEM_LANDSCAPE
state = TO_BE
contains CustomerManagement, PolicyManagement, ClaimsContext
CustomerManagement [D, ACL] <- PolicyManagement [U, OHS, PL]
PolicyManagement [U] -> ClaimsContext [D, CF]
}
BoundedContext CustomerManagement {
Aggregate Customers {
Entity Customer {
aggregateRoot
String customerId
String name
List<Address> addresses
}
}
}
Relationship patterns (from DDD): U = Upstream, D = Downstream, ACL = Anti-Corruption Layer, OHS = Open Host Service, PL = Published Language, CF = Conformist.
ContextMapper can also reverse-engineer existing Java codebases into a CML model via its discovery library — useful for documenting legacy systems.
Structurizr + C4 Model
Docs: https://docs.structurizr.com/
Created by Simon Brown (also creator of C4). Single DSL model → multiple diagram levels auto-generated.
The C4 Levels
| Level | Audience | Shows |
|---|---|---|
| L1: System Context | Everyone incl. non-technical | Your system + external users & systems |
| L2: Container | Developers, architects | Apps, databases, services inside your system |
| L3: Component | Developers | Internals of one container |
| L4: Code | Developers | Classes, functions (usually auto-generated from IDE) |
Structurizr DSL Example
workspace "Online Shop" {
model {
customer = person "Customer"
shop = softwareSystem "Online Shop" {
webapp = container "Web App" "React SPA" "TypeScript/React"
api = container "API" "REST API" "Spring Boot"
db = container "Database" "Orders DB" "PostgreSQL" { tags "Database" }
}
payments = softwareSystem "Payment Provider" { tags "External" }
customer -> webapp "Browses and orders"
webapp -> api "API calls" "HTTPS/JSON"
api -> db "Reads/writes"
api -> payments "Charges card" "HTTPS"
}
views {
systemContext shop "SystemContext" { include * }
container shop "Containers" { include * }
}
}
One model definition → generates System Context and Container diagrams. Add component blocks → generates Component diagrams too.
Why it beats draw.io: The model is the single source of truth. Change a relationship once — all affected diagrams update. No stale diagrams.
Architecture-Code-Gap: jQAssistant + Structurizr
Tool: structurizr-jqassistant plugin
jQAssistant can:
- Parse your source code into a Neo4j graph
- Import your Structurizr model
- Compare them — flag where code diverges from documented architecture
This runs in CI. If a developer adds a dependency between components that the architecture says shouldn’t exist, the build fails.
# Example jQAssistant rule (Cypher query)
- id: architecture:NoDirectDBAccessFromAPI
description: "Only Repository classes may access the database"
cypher: |
MATCH (c:Class)-[:CALLS]->(db:Class {name: "DataSource"})
WHERE NOT c.name ENDS WITH "Repository"
RETURN c.fqn AS violatingClass
severity: MAJOR
Combined workflow:
Structurizr DSL (architecture intent)
↓
jQAssistant imports model + scans source code
↓
CI runs Cypher rules against the combined graph
↓
Build fails on architecture violations
This closes the architecture-code gap automatically — the documented architecture becomes an enforced constraint, not a suggestion.
Summary: Minimal Viable Architecture Doc Stack
| Need | Tool |
|---|---|
| Overall structure | Arc42 (pick 3–5 sections max) |
| Decision history | ADRs in docs/adr/ |
| Context + container diagrams | Structurizr DSL in repo |
| DDD context mapping | ContextMapper CML |
| Enforcement in CI | jQAssistant + Structurizr plugin |