The Problem C4 Solves

Classic architecture diagrams fail in a predictable way: they mix deployment topology (EC2 instances, load balancers) with service boundaries (microservices) with data flows (API calls) with code structure (classes) — all on one canvas. The result looks comprehensive but communicates nothing clearly to anyone. A non-technical stakeholder can't extract the business picture. A new developer can't understand where to make a change. An ops engineer can't see the deployment dependencies.

Simon Brown created the C4 model to fix this by applying a simple analogy: Google Maps. You don't look at satellite imagery when you want to navigate between cities. You choose the zoom level that answers your current question. C4 applies the same zoom logic to software:

L1

System Context

Your system and the external things it interacts with — users, external services, other systems. Non-technical stakeholders read this. One box = your entire system.

L2

Container

The major deployable units inside your system — web apps, APIs, databases, queues. Tech is shown. Each box is something you run separately.

L3

Component

Inside a single container — the major code components, bounded contexts, or modules. Used by developers working on that service.

L4

Code

Classes, interfaces, functions inside a component. Rarely worth maintaining by hand — generate from code with IDE tooling.

Level 1: The Context Diagram

The context diagram shows your system as a single box. Surround it with the external people and systems it interacts with. Nothing inside the box is visible. This is the "what does this system do, and who uses it?" diagram.

Rule: Every element in a context diagram is either (a) your system or (b) something external to your system. No technology. No deployment. No implementation.

C4Context
    title System Context — Tools-Hut

    Person(dev, "Developer / Professional", "Uses free browser tools for daily tasks")
    Person(admin, "Platform Admin", "Monitors usage, manages content")

    System(th, "Tools-Hut", "25 free developer tools, articles, and calculators. Browser-first by design.")

    System_Ext(cognito, "Amazon Cognito", "Optional user authentication")
    System_Ext(stripe, "Stripe", "Premium subscription billing")
    System_Ext(aws_ai, "AWS Rekognition + Textract", "AI-powered image analysis and OCR")
    System_Ext(ga, "Google Analytics", "Usage analytics")

    Rel(dev, th, "Uses tools, reads articles", "HTTPS")
    Rel(admin, th, "Manages platform", "HTTPS")
    Rel(th, cognito, "Authenticates signed-in users", "HTTPS")
    Rel(th, stripe, "Manages subscriptions", "HTTPS")
    Rel(th, aws_ai, "Sends images for analysis", "HTTPS")
    Rel(th, ga, "Sends anonymised usage data", "HTTPS")

Level 2: The Container Diagram

Zoom into your system. Each box is now a separately deployable unit — a web application, an API service, a database, a message broker. Show the technology. Show the communication protocols between containers.

Rule: A "container" in C4 is not a Docker container — it's any deployable unit: a web app, a serverless function, a database, an S3 bucket. If you deploy it separately, it's a container.

C4Container
    title Container Diagram — Tools-Hut Platform

    Person(user, "User", "Developer or professional")

    System_Boundary(th, "Tools-Hut") {
        Container(spa, "Static Site", "HTML/CSS/JS, S3 + CloudFront", "Serves all 25 tools and articles")
        Container(api, "HTTP API", "AWS API Gateway + Lambda (Java 21)", "REST API for auth, profile, billing")
        ContainerDb(dynamo, "DynamoDB", "AWS DynamoDB", "User profiles, auth audit log")
        Container(cognito, "Auth Service", "Amazon Cognito User Pools", "JWT issuance, social login")
    }

    System_Ext(stripe, "Stripe", "Subscription billing")
    System_Ext(rekognition, "AWS Rekognition", "Image analysis")

    Rel(user, spa, "Visits", "HTTPS/CloudFront")
    Rel(user, api, "Calls (authenticated routes)", "HTTPS")
    Rel(spa, api, "Makes API calls", "HTTPS/JSON")
    Rel(api, dynamo, "Reads/writes profiles", "AWS SDK")
    Rel(api, cognito, "Validates JWT tokens", "HTTPS")
    Rel(api, stripe, "Creates checkout sessions", "HTTPS")
    Rel(api, rekognition, "Submits images for analysis", "AWS SDK")
