Introduction
Every engineering organization eventually inherits a system nobody fully understands anymore. The original authors have moved on, the wiki that once described the design decisions was abandoned years ago, and the only reliable source of truth is the codebase itself - sprawling, inconsistently structured, and full of implicit assumptions. Teams inheriting these systems often ask the same question: where do we even start if we want to document the architecture properly?
This article walks through a pragmatic, field-tested approach to bootstrapping architecture documentation for a legacy system that has none. It is not about writing a single exhaustive document in one heroic sprint. It is about establishing a repeatable discovery process, choosing lightweight documentation formats that survive contact with reality, and building institutional memory incrementally so that the next engineer who touches the system doesn't start from zero. The goal is not perfection - it's traceability, shared understanding, and a foundation that can evolve alongside the code.
Context: Why Legacy Systems Lose Their Architecture Story
Architecture documentation rarely disappears all at once. It erodes gradually, through a series of individually reasonable decisions. A team ships fast during a critical launch and skips writing the design doc "for now." A key decision gets discussed in a meeting and never recorded anywhere except in the memory of the people in the room. Six months later those people move to a different team, and the reasoning behind a particular database choice or service boundary is gone, leaving only the code as evidence - and code, notoriously, only tells you what happens, not why.
This erosion is compounded by tooling churn. Diagrams drawn in a design tool that the company no longer pays for become inaccessible. Confluence spaces get reorganized or archived. README files reference deployment processes that were replaced two migrations ago. Even when documentation technically exists, if it contradicts what the running system actually does, it is often worse than no documentation at all, because it actively misleads people who trust it.
There's also an organizational dimension worth naming directly. Documentation is frequently treated as a deliverable rather than a practice - something produced once for a compliance checklist or an audit, then never revisited. Without an owner, a review cadence, or an obvious place for updates to live, documents drift out of sync with the system they describe. Any strategy for rebuilding architecture documentation on a legacy project has to account for this pattern, or the new documentation will suffer the same fate as the old.
Deep Dive: What "Architecture Documentation" Actually Means
Before diving into process, it helps to be precise about scope, because "architecture documentation" means different things to different people, and vague scope is the fastest way to stall a documentation effort. A widely used reference point is the ISO/IEC/IEEE 42010 standard, which frames architecture description in terms of stakeholders, concerns, and viewpoints - the idea being that no single diagram or document can serve every audience, and different concerns (security, deployment, data flow, team ownership) warrant different views of the same system.
In practice, most useful architecture documentation for a legacy system converges on a small set of complementary artifacts: a high-level context diagram showing how the system fits into its environment, container and component views showing major internal building blocks, a record of significant decisions and the trade-offs behind them, and a small amount of narrative text explaining constraints that aren't visible in any diagram - regulatory requirements, historical incidents, or organizational boundaries that shaped the design. Frameworks like the C4 model (Context, Containers, Components, Code) popularized by Simon Brown, and the arc42 template, exist precisely because reinventing this structure for every project wastes time better spent on the actual discovery work.
A Practical Method for Reconstructing Architecture Docs
The first concrete step is not drawing diagrams - it's inventorying reality. Before you can describe an architecture, you need to know what actually runs in production, which is often surprisingly different from what the org chart or the repository list suggests. This means cataloguing deployed services, scheduled jobs, message queues, databases, and any third-party integrations, using whatever observability tooling exists: cloud provider resource inventories, CI/CD pipeline definitions, infrastructure-as-code repositories, and APM traces. If the system has any request tracing or logging in place, following a handful of real user journeys end-to-end is one of the fastest ways to discover components that nobody remembered were still load-bearing.
The second step is static analysis of the codebase itself. Dependency graphs between modules or services reveal coupling that isn't obvious from folder structure alone, and they often surface architectural violations - a "presentation layer" module quietly importing directly from a database client, for instance. This kind of analysis doesn't require expensive tooling; a modest script that walks import statements and builds a graph is usually enough to produce a first, honest picture of how the system is actually wired together, as opposed to how it was originally intended to be wired together.
The third step is talking to people, and it matters more than most teams expect. Even on a system with no written documentation, there is almost always tacit knowledge distributed across current and former team members, support engineers, and product managers who remember why a workaround exists. Structured interviews - asking specifically about painful incidents, unusual configuration, and "things you'd warn a new hire about" - tend to surface far more architecturally relevant information than open-ended "tell me about the system" questions, because people default to describing the happy path unless prompted for the exceptions.
The fourth step is converging these three inputs - infrastructure reality, code structure, and human memory - into a first draft model, explicitly marked as provisional. This draft should be reviewed by whoever has the most operational familiarity with the system, not because it needs to be perfect before publishing, but because a wrong diagram that goes unchallenged becomes exactly the kind of misleading documentation the effort was meant to avoid.
Implementation: Tools and Code Patterns
Turning discovery into durable documentation benefits from a small, boring toolchain rather than a heavyweight modeling suite, because boring tools are the ones that survive staff turnover. Diagrams-as-code approaches - using Mermaid or PlantUML syntax committed alongside the source code - mean architecture diagrams are versioned, diffable, and reviewed through the same pull request process as everything else. This matters enormously for legacy systems specifically, because it ties the documentation's lifecycle to the code's lifecycle instead of to a separate wiki that nobody remembers to update.
A useful first artifact is a dependency scanner that produces a machine-generated starting point for the component view, which humans then annotate and correct rather than draw from scratch. The following Python example walks a codebase, extracts import relationships, and emits a Mermaid graph definition that can be pasted directly into a Markdown document or rendered by a CI pipeline:
import ast
import os
from collections import defaultdict
from pathlib import Path
def extract_imports(file_path: Path, root_package: str) -> set[str]:
"""Parse a Python file and return internal module imports only."""
tree = ast.parse(file_path.read_text(encoding="utf-8"), filename=str(file_path))
internal_imports = set()
for node in ast.walk(tree):
if isinstance(node, ast.Import):
for alias in node.names:
if alias.name.startswith(root_package):
internal_imports.add(alias.name.split(".")[1])
elif isinstance(node, ast.ImportFrom) and node.module:
if node.module.startswith(root_package):
parts = node.module.split(".")
if len(parts) > 1:
internal_imports.add(parts[1])
return internal_imports
def build_dependency_graph(src_root: str, root_package: str) -> dict[str, set[str]]:
graph: dict[str, set[str]] = defaultdict(set)
root_path = Path(src_root)
for py_file in root_path.rglob("*.py"):
relative = py_file.relative_to(root_path)
parts = relative.parts
if len(parts) < 2:
continue
module = parts[1]
try:
deps = extract_imports(py_file, root_package)
except SyntaxError:
continue
deps.discard(module)
graph[module].update(deps)
return graph
def to_mermaid(graph: dict[str, set[str]]) -> str:
lines = ["graph TD"]
for module, deps in sorted(graph.items()):
for dep in sorted(deps):
lines.append(f" {module} --> {dep}")
return "\n".join(lines)
if __name__ == "__main__":
graph = build_dependency_graph("src", root_package="myapp")
print(to_mermaid(graph))
This script is deliberately unglamorous - it doesn't use a fancy static analysis framework - but that's the point for a legacy codebase where you can't assume modern tooling compatibility. Once the raw dependency graph exists, the human work is pruning noise, grouping modules into meaningful architectural components, and labeling boundaries that the automated pass can't infer, such as which components represent bounded contexts in a domain-driven design sense.
Alongside diagrams, Architecture Decision Records (ADRs) are the single highest-leverage documentation format for a legacy system, because they capture why rather than only what. Michael Nygard's original ADR format - a short Markdown file recording context, decision, and consequences - is intentionally lightweight enough that engineers actually write them during code review instead of deferring them indefinitely. For a legacy system, the first batch of ADRs is often written retroactively, reconstructing past decisions from git history, incident postmortems, and the interviews mentioned earlier, explicitly dated and marked as reconstructed rather than contemporaneous.
Trade-offs and Common Pitfalls
The most common failure mode is attempting to document everything before publishing anything. Teams sometimes set out to produce a comprehensive architecture manual and get stuck for months trying to achieve completeness on a system that is actively changing underneath them. A partial, clearly-scoped document that covers the three or four most business-critical flows and is actually read by the team delivers more value than an ambitious document that never ships. It's worth treating the first version explicitly as a minimum viable architecture description, with known gaps listed rather than hidden.
A second pitfall is documenting the system as it "should" be rather than as it actually is. When engineers reconstruct architecture from memory or from outdated design intentions, there's a natural temptation to describe the idealized version - the one from the original design proposal - rather than the messier reality that includes the workarounds, the deprecated-but-still-running service, and the manual step someone performs before every deploy. Documentation that omits known ugliness erodes trust the first time a reader discovers the discrepancy, and once trust is lost, people stop consulting the docs altogether, which defeats the purpose of writing them.
Best Practices for Sustaining Architecture Documentation
Documentation that survives past the initial push tends to share a few characteristics. First, it lives close to the code - in the same repository, reviewed through the same pull request workflow - rather than in a separate system with different permissions and different notification habits. Second, it has an explicit owner or a rotating responsibility, even if that just means a named team is accountable for reviewing architecture docs quarterly rather than letting them go stale indefinitely.
Third, successful teams treat significant technical decisions as inherently requiring an ADR, the same way many teams treat public API changes as inherently requiring a changelog entry. This turns documentation into a byproduct of normal engineering work rather than a separate initiative competing for sprint capacity. Automating a linter or pull request template that nudges engineers to add an ADR when they touch certain high-impact directories - service boundaries, shared libraries, infrastructure configuration - helps make this a default rather than a discipline that erodes under deadline pressure.
Fourth, architecture documentation benefits from being tested the way code is tested. Some organizations adopt fitness functions, a concept described in Building Evolutionary Architectures by Neal Ford, Rebecca Parsons, and Patrick Kua, where automated checks verify that the system still respects documented architectural constraints - for example, that a particular module never directly imports a database client. The following TypeScript example sketches a simple fitness function using a dependency-cruiser style rule, run in CI to catch architecture drift before it silently invalidates the documentation:
import { readFileSync } from "fs";
import { globSync } from "glob";
interface ArchitectureRule {
name: string;
forbiddenImportPattern: RegExp;
appliesTo: string; // glob pattern for files this rule governs
}
const rules: ArchitectureRule[] = [
{
name: "presentation-layer-cannot-import-db-client",
forbiddenImportPattern: /from ['"].*\/db\/client['"]/,
appliesTo: "src/presentation/**/*.ts",
},
{
name: "domain-layer-cannot-import-http-framework",
forbiddenImportPattern: /from ['"]express['"]/,
appliesTo: "src/domain/**/*.ts",
},
];
function checkArchitectureRules(rules: ArchitectureRule[]): string[] {
const violations: string[] = [];
for (const rule of rules) {
const files = globSync(rule.appliesTo);
for (const file of files) {
const content = readFileSync(file, "utf-8");
if (rule.forbiddenImportPattern.test(content)) {
violations.push(`[${rule.name}] violated in ${file}`);
}
}
}
return violations;
}
const violations = checkArchitectureRules(rules);
if (violations.length > 0) {
console.error("Architecture fitness function failures:");
violations.forEach((v) => console.error(` - ${v}`));
process.exit(1);
}
console.log("All architecture fitness checks passed.");
This kind of check is deliberately narrow in scope, but that narrowness is a feature: it turns a handful of the most important architectural invariants into something enforced automatically, rather than trusting that every future contributor reads and remembers the documentation before making a change.
Key Takeaways
For teams starting this work today, five concrete actions tend to produce disproportionate value early on:
- Inventory before you diagram. Confirm what's actually running in production before drawing any component you haven't verified exists.
- Trace a handful of real user journeys end-to-end using existing logs or traces; this surfaces hidden dependencies faster than reading code alone.
- Write your first ADRs retroactively, reconstructing the most consequential past decisions from git history and interviews, clearly labeled as reconstructed.
- Store diagrams and decisions as code in the same repository as the system, so documentation changes go through the same review process as everything else.
- Automate at least one fitness function for your most important architectural rule, so the documentation is backed by an enforcement mechanism rather than good intentions alone.
Analogies and Mental Models
It helps to think of documenting a long-running legacy system less like writing a book and more like an archaeological excavation. An archaeologist doesn't wait until they understand an entire site before publishing any findings; they document each layer as it's uncovered, note uncertainty explicitly, and revise earlier conclusions as deeper layers change the picture. Applied to software, this means publishing the context diagram as soon as it's verified, rather than waiting for the component-level detail to be complete - and being comfortable marking sections as "unverified" or "pending" instead of leaving gaps unmentioned.
A second useful mental model is the idea of a living map versus a photograph. A photograph captures a single moment and immediately starts becoming inaccurate the moment conditions change; a living map, like the ones used in collaborative wikis or version-controlled repositories, is expected to be edited continuously and treats staleness as a bug to be fixed rather than an inevitability to be tolerated. Framing architecture documentation as a living map from the outset changes how teams design its storage, ownership, and update process, and it sets the right expectation with stakeholders that the first version is a starting point, not a finished deliverable.
The 80/20 Insight
A small number of practices account for most of the value in this entire effort: tracing real request flows instead of relying on memory, writing lightweight ADRs for decisions rather than exhaustive design documents, and keeping diagrams as versioned code next to the system they describe. Teams that adopt only these three habits, without ever producing a comprehensive architecture manual, typically end up with documentation that is more accurate and more used than teams that invest far more effort in a single polished but static document.
Conclusion
Starting architecture documentation for a legacy system with no existing docs is less about writing and more about disciplined discovery - confirming what's actually deployed, tracing how requests really move through the system, and capturing the reasoning behind past decisions before it disappears entirely. The frameworks and standards referenced throughout this article, from C4 and arc42 to ADRs and ISO/IEC/IEEE 42010, exist to give that discovery process a shared vocabulary, not to dictate a rigid process that must be followed to the letter.
The teams that succeed at this long-term are the ones that treat documentation as an ongoing practice tied to the code's own lifecycle, rather than a one-time project with a deadline. A partial, honest, continuously updated architecture description will outlast and outperform a comprehensive document written once and never revisited - because the system it describes was never going to stand still either.
References
- ISO/IEC/IEEE 42010:2011, Systems and software engineering - Architecture description
- Simon Brown, "The C4 model for visualising software architecture," c4model.com
- arc42 documentation template, arc42.org
- Michael Nygard, "Documenting Architecture Decisions," 2011 (origin of the ADR format)
- Len Bass, Paul Clements, Rick Kazman, Software Architecture in Practice, Addison-Wesley
- Paul Clements et al., Documenting Software Architectures: Views and Beyond, Addison-Wesley
- Eric Evans, Domain-Driven Design: Tackling Complexity in the Heart of Software, Addison-Wesley
- Neal Ford, Rebecca Parsons, Patrick Kua, Building Evolutionary Architectures, O'Reilly Media
- Mermaid documentation, mermaid.js.org
- PlantUML documentation, plantuml.com