Architecture
C4: Changing the Zoom Level of an Architecture Explanation
How the C4 model lets you describe one software system at several levels of detail, choosing the level by the question the reader needs answered and…
A Software Architecture Document (SAD) gathers a system's decisions, boundaries and important flows in one place. This unit deals with a different problem: how to describe the same system at different depths, so that a product owner never has to read through class methods, while the author of a component still sees exactly the boundaries that matter to them.
C4 is a model of architectural abstractions and the kinds of description that go with them. The name refers to four levels — Context, Containers, Components, Code — which narrow the field of view step by step, from the system in its surroundings down to implementation details. C4 is not four mandatory documents, not an organisational process, and not a requirement to draw diagrams. When graphics are unavailable or unnecessary, the same boundaries and relationships can be explained with text and tables.
By the end of this unit you will be able to:
- choose the right level based on the question you need to answer;
- avoid confusing a system, a person, an application, a component, a function and a physical environment;
- label relationships with their direction, purpose and, where relevant, protocol or format;
- keep names, boundaries and responsibilities stable from one level to the next;
- separate static structure from sequences of steps and from physical deployment;
- name what C4 does not explain and what you need to add alongside it.
Why change the zoom level of a description
An architecture explanation has to answer the reader's question. A product owner wants to know which people and external systems depend on the product, where the team's responsibility ends, and what breaks when a vendor fails. An engineer changing the availability check wants to find the container that handles the request, and inside it the component that makes the decision. The level is set by the question, not by the reader's job title: a developer may start with the context if they do not yet know why the system exists; a product owner may drop down to containers when discussing hosting or a vendor boundary. Show both of them every class and function at once, and both lose the main idea.
Think of a city map versus directions from the front door to a particular office. A city overview does not need room numbers; knowing the country does not help you find the office. The analogy has limits: an architectural level is not simply "more pixels on the map". Each zoom step changes the types of element, the boundary and the questions the description answers. You cannot drop a code detail into a context description just because it is easier to find there.
The official site of Simon Brown, the author of C4, describes the model as a hierarchy of abstractions with matching diagram types, plus supplementary views: system landscape, dynamic and deployment (physical placement) views. It explicitly calls the model independent of notation and tooling. C4 helps describe static structure; processes, workflows, data models and states can be added as separate descriptions when needed (official C4 site, C4 model FAQ).
So the name of a level sets the scale of the description, not "who is allowed to talk" about architecture. Context is there so you can see responsibility and dependencies; the lower levels are for when the reader has to find a specific boundary or check a specific rule.
Level 1: the software system context

A software system in C4 is the whole software product whose boundary we are currently looking at. At the System Context level the system sits alongside people and other systems. We state its purpose, its audience, the external actors and the relationships with them. At this level there are no libraries, classes, databases or internal services inside the system: it is a deliberate step back from the implementation.
| Element | What to record | What not to add at this level |
|---|---|---|
| Person or role | Who interacts with the system and for what purpose | Every internal authorisation role, unless it changes how the context is understood |
| Software system | Its name, a short purpose and the boundary of its responsibility | The technology stack and the details of each module |
| External system | Which separate product or vendor interacts with our system | An internal database that the team considers part of its own product |
| Relationship | Who initiates the interaction, what is sent or received, and why | An unnamed "connected to" line with no direction or meaning |
The context exists to agree on responsibility and dependencies. If a confirmation email goes out through an external mail service, the team is responsible for sending the correct request and for the visible state of the booking, but not for the mail vendor's internal queue. If the vendor changes, the relationship boundary and the contract are reviewed separately.
Level 2: containers inside the system

