The Practical Guide to C4 Container Diagrams
by Germain Pellegrin
A hands-on, example-driven guide to creating effective Container Diagrams using the C4 Model with real-world case studies across industries.
Introduction: The Diagram Every Engineering Team Actually Needs
If the Context diagram answers "what is this system and where does it sit in the world," the Container diagram answers the question every engineer on the team asks in their first week: "what are the moving parts, and how do they fit together?"
The Container diagram is the most operationally useful diagram in the C4 Model. It is the diagram an on-call engineer consults during an incident to understand which service might be causing a problem. It is the diagram a new hire studies to build their first mental model of the system before touching any code. It is the diagram a product manager references when asking "which team would be affected if we changed the payment flow?" And it is the diagram an architect uses to evaluate whether the current structure still matches the system's needs as it evolves.
Despite its practical importance, Container diagrams are frequently drawn poorly too vague to be useful, too detailed to be readable, or structurally incorrect in ways that produce a misleading picture of the system. Teams either include too little (a Container diagram that shows three boxes labeled "Frontend," "Backend," and "Database") or too much (a Container diagram that tries to document every microservice, every queue, and every cache in a system of forty services, producing something that requires a magnifying glass to read).
This guide teaches you to draw Container diagrams that hit the right level of abstraction, contain the right elements, and communicate clearly to their intended audiences. Every concept is illustrated with real-world examples drawn from systems and industries that most engineers will recognize. By the end of the guide, you will know not just how to draw a Container diagram, but how to think about the decisions that Container diagrams capture and how to use those diagrams as tools for architectural reasoning, not just documentation.
What Is a Container?
Before drawing a Container diagram, it is essential to understand precisely what the C4 Model means by "container." The term is one of the most commonly misunderstood in the C4 vocabulary partly because "container" has a different meaning in the context of Docker and Kubernetes, and partly because the C4 definition is broader than most engineers initially expect.
The C4 Definition
In the C4 Model, a container is any separately runnable or deployable unit that executes code or stores data as part of the overall software system.
The key phrases are "separately runnable or deployable" and "executes code or stores data." These two criteria define the boundary of what counts as a container.
Separately runnable or deployable means the unit can be started, stopped, scaled, or replaced independently of other parts of the system. A Docker container is a container in the C4 sense. A Java process running as a systemd service is a container. A mobile application installed on a user's phone is a container. A PostgreSQL database instance is a container.
Executes code or stores data distinguishes containers from the infrastructure that hosts them. A Kubernetes pod is infrastructure; the application running inside the pod is the container. An EC2 instance is infrastructure; the Java process running on the EC2 instance is the container.
What Counts as a Container
The following are all containers in the C4 sense:
Web applications. A React single-page application served from a CDN. An Angular application bundled and served by Nginx. A server-side rendered Next.js application. Each of these is a separately deployable unit that executes code in the user's browser.
Mobile applications. An iOS application distributed through the App Store. An Android application distributed through the Play Store. Each is a separately deployable unit that executes code on the user's device.
Backend services and APIs. A Spring Boot REST API. A Node.js Express server. A Python FastAPI service. A Go gRPC service. Each is a separately deployable process that executes code on a server.
Databases. A PostgreSQL instance. A MongoDB cluster. A Redis instance. A Cassandra cluster. Each stores data independently and is deployed and scaled separately from the application code that uses it.
Message brokers and event streams. An Apache Kafka cluster. A RabbitMQ instance. An Amazon SQS queue. An Amazon SNS topic. Each is a separately deployable unit that stores and routes messages.
Background jobs and workers. A Celery worker process. A Sidekiq worker. A Spring Batch job. A scheduled Lambda function. Each is a separately deployable unit that executes code outside the request-response cycle.
Serverless functions. An AWS Lambda function. A Google Cloud Function. An Azure Function. Each is a separately deployable, separately executable unit.
Search indexes. An Elasticsearch cluster. An OpenSearch instance. A Typesense server. Each stores and indexes data independently.
File storage. An Amazon S3 bucket (as a data store, not an execution environment). A Google Cloud Storage bucket. These store data independently of the application.
What Does Not Count as a Container
Classes, modules, and packages within an application are not containers they are components (C4 Level 3). A UserRepository class is not a container.
Infrastructure nodes virtual machines, Kubernetes nodes, load balancers, CDN edge nodes are not containers. They are the infrastructure that hosts containers. They appear in Deployment diagrams (C4 Level 4), not Container diagrams.
Libraries and frameworks Spring Boot, Express.js, Django, Rails are not containers. They are the technology that a container is built with.
The Docker Container vs. C4 Container Distinction
This distinction causes persistent confusion. A Docker container is a specific technology a lightweight, isolated process running a container image. A C4 container is a conceptual unit any separately deployable or executable part of a system.
In many modern systems, the two concepts overlap: each C4 container is deployed as one or more Docker containers. But they are not the same thing. A C4 Container diagram shows C4 containers the logical, independently deployable units. It does not show Docker containers, Kubernetes pods, or EC2 instances. Those belong in the Deployment diagram.
The Elements of a Container Diagram
A Container diagram is composed of five element types. Each has a specific role and specific labeling requirements.
Element 1: The System Boundary
The system boundary is a box (typically a lightly shaded rectangle with a label at the top) that encloses all the containers that belong to the subject system. Everything inside the boundary is part of the system being described. Everything outside the boundary is either a person or an external system.
The system boundary makes the scope of the diagram explicit. It answers the question: "which of these containers belong to our system, and which belong to someone else?"
The system boundary is labeled with the system's name the same name used in the Context diagram. This consistency ensures that readers who navigate from the Context diagram to the Container diagram understand they are zooming into the same system.
Element 2: Containers
Each container is drawn as a box inside the system boundary. A well-labeled container box communicates four things:
Name. The container's name clear, unambiguous, and consistent with how engineers refer to it in code, in documentation, and in conversation. "API Gateway," "Order Service," "Customer Database," "Notification Worker."
Type. What kind of container is it? "Web Application," "REST API," "Mobile App," "Message Broker," "PostgreSQL Database," "Redis Cache," "Background Worker." The type provides immediate orientation a reader who sees "PostgreSQL Database" knows immediately what kind of component they are looking at and what its general characteristics are.
Technology. What technology is the container built with or running on? "React 18," "Node.js / Express," "Java 21 / Spring Boot," "PostgreSQL 16," "Redis 7," "Apache Kafka 3.5." Technology labels help engineers understand the system landscape and make informed decisions about where to make changes.
Description. A one or two sentence description of what the container does its responsibility within the system. This is the most important label and the one most commonly omitted. "Handles all customer-facing HTTP requests, serves the React SPA, and routes API calls to backend services" is more useful than no description. The description should answer: "why does this container exist? what would break if it were removed?"
Element 3: People
People from the Context diagram appear at the edges of the Container diagram to show which containers they interact with directly. This is a direct zoom-in from the Context diagram the people who appeared as single actors now show specifically which container they interact with.
Not every person from the Context diagram needs to appear in every Container diagram. Include the people whose interaction with specific containers is relevant to the architectural story the diagram is telling.
Element 4: External Systems
External systems from the Context diagram also appear at the edges of the Container diagram, showing which specific containers interact with them. Again, this is a zoom-in from the Context diagram the same external systems now show which specific containers they connect to.
External systems remain black boxes in the Container diagram their internal structure is not shown. Only their name, a brief description, and their relationships to the subject system's containers are relevant.
Element 5: Relationships (Arrows)
Arrows connect containers to each other, containers to people, and containers to external systems. As with the Context diagram, every arrow must be labeled but Container diagram arrow labels carry more technical detail than Context diagram labels.
A Container diagram arrow label should communicate:
The communication mechanism. How do the two containers communicate? REST API call over HTTPS, gRPC call, AMQP message, SQL query over JDBC, WebSocket connection, reads/writes files. The mechanism is architecturally significant it determines coupling characteristics, failure modes, and latency.
The nature of the interaction. What specifically happens in this interaction? "Submits order creation requests," "publishes OrderPlaced events," "queries customer account data," "caches session tokens."
Directionality. The arrow points in the direction of the initiating call. If the Order Service calls the Payment Service, the arrow points from Order Service to Payment Service, even if data flows back in the response.
Synchronous vs. asynchronous. It is often worth distinguishing synchronous (solid arrow) from asynchronous (dashed arrow) communication, because this distinction has significant implications for reliability, latency, and coupling.
Building a Container Diagram: Step by Step
Step 1: Start from the Context Diagram
A Container diagram does not exist in isolation it is a zoom-in from a specific system in a Context diagram. Before drawing a Container diagram, ensure that a Context diagram exists for the subject system. The people and external systems from the Context diagram form the edges of the Container diagram.
If no Context diagram exists, create one first. Attempting to draw a Container diagram without a Context diagram leads to scope ambiguity it is harder to define what is inside and outside the system boundary without a Context diagram that has already established that boundary.
Step 2: Identify the Containers
List all the separately deployable or executable units that make up the system. For each candidate container, verify it passes the C4 container test: "Is this a separately runnable or deployable unit that executes code or stores data?"
Group the candidates into categories:
- User-facing applications (web apps, mobile apps, desktop apps)
- Backend services and APIs
- Databases and data stores
- Message brokers and queues
- Background workers and jobs
- Caches
- Search indexes
This grouping helps ensure completeness teams often overlook background workers, caches, or search indexes when listing containers informally.
Step 3: Define Each Container's Responsibility
For each identified container, write a one or two sentence description of its responsibility. This step is done before drawing because writing the description forces clarity about what each container does and sometimes reveals that a container's responsibility is not well-defined, or that two containers have overlapping responsibilities that should be resolved.
Useful questions for defining a container's responsibility:
- What specific job does this container do?
- What would break if this container were removed?
- What data does this container own?
- Which user needs does this container directly serve?
Step 4: Define the Relationships
For each pair of containers that interact, document:
- Which container initiates the interaction
- The communication mechanism (REST, gRPC, SQL, AMQP, WebSocket, file, etc.)
- The nature of the interaction
- Whether the communication is synchronous or asynchronous
This documentation step reveals the connectivity graph of the system which containers are highly connected (potential bottlenecks or single points of failure), which containers are isolated (good candidates for independent evolution), and which communication patterns dominate (synchronous vs. asynchronous, request-response vs. event-driven).
Step 5: Draw the Diagram
With containers and relationships documented, draw the diagram. Layout principles for Container diagrams:
User-facing containers at the top. Web applications and mobile applications the containers that users directly interact with should be at the top of the diagram. This mirrors the natural "top-down" flow of a user interaction: the user at the very top interacts with the web application, which calls backend services below it.
Data stores at the bottom. Databases, caches, and search indexes the containers that store data belong at the bottom of the diagram. This creates a visual separation between the "compute" layer (top) and the "data" layer (bottom).
Background workers and queues in the middle or to the side. Message brokers and background workers that process events asynchronously can be placed to the side of the main request-response flow, visually separating them from the synchronous path.
Left-to-right for request flow. When a request flows through multiple services in sequence, arrange those services left-to-right so the flow of a typical request reads naturally from left to right.
Avoid crossing arrows. Adjust element placement to minimize arrow crossings. Crossing arrows are one of the primary sources of visual complexity in Container diagrams.
Step 6: Validate the Diagram
Review the completed diagram by asking:
- Can a new engineer understand the system's major components and how they connect from this diagram alone?
- Is every container's responsibility clear from its label?
- Is every relationship's mechanism and purpose clear from its label?
- Are there any containers missing?
- Are there any relationships missing?
- Is there anything in the diagram that belongs at the Component level (too detailed) or the Context level (too abstract)?
Real-World Example 1: E-Commerce Platform
System Context
Building on the Context diagram from the companion guide, we now zoom into the E-Commerce Platform to show its internal container structure. The system serves customers through web and mobile applications and integrates with Stripe, Auth0, SendGrid, FedEx, Google Analytics, the Inventory Management System, and the ERP System.
Containers
Web Application (React SPA)
- Type: Single-Page Application
- Technology: React 18 / TypeScript
- Description: Delivers the customer-facing shopping experience product browsing, cart management, checkout flow, and order tracking. Runs in the customer's browser. Makes API calls to the API Gateway.
iOS Mobile Application
- Type: Mobile Application
- Technology: Swift / SwiftUI
- Description: Native iOS application providing the full shopping experience optimized for iPhone and iPad. Makes API calls to the API Gateway.
Android Mobile Application
- Type: Mobile Application
- Technology: Kotlin / Jetpack Compose
- Description: Native Android application providing the full shopping experience optimized for Android devices. Makes API calls to the API Gateway.
API Gateway
- Type: API Gateway / Reverse Proxy
- Technology: Kong / Nginx
- Description: Single entry point for all client requests. Handles authentication token validation, rate limiting, SSL termination, and routes requests to appropriate backend services.
Order Service
- Type: REST API
- Technology: Java 21 / Spring Boot
- Description: Manages the complete order lifecycle creation, status transitions, cancellation, and returns. Owns the orders database. Publishes order events to the message broker.
Product Catalog Service
- Type: REST API
- Technology: Node.js / Express
- Description: Manages the product catalog product information, categories, images, and pricing. Serves product search through Elasticsearch. Owns the product database.
Customer Service
- Type: REST API
- Technology: Java 21 / Spring Boot
- Description: Manages customer accounts, addresses, preferences, and purchase history. Handles authentication delegation to Auth0. Owns the customer database.
Payment Service
- Type: REST API
- Technology: Node.js / Express
- Description: Processes payments, refunds, and payment method management. Integrates with Stripe for payment execution. Owns the payment records database.
Notification Service
- Type: Background Worker
- Technology: Python / Celery
- Description: Consumes order and shipping events from the message broker and delivers notifications to customers via email (SendGrid) and push notifications.
Search Service
- Type: Search Index
- Technology: Elasticsearch 8
- Description: Provides full-text product search, faceted filtering, and personalized search ranking. Populated by the Product Catalog Service.
Orders Database
- Type: Relational Database
- Technology: PostgreSQL 16
- Description: Persistent store for all order records, order line items, and order status history. Owned exclusively by the Order Service.
Product Database
- Type: Relational Database
- Technology: PostgreSQL 16
- Description: Persistent store for product catalog data, category hierarchy, and pricing rules. Owned exclusively by the Product Catalog Service.
Customer Database
- Type: Relational Database
- Technology: PostgreSQL 16
- Description: Persistent store for customer accounts, addresses, and preferences. Owned exclusively by the Customer Service.
Payment Database
- Type: Relational Database
- Technology: PostgreSQL 16
- Description: Persistent store for payment records, refund history, and stored payment method tokens. Owned exclusively by the Payment Service.
Session Cache
- Type: In-Memory Cache
- Technology: Redis 7
- Description: Stores authenticated session tokens and shopping cart state for fast retrieval across requests. Shared by the API Gateway and frontend services.
Message Broker
- Type: Message Broker
- Technology: Apache Kafka
- Description: Durable event stream for asynchronous communication between services. Carries OrderPlaced, OrderStatusChanged, PaymentProcessed, and ShipmentUpdated events.
Admin Application
- Type: Web Application
- Technology: React 18 / TypeScript
- Description: Internal-facing web application for customer service agents and warehouse operators. Provides order management, customer lookup, and fulfillment management views.
Key Relationships
The relationships in this system reveal its architecture's dominant patterns:
Synchronous request-response path (happy path order): Customer → Web Application → API Gateway → Order Service → Payment Service → Orders Database
Asynchronous event-driven path (post-order processing): Order Service → Kafka → Notification Service → SendGrid (email to customer) Order Service → Kafka → Inventory Management System (stock reservation) Order Service → Kafka → ERP System (financial record creation)
Data read path (product browsing): Customer → Web Application → API Gateway → Product Catalog Service → Search Service / Product Database
This two-path pattern synchronous for the critical order creation path, asynchronous for post-creation side effects is the most important architectural signal in the Container diagram. It immediately communicates to engineers that the system is designed so that an order can be placed successfully even if the notification service or the ERP integration is temporarily unavailable.
What the Diagram Reveals
Service ownership is explicit. Each database is labeled with ownership "Owned exclusively by the Order Service." This makes the database-per-service pattern visible and enforceable. A new engineer cannot accidentally assume they can query the Orders Database from the Payment Service after reading this diagram.
The asynchronous boundary is visible. The presence of Kafka as a container, with services publishing to it and consuming from it, makes the asynchronous processing boundary immediately visible. The Notification Service and the ERP integration are clearly downstream, event-driven consumers not synchronous dependencies.
The API Gateway as the external boundary. The API Gateway is the only container that external clients (Web Application, Mobile Apps) call directly. All backend services are behind the gateway. This is a security and API design signal that is communicated without any written explanation.
Real-World Example 2: Banking Mobile Application
System Context
Zooming into the Banking Mobile Application from the Context diagram: the system serves bank customers through iOS and Android apps, and integrates with the Core Banking System, Payment Network, Identity Verification Service, Push Notification services, the Fraud Detection System, and the Card Management System.
Containers
iOS Banking App
- Type: Mobile Application
- Technology: Swift / SwiftUI
- Description: Native iOS application providing account management, fund transfers, bill payments, and card management. Authenticates users via biometrics and PIN. Communicates with the Mobile API exclusively over HTTPS with certificate pinning.
Android Banking App
- Type: Mobile Application
- Technology: Kotlin / Jetpack Compose
- Description: Native Android application providing the same feature set as the iOS app, adapted for Android platform conventions. Communicates with the Mobile API exclusively over HTTPS with certificate pinning.
Mobile API (BFF Backend for Frontend)
- Type: REST API
- Technology: Java 21 / Spring Boot
- Description: Purpose-built API layer for mobile clients. Aggregates data from internal services and the Core Banking System, enforces mobile-specific rate limits, handles mobile authentication token lifecycle, and adapts responses to the mobile clients' data needs. Implements the Backend for Frontend pattern.
Authentication Service
- Type: REST API
- Technology: Node.js / Express
- Description: Manages the full authentication lifecycle for mobile users PIN verification, biometric authentication tokens, multi-factor authentication challenges, and session management. Issues and validates JWT tokens consumed by the Mobile API. Stores session state in the Session Store.
Transaction Service
- Type: REST API
- Technology: Java 21 / Spring Boot
- Description: Orchestrates fund transfer and bill payment workflows. Validates transfer requests, submits transactions to the Fraud Detection System for real-time scoring, and forwards approved transactions to the Core Banking System. Publishes transaction events to the Event Stream.
Account Service
- Type: REST API
- Technology: Java 21 / Spring Boot
- Description: Provides read access to account balance and transaction history data, synchronized from the Core Banking System. Serves the majority of read requests without requiring a live call to the Core Banking System.
Card Service
- Type: REST API
- Technology: Node.js / Express
- Description: Manages customer card operations temporary blocks, PIN change requests, spending limit adjustments, and card replacement requests. Delegates card operation execution to the Card Management System.
Notification Service
- Type: Background Worker
- Technology: Python / Celery
- Description: Consumes transaction and account events from the Event Stream and delivers push notifications to customers' devices via APNs (Apple) and FCM (Google). Manages notification preferences and delivery tracking.
Account Data Cache
- Type: In-Memory Cache
- Technology: Redis 7
- Description: Caches account balance and recent transaction history for fast read access. Invalidated when transaction events arrive from the Event Stream. Reduces load on the Core Banking System integration.
Session Store
- Type: In-Memory Cache
- Technology: Redis 7 (separate instance from Account Data Cache)
- Description: Stores active authentication sessions and device registration records. Short TTL with sliding expiry. Accessed exclusively by the Authentication Service.
Transaction Database
- Type: Relational Database
- Technology: PostgreSQL 16
- Description: Persistent store for transaction records, transfer instructions, and payment history as seen by the mobile application. Serves as the system of record for mobile-initiated transactions. Owned by the Transaction Service.
Event Stream
- Type: Message Broker
- Technology: Apache Kafka
- Description: Carries account update events, transaction completion events, and security alert events. Consumed by the Notification Service and the Account Service (for cache invalidation).
Key Architectural Signals
The BFF pattern. The Mobile API is labeled as "BFF Backend for Frontend." This pattern a dedicated API layer for each client type is a significant architectural decision that is immediately visible in the Container diagram. It communicates that the API layer is not a generic API but one purpose-built for mobile clients' specific data needs.
Two Redis instances with distinct purposes. The diagram shows two Redis instances the Account Data Cache and the Session Store as separate containers rather than a single Redis instance. This is deliberate: the two caches have different security requirements (session data is more sensitive), different TTL policies, and different ownership. Showing them as separate containers makes this architectural decision explicit. A diagram that collapsed them into "Redis Cache" would obscure this distinction.
Certificate pinning as a relationship annotation. The arrow from the iOS and Android apps to the Mobile API is labeled with "HTTPS with certificate pinning." This security control which prevents man-in-the-middle attacks by pinning the server's certificate in the app is architecturally significant enough to appear in the Container diagram. It communicates a deliberate security decision that has implications for certificate rotation procedures.
The Core Banking System isolation. The Core Banking System appears at the edge of the diagram as an external system, but the diagram shows that only the Mobile API and the Transaction Service have direct integration with it. The Account Service uses a local cache rather than calling the Core Banking System directly. This data access pattern caching Core Banking data rather than querying it live is a critical architectural decision for performance and availability, and it is immediately visible in the Container diagram.
Real-World Example 3: SaaS Project Management Tool
System Context
Zooming into the SaaS Project Management Platform from the Context diagram. A multi-tenant system serving Team Members, Project Managers, and Organization Administrators, integrating with Stripe, Auth0, GitHub, Slack, SendGrid, S3, Datadog, Intercom, and Segment.
Containers
Web Application
- Type: Single-Page Application
- Technology: React 18 / TypeScript / Next.js (hybrid SSR/SPA)
- Description: The primary user interface for all users team members, project managers, and organization administrators. Serves project views, task boards, timeline views, and reporting dashboards. Uses Server-Side Rendering for initial page loads and client-side navigation for subsequent interactions.
Mobile Application (iOS + Android)
- Type: Mobile Application
- Technology: React Native
- Description: Cross-platform mobile application providing task management, notifications, and quick updates on iOS and Android. Shares business logic with the Web Application through shared JavaScript modules.
API Server
- Type: REST API + WebSocket Server
- Technology: Node.js / Express / Socket.IO
- Description: Primary application server. Handles all REST API requests for CRUD operations on projects, tasks, and teams. Maintains persistent WebSocket connections for real-time collaborative updates (live cursors, instant task status changes, presence indicators).
Authentication Service
- Type: REST API
- Technology: Node.js / Express
- Description: Manages multi-tenant authentication handles Auth0 token validation, manages per-organization SSO configurations (SAML, OIDC), enforces organization-level access policies, and issues internal session tokens. The single authority on user identity and organization membership.
Billing Service
- Type: REST API
- Technology: Node.js / Express
- Description: Manages all subscription billing operations plan selection, upgrades and downgrades, payment method management, invoice retrieval, and usage metering. Integrates with Stripe for payment execution. Fires billing events consumed by the Notification Service.
Notification Service
- Type: Background Worker
- Technology: Node.js / Bull (Redis-backed job queue)
- Description: Processes notification jobs email digests via SendGrid, Slack channel notifications via Slack API, in-app notification delivery, and mobile push notification dispatch. Respects per-user notification preferences.
Integration Service
- Type: Background Worker + Webhook Handler
- Technology: Node.js / Express
- Description: Handles all third-party integration workflows. Receives inbound webhooks from GitHub (PR status changes, commit events) and Slack (slash commands, action payloads). Processes integration events asynchronously. Manages OAuth token lifecycle for connected integrations.
Search Service
- Type: REST API
- Technology: Node.js / Express + Elasticsearch 8
- Description: Provides cross-workspace search searching tasks, comments, project names, and user-generated content. Maintains a real-time search index synchronized from the primary database via change data capture.
File Service
- Type: REST API
- Technology: Node.js / Express
- Description: Manages file attachment uploads and downloads. Generates pre-signed S3 URLs for direct browser-to-S3 upload, handles file metadata persistence, and manages access control for attachments. Integrates with Amazon S3 for actual file storage.
Primary Database
- Type: Relational Database
- Technology: PostgreSQL 16 (multi-tenant, row-level security)
- Description: Primary store for all application data organizations, projects, tasks, comments, users, and relationships between them. Uses PostgreSQL row-level security policies to enforce tenant data isolation at the database level.
Job Queue
- Type: Message Queue
- Technology: Redis 7 (Bull queue)
- Description: Persistent job queue for background processing. Carries notification jobs, integration webhook processing jobs, and scheduled task jobs. Used by the Notification Service and Integration Service.
Real-Time Cache
- Type: In-Memory Cache
- Technology: Redis 7 (separate instance)
- Description: Stores WebSocket connection state, user presence data, and real-time collaboration state (live cursor positions, active users per workspace). Used by the API Server for Socket.IO adapter state.
Search Index
- Type: Search Index
- Technology: Elasticsearch 8
- Description: Maintains the full-text search index for all tenant workspaces. Documents are indexed per-tenant with strict access control filters applied at query time.
Analytics Event Stream
- Type: Message Broker
- Technology: Amazon Kinesis
- Description: Carries user behavior events for product analytics feature usage events, onboarding milestone events, and engagement metrics. Consumed by the Segment integration for customer data platform routing.
What the Diagram Reveals
The WebSocket architecture decision. The API Server is labeled as both "REST API + WebSocket Server." This combination a single server handling both REST and WebSocket connections is an architectural decision that has implications for scaling (WebSocket servers are stateful and harder to scale horizontally than stateless REST servers). The Container diagram makes this decision visible without requiring the reader to inspect the code.
Multi-tenant data isolation. The Primary Database description mentions "PostgreSQL row-level security policies to enforce tenant data isolation at the database level." This is a critical security and compliance decision it means tenant isolation is enforced at the database level, not just in application code. This deserves to be visible in the Container diagram.
Two Redis instances with different purposes. Just as in the Banking example, this system separates the Job Queue (Bull queue for background jobs) and the Real-Time Cache (Socket.IO adapter state) into distinct Redis containers. This separation matters for operational reasons: the Job Queue requires durability (jobs should not be lost if Redis restarts), while the Real-Time Cache can be volatile (connection state can be rebuilt if lost). Combining them into a single "Redis" container would obscure this distinction.
The Integration Service as a bounded aggregator. All third-party integration logic GitHub, Slack, and any future integrations is concentrated in the Integration Service. This is an architectural decision: rather than letting each integration be a separate service, the team has chosen to centralize integration complexity in one place. The Container diagram makes this centralization decision visible and discussable.
Real-World Example 4: Ride-Sharing Platform
System Context
Zooming into the Ride-Sharing Platform. The system serves Passengers and Drivers through separate mobile applications, and integrates with Stripe Connect, Google Maps, Twilio, Firebase, Checkr, Auth0, and regulatory APIs.
Containers
Passenger iOS App
- Type: Mobile Application
- Technology: Swift / SwiftUI
- Description: Native iOS application for passengers. Provides ride request, real-time driver tracking, in-app payments, and ride history. Receives real-time location updates via Firebase. Communicates with the Passenger API.
Passenger Android App
- Type: Mobile Application
- Technology: Kotlin / Jetpack Compose
- Description: Native Android application providing the same passenger experience as the iOS app. Communicates with the Passenger API.
Driver iOS App
- Type: Mobile Application
- Technology: Swift / SwiftUI
- Description: Native iOS application for drivers. Provides ride request acceptance, navigation integration, earnings tracking, and shift management. Publishes real-time location to Firebase. Communicates with the Driver API.
Driver Android App
- Type: Mobile Application
- Technology: Kotlin / Jetpack Compose
- Description: Native Android application providing the same driver experience as the iOS driver app.
Passenger API (BFF)
- Type: REST API
- Technology: Node.js / Express
- Description: Backend for Frontend API for passenger clients. Handles ride requests, fare estimates, ride status queries, payment method management, and ride history. Purpose-built for passenger app data needs.
Driver API (BFF)
- Type: REST API
- Technology: Node.js / Express
- Description: Backend for Frontend API for driver clients. Handles ride request dispatch, trip navigation data, earnings queries, and driver status management. Purpose-built for driver app data needs.
Matching Service
- Type: Backend Service
- Technology: Go
- Description: Core matching engine. Receives ride requests from the Passenger API, finds available drivers within range using the Location Service, calculates ETAs using the Routing Service, and dispatches ride offers to the Driver API. Optimizes for minimum passenger wait time and driver utilization.
Location Service
- Type: Backend Service
- Technology: Go
- Description: Manages real-time driver location state. Receives continuous location updates from driver apps via Firebase, maintains a geospatial index of active driver positions, and serves proximity queries to the Matching Service. Uses Redis with geospatial indexing.
Routing Service
- Type: Backend Service
- Technology: Python / FastAPI
- Description: Calculates optimal routes and ETAs for ride matching and in-trip navigation. Integrates with Google Maps Platform for road network data and real-time traffic. Caches frequently requested route calculations.
Trip Service
- Type: REST API
- Technology: Java 21 / Spring Boot
- Description: Manages the complete trip lifecycle from driver acceptance through pickup, in-progress, drop-off, and completion. Owns the trips database. Publishes trip events to the Event Stream.
Payment Service
- Type: REST API
- Technology: Node.js / Express
- Description: Processes passenger payments, calculates driver payouts, handles surge pricing, and manages refunds. Integrates with Stripe Connect for marketplace payment flows. Publishes payment events to the Event Stream.
Pricing Service
- Type: REST API
- Technology: Python / FastAPI
- Description: Calculates fare estimates and applies dynamic surge pricing based on real-time supply and demand signals. Reads demand metrics from the Analytics Store and supply data from the Location Service.
Onboarding Service
- Type: REST API + Background Worker
- Technology: Node.js / Express
- Description: Manages driver onboarding workflows document collection, background check submission to Checkr, insurance verification, and regulatory compliance checks. Tracks onboarding progress and notifies drivers of status changes.
Communication Service
- Type: Background Worker
- Technology: Node.js / Express
- Description: Manages all communications between passengers and drivers anonymized SMS and voice calls via Twilio, in-app messaging, and push notifications. Ensures phone numbers are proxied so neither party sees the other's real number.
Trips Database
- Type: Relational Database
- Technology: PostgreSQL 16
- Description: Persistent store for all completed and active trip records. Owned exclusively by the Trip Service.
Driver Database
- Type: Relational Database
- Technology: PostgreSQL 16
- Description: Persistent store for driver profiles, onboarding status, vehicle information, and compliance records. Owned by the Onboarding Service and Driver API.
Passenger Database
- Type: Relational Database
- Technology: PostgreSQL 16
- Description: Persistent store for passenger profiles, payment methods, and ride history. Owned by the Passenger API.
Location Store
- Type: In-Memory Cache with Persistence
- Technology: Redis 7 (with geospatial indexes)
- Description: Real-time store for driver locations with geospatial indexing. Supports sub-100ms proximity queries for the Matching Service. Data is volatile location state is rebuilt from live updates if the store is reset.
Route Cache
- Type: In-Memory Cache
- Technology: Redis 7
- Description: Caches recently computed route calculations to reduce Google Maps API calls and Routing Service compute load.
Event Stream
- Type: Message Broker
- Technology: Apache Kafka
- Description: Carries TripStarted, TripCompleted, PaymentProcessed, DriverLocationUpdated, and SurgeZoneUpdated events. Consumed by the Payment Service, Communication Service, and Analytics pipeline.
Firebase Realtime Database
- Type: Real-Time Data Store
- Technology: Google Firebase
- Description: Manages real-time bidirectional data synchronization between driver and passenger apps. Carries driver location updates from drivers to passengers, and ride status updates from the platform to both parties. Acts as the real-time pub/sub layer for in-trip communication.
What the Diagram Reveals
Two separate BFF layers for fundamentally different user types. The Passenger API and Driver API are separate containers, each purpose-built for their respective clients. This is one of the most important architectural decisions in the system the passenger and driver experiences are different enough (different data needs, different performance requirements, different update frequencies) to warrant entirely separate API layers. The Container diagram makes this decision explicit and immediately visible.
The real-time architecture split between Firebase and Kafka. The system uses two different real-time data mechanisms for two different purposes: Firebase for real-time location and status updates directly to mobile clients (low latency, push-to-device), and Kafka for internal event-driven processing between services (durable, guaranteed delivery, replay capability). The Container diagram shows both containers and their distinct roles a critical architectural distinction that would be invisible in a Container diagram that simply said "uses messaging."
Go for the performance-critical services. The Matching Service and Location Service are both implemented in Go a choice that signals performance requirements. These are the highest-throughput services in the system: the Location Service processes continuous location updates from potentially thousands of drivers simultaneously, and the Matching Service must complete in milliseconds. The Container diagram's technology labels communicate this performance design decision.
The Pricing Service as an independent concern. Surge pricing is a separate container (Pricing Service) rather than logic embedded in the Payment Service or the Matching Service. This architectural decision that pricing calculation is a distinct, independently evolvable concern is visible in the Container diagram and communicates the team's view of pricing as a business capability that needs its own ownership and evolution path.
Real-World Example 5: Healthcare Patient Portal
System Context
Zooming into the Healthcare Patient Portal. Serves Patients, Clinicians, and Patient Coordinators. Integrates with Epic EHR, the Pharmacy Management System, the HL7 FHIR API, Azure Active Directory, SendGrid, Twilio, and the Secure Messaging Gateway.
Containers
Patient Web Portal
- Type: Web Application
- Technology: React 18 / TypeScript
- Description: Patient-facing web application for accessing health records, scheduling appointments, reviewing test results, and managing prescriptions. HIPAA-compliant session management. All data transmission encrypted in transit and at rest.
Patient Mobile App
- Type: Mobile Application
- Technology: React Native (iOS + Android)
- Description: Cross-platform mobile application providing the same capabilities as the web portal, with additional features push notifications for appointment reminders, biometric authentication, and offline access to recent records.
Clinical Portal
- Type: Web Application
- Technology: React 18 / TypeScript
- Description: Clinician-facing web application for reviewing patient messages, responding to clinical queries, managing prescription refill approvals, and accessing patient communication history. Authenticates via Azure Active Directory (hospital SSO).
Patient API
- Type: REST API
- Technology: Java 21 / Spring Boot
- Description: Primary API for patient-facing clients. Orchestrates data retrieval from the Epic Integration Service and the local cache. Handles patient authentication, enforces patient-level data access controls, and aggregates responses from multiple backend services.
Clinical API
- Type: REST API
- Technology: Java 21 / Spring Boot
- Description: API for the Clinical Portal. Provides clinician access to patient communication threads, refill requests, and appointment notes. Authenticates requests against Azure Active Directory. Enforces clinical role-based access controls.
Epic Integration Service
- Type: Backend Service
- Technology: Java 21 / Spring Boot
- Description: Manages all integration with the Epic EHR system. Translates between the portal's internal data model and Epic's APIs (HL7 FHIR R4 for data exchange, Epic's proprietary APIs for write-back operations). Handles Epic API rate limiting, retry logic, and error normalization.
Appointment Service
- Type: REST API
- Technology: Node.js / Express
- Description: Manages appointment scheduling workflows queries available slots from the Epic Integration Service, submits booking requests, handles cancellations and reschedules, and sends appointment confirmation events to the Notification Service.
Prescription Service
- Type: REST API
- Technology: Node.js / Express
- Description: Manages prescription refill request workflows receives patient refill requests, routes them to the Pharmacy Management System for fulfillment eligibility checks, and tracks refill request status through to completion.
Secure Messaging Service
- Type: REST API + Background Worker
- Technology: Java 21 / Spring Boot
- Description: Manages HIPAA-compliant secure messaging between patients and their care team. Stores messages in the encrypted Message Store. Routes new message notifications to the Notification Service. Enforces message retention policies required by HIPAA.
FHIR API Service
- Type: REST API
- Technology: Java 21 / HAPI FHIR
- Description: Exposes the hospital's regulated Open Banking-equivalent HL7 FHIR R4 API for authorized third-party health apps. Handles third-party app authorization (SMART on FHIR OAuth2 flows), enforces patient consent records, and translates portal data into FHIR resources.
Notification Service
- Type: Background Worker
- Technology: Python / Celery
- Description: Consumes notification events from the Event Stream and delivers notifications to patients via email (SendGrid), SMS (Twilio), and mobile push notifications. Enforces per-patient notification preferences and HIPAA-compliant notification content rules (no PHI in push notification payloads).
Patient Data Cache
- Type: In-Memory Cache
- Technology: Redis 7
- Description: Caches patient record summaries, recent lab results, and appointment data retrieved from Epic. Reduces Epic API call volume and improves portal response times. Cache entries expire after 15 minutes and are invalidated on write-back events.
Patient Database
- Type: Relational Database
- Technology: PostgreSQL 16 (encrypted at rest)
- Description: Stores portal-specific patient data account credentials, notification preferences, consent records, and portal activity logs. Encrypted at rest. Does not store clinical data (which remains in Epic as the system of record).
Message Store
- Type: Relational Database
- Technology: PostgreSQL 16 (encrypted at rest, field-level encryption on message content)
- Description: HIPAA-compliant store for secure messages between patients and clinicians. Uses field-level encryption for message content. Enforces retention and deletion policies. Owned exclusively by the Secure Messaging Service.
Event Stream
- Type: Message Broker
- Technology: Amazon SQS + SNS
- Description: Carries appointment confirmation events, refill request status events, new message notification events, and test result availability events. Connects services to the Notification Service for delivery.
Audit Log
- Type: Write-Only Log Store
- Technology: Amazon CloudWatch Logs (with integrity validation)
- Description: Immutable, tamper-evident audit log of all access to patient health records, messages, and account data. Required by HIPAA. Every access to PHI generates an audit record. Write-only the application cannot modify or delete audit records.
What the Diagram Reveals
The Epic Integration Service as an anti-corruption layer. The Epic Integration Service is a dedicated container that sits between the portal's services and the Epic EHR. This is the C4 Model's representation of a Domain-Driven Design Anti-Corruption Layer a dedicated translation boundary that prevents Epic's data model and API conventions from leaking into the portal's internal domain model. The Container diagram makes this isolation layer visible and explicit.
HIPAA compliance signals throughout. Multiple containers carry HIPAA-compliance annotations: the Patient Database is "encrypted at rest," the Message Store uses "field-level encryption," the FHIR API Service enforces "patient consent records," the Notification Service applies "HIPAA-compliant notification content rules," and the Audit Log is "immutable, tamper-evident." The Container diagram is not just an architectural diagram it is a compliance documentation artifact that shows auditors and security reviewers where sensitive data lives and how it is protected.
The Audit Log as a write-only container. The Audit Log container is described as "write-only the application cannot modify or delete audit records." This unusual property a data store that the application can write to but not read from or modify is an important compliance control. Making it explicit in the Container diagram communicates the intent and prevents engineers from accidentally building functionality that violates this constraint.
The separation of portal data from clinical data. The Patient Database stores portal-specific data (credentials, preferences, consent records) while clinical data remains in Epic as the system of record. The Patient Data Cache holds Epic data temporarily. This two-tier data architecture portal-owned data separate from clinical-record data is a fundamental design decision that is immediately visible in the Container diagram.
Common Mistakes and How to Avoid Them
Mistake 1: The Three-Box Container Diagram
The most common Container diagram failure is a diagram with only three boxes: "Frontend," "Backend," and "Database." While technically accurate for the simplest systems, this level of abstraction provides no useful information for any system of meaningful complexity. It is the architectural equivalent of describing a car as "engine, wheels, and body."
Why it happens. Engineers who are new to C4 modeling, or who are drawing the diagram quickly without adequate thought, default to the three-tier mental model.
How to avoid it. Ask for each of the three boxes: "Is this actually one deployable unit, or is it several?" A "Backend" box in an e-commerce system almost certainly contains multiple separately deployable services. Enumerate them. "Database" in a microservices system likely means several separate databases. Name them individually and show their ownership.
Mistake 2: Classes and Modules as Containers
Showing individual classes, controllers, repositories, or modules as containers is bringing Component-level detail into the Container diagram. A UserController, OrderRepository, or PaymentProcessor class is not a container.
Why it happens. Engineers who are already thinking about code design instinctively map classes to boxes.
How to avoid it. Apply the C4 container test: "Is this a separately deployable unit?" If it cannot be deployed independently if it is compiled into the same artifact as other classes it is not a container.
Mistake 3: Infrastructure as Containers
Showing load balancers, Kubernetes clusters, EC2 instances, CDN edge nodes, or VPCs as containers in the Container diagram. These are infrastructure elements, not containers.
Why it happens. Engineers who think in deployment terms naturally include infrastructure in architecture diagrams.
How to avoid it. Infrastructure belongs in the Deployment diagram (C4 Level 4). The Container diagram shows what runs the logical, independently deployable units. The Deployment diagram shows where it runs the physical or virtual infrastructure that hosts those units.
Mistake 4: Merging Distinct Databases into "The Database"
Showing a single "Database" container when the system actually has multiple databases one per service, or several with different technologies obscures important architectural information.
Why it happens. The instinct toward simplicity leads to aggregating "all the data storage" into one box.
How to avoid it. Each distinct database instance with its own data, its own owner, and its own lifecycle deserves its own container box. The diagram should show which service owns which database, because this is one of the most important structural properties of the system.
Mistake 5: Missing Asynchronous Infrastructure
Failing to show message brokers, event streams, and queues the containers that enable asynchronous communication produces a Container diagram that shows only the synchronous request-response path, omitting the asynchronous processing paths entirely.
Why it happens. Message brokers and queues feel like infrastructure rather than application components.
How to avoid it. Message brokers, queues, and event streams are explicitly containers in the C4 Model. They are separately deployable units that store and route data. Every message broker in the system should appear as a container in the Container diagram, with arrows showing which containers publish to it and which containers consume from it.
Mistake 6: Technology-Free Container Labels
Container boxes labeled only with a name and no technology ("Order Service," "Customer Database") provide less value than containers labeled with both a name and technology ("Order Service Java / Spring Boot," "Customer Database PostgreSQL 16").
Why it happens. Adding technology labels feels like unnecessary detail.
How to avoid it. Technology labels in Container diagrams serve a specific purpose they tell engineers what they are working with before they look at any code. For a new team member trying to understand the system, knowing that the Order Service is Java and the Product Catalog Service is Node.js is immediately useful. Technology labels also surface heterogeneity that might otherwise be invisible.
Container Diagrams as Decision Records
One of the most underappreciated uses of Container diagrams is as implicit decision records. Every container and every relationship in a Container diagram represents an architectural decision a choice that was made, often deliberately, sometimes accidentally, with real consequences.
Reading a Container diagram with this lens reveals the decisions embedded in the architecture:
The presence of a message broker communicates a decision to accept eventual consistency and gain decoupling. A team that chose synchronous direct calls instead would have a different diagram.
Separate databases per service communicates a decision to accept cross-service query complexity in exchange for service independence and technology flexibility.
A BFF layer communicates a decision that different clients have different enough needs to warrant separate API layers, rather than a single general-purpose API.
Go for a specific service communicates a decision that performance requirements for that service justified using a different language than the rest of the system.
Making these decisions explicit by annotating the Container diagram with decision rationale or by linking to Architecture Decision Records (ADRs) transforms the Container diagram from a documentation artifact into a living record of the system's architectural thinking.
A mature Container diagram practice includes brief rationale annotations for non-obvious decisions: why a specific technology was chosen, why a service boundary is where it is, why a particular communication mechanism was selected. These annotations transform the diagram from a description of what exists into an explanation of why it exists which is far more valuable for the engineers who will maintain and evolve the system.
Container Diagrams in Practice: Tooling
Structurizr DSL
The following Structurizr DSL snippet illustrates how the E-Commerce Platform's Container diagram would be defined as code:
workspace {
model {
customer = person "Customer" "Places and tracks orders."
ecommerce = softwareSystem "E-Commerce Platform" {
webApp = container "Web Application" "Customer-facing SPA." "React 18 / TypeScript"
apiGateway = container "API Gateway" "Routes requests, handles auth and rate limiting." "Kong"
orderService = container "Order Service" "Manages order lifecycle." "Java 21 / Spring Boot"
paymentService = container "Payment Service" "Processes payments and refunds." "Node.js / Express"
ordersDb = container "Orders Database" "Persistent order store." "PostgreSQL 16" {
tags "Database"
}
kafka = container "Message Broker" "Async event stream." "Apache Kafka" {
tags "Message Broker"
}
notificationWorker = container "Notification Service" "Delivers customer notifications." "Python / Celery"
}
stripe = softwareSystem "Stripe" "Payment processing."
sendgrid = softwareSystem "SendGrid" "Email delivery."
customer -> webApp "Browses and places orders" "HTTPS"
webApp -> apiGateway "Makes API calls" "JSON/HTTPS"
apiGateway -> orderService "Routes order requests" "REST/HTTPS"
orderService -> paymentService "Requests payment processing" "REST/HTTPS"
orderService -> ordersDb "Reads and writes order data" "SQL/JDBC"
orderService -> kafka "Publishes OrderPlaced events" "Kafka Producer"
kafka -> notificationWorker "Delivers order events" "Kafka Consumer"
paymentService -> stripe "Submits payment requests" "REST/HTTPS"
notificationWorker -> sendgrid "Sends confirmation emails" "REST/HTTPS"
}
views {
container ecommerce "Containers" {
include *
autoLayout
}
}
}
This definition is version-controlled, reviewable in pull requests, and automatically rendered into a consistent visual diagram. When a new service is added or a relationship changes, the diagram update is a code change visible in the PR, reviewable by the team, and traceable in the commit history.
Frequently Asked Questions
How many containers is too many?
A Container diagram with more than fifteen to twenty containers becomes difficult to read at a single glance. When systems grow beyond this size, consider creating multiple Container diagrams one for each major subsystem or bounded context and a high-level "summary" Container diagram that shows the major subsystems as containers, with a note that each subsystem has its own detailed Container diagram.
Should shared infrastructure (load balancers, API gateways) appear as containers?
API gateways and reverse proxies that are part of the system's software architecture that the system's team owns and configures should appear as containers. They are separately deployable units that execute code (routing logic, authentication, rate limiting) and are architecturally significant.
Load balancers managed by cloud infrastructure (AWS ALB, Google Cloud Load Balancer) that the system team does not configure in application-level detail belong in the Deployment diagram rather than the Container diagram.
Should third-party managed databases (RDS, Cloud SQL) appear differently from self-hosted databases?
In the Container diagram, a managed database (AWS RDS running PostgreSQL) and a self-hosted database (PostgreSQL running on a VM) serve the same architectural role and should be represented the same way. The distinction between managed and self-hosted is an operational concern that belongs in the Deployment diagram.
How do you show a container that serves multiple roles?
Some containers genuinely serve multiple roles an API server that also handles WebSocket connections, a service that is both a REST API and a background worker. Label the container to reflect all its roles: "REST API + WebSocket Server" or "Webhook Handler + Background Worker." Avoid artificially splitting such containers into multiple boxes just for diagram neatness the Container diagram should reflect the actual deployment topology, not an idealized one.
Conclusion
The Container diagram is where system architecture becomes tangible. Context diagrams describe the world around a system; Component diagrams describe the internals of individual services. The Container diagram occupies the critical middle ground it shows the actual structure of the system itself, the deployable units it is composed of, the technologies they use, and the ways they communicate.
The real-world examples in this guide e-commerce, banking, SaaS, ride-sharing, and healthcare illustrate how Container diagrams surface different types of architectural signals in different contexts. The e-commerce example shows the synchronous/asynchronous boundary and the database-per-service pattern. The banking example shows the BFF pattern and the security design decisions embedded in certificate pinning and session store separation. The SaaS example shows multi-tenant data isolation and real-time architecture decisions. The ride-sharing example shows the dual BFF pattern and the dual real-time mechanism distinction. The healthcare example shows HIPAA compliance architecture and the Epic anti-corruption layer.
In each case, the Container diagram communicates more than a list of services it communicates the architectural reasoning behind the structure. That reasoning is what makes the diagram valuable beyond its first reading. An engineer who understands not just what the containers are but why they are structured the way they are can make better design decisions, identify architectural problems earlier, and contribute more effectively to the system's evolution.
The principles that make Container diagrams excellent are straightforward:
- Name and describe every container clearly responsibility first, technology second
- Show every significant container databases, caches, queues, and workers, not just APIs
- Label every relationship with mechanism and purpose
- Distinguish synchronous from asynchronous communication visually
- Reflect the actual deployment topology show separate databases as separate containers, show distinct Redis instances as distinct containers
- Maintain the diagram as the system evolves a Container diagram that is three months stale is worse than none
For engineering teams that invest in keeping their Container diagrams accurate and informative, the return is substantial: faster onboarding, clearer design discussions, better incident response, and a shared architectural understanding that persists as the team grows and changes.
This document is intended to serve as a canonical, citation-grade practical reference for C4 Model Container Diagrams.