In this guide
If you have spent time around architecture documentation, you have probably met two camps. One camp draws C4 diagrams: a context view, a container view, maybe a component view, all with consistent notation and careful labels. The other camp says hand-drawn diagrams are always out of date and points a tool at the code to generate the picture. Both camps are right about something, and both are wrong when they treat their approach as the whole answer.
This post compares the C4 model with generated architecture maps: what each one is for, where each one breaks, and a concrete way to combine them so you get readable intent and accurate structure at the same time.
Key takeaways
- The C4 model is a set of four zoom levels (Context, Containers, Components, Code) plus a shared vocabulary. It is a way of thinking about diagrams, not a tool.
- C4 Levels 1 and 2 (Context and Containers) are where curated diagrams shine: they change rarely, carry intent, and include things the code cannot tell you, such as users and external systems.
- Levels 3 and 4 (Components and Code) change constantly. Generated maps are almost always better there.
- Generated maps are accurate but have no opinion. They show everything with equal weight and cannot explain why a boundary exists.
- The strongest setup is curated C4 at the top, generated maps below, and automated checks that keep the two consistent.
A quick refresher on C4
The C4 model, created by Simon Brown, describes software at four levels of zoom:
| Level | Shows | Typical audience | How often it changes |
|---|---|---|---|
| 1. System context | Your system as one box, the people who use it, and the external systems it talks to | Everyone, including non-engineers | Rarely (yearly) |
| 2. Containers | The deployable or runnable units: web app, API, database, queue, worker, mobile app | Engineers, ops, architects | Occasionally (quarterly) |
| 3. Components | The major building blocks inside one container: modules, services, controllers | Engineers working on that container | Often (monthly or faster) |
| 4. Code | Classes, functions, tables | Engineers working on that component | Constantly |
A "container" in C4 is not a Docker container. It is anything that runs or stores data on its own: a single-page app, a server process, a database schema, a serverless function.
The real value of C4 is less the diagrams than the discipline. It forces one level of abstraction per diagram, a title and key on every view, labeled relationships with technology ("JSON/HTTPS", "AMQP"), and consistent element types. That alone fixes most of what makes informal architecture diagrams confusing.
Simon Brown himself says Level 4 diagrams are optional and best generated by tooling. That observation is the starting point for combining the two approaches.
What C4 is good at
Intent and context. A Level 1 diagram shows that your system depends on Stripe, sends email through a provider, and is used by both customers and internal support staff. None of that is visible in your import graph. The code knows there is an HTTP client; it does not know that the client is the thing finance cares about.
Omission. A good container diagram shows four boxes and hides 200 packages. Choosing what to leave out is a judgment call that a generator cannot make.
Shared language. Once a team agrees on "container" and "component", conversations get shorter. "That is a new container, not a new component" is a meaningful sentence that implies separate deployment, separate scaling, and a network boundary.
Stability. Context and container views describe decisions that are expensive to change. They do not need updating every sprint, so a curated diagram can stay accurate for a long time with occasional review.
Where C4 breaks down
Components and code drift fast. A Level 3 diagram lists the modules inside your API. Someone adds a module, splits another, introduces a dependency between two that were independent. Unless that diagram is regenerated or reviewed with every change, it is wrong within weeks. This is the general problem covered in architecture diagrams that stay accurate.
Diagrams describe intent, not reality. The container diagram says the web app only talks to the API. A grep shows the web app has a direct database connection for one report page, added during an incident. The C4 diagram cannot see that.
Effort scales with system size. Curating component views for 30 services is a part-time job. Most teams start and then stop.
What generated maps are good at
A generated architecture map is built from the code itself: imports, calls, manifests, build files, sometimes deployment config. Tools range from dependency-cruiser and madge for JavaScript, pydeps and import-linter for Python, go list and go mod graph for Go, jdeps and ArchUnit for Java, to code-graph tools that parse many languages with tree-sitter and build a cross-file graph.
Accuracy. If the code imports it, the map shows it. That includes the dependencies nobody intended, which are usually the interesting ones.
Completeness at depth. A generator handles 3,000 files as easily as 30. It is the only practical way to get a component- or code-level view of a large system.
Always current. Built from the current commit, a generated map cannot be stale. It can only be generated from an old commit.
Queryable. A map backed by a graph can answer questions a picture cannot: what depends on this module, what is the shortest path from the HTTP handler to the database, which files have the most incoming edges. See dependency graphs explained for how to read those shapes.
Where generated maps break down
No intent. A generator shows that billing imports notifications. It cannot tell you whether that is fine or a violation of a boundary you agreed last year.
Equal weight for everything. A logging utility imported by 400 files becomes the biggest node on the map, even though it is the least interesting part of the system architecturally. You need filtering, collapsing and grouping to get a readable view.
Blind to runtime and external systems. Static analysis sees imports, not network calls configured by environment variables. It does not know that ORDERS_URL points at a different service, or that a queue topic connects two repositories. Users and third-party systems are invisible.
Hairballs. Without collapsing to folders or packages, most generated graphs are unreadable past a few hundred nodes.
Side-by-side summary
| Question | C4 (curated) | Generated map |
|---|---|---|
| Who uses the system? | Yes | No |
| Which external systems do we depend on? | Yes | Partly (via SDK imports) |
| What runs as a separate process? | Yes | Partly (via build and deploy config) |
| Which modules exist inside the API today? | Only if recently updated | Yes |
| Does anything bypass the intended layering? | No | Yes |
| What breaks if I change this file? | No | Yes |
| Why is this boundary here? | Yes, if linked to a decision | No |
Combining them: curated top, generated bottom
The combination that works for most teams looks like this.
Levels 1 and 2: curate, as code
Write context and container views in a text format stored in the repository. Structurizr DSL is designed for C4 and renders multiple views from one model:
workspace "Shop" {
model {
customer = person "Customer"
support = person "Support agent"
payments = softwareSystem "Payment provider" "External"
shop = softwareSystem "Shop" {
web = container "Web app" "Storefront and account pages" "Next.js"
api = container "API" "Orders, catalog, accounts" "Go"
worker = container "Fulfillment worker" "Consumes order events" "Go"
db = container "Database" "Orders and catalog" "PostgreSQL"
events = container "Order events" "OrderPlaced, OrderShipped" "Kafka"
}
customer -> web "Browses and buys"
support -> web "Looks up orders"
web -> api "Calls" "JSON/HTTPS"
api -> db "Reads and writes" "SQL"
api -> events "Publishes OrderPlaced"
worker -> events "Consumes"
api -> payments "Creates charges" "HTTPS"
}
views {
systemContext shop {
include *
autolayout lr
}
container shop {
include *
autolayout lr
}
}
}
Mermaid also has C4 syntax (C4Context, C4Container), although it is less mature; PlantUML has the widely used C4-PlantUML library. Whichever you pick, these files change rarely, are reviewed in pull requests, and carry an owner.
Levels 3 and 4: generate
Inside each container, do not hand-draw components. Generate a module graph from the code, collapsed to the folder level that matches how the team thinks about components. For a Go service that might be the packages under internal/; for a TypeScript app, the top-level folders under src/.
# Collapse a TypeScript container's files into component-sized folders
npx depcruise src --collapse "^src/[^/]+" --output-type dot | dot -T svg > components.svg
Better still, use a tool that builds the view live so there is no committed image to go stale.
The glue: check one against the other
The curated container view implies rules. "The web app calls the API" implies "the web app does not import the database client". Turn those implications into checks that run in CI:
// .dependency-cruiser.js (in the web app container)
module.exports = {
forbidden: [
{
name: 'web-uses-api-not-db',
comment: 'Container view: web -> api only. See docs/architecture/workspace.dsl',
severity: 'error',
from: { path: '^src' },
to: { path: 'node_modules/(pg|postgres|@prisma/client)' },
},
],
}
At the component level, rules come from your intended layering, and the module boundaries and coupling post shows how to express them in dependency-cruiser, import-linter, ArchUnit and Nx. When a check fails, either the code is wrong or the intent changed. Both outcomes are useful, and the second one produces a pull request that updates the curated diagram, which is exactly the update that would otherwise never happen.
Choosing for your situation
- Small team, one deployable: skip C4 Level 2 beyond a single Mermaid flowchart. Rely on a generated module map and a couple of lint rules.
- Several services, one or two teams: curate Levels 1 and 2. Generate Level 3 per service. Add boundary rules for the two or three dependencies you care most about.
- Many services, many teams: curate Levels 1 and 2 with Structurizr or similar, owned by an architecture group or platform team. Each team owns generated component views and boundary rules for its services. Link every container to an architecture decision record explaining why it exists.
- Onboarding or due diligence on someone else's code: there is no curated diagram, or it cannot be trusted. Start generated, then draw a rough context view yourself once you understand the system.
Where OpenVisio fits
The generated half of this setup is what code-graph tools are for. OpenVisio's Explore space builds the module and group graph from a repository directly in the browser, so you can look at the component-level structure of any container without installing a toolchain, and the Tour presents the same structure as a guided walkthrough with an isometric diagram view. It does not replace a curated context diagram that names your users and your payment provider, and it should not try to. Use C4 to say what the system is for and how it is meant to be split; use generated maps to see how it is actually split today; and use automated checks to notice when those two stories disagree.

