If you've worked with software architecture, you've probably experienced something interesting: after weeks of discussing technical decisions, integrations, and responsibilities, it's finally time to present the solution, and someone asks a seemingly simple question:
"But how does all of this work together?"
It's a fair question. And depending on how we've documented our architecture, answering it can be surprisingly difficult.
In large-scale systems, we deal with applications, APIs, databases, messaging infrastructure, external services, and different technologies working together. The challenge isn't just making these elements function properly. It's also explaining their responsibilities and relationships.
A developer needs to understand where to implement a particular feature. An architect needs to identify dependencies, boundaries, and risks. Meanwhile, someone from the business side probably wants to understand what capabilities the solution provides and which systems it depends on.
Presenting the same diagram to all these audiences usually leads to one of two outcomes: too much technical information for some people and not enough detail for others.
This is exactly where the C4 Model becomes valuable.
Created by Simon Brown, C4 provides a way to visualize software architecture through different levels of abstraction. Its main contribution is allowing us to present a solution progressively, starting with the big picture and moving toward implementation details when necessary.
The idea is simple: we don't need to show everything at once.
Understanding the C4 Model
An analogy I find particularly useful is to think of C4 as a map.
When we want to understand where a city is located, we start by looking at a larger region. Then we zoom in to identify neighborhoods, streets, and eventually specific addresses.
Software architecture follows a similar principle.
The C4 Model organizes architectural visualization into four main levels:
Context, to understand how a system interacts with users and other systems.
Containers, to identify the applications and data stores that make up the solution.
Components, to explain how responsibilities are organized within a container.
Code, to examine implementation details when they actually matter.
These levels aren't mandatory development stages, nor do they dictate how a team should work. They're simply different perspectives of the same architecture.
Another important point: C4 isn't a methodology for deciding whether we should use microservices, hexagonal architecture, event-driven systems, or any other architectural approach. It helps us represent and communicate the decisions we've already made.
To make things more concrete, let's follow a fictional example: an e-commerce platform that processes orders, handles payments, and integrates with shipping providers.
We'll use this same system throughout all four levels.
Context: Understanding the System Before the Technology
The first level, known as C1 or the System Context Diagram, presents the system as part of a larger environment.
At this stage, we don't need to explain which programming languages were used, where data is stored, or how many services exist.
We want to answer more fundamental questions: Who uses the solution? What is its purpose? Which external systems does it interact with?
Let's consider our e-commerce platform.
Customers access the platform to browse products and place orders. The solution communicates with a payment provider to process transactions and with a shipping provider to arrange deliveries. Internal operations staff can also use the platform to monitor orders.
These relationships are enough to begin understanding the system's context.
The diagram should clearly identify the system we're examining and which elements fall outside its direct responsibilities.
That might sound simple, but it has important implications.
When we identify external systems, we begin recognizing dependencies. An outage at the payment provider, for example, could prevent customers from completing purchases. A failure in the shipping integration could affect delivery updates.
The diagram doesn't explain how these problems will be handled yet, but it helps us identify where they might occur.
This level is particularly useful when talking to stakeholders, product managers, business teams, and anyone who needs to understand the solution without getting into implementation details.
A good C1 diagram should make sense even to someone unfamiliar with the technical implementation.
If we have to explain infrastructure details for someone to understand a system context diagram, we're probably showing more information than necessary.
Containers: Discovering How the Solution Is Organized
Once we understand the system from the outside, we can start looking at its internal structure.
This is where C2, the Container Diagram, comes in.
Before going any further, let's clear up a common misconception: in the C4 Model, a container doesn't necessarily mean Docker or Kubernetes.
A container represents an application or data store with a specific responsibility within the system. It might be a web application, an API, a background service, a database, or something similar.
Our e-commerce platform could consist of:
- A web application built with React, responsible for the user experience.
- An ASP.NET Core API, responsible for processing business operations.
- A SQL Server database, used to store products, customers, and orders.
- A message queue, used for asynchronous communication.
- A .NET worker service, responsible for processing certain background tasks.
External payment and shipping systems are still part of the picture, but now we can see which parts of our platform interact with them.
Notice how the questions we're answering begin to change.
How does the web application communicate with the backend? Which parts of the solution access the database? Is order processing entirely synchronous? Which application sends information to the shipping provider?
At this level, identifying relevant technologies and protocols makes sense. For example, we might show that the frontend uses HTTPS to communicate with the API, that the API persists information in SQL Server, and that it publishes messages consumed by the worker service.
These details help us understand the solution's structure without requiring knowledge of the source code.
C2 is also an excellent tool for discussing architectural responsibilities.
If several applications access the same database directly, does that introduce coupling worth investigating? If an operation depends on five consecutive synchronous calls, what are the potential consequences for availability?
The diagram won't automatically answer these questions, but it makes the relationships visible so we can discuss them.
It's worth emphasizing that using C4 doesn't mean encouraging distributed architecture. A monolithic application can be represented perfectly well using this model.
The goal is to document the existing or planned architecture, regardless of the architectural style being used.
Components: Understanding Internal Responsibilities
The third level, called C3 or the Component Diagram, brings us closer to implementation.
We now select a specific container and examine how it's organized internally.
Let's use our platform's API as an example.
We know it receives purchase requests, validates information, applies business rules, stores orders, and communicates with external services.
But how are these responsibilities distributed?
One possible design could organize the application into components such as:
Order API: Receives HTTP requests and forwards operations to the appropriate application logic.
Order Processing: Coordinates order creation and the execution of the required operations.
Order Domain: Contains the business rules and invariants that must be preserved.
Persistence: Abstracts the operations required to read and write information.
Payment Integration: Encapsulates communication with the external payment provider.
These components represent just one possible organization, not a structure required by C4.
What's important is understanding that a component represents a group of related functionality encapsulated behind a well-defined interface or responsibility. It doesn't necessarily correspond directly to a class, folder, or project in the repository.
For example, the component responsible for processing orders might involve several classes working together.
This is where we can start discussing cohesion, dependencies, and separation of concerns.
Should a business rule live inside a controller? Is an external integration too tightly coupled to the domain? Can we replace a persistence mechanism without changing the core business logic?
These questions become much easier to discuss when we can visualize the application's internal relationships.
It's also important to avoid a common mistake: trying to represent every class in the project in a C3 diagram.
If the diagram contains dozens of elements without clear organization, it has probably stopped serving its purpose.
In large systems, creating separate component diagrams for different areas or features is perfectly reasonable.
Documentation should make complexity easier to understand, not reproduce all that complexity in a single image.
Code: When We Need to Examine the Implementation
We've reached C4 or the Code Diagram, the most detailed level of the model.
Here, we move beyond architectural responsibilities and begin examining concrete implementation elements, such as classes, interfaces, functions, and their relationships.
Continuing with our example, let's focus on the component responsible for processing orders.
Imagine it contains a class called CreateOrderHandler, responsible for coordinating a purchase operation.
This class depends on an IOrderRepository interface for persistence and an IPaymentGateway interface that abstracts the payment integration.
We also have an Order entity representing important domain information and behavior.
At this level, we can visualize these dependencies and understand how the implementation is structured.
This can be especially useful when analyzing components with complex business rules, important algorithms, or relationships that aren't immediately obvious.
It can also help during refactoring, when we need to evaluate which classes might be affected by a particular change.
However, there's an important recommendation worth remembering: the code level is optional.
There's no need to create class diagrams for every feature we develop.
In most projects, development tools and static analysis utilities can provide this information directly from the source code.
Manually maintaining highly detailed diagrams can become difficult, especially when the implementation changes frequently.
My preference is to reserve this level for situations where a diagram genuinely helps explain a decision or understand a specific problem.
Otherwise, the code itself, supported by tests and good conventions, is usually a more appropriate source of implementation details.
Large-Scale Architecture: Are the Four Levels Enough?
So far, we've followed a relatively straightforward progression: starting with the system from the outside and eventually examining its internal implementation.
But large-scale systems introduce concerns that aren't fully explained by these four levels alone.
For example, a Container Diagram might show that an API communicates with a database. That doesn't necessarily explain how many application instances exist, how load balancing works, or what strategies are used for failure recovery.
Similarly, understanding an application's components doesn't automatically explain the sequence of events that occurs when a customer places an order.
For this reason, C4 also includes supplementary diagrams.
Dynamic Diagrams: Showing How the System Behaves
Dynamic diagrams illustrate how certain elements collaborate during the execution of a feature or use case.
We could use one to demonstrate the sequence of operations involved in creating an order.
The application receives the request, validates information, records the order, requests payment, and initiates the following processing steps.
By visualizing these interactions, we can discuss timeouts, error handling, data consistency, and potential compensating operations.
This type of representation is especially useful when dealing with asynchronous communication or complex integration flows.
Deployment Diagrams: Connecting Architecture and Infrastructure
Deployment Diagrams show how architectural elements are deployed and executed within a particular infrastructure environment.
We can represent cloud environments, application instances, managed services, databases, and network resources.
This perspective helps us discuss availability, scalability, redundancy, and environment isolation.
It's important to understand that a Deployment Diagram doesn't replace C2. They answer different questions.
While C2 explains which applications and data stores make up the solution, a Deployment Diagram shows where their instances are running.
System Landscape Diagrams: Looking Across the Organization
In organizations with dozens or hundreds of applications, a System Landscape Diagram can also be valuable.
It provides a view of the systems that make up a department or organization without necessarily placing a single system at the center.
This helps us understand dependencies between platforms, identify critical integrations, and support modernization initiatives.
In large enterprise environments, this broader perspective can be just as important as the diagrams documenting individual applications.
How to Apply C4 Without Creating Bureaucracy
One legitimate concern for any engineering team is the cost of keeping documentation up to date.
After all, there's little value in producing excellent diagrams at the beginning of a project if they stop reflecting reality after a few releases.
That's why I recommend starting with what you actually need.
In many projects, a System Context Diagram and a Container Diagram already provide a useful foundation for architectural discussions and onboarding new team members.
Component Diagrams can be introduced gradually, particularly for the most important or complex parts of the solution.
The code level comes into play when there's a specific reason to use it.
Another valuable practice is keeping documentation close to the development workflow.
Tools such as Structurizr DSL allow us to describe architectural models using text files, making it easier to version them with Git and review changes.
This means architectural changes can follow the same review process used for source code.
I also believe it's important to connect diagrams with Architecture Decision Records (ADRs).
Diagrams describe the system's structure and relationships. ADRs preserve the context behind architectural decisions, the alternatives considered, and the reasoning that led to a particular choice.
Together, these resources help answer two fundamental questions:
"How is the system organized?"
"Why did we decide to organize it this way?"
This combination becomes particularly valuable when a team needs to understand decisions made months or even years earlier.
Still, no tool can solve the problem of outdated documentation on its own.
Teams need clear ownership and a process that incorporates architectural documentation into the ongoing evolution of the software.
What Makes a Good Diagram Different From a Complicated Drawing?
One of the biggest traps in architecture documentation is assuming that adding more information automatically makes a diagram better.
In practice, the opposite often happens.
Imagine presenting an image with forty services, multiple databases, and dozens of overlapping arrows. Even if every element is technically correct, the visualization might be extremely difficult to understand.
The first thing to consider is the purpose of the diagram.
A diagram intended to explain payment processing doesn't need to include every service responsible for authentication, notifications, or reporting unless those elements are relevant to the flow being discussed.
Another important consideration is how we describe relationships.
An arrow labeled simply "uses" tells us very little. A relationship labeled "publishes order-created event through messaging" communicates something much more useful.
It's also worth clearly identifying system boundaries, element responsibilities, and technologies whenever they're relevant to the level being presented.
Consistency across diagrams matters too. If an application has a particular name in C2, it shouldn't suddenly appear under another name in C3 without a clear reason.
Finally, we should remember that diagrams need to reflect the actual architecture, including limitations and dependencies that might not be ideal.
Architecture documentation shouldn't be a polished fantasy of how the system should work. It should be a tool for understanding how the system actually works and how it can evolve.
The Real Value of C4 Lies in Communication
There's a natural tendency to associate software architecture with sophisticated technical decisions.
We discuss patterns, platforms, protocols, and technologies. All of these matter, but an architecture also needs to be understandable to the people working with it.
When we can present a system at different levels of detail, we make conversations easier that might otherwise require long explanations.
Business stakeholders can understand the solution's fundamental dependencies. New developers can find the components they'll be working on more quickly. Operations and security teams can identify relationships relevant to their analyses.
And architects gain a shared representation for discussing changes and their consequences.
In large systems, this clarity helps reduce ambiguity, accelerates understanding, and supports better-informed decisions.
C4 doesn't eliminate architectural complexity. Its strength lies in helping us organize that complexity into perspectives people can understand.
Further Reading and Resources
If you'd like to explore the C4 Model in greater depth, I recommend starting with the official documentation and then experimenting with the model using a system you already know.
C4 Model, Official Website by Simon Brown
The primary reference for understanding the concepts, abstractions, and different diagram types. I particularly recommend the sections covering containers, components, and supplementary diagrams.
An excellent resource for anyone interested in architecture as code. The Structurizr DSL tutorial demonstrates how to build models and generate different views from them.
C4 Model Diagram Review Checklist
A practical resource for evaluating whether your diagrams actually communicate what they need to. It can be used during architecture reviews and team discussions.
Markdown Architectural Decision Records (MADR)
A complementary reference for documenting architectural decisions. It helps preserve the reasoning behind the choices represented in your diagrams.
As a practical exercise, choose an application you know well and try drawing its System Context and Container Diagrams.
Don't worry about sophisticated tools or visually impressive diagrams at first. Focus on clearly explaining what each element does and how it relates to the others.
If someone else can understand the solution from those diagrams, you've already made meaningful progress.
Because ultimately, good architecture documentation isn't about impressing people with the amount of detail it contains.
It's about helping people understand systems, make decisions, and work better together.




Top comments (0)