Most Projects Only Need L1 + L2

For the majority of systems, a context diagram (L1) and a container diagram (L2) are sufficient. They cover 90% of onboarding and architecture review needs. Only create L3 component diagrams for complex services where the internal structure is non-obvious and changes frequently.

Level 3: The Component Diagram

Zoom into a single container and show its major internal components. In a microservices system, L3 is typically for your most complex services. In a monolith, it might show major bounded contexts or modules.

When to create an L3: A new developer joins and asks "where does X live?" regularly. Or your service has grown to the point where it's not obvious which part to change for a given feature.

C4Component
    title Component Diagram — Tools-Hut API Service

    Container_Boundary(api, "HTTP API (Lambda)") {
        Component(auth_handler, "Auth Handler", "Lambda Function", "Handles JWT validation and Cognito triggers")
        Component(profile_handler, "Profile Handler", "Lambda Function", "GET/PUT user profile")
        Component(billing_handler, "Billing Handler", "Lambda Function", "Stripe checkout and portal")
        Component(webhook_handler, "Webhook Handler", "Lambda Function", "Processes Stripe subscription events")
        Component(user_repo, "User Repository", "Java Class", "DynamoDB read/write for user profiles")
        Component(tier_service, "Tier Service", "Java Class", "Manages tier upgrades and downgrades")
    }

    ContainerDb(dynamo, "DynamoDB", "User profiles table")
    System_Ext(stripe, "Stripe API", "Payment processing")
    System_Ext(cognito, "Cognito", "Auth token validation")

    Rel(auth_handler, cognito, "Validates tokens", "Cognito SDK")
    Rel(profile_handler, user_repo, "Reads/writes profiles")
    Rel(billing_handler, stripe, "Creates sessions", "Stripe SDK")
    Rel(webhook_handler, tier_service, "Updates tier on event")
    Rel(tier_service, user_repo, "Persists tier change")
    Rel(user_repo, dynamo, "Read/write", "AWS SDK")

Tooling: Mermaid vs Structurizr

FeatureMermaid C4Structurizr DSL
SetupZero — works in GitHub/GitLab Markdown immediatelyRequires Structurizr CLI or cloud account
All 4 C4 levelsL1-L3 (L4 planned)All 4 levels
Auto-layoutDagre (good for small diagrams)Superior layout engine
Model consistencyEach diagram is independentOne DSL model, multiple views — elements stay consistent
Codebase syncManual onlyArchitecture tests via Structurizr Java/C# library
Best forSmall teams, quick diagrams, GitHub-nativeLarge systems, strict consistency, architecture governance
The Sweet Spot

For most teams: Mermaid C4Context + C4Container stored in docs/architecture/ in the repo. The zero-friction GitHub rendering makes it dramatically more likely the diagrams get updated. Graduate to Structurizr only when you need cross-diagram consistency validation or architecture fitness functions.

Common C4 Anti-Patterns

Mixing levels: Don't put implementation details (class names, database tables) on a container diagram. The level determines the abstraction — keep it consistent.

Too many boxes: A context diagram with 20 external systems is unreadable. Group related external systems, or create multiple context diagrams for different stakeholder perspectives.

Over-diagramming: You don't need L3 for every service. If a service is simple and rarely onboards new contributors, L2 is enough. Premature L3 diagrams are the diagrams most likely to go stale.

No legend: C4 uses specific shapes and colors. Always include a legend or use the standard C4 notation so readers know what each shape means without guessing.

One-time creation: Architecture diagrams created at project kickoff and never updated are worse than no diagrams — they actively mislead. Build the update into your process: diagram changes belong in the same PR as architecture changes.