A container in C4 is a separately running application, or a data store, that has a noticeable responsibility of its own and interacts with other parts of the system. Examples are a single-page application running in the browser, a server-side application, a mobile client or a database. Here "container" does not necessarily mean a Docker container. One physical image or process can hold several logical parts, and one logical C4 container can run as several copies in a deployment. The container model shows the main technologies and communication, not a full inventory of machines and instances (C4: container diagram).
For each container, state:
- its responsibility, and the data it owns or is accountable for;
- where it runs — in the browser, in its own process, in an application, or in a data store;
- which technologies matter for understanding its boundary;
- who interacts with it, in which direction, for what purpose and over which protocol;
- whether it is part of the system being described or an external system.
From here on we use the term application programming interface (API). For network communication, a browser and a server might use HTTP (Hypertext Transfer Protocol) or its secure variant, HTTPS (Hypertext Transfer Protocol Secure); JSON (JavaScript Object Notation) is a format for structured data. These are just the example's assumed technology choices, not something C4 requires.
At this level, the official model places applications and data stores inside the boundary of one system, and shows the directly involved people and software systems outside it. For example: the user opens the web application over a secure connection, the web application calls the backend, and the backend reads and writes data in the data store. These statements explain responsibility and communication; how three copies of the backend are started belongs in a different view. Keeping them apart prevents a common mistake: calling every database an "external system" even though it is part of the product, or calling a system a container just because it runs on Kubernetes or Docker.
A single web application may show its browser interface and its server application as two different C4 containers. At the same time, "Frontend" and "Backend" are not the only correct boundaries: in a small desktop application or a single-process program the answer may be different. Define a container by whether it runs independently, what it stores and how it interacts — not by the name of a folder.
Level 3: components inside a chosen container
A component is a grouping of related responsibilities inside a chosen container that does part of that container's work and interacts with other parts through clear boundaries. It can be a module, a subsystem, a library layer or a body of code behind an interface. A component does not have to be a separate server, a Docker container, a team or a microservice. One codebase can contain many components running in a single process.
At this level you clarify:
- Scope: which container exactly you are detailing, and why;
- Responsibility: what work the component does and what it does not do;
- Boundary: which data or decisions belong to it;
- Interface: what it receives and what it provides to other parts;
- Relationships: who calls whom, or which events it subscribes to.
The official C4 site calls the component view optional: create it for a container or a question where it clarifies the structure, not as a complete inventory of every library (C4: component diagram). A description may show a single functional grouping — booking processing, say — even if in the code it spans several classes and tables.
Level 4: code details for a specific question
The Code level can name classes, functions, interfaces, tables or other implementation elements. It answers a narrow question: "Where is the rule against double booking enforced?" or "Which class translates the external response into our model?" It is not a requirement to list every function by hand. The official C4 documentation warns against excessive detail; for code that changes often, a short generated description or the navigation in your editor can be more reliable than a hand-drawn map that has gone stale (C4: code diagram).
If the reader only needs to know that booking checks for free time and saves the record atomically, the component and its contract are usually enough. Names such as BookingEndpoint.Create, ReservationCoordinator.ReserveSlot and a table constraint are worth adding when the team is investigating a specific concurrency defect. If the next refactoring renames the classes, the system boundary stays the same.
Relationships: direction, meaning and context
The phrase "the service uses the database" leaves important things unknown. Does it write bookings, or only read available time slots? Who initiates the request? What carries the data? What happens if something fails?
For a structural relationship, where it matters, state:
- the source and the target;
- the action from the source's point of view: "fetches the list", "writes the confirmed booking", "hands over the email";
- the type of data or the result;
- the protocol or format, if it helps explain the boundary — for example, secure HTTP and JSON;
- who is responsible for errors or confirmation, when that shapes the architecture.
A protocol name on its own is not enough. "The API calls the service over HTTPS" tells you the transport, but not what the data is for. Conversely, you do not need to write "HTTP" for an internal function that is called within the same process.
A structural relationship shows that one element depends on, or can interact with, another. A sequence of events shows what happened first and what happened next in a specific scenario. "Booking API sends the confirmation via Notification Worker" can be a structural relationship. "After the transaction, the Worker reads the event, fetches the address, sends the email, and later retries a failed delivery" is a sequence. You describe it as a separate numbered scenario or a dynamic view, without trying to attach timing to every line of the structure.
Supplementary views: dynamics and physical deployment
The four C4 names are not the only possible views. The author also describes the system landscape, the dynamic view and the deployment view (physical placement). The dynamic view should be used selectively, when an important interaction scenario is not obvious from the structural description; the author advises against creating one without a need. These views do not create a "fifth level of components" and do not replace the four levels.
| View | Which question it answers | Example text description |
|---|---|---|
| Dynamic | What interaction steps happen in one important scenario? | The user submits the form; the API checks access; the service reserves the slot; the data store commits the booking; after confirmation, a background job sends the email. |
| Deployment (physical placement) | On which nodes, environments or cloud resources do the instances of the software containers run? | Three copies of Booking API run behind a load balancer; the database is hosted in a managed data store; the Worker runs as a separate process. |
| System landscape | Which systems, and which responsible teams, exist in the wider organisational context? | Booking interacts with corporate identity and the mail platform, which are looked after by other teams. |
A deployment view describes a specific environment. Information about load balancing, clusters, replication and fault tolerance belongs in a separate view for that environment; the general container structure proves none of it on its own (C4: deployment diagram). Add a dynamic view when an important sequence cannot be seen from the structure — not for every possible interaction (C4: dynamic diagram).
Worked example 1: a football statistics site at four levels
What follows is an invented teaching example of a football statistics site that shows current team squads and the history of player transfers; it is not a diagram of a product actually deployed anywhere. The example's boundary covers the interface, the application logic, the site's own data store and a background importer. An imaginary official transaction feed is the external source of confirmed Team A → Team B events. The site stores the current view of each squad, one canonical transfer per event, and the version history from the source. The effective_at field defines the moment a transfer takes effect on the current squad; the time the message was received is not a substitute for it. This is the same example used in the Software Architecture Document unit; here we follow the change of scale from the system's context down to its containers, one component and the relevant piece of code.
Context: who gets the result and what lies outside the team's responsibility
| Element type | Name | Purpose | Direction and purpose of interaction |
|---|---|---|---|
| Person | Statistics reader | Views a team's current squad and a player's past transfers | Asks the site about a team or a player's history. |
| Person | Data editor | Reviews import errors and inconsistencies | Examines rejected events; does not change the external source. |
| System | Football statistics site | Shows current squads and the stored transfer history | Reads its own data store and imports confirmed events from the transaction feed. |
| External system | Official transaction feed (invented for the example) | Publishes confirmed moves between teams and corrected versions of them | Supplies the site with the event key, version number, participants and the effective time or date. |
At this level we do not show the browser, database tables, the importer class or the HTTP library. The reader sees the purpose, the external actor and the boundary of responsibility: the feed publishes events, and the site is responsible for the current view and the history. If the external provider changes, the external relationship and the contract have to be reviewed, but the site's internal data store does not necessarily change.
Containers: independently running applications and data stores inside the boundary
| Type and name | Responsibility | Technology in the teaching example | Structural relationships |
|---|---|---|---|
| Single-page application (SPA) running in the browser | Shows a team's current squad and a player's transfer timeline | TypeScript and the browser | The reader drives the interface; the SPA sends requests to Roster API over HTTPS in JSON. |
| Roster API — server application | Returns the current squad and the history; validates request parameters | ASP.NET Core | Accepts JSON requests from the SPA and reads the squad projection and history from the relational data store. |
| Transaction Importer — separate background application | Fetches and validates transaction versions, stores the accepted transfer and schedules its effect on the current squad | .NET background process | Reads the official feed over HTTPS; after validation, writes the source version and the canonical transfer, and triggers the projection no earlier than effective_at. |
| Squad and history store | Stores current membership, one canonical transfer, the source's versions and content fingerprints, and scheduled projections | PostgreSQL; queries in SQL (Structured Query Language) | Roster API reads squads and history; the Importer stores versions and the canonical transfer; the projector changes a squad only once the transfer has taken effect. |
| Official transaction feed — external system | Publishes official confirmed transfer events | An HTTP API in the teaching example | Transaction Importer periodically requests new events and receives a structured response. |
The Importer is a separate container because it has an independent lifecycle and polls an external source. If the same logic ran inside the same process, the team could choose fewer containers; the description has to match the implementation, not a notional table. The acceptance transaction stores the validated source version, a digest (fingerprint) of its content, and the update to the single canonical transfer; a separate scheduled action projects that transfer onto the current squad no earlier than effective_at. If the source supplies only a date, without a time or time zone, you need the source's own documented rule or a clarification; do not substitute the time of receipt for an unknown moment. The data store is a container in the C4 sense, not the specific server or cluster it runs on.
Components: detailing Transaction Importer
| Component | Responsibility and boundary | What it does not decide on its own |
|---|---|---|
| Feed Client | Fetches the next page of events from the external feed | Does not determine whether a transfer is authoritative or correct. |
| Event Validator | Checks the schema, stable keys, version, entities and the format of effective_at |
Does not match an unknown player by a similar name, and does not invent a time zone. |
| Transfer Coordinator | Distinguishes an exact repeat, a newer version, a stale revision and a conflict; coordinates acceptance | Does not apply a version that has failed the contract check and the owner's review. |
| Source Revision Repository | Stores every observation from the source, the revision number, the digest and any conflicts | Does not change the canonical transfer while a conflict is unresolved. |
| Canonical Transfer Repository | Stores one accepted transfer per (source_id, event_id) and the latest accepted revision |
Does not lose earlier source versions and does not treat arrival time as effective time. |
| Effective-time Projector | Projects the accepted transfer onto the current squad at or after effective_at |
Checks that the job refers to the latest accepted revision; a stale scheduled job changes nothing. |
This is one possible split of logical responsibility. C4 does not require a separate process, service or team for each component. In real code several duties may be combined, as long as the boundary and the checks remain clear.
Code: answering a question about accepting a correction
The code level is needed when you have to find where the system rejects a repeat or chooses which version to project. The names below are notional navigation points, not an inventory of every function:
| Code element | Its role in the question |
|---|---|
FeedClient.ReadNext and EventValidator.Validate |
Fetch an event and check its fields, identifiers and time data. |
TransferCoordinator.AcceptRevision |
An exact repeat of a version with the same digest does not change state; different content under the same version number is sent for reconciliation; an older version does not replace a newer one. |
SourceRevisionRepository.Record |
Records (source_id, event_id, revision), the digest and conflicting observations, without overwriting earlier history. |
CanonicalTransferRepository.Upsert |
Updates the single canonical transfer after a newer version is accepted. |
EffectiveTimeProjector.ApplyDue |
Applies the current accepted version no earlier than effective_at; skips a stale job. |
The database schema can back these rules with unique keys: (source_id, event_id, revision) for a source version and (source_id, event_id) for the single canonical transfer. This has to be verified in the schema itself.
Dynamic view: acceptance, a repeat and a late correction
Structural tables do not show the order of actions. In this scenario:
- Transaction Importer receives the event
(feed, event-204, revision=1)from the notional official feed: a player is moving from Team A to Team B, andeffective_atis a future time defined by the source's contract. - Event Validator checks the fields and identifiers. In a single transaction, the importer stores revision 1 and its digest, creates one canonical transfer for
(feed, event-204)and schedules the projection foreffective_at. For now the current squad still shows Team A. - If the feed sends an exact repeat of
(feed, event-204, revision=1)with the same digest, it is a no-op: no new transfer is created. If the same version number arrives with a different digest, the observation is stored for reconciliation and revision 1 is not overwritten. - Before the scheduled time, the feed sends a correction,
revision=2. After validation, the importer stores revision 2, keeps revision 1 in the source history, and updates the one canonical record, marking revision 2 as the latest accepted. In this example the corrected date is also in the future. - The scheduled job from revision 1, if it is still in the queue, checks the current accepted version and changes nothing. At or after the corrected
effective_at, the job from revision 2 moves the player to Team B; the site shows one transfer in the football history, not a separate transfer for each source version. - If the feed resends revision 1 after revision 2 has been accepted, the importer may record that the old version was received, but it does not change the canonical transfer and does not roll back the current squad. A date without a time, or without a defined time zone, does not mean the delivery time: the system stores the original value and applies only the source's documented rule; without such a rule, the case is sent for reconciliation.
If a source that is authoritative under the contract sends a revision that explicitly replaces a transfer that has already taken effect, the system keeps both revisions and, according to a defined priority, recalculates the historical and current projections from the corrected effective_at, without rewriting the original observation. This dynamic view adds sequence and conditional branches; it is not a fifth C4 level.
Worked example 2: from a jumbled list to consistent levels
The starting description reads: "The operator closes the ticket; PostgreSQL runs in AWS (Amazon Web Services, Amazon's cloud platform); validateTicket() checks the form; the system uses AWS." It puts a person, a data store, a function, a cloud platform and a vague dependency side by side. That is not one C4 level, and it is not an explanation of the architecture.
| Fragment | What it might mean | Where it belongs |
|---|---|---|
| "User" | A person or role who interacts with the system | Context; if a specific permission matters, an access component or a separate authorisation description |
| "PostgreSQL" | A data store technology; if the database is part of the system, it is a C4 data store container | Container level; table-level detail belongs in the data model |
| validateTicket() | An implementation function, not a separate system or container | Code level, when the function answers an important question |
| "AWS" | The name of Amazon's cloud platform; not precise enough for a deployment view | Deployment; name a specific resource or node only if it really matters — a managed database or a runtime environment, for instance |
| "uses AWS" | Unknown who initiates what and where the data goes | Rewrite as a directed action: for example, Support API stores tickets in managed PostgreSQL over the database protocol |
Let's rebuild the description level by level:
- Context: a customer creates a ticket; the system verifies their identity through an external identity provider and sends updates through a mail service. The boundary does not include the services of the chosen cloud.
- Containers: the browser application sends JSON to Support API over HTTPS; the API validates the request and reads or changes the ticket in PostgreSQL; a separate Worker sends notifications.
- Components inside the API: the endpoint validates the form; Access Policy decides whether the action is allowed; Ticket Coordinator applies the state transition; Ticket Repository stores it.
- Code: validateTicket() may check the fields, but the function on its own does not prove that the operator is allowed to close the ticket or that the state transition is correct.
- Deployment: the description names the environment, the number of instances, the trust boundary and the access settings. The word "AWS" by itself explains none of this.
Once the description is rebuilt, the next questions become visible: who owns the ticket's state, how the system behaves when identity or mail fails, which roles may change the ticket, and what confirms the actual topology. C4 sorts a description by scale, but it does not answer product and operational questions by itself.
A parallel example: booking from context to code
Let's apply the same method to the teaching booking service from the Software Architecture Document unit. A customer reserves a consultation; corporate identity and the mail vendor stay external. At the context level, what matters is seeing the customer, the administrator, the booking service and those two external relationships. At the container level, the browser application sends a request to Booking API over HTTPS, the API works with the booking store, and Notification Worker sends an email after confirmation. The assumed technology might be PostgreSQL, but the name alone does not describe a guarantee.
Inside Booking API, the endpoint checks the request format, Availability Service reads availability, Reservation Coordinator coordinates the reservation, and Booking Repository stores the confirmed record and protects the slot invariant (the rule that must always hold for a slot). validateBooking() checks the fields, but it does not prove the slot was still free at commit time: a concurrent request needs an atomic constraint in the data store. For a narrow check you can name the code path and symbol, but do not turn a function into a new architectural container. If the email is not sent, an already confirmed booking is not cancelled; delayed delivery is described as a separate scenario.
Worked example 3: one logical system, several instances
Take the Roster API container from the earlier example. To improve availability, the team might run three identical instances of it behind a load balancer. All of them serve the same read contract. A deployment description could say:
| Logical C4 container | Physical placement in a notional production environment | What needs clarifying |
|---|---|---|
| SPA | The customer's browser; static files are served by a web server | Does the SPA version match the API contract? |
| Roster API | Three identical instances on three application nodes behind one load balancer | Does session state rely on an instance's local memory? |
| Squad and history store | One managed database; backups and the recovery mechanism are described separately | Which node accepts writes, is there a read replica, and how much lag is acceptable? |
| Transaction Importer | Two worker instances | How are feed polling and reprocessing of the same event coordinated? |
"Roster API" remains one logical container, even though it runs as three copies. Calling them three components would be a level mistake: components are internal parts of a container; instances are the physical embodiment of a container in a specific deployment. If the copies have different boundaries, code or contracts, it is worth checking whether this really is one container.
The same principle shows up in the booking service. One logical Booking API can run as three instances behind a load balancer, one database can accept writes, and two Notification Workers can process different jobs using a lease or locking mechanism. A possible deployment description for a notional environment:
| Logical C4 container | Physical placement in the teaching production scenario | What needs clarifying |
|---|---|---|
| SPA | In the customer's browser; static files are served by a web server | Does the SPA version match the API contract? |
| Booking API | Three identical instances on application nodes behind one load balancer | Does session state rely on an instance's local memory? |
| Booking store | Managed database; backups and the recovery mechanism are described separately | Which node accepts writes, is there a read replica, and how much lag is acceptable? |
| Notification Worker | Two instances that take different jobs using a lock or lease rule | Is resending an email safe, how do we avoid losing a job, and how do we notice a duplicate? |
These are not three Booking API components: components are internal parts of a container; instances are the physical embodiment of a container in a specific deployment. If the copies have different boundaries, code or a separate contract, that is a reason to check whether several logical containers are hiding there. Three copies do not by themselves prove high availability: that conclusion needs data on failures of nodes, the load balancer, the database, sessions, background jobs and recovery in the actual environment.
A static container description does not replace procedures for startup, failover or data migration. Nor does this teaching scenario prove that such a topology is needed or that it delivers availability.
Consistency check: a lower level refines the same story

