See it as a diagram
Everything below, as a diagram you can edit. Describe yours and see it in seconds.
No account needed · Editable canvas, not a picture
Why a separate file beats a longer README
A README is read by everyone, including people evaluating the project in thirty seconds. An ARCHITECTURE.md is read by people who have already decided to work in the code. Those are different documents with different budgets, and merging them means the second audience scrolls past the install instructions every time while the first audience scrolls past module boundaries they will never care about.
The usual threshold for splitting is a repository large enough that no single person holds the whole layout in their head, which in practice starts somewhere around ten thousand lines and is certain by a hundred thousand. Multi-service repositories cross it immediately, whatever the line count.
The most valuable property of a separate file is that it has an owner and a review trigger. A section buried in a README is nobody responsibility. A file with a name is something a reviewer can ask about when a pull request moves a module.
What a good one contains
The codebase map. One line per top level directory saying what lives there, in the vocabulary the team uses out loud rather than the vocabulary of the framework. This is the section people come for and the section that pays for the file.
The boundaries. Which modules are allowed to depend on which, and which dependencies are deliberately forbidden. A sentence like nothing under core imports from ui is worth more than a page of prose, because it is checkable and because violating it is the most common way an architecture erodes.
The invariants and the surprises. The things that are true and non-obvious: this queue is at-least-once so every consumer is idempotent, this table is append only, this cache is allowed to be wrong for sixty seconds. Newcomers cannot derive these from the code in reasonable time, and they are what senior engineers actually carry in their heads.
One diagram of the runtime shape. Not a class hierarchy, not a call graph. The processes, the stores and the wiring, at the same level of detail as the codebase map so the two can be read side by side.
What is deliberately not here: decision rationale, which belongs in numbered ADRs, and generated reference material, which belongs wherever it is generated.
# Architecture ## Codebase map - `cmd/` entry points, one directory per binary - `internal/` everything that is not importable from outside - `pkg/api/` request handling and serialisation only - `pkg/domain/` business rules, no I/O, no framework imports - `infra/` Terraform for every environment ## Boundaries - `pkg/domain` imports nothing from `pkg/api` or `infra`. - Only `internal/store` opens a database connection. ## Invariants - The ingest queue is at-least-once. Every consumer is idempotent. - `events` is append only. Corrections are new rows, never updates. ## Runtime shape 
Generate the inventory, hand-write the intent
The file has two halves with completely different decay rates. Boundaries and invariants change a handful of times a year and are worth writing carefully by hand. The inventory, which modules exist, what they contain, how they are wired, changes every sprint and is exactly the kind of writing nobody does twice.
So split the file along that line. Keep the hand-written half at the top, and let the inventory half be produced from the repository. Repository sync connects read only to GitHub, GitLab or Azure DevOps, reads a branch or a tag, and regenerates both a diagram and an architecture doc on a daily cycle. For Terraform repositories the generated doc carries a Module reference table with five columns, stack, module, source, resource count and notes, composed from the parsed .tf files rather than written by the model, so the source string and the count are what the repository declares. A module whose source points at the registry or a git URL cannot be expanded from your checkout, so its count cell reads external instead of a number that would be a guess.
The diagram half benefits from the same treatment for a different reason. Node identity is derived from Terraform addresses rather than assigned per run, so a re-sync updates the picture in place instead of recomposing it, and a sync whose structure fingerprint matches the previous one stops before generation and leaves the diagram alone. That is what makes it safe to reference the picture from a file people are supposed to trust.
The map has to be cheaper than the guess
Keeping it honest over time
Add one line to the pull request template: if this change moves a module boundary, update ARCHITECTURE.md. It is a weak control and it still catches most of the drift, because the people moving boundaries are the people who know what changed.
Turn the boundaries section into a test where the language allows it. Import linters, module visibility rules and dependency-cruiser style checks turn a documented rule into an enforced one, and an enforced rule cannot silently become false.
Date the file. A reader who knows the map was regenerated last night reads it differently from a reader looking at a document with no date, and the difference is whether they trust it enough to act without checking.
FAQ
What is an ARCHITECTURE.md file?
How is it different from a README or an ADR?
When should a repository have one?
Which parts should be generated rather than written?
Can an architecture document be generated from the repository itself?