Docs & Code Guide

ARCHITECTURE.md: the file that tells a newcomer where the code is

By the engineer who builds Datadef, from client work on data platforms · Reviewed August 21, 2026

The convention is simple enough that it spread on its own: a file next to README.md and CONTRIBUTING.md that gives a bird eye view of the codebase, aimed squarely at the person who has a change to make and no idea which directory to open. It is not a design document, it is not a decision log, and it is not an API reference. It is a map, and the reason so few repositories have a good one is that the map is the part that goes stale fastest.

7 min readFor maintainers of repositories big enough that new contributors get lost

See it as a diagram

Everything below, as a diagram you can edit. Describe yours and see it in seconds.

145/20003 credits left
Try:

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

![Architecture](https://datadef.io/embed/<diagram-id>.png)

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

An ARCHITECTURE.md survives only while updating it costs less than reading the code. Generating the inventory is what keeps it on the right side of that line. See repo to diagram.

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?

It is a file kept next to README.md and CONTRIBUTING.md that gives a bird eye view of a codebase: a map from directories to responsibilities, the boundaries between modules, the non-obvious invariants, and one diagram of the runtime shape. Its audience is a contributor trying to work out where to make a change.

How is it different from a README or an ADR?

The README is the front door for everyone, including people who will never write code. An ADR records one decision at one moment and is immutable once accepted. ARCHITECTURE.md describes the current shape of the code and is expected to change whenever the shape does.

When should a repository have one?

When no single person holds the whole layout in their head. That usually starts somewhere around ten thousand lines of code and is certain past a hundred thousand, and it arrives immediately for any repository containing more than one deployable component.

Which parts should be generated rather than written?

The inventory: which modules exist, what they contain, how they are wired, and any reference table such as a list of Terraform modules with their source and resource count. Boundaries and invariants change a few times a year and are worth writing by hand, because they are judgement rather than fact and no generator can recover them from the code.

Can an architecture document be generated from the repository itself?

Yes. A read-only connection to GitHub, GitLab or Azure DevOps, pinned to a branch or a tag, is enough to regenerate both a diagram and an architecture document on a daily cycle. Terraform repositories are parsed from the .tf source, with no state file and no cloud credentials involved.