The levels should form a chain of explanations. A container belongs to the system from the context level; a component sits inside a named container; a code element implements part of a specific component's responsibility. Check consistency like this:
| Check | Sign of consistency | Typical gap |
|---|---|---|
| Is it the same system boundary? | The transaction feed stays an external source at the lower levels | At the component level it is suddenly described as an internal library, with no explanation |
| Do the names match? | Roster API and the squad and history store keep the same responsibilities in the text and in the contract | An importer component changes the public squad, even though the data store was earlier named as its owner |
| Is it clear who owns the state? | There is a source of truth and a defined path for accepting a transfer | Several components can quietly change current membership |
| Is the direction of the relationship true? | The initiator and target match a real request or data transfer | The arrow and its label appeared only because two boxes happened to sit next to each other |
| Is the code detail really needed? | It explains the version check or the effective time | The list of classes copies the code navigator and quickly goes stale |
| Are structure and execution kept apart? | A structural dependency has its own short description; the failure steps have their own scenario | A dependency line is passed off as a guarantee of ordering or delivery |
Do not fill in levels for the sake of completeness: detail components and code only when they help answer a specific question.
Explaining a system to an agent or a new colleague
When you give an agent an architecture task, state the system's context, its boundary, the question and the level you need. Without that, a request like "show me the architecture" can mix up hosts, libraries, components and roles. A text table works well, provided it clearly marks the element type and the relationship.
An example of a bounded brief:
System: the football statistics site in the repository at commit [identifier].
Boundary: the site's own code; the official transaction feed is external.
Question: how does an accepted transfer make its way into the current squad?
First briefly show the system's context and its containers, then expand Transaction Importer.
For every important claim, give the code path and symbol or a link to the contract.
Mark the status: confirmed, contradicts the source, unknown, or agreed target.
Do not draw conclusions about the production deployment from the local code.
Return a table: element — type and boundary — responsibility — relationship — evidence.
Do not add graphics, an invented version number, or elements just for completeness.
This is task discipline, not a guarantee of what the agent can do. A person checks the result; C4 does not define the development process or how roles are split within a team (C4 model FAQ).
The limits of C4: what needs a separate description
C4 helps describe the static parts of a software system and the relationships between them. Outside its core purpose lie user needs and business goals, the full data model, state rules, threats and the trust model, quality of service, business processes, timing guarantees, budget and trade-off analysis. The official FAQ advises supplementing the model with other methods when you need to show processes, states, the domain, data or time-related aspects (C4 model FAQ).
That is why a short context such as "the customer uses the service" is not a requirements specification. A table of components does not prove that access is checked securely. A deployment view with three nodes does not prove readiness for disaster recovery. The name of a queue does not describe a delivery guarantee. Each of those conclusions needs its own contract, check or operational evidence.
The model does not impose a particular colour, box shape, drawing tool or layout. But visual clarity is no excuse for leaving an element's type, its responsibility or the meaning of a direction unexplained. In a text version, the table should state the scale and the boundary, so the reader does not have to infer them from where the boxes sit (C4: notation).
A real text example: C4 in the Lighthouse repository
The public LetPeopleWork/Lighthouse repository contains a general ARCHITECTURE.md and a separate file of C4 views. The first calls itself an overview of the target architecture and refers to the second as a set of context, container and component descriptions. In the C4 file, the authors first name the administrators, the reader and the identity provider, then go on to detail the browser SPA, the Backend API, the database and an external system. The component example goes inside the Backend API: an authorisation controller, a role management service, the service interface, a policy handler and the data store context. RBAC stands for role-based access control. Some relationships name both the action and the technology: for example, the SPA sends requests to the API over HTTPS and JSON, and the API reads or writes role entities through Entity Framework Core (EF Core), a .NET data access library. This is the authors' own breakdown of one specific feature, not a mandatory C4 vocabulary; a class becomes a useful architectural detail only when it explains responsibility (Lighthouse architecture overview, Lighthouse C4 views).
The dated RBAC descriptions from 10 May 2026 name React 18 and .NET 8, while the general overview describes React 19 and .NET 10 as the target. The texts themselves do not show whether this is a historical description of the feature, an outdated fragment, or a deliberate difference between versions. A commit from 8 August 2026 added C4 views for another part of the same file. Until the owner and the code have been checked, none of these states can be declared current; the version history explains where a text came from, but does not prove what is deployed (commit adding the C4 views).
So the public repository is the primary source for what these files say, but it does not explain why the versions diverge or whether any of the descriptions reflects production. We did not check the behaviour of a deployed Lighthouse, its external environment or the correctness of all its feature documents; this is an example of editorial cross-checking, not an independent audit of the product.
A practical sequence for preparing a text C4 description

- Name the system and the reader's goal. State which decision or question the description should clarify.
- Pin down the boundary and the date. Name the repository, branch or commit; separate your own product from external vendors.
- Describe the context. Add the important people, systems and directions of interaction, not every internal detail.
- Identify the containers. Name the applications and data stores, their responsibilities, technology boundaries and protocols.
- Choose a container to detail. Explain why it matters for the question, and list only the components that clarify the structure.
- Go down to code only when needed. Give the path, symbol or constraint that answers a specific question.
- Separate structure from scenario. For an important flow, write out the steps for success, failure, retry or unknown state separately from the structural dependencies.
- Add physical deployment if needed. Name the environment and the number of instances; do not relabel instances as components.
- Check that names and evidence are stable. Cross-check the boundaries against requirements, contracts and code; label what is actual, target, assumed and unknown separately.
- Mark the gaps and check readability. If the question concerns threats, data, a business process or quality attributes, supplement C4 with another model. Make sure a person understands the boundary, the relationships and the reason for the next level of detail.
The readability test is simple: a person should understand what exactly is being described, where the boundary is, who interacts with whom, and why each table is more detailed than the one before it. There is no required number of components, containers or pages.
Summary
C4 gives shared names for zooming in on an architecture step by step: the system in its surroundings, its applications and data stores, its internal components and the code details you actually need. A container means a logical software part or data store, not necessarily a Docker object; a component does not automatically become a separate service. Directed relationships should explain the purpose of an interaction, while a sequence over time and physical deployment are described in their own views. Cross-check names, boundaries, responsibilities and the source version between levels. C4 helps you see structure, but it does not replace requirements, a data model, risk analysis or evidence that a production system works.
Sources and further reading
The official C4 pages and the public Lighthouse repository were checked on 10 October 2026. The Lighthouse file was checked on the current main branch; dates and statuses in its feature descriptions may refer to a particular snapshot of the project.
- Simon Brown. Official C4 model site: definition of the four hierarchical abstractions, the supplementary view types, independence from notation and tooling.
- Simon Brown. C4 model FAQ: the model's purpose and limits, static structure, the difference from a development process, compatibility with arc42, and supplementing the description with other methods.
- Simon Brown. Container diagram: a container as an application or a data store; the boundary of one system; keeping deployment details separate.
- Simon Brown. Component diagram: detailing a chosen container, and the optional nature of this level.
- Simon Brown. Code diagram: implementation details and the limits of documenting code by hand.
- Simon Brown. Deployment diagram, dynamic diagram and notation and relationship-labelling guidance.
- LetPeopleWork. Lighthouse ARCHITECTURE.md and the C4 views text. Used to analyse the authors' boundaries and to cross-check a dated feature view against the general overview; not an independent assessment of the product.
- LetPeopleWork. Commit of 8 August 2026 adding the C4 views for Epic 5687. Used as an example of the change history of architecture texts, not as evidence of the current production state.