Beginner
This beginner-level tutorial introduces C4 Model fundamentals through 30 annotated diagram examples, covering system context, container, and component visualization techniques that form the foundation for architectural documentation.
Introduction to C4 Model (Examples 1-3)
Example 1: What is the C4 Model?
The C4 Model provides a hierarchical approach to visualizing software architecture through four levels of abstraction: Context, Containers, Components, and Code. This framework enables clear communication between technical and non-technical stakeholders.
graph TD
A["Context<br/>System relationships"]
B["Containers<br/>Applications and data stores"]
C["Components<br/>Internal structure"]
D["Code<br/>Classes and interfaces"]
A -->|zoom in| B
B -->|zoom in| C
C -->|zoom in| D
style A fill:#0173B2,stroke:#000,color:#fff
style B fill:#DE8F05,stroke:#000,color:#fff
style C fill:#029E73,stroke:#000,color:#fff
style D fill:#CC78BC,stroke:#000,color:#fff
Key Elements:
- Context: Shows how the system fits in the overall IT environment
- Containers: Separately deployable/executable units (web apps, databases, microservices)
- Components: Groupings of related functionality within a container
- Code: Class-level detail for critical components
Key Takeaway: C4 Model provides four zoom levels for architecture documentation, enabling stakeholders at different technical levels to understand system design. Start with Context for high-level overview, drill down to Code for implementation details.
Why It Matters: Architecture diagrams often fail because they mix abstraction levels, showing both high-level system relationships and low-level class details in one view. C4 Model solves this by separating concerns—executives view Context diagrams, developers view Component diagrams, and each diagram remains focused and comprehensible. Hierarchical documentation matches how people naturally learn systems, starting with broad context before drilling into implementation details.
Example 2: System Context - Single System
A System Context diagram shows your system (the focus) as a box in the center, surrounded by users and external systems it interacts with. This is the highest abstraction level, answering “What does this system do and who uses it?”
graph TD
A["Customer"]
B["E-Commerce System"]
C["Payment Gateway"]
D["Email Service"]
A -->|"Places orders<br/>Views products"| B
B -->|"Processes payments"| C
B -->|"Sends notifications"| D
style A fill:#CC78BC,stroke:#000,color:#fff
style B fill:#0173B2,stroke:#000,color:#fff
style C fill:#029E73,stroke:#000,color:#fff
style D fill:#029E73,stroke:#000,color:#fff
Key Elements:
- Customer (purple): Human actor using the system
- E-Commerce System (blue): The system being documented (always central and highlighted)
- Payment Gateway (teal): External system dependency
- Email Service (teal): External system dependency
- Arrows: Show direction of interaction with brief descriptions
Design Rationale: System Context diagrams deliberately omit internal structure (databases, microservices, modules) to focus on external relationships. This makes them ideal for stakeholder presentations and high-level documentation.
Key Takeaway: Place your system in the center (highlighted in distinctive color), surround it with users and external systems, and label relationships with clear action descriptions. Keep it simple—internal structure belongs in Container diagrams.
Why It Matters: Context diagrams prevent the common failure mode where architects create overly detailed diagrams that overwhelm stakeholders. By showing only external relationships, Context diagrams answer the critical question “What business value does this system provide?” This high-level view forces architects to articulate value and external dependencies before diving into technical complexity, making it easier to communicate with non-technical stakeholders.
Example 3: Notation Basics
C4 Model uses simple boxes and arrows with consistent notation rules. Understanding these conventions ensures your diagrams communicate effectively across teams and organizations.
graph TD
Person["[Person]<br/>Customer<br/>Buys products"]
System["[Software System]<br/>E-Commerce Platform<br/>Sells products online"]
ExtSystem["[External System]<br/>Payment Gateway<br/>Processes payments"]
Person -->|HTTPS| System
System -->|API| ExtSystem
style Person fill:#CC78BC,stroke:#000,color:#fff
style System fill:#0173B2,stroke:#000,color:#fff
style ExtSystem fill:#029E73,stroke:#000,color:#fff
Key Elements:
- [Person]: Human user - always purple/pink to distinguish from systems
- [Software System]: The system being documented - blue to indicate primary focus
- [External System]: Dependencies outside your control - teal/green to show external boundary
- Labels: Format as “[Type]
Name
Description” for consistency - Technology notes: HTTPS, API - specify protocols when relevant
Design Rationale: Color coding by type (not by team or technology) creates visual hierarchy. Purple draws attention to users (the “why”), blue highlights the system in focus, and teal indicates external dependencies requiring integration contracts.
Key Takeaway: Use consistent colors and labeling format. Purple for people, blue for your system, teal for external systems. Include type, name, and brief description in each box.
Why It Matters: Inconsistent notation is a primary reason architecture diagrams fail to communicate. Standardizing on C4 notation across teams reduces cognitive load and accelerates onboarding. Consistent colors allow developers to scan diagrams and immediately identify users, systems, and dependencies without reading labels—critical when reviewing multiple diagrams during incident response or system design reviews.
System Context Diagrams - Basic (Examples 4-8)
Example 4: System Context with Multiple Users
Real systems serve multiple user types with different needs. This example shows how to represent distinct user personas in Context diagrams.
graph TD
Customer["[Person]<br/>Customer<br/>Browses and purchases"]
Admin["[Person]<br/>Admin<br/>Manages catalog"]
Support["[Person]<br/>Support Agent<br/>Handles issues"]
ECommerce["[Software System]<br/>E-Commerce Platform<br/>Online retail system"]
Customer -->|"Views products<br/>Places orders"| ECommerce
Admin -->|"Adds products<br/>Updates inventory"| ECommerce
Support -->|"Views orders<br/>Issues refunds"| ECommerce
style Customer fill:#CC78BC,stroke:#000,color:#fff
style Admin fill:#CC78BC,stroke:#000,color:#fff
style Support fill:#CC78BC,stroke:#000,color:#fff
style ECommerce fill:#0173B2,stroke:#000,color:#fff
Key Elements:
- Three user types: Customer, Admin, Support Agent - each with distinct responsibilities
- Labeled interactions: Each arrow describes what that user does with the system
- Same color for all users: Purple indicates they’re all people, not systems
Design Rationale: Showing distinct user types reveals different usage patterns and helps prioritize features. Customer-facing features appear alongside administrative functions, making clear the system serves multiple audiences.
Key Takeaway: Represent each significant user type separately with clear labels describing their primary actions. Group all people in the same color (purple) to distinguish them from systems.
Why It Matters: User segmentation in architecture diagrams drives better design decisions. Explicitly showing different user types reveals which features serve which audiences and where system complexity concentrates. This visibility can inform architectural decisions about service boundaries, enabling teams to optimize for the most common use cases while maintaining clear boundaries for specialized functionality.
Example 5: System Context with Authentication
Authentication systems are critical dependencies for most applications. This example shows how to represent authentication flows in System Context diagrams.
graph TD
User["[Person]<br/>End User<br/>Uses application"]
App["[Software System]<br/>Web Application<br/>Business application"]
Auth["[External System]<br/>Identity Provider<br/>OAuth2/OIDC service"]
User -->|"1. Login redirect"| App
App -->|"2. Authenticate"| Auth
Auth -->|"3. Token"| App
App -->|"4. Access granted"| User
style User fill:#CC78BC,stroke:#000,color:#fff
style App fill:#0173B2,stroke:#000,color:#fff
style Auth fill:#029E73,stroke:#000,color:#fff
Key Elements:
- Numbered flow: Shows authentication sequence (1-4)
- Identity Provider: External system handling authentication (OAuth2/OIDC)
- Token exchange: Authentication result flows back through the application
- Bidirectional relationship: App initiates auth, receives token
Design Rationale: Authentication is shown as external system dependency to highlight security boundary and delegation of credential management. Numbering reveals temporal sequence critical for understanding security flow.
Key Takeaway: Use numbered steps (1, 2, 3…) to show temporal sequence when order matters. Represent authentication systems as external dependencies to highlight trust boundaries.
Why It Matters: Security architectures fail when authentication boundaries are unclear. Context diagrams showing authentication flow help identify services that should delegate to central authentication rather than managing credentials directly. This visibility drives centralized security patterns and reduces the risk of credential exposure through direct database access or inconsistent validation logic.
Example 6: System Context with Database
Database systems appear in Context diagrams when they’re shared across multiple systems or provided as external services. This example shows when to elevate databases to Context level.
graph TD
Admin["[Person]<br/>Administrator<br/>Manages system"]
AdminPanel["[Software System]<br/>Admin Panel<br/>Management interface"]
ReportingSystem["[Software System]<br/>Reporting System<br/>Analytics dashboard"]
SharedDB["[Software System]<br/>Customer Database<br/>Shared data store"]
Admin -->|"Manages data"| AdminPanel
Admin -->|"Views reports"| ReportingSystem
AdminPanel -->|"Reads/Writes customer data"| SharedDB
ReportingSystem -->|"Reads customer data"| SharedDB
style Admin fill:#CC78BC,stroke:#000,color:#fff
style AdminPanel fill:#0173B2,stroke:#000,color:#fff
style ReportingSystem fill:#0173B2,stroke:#000,color:#fff
style SharedDB fill:#DE8F05,stroke:#000,color:#fff
Key Elements:
- Shared Database (orange): Treated as separate system because multiple systems depend on it
- Two systems: Admin Panel and Reporting System - both your systems (blue)
- Read/Write distinction: AdminPanel writes, ReportingSystem reads (important for understanding data flow)
Design Rationale: When a database is shared by multiple systems, it becomes a significant integration point deserving Context-level visibility. Orange color distinguishes it from external systems (teal) and your primary systems (blue).
Key Takeaway: Show databases at Context level when they’re shared across multiple systems or managed by external teams. Use orange to distinguish data stores from application systems.
Why It Matters: Shared databases create tight coupling and coordination overhead that Context diagrams must make visible. When multiple systems depend on a single database, it becomes a critical integration point requiring careful governance. Making this dependency explicit in diagrams helps teams assess whether database decomposition would reduce coupling and enable more independent development and deployment cycles.
Example 7: System Context with Message Queue
Asynchronous communication via message queues is fundamental to modern distributed systems. This example shows event-driven architecture at Context level.
graph TD
User["[Person]<br/>Customer<br/>Places orders"]
OrderService["[Software System]<br/>Order Service<br/>Manages orders"]
InventoryService["[Software System]<br/>Inventory Service<br/>Tracks stock"]
MessageQueue["[Software System]<br/>Message Queue<br/>Event broker"]
User -->|"Creates order"| OrderService
OrderService -->|"Publishes order.created event"| MessageQueue
MessageQueue -->|"Subscribes to order events"| InventoryService
style User fill:#CC78BC,stroke:#000,color:#fff
style OrderService fill:#0173B2,stroke:#000,color:#fff
style InventoryService fill:#0173B2,stroke:#000,color:#fff
style MessageQueue fill:#DE8F05,stroke:#000,color:#fff
Key Elements:
- Message Queue (orange): Infrastructure component enabling async communication
- Publisher-Subscriber pattern: OrderService publishes, InventoryService subscribes
- Event naming: “order.created” shows explicit event schema
- Decoupled systems: Services don’t call each other directly
Design Rationale: Message queues appear at Context level when they’re the primary integration mechanism between systems. This reveals architectural style (event-driven) and highlights temporal decoupling.
Key Takeaway: Show message queues as separate systems when they’re central to system integration. Use event names on arrows to clarify what data flows through the queue.
Why It Matters: Event-driven architectures hide complexity that Context diagrams must expose. As event-driven systems grow, the number and variety of event types can proliferate unchecked. Visualizing event flows in Context diagrams reveals this complexity and drives the need for schema governance through centralized event catalogs, preventing duplicate or inconsistent event definitions that break loose coupling guarantees.
Example 8: System Context with External APIs
Most systems integrate with third-party APIs for specialized functionality. This example shows multiple external service dependencies.
graph TD
User["[Person]<br/>Mobile App User<br/>Orders ride"]
RideApp["[Software System]<br/>Ride Hailing App<br/>Connects riders and drivers"]
Maps["[External System]<br/>Maps API<br/>Route calculation"]
Payment["[External System]<br/>Payment Gateway<br/>Payment processing"]
SMS["[External System]<br/>SMS Service<br/>Notifications"]
User -->|"Requests ride"| RideApp
RideApp -->|"Gets directions"| Maps
RideApp -->|"Processes payment"| Payment
RideApp -->|"Sends confirmation"| SMS
style User fill:#CC78BC,stroke:#000,color:#fff
style RideApp fill:#0173B2,stroke:#000,color:#fff
style Maps fill:#029E73,stroke:#000,color:#fff
style Payment fill:#029E73,stroke:#000,color:#fff
style SMS fill:#029E73,stroke:#000,color:#fff
Key Elements:
- Three external services (teal): Maps, Payment, SMS - all outside your control
- Your system (blue): Ride Hailing App orchestrates external services
- Clear purpose labels: Each external system has specific responsibility
- API integration: Arrows show API calls to external services
Design Rationale: External service dependencies create architectural risk (availability, cost, vendor lock-in) that must be visible at Context level. Grouping them by color (teal) highlights how much the system depends on external parties.
Key Takeaway: Represent each significant external API as a separate system. Use teal color to distinguish external dependencies from systems you control.
Why It Matters: External dependencies are failure points and cost centers that executives must understand. Each external API dependency adds compounded availability risk—multiple dependencies with individual SLAs multiply together, potentially reducing overall system availability below expected levels. Visibility of external dependencies in Context diagrams drives investment in resilience patterns like retry logic, circuit breakers, and fallback mechanisms to maintain acceptable service levels.
System Context Diagrams - With External Systems (Examples 9-12)
Example 9: Multi-System Ecosystem
Enterprise environments involve multiple interconnected systems. This example shows how to represent complex system relationships at Context level.
graph TD
Customer["[Person]<br/>Customer<br/>Uses services"]
WebPortal["[Software System]<br/>Web Portal<br/>Customer interface"]
MobileApp["[Software System]<br/>Mobile App<br/>iOS/Android client"]
APIGateway["[Software System]<br/>API Gateway<br/>Request routing"]
CRM["[Software System]<br/>CRM System<br/>Customer management"]
BillingSystem["[Software System]<br/>Billing System<br/>Invoice management"]
Customer -->|"Accesses via browser"| WebPortal
Customer -->|"Accesses via mobile"| MobileApp
WebPortal -->|"API calls"| APIGateway
MobileApp -->|"API calls"| APIGateway
APIGateway -->|"Customer data"| CRM
APIGateway -->|"Billing data"| BillingSystem
style Customer fill:#CC78BC,stroke:#000,color:#fff
style WebPortal fill:#0173B2,stroke:#000,color:#fff
style MobileApp fill:#0173B2,stroke:#000,color:#fff
style APIGateway fill:#0173B2,stroke:#000,color:#fff
style CRM fill:#DE8F05,stroke:#000,color:#fff
style BillingSystem fill:#DE8F05,stroke:#000,color:#fff
Key Elements:
- Two client systems: Web Portal and Mobile App (both blue, your systems)
- API Gateway: Central routing point (blue, your system)
- Backend systems: CRM and Billing (orange, shared data systems)
- Layered architecture: Clients → Gateway → Backends
Design Rationale: API Gateway pattern centralizes routing, authentication, and rate limiting. Showing this at Context level reveals that multiple client systems share backend infrastructure through a common gateway.
Key Takeaway: Use layered layout (top to bottom or left to right) to show architectural tiers. Group systems by role (clients, gateways, backends) for visual clarity.
Why It Matters: API Gateway patterns prevent direct client-to-backend coupling but introduce a critical single point of failure. Context diagrams showing many backend services routed through one gateway reveal this concentration of risk and drive investment in redundancy, caching, and graceful degradation strategies. Without resilience patterns, gateway failures can cascade to all dependent services; with proper circuit breakers and fallback routes, systems can maintain partial functionality during outages.
Example 10: Cross-Organization Integration
B2B systems integrate across organizational boundaries. This example shows how to represent partner systems and integration contracts.
graph TD
Employee["[Person]<br/>Employee<br/>Books travel"]
TravelBookingSystem["[Software System]<br/>Travel Booking System<br/>Internal travel management"]
AirlineAPI["[External System]<br/>Airline API<br/>Flight booking - Partner A"]
HotelAPI["[External System]<br/>Hotel API<br/>Accommodation - Partner B"]
ExpenseSystem["[External System]<br/>Expense System<br/>Finance department"]
Employee -->|"Requests travel"| TravelBookingSystem
TravelBookingSystem -->|"Books flights"| AirlineAPI
TravelBookingSystem -->|"Books hotels"| HotelAPI
TravelBookingSystem -->|"Submits expenses"| ExpenseSystem
style Employee fill:#CC78BC,stroke:#000,color:#fff
style TravelBookingSystem fill:#0173B2,stroke:#000,color:#fff
style AirlineAPI fill:#029E73,stroke:#000,color:#fff
style HotelAPI fill:#029E73,stroke:#000,color:#fff
style ExpenseSystem fill:#029E73,stroke:#000,color:#fff
Key Elements:
- Partner systems (teal): Airline and Hotel APIs - external organizations
- Internal external system (teal): Expense System - different department, outside your control
- Clear ownership: Labels indicate Partner A, Partner B, Finance department
- Integration points: Each arrow represents an API contract
Design Rationale: Distinguishing between external partners (Airline, Hotel) and internal-but-external systems (Finance) helps identify different coordination mechanisms. Partner APIs require formal contracts; internal systems may allow informal coordination.
Key Takeaway: Use teal for all systems outside your direct control, whether external companies or other departments. Add ownership labels (Partner A, Finance Dept) to clarify governance.
Why It Matters: Cross-organizational dependencies have different SLAs, governance, and change management than systems you control. Context diagrams showing external partner integrations help teams plan for graceful degradation strategies. By understanding which dependencies are critical versus optional, systems can maintain core functionality even when external services are unavailable, prioritizing essential features over peripheral ones during partner outages.
Example 11: Real-Time Data Feeds
Systems consuming real-time data from external sources require special consideration. This example shows streaming data architecture at Context level.
graph TD
Trader["[Person]<br/>Trader<br/>Monitors market"]
TradingPlatform["[Software System]<br/>Trading Platform<br/>Financial trading system"]
MarketDataFeed["[External System]<br/>Market Data Feed<br/>Real-time stock prices"]
NewsAPI["[External System]<br/>News API<br/>Financial news stream"]
RiskEngine["[External System]<br/>Risk Engine<br/>Compliance checking"]
Trader -->|"Executes trades"| TradingPlatform
MarketDataFeed -->|"Price updates (WebSocket)"| TradingPlatform
NewsAPI -->|"News events (SSE)"| TradingPlatform
TradingPlatform -->|"Trade validation"| RiskEngine
style Trader fill:#CC78BC,stroke:#000,color:#fff
style TradingPlatform fill:#0173B2,stroke:#000,color:#fff
style MarketDataFeed fill:#029E73,stroke:#000,color:#fff
style NewsAPI fill:#029E73,stroke:#000,color:#fff
style RiskEngine fill:#029E73,stroke:#000,color:#fff
Key Elements:
- Streaming protocols: WebSocket and SSE (Server-Sent Events) noted on arrows
- Push vs Pull: Market data and news push to platform (arrows point inward)
- Synchronous validation: Trade validation happens via request/response (outward arrow)
- Real-time requirements: Protocol choices reveal latency requirements
Design Rationale: Distinguishing push (real-time feeds) from pull (API calls) reveals system responsiveness requirements. WebSocket choice indicates sub-second latency needs; SSE shows acceptable one-way streaming.
Key Takeaway: Specify protocols (WebSocket, SSE, HTTP) when they reveal architectural constraints. Show data flow direction (push vs pull) with arrow direction.
Why It Matters: Real-time systems have fundamentally different availability and latency requirements than batch systems. Context diagrams help identify when real-time feeds and batch processing are incorrectly sharing infrastructure. Separating these workloads—dedicated infrastructure for low-latency streaming versus batch computation clusters—improves overall system availability during peak load by preventing resource contention between fundamentally different processing patterns.
Example 12: Compliance and Audit
Regulatory requirements often mandate audit trails and compliance systems. This example shows how to represent compliance architecture at Context level.
graph TD
Customer["[Person]<br/>Customer<br/>Uses banking app"]
Auditor["[Person]<br/>Auditor<br/>Reviews transactions"]
BankingApp["[Software System]<br/>Banking Application<br/>Customer banking"]
AuditLog["[Software System]<br/>Audit Log<br/>Immutable event store"]
ComplianceEngine["[External System]<br/>Compliance Engine<br/>Regulatory checks"]
RegulatoryReporting["[External System]<br/>Regulatory Reporting<br/>Government system"]
Customer -->|"Performs transactions"| BankingApp
BankingApp -->|"Records all events"| AuditLog
BankingApp -->|"Validates compliance"| ComplianceEngine
Auditor -->|"Reviews logs"| AuditLog
AuditLog -->|"Daily reports"| RegulatoryReporting
style Customer fill:#CC78BC,stroke:#000,color:#fff
style Auditor fill:#CC78BC,stroke:#000,color:#fff
style BankingApp fill:#0173B2,stroke:#000,color:#fff
style AuditLog fill:#DE8F05,stroke:#000,color:#fff
style ComplianceEngine fill:#029E73,stroke:#000,color:#fff
style RegulatoryReporting fill:#029E73,stroke:#000,color:#fff
Key Elements:
- Two user types: Customer (operational) and Auditor (oversight)
- Audit Log (orange): Critical data store requiring immutability guarantees
- Compliance Engine (teal): External system enforcing regulatory rules
- Regulatory Reporting (teal): Government-operated system receiving reports
- Event recording: All transactions logged for audit trail
Design Rationale: Separating operational flow (Customer → Banking App) from audit flow (Banking App → Audit Log → Regulatory Reporting) clarifies compliance architecture. Auditor access to logs (not the app) enforces separation of duties.
Key Takeaway: Show audit and compliance systems explicitly. Use separate actors (Customer vs Auditor) to reveal different access patterns and governance requirements.
Why It Matters: Compliance failures often stem from invisible audit flows. Context diagrams explicitly showing audit and compliance systems help ensure proper separation of concerns. Audit logs stored separately from operational data with immutability guarantees prevent tampering and meet regulatory requirements. Separating audit systems from operational systems in architecture diagrams drives better compliance design and makes governance requirements visible to all stakeholders.
Container Diagrams - Basic Web Apps (Examples 13-17)
Example 13: Simple Web Application
Container diagrams zoom into a single system (from Context) and show its major building blocks. This example demonstrates a basic three-tier web architecture.
graph TD
User["[Person]<br/>User"]
WebApp["[Container: Web Application]<br/>React SPA<br/>Runs in browser"]
APIServer["[Container: API Server]<br/>Node.js/Express<br/>REST API"]
Database["[Container: Database]<br/>PostgreSQL<br/>Stores user data"]
User -->|"HTTPS"| WebApp
WebApp -->|"JSON/HTTPS<br/>API calls"| APIServer
APIServer -->|"SQL<br/>Reads/Writes"| Database
style User fill:#CC78BC,stroke:#000,color:#fff
style WebApp fill:#0173B2,stroke:#000,color:#fff
style APIServer fill:#0173B2,stroke:#000,color:#fff
style Database fill:#DE8F05,stroke:#000,color:#fff
Key Elements:
- [Container: Type] notation: Each box labeled with container type
- Technology stack: React, Node.js, PostgreSQL specified
- Three tiers: Presentation (Web App), Logic (API Server), Data (Database)
- Protocols: HTTPS for web, JSON/HTTPS for API, SQL for database
- Deployment units: Each container can be deployed independently
Design Rationale: Three-tier architecture separates concerns: WebApp handles UI, APIServer handles business logic, Database handles persistence. This enables independent scaling (e.g., 10 API servers, 1 database) and technology choices per tier.
Key Takeaway: Label each container with [Container: Type], technology stack, and brief description. Show protocols on arrows. Use orange for databases to distinguish them from application containers.
Why It Matters: Container diagrams reveal deployment and scaling strategies. Visualizing deployment units helps identify opportunities to split monolithic applications into independently scalable components. Separating concerns like web serving, API handling, and background job processing enables targeted scaling of bottleneck components. Container-level visibility drives infrastructure decisions—where to add caching, which components to containerize first, and what needs CDN support.
Example 14: Web App with File Storage
Modern web applications often handle file uploads and storage. This example shows how to represent blob storage in Container diagrams.
graph TD
User["[Person]<br/>User<br/>Uploads photos"]
WebApp["[Container: Web Application]<br/>Vue.js SPA<br/>Photo gallery UI"]
APIServer["[Container: API Server]<br/>Python/FastAPI<br/>REST API"]
Database["[Container: Database]<br/>PostgreSQL<br/>Photo metadata"]
BlobStorage["[Container: Blob Storage]<br/>S3-compatible storage<br/>Photo files"]
User -->|"HTTPS"| WebApp
WebApp -->|"Upload photo<br/>JSON/HTTPS"| APIServer
APIServer -->|"Save metadata<br/>SQL"| Database
APIServer -->|"Store file<br/>S3 API"| BlobStorage
WebApp -->|"Direct download<br/>Pre-signed URLs"| BlobStorage
style User fill:#CC78BC,stroke:#000,color:#fff
style WebApp fill:#0173B2,stroke:#000,color:#fff
style APIServer fill:#0173B2,stroke:#000,color:#fff
style Database fill:#DE8F05,stroke:#000,color:#fff
style BlobStorage fill:#DE8F05,stroke:#000,color:#fff
Key Elements:
- Blob Storage (orange): File storage separate from database
- Metadata vs Files: Database stores metadata (filename, size), Blob Storage stores actual files
- Pre-signed URLs: WebApp downloads directly from storage (not through API)
- S3-compatible API: Standard protocol for object storage
Design Rationale: Separating file storage from database prevents database bloat and enables CDN caching. Direct download from blob storage (using pre-signed URLs) reduces API server load and improves download performance.
Key Takeaway: Show blob/object storage as separate container from database. Use direct connections (WebApp to BlobStorage) when appropriate to reveal optimization patterns like pre-signed URLs.
Why It Matters: File storage architecture affects costs and performance dramatically. Container diagrams showing file flows help identify inefficient patterns like API servers proxying large file downloads. Implementing direct client-to-storage access patterns (like pre-signed URLs) reduces API server CPU load and improves download performance by eliminating unnecessary intermediaries. Visualizing file flow paths in Container diagrams drives these architectural optimization decisions.
Example 15: Web App with Background Jobs
Long-running tasks should not block web requests. This example shows how to represent background job processing in Container diagrams.
graph TD
User["[Person]<br/>User<br/>Requests report"]
WebApp["[Container: Web Application]<br/>React SPA<br/>Dashboard UI"]
APIServer["[Container: API Server]<br/>Django REST<br/>HTTP API"]
JobQueue["[Container: Message Queue]<br/>Redis/Celery<br/>Job queue"]
Worker["[Container: Background Worker]<br/>Python/Celery<br/>Processes jobs"]
Database["[Container: Database]<br/>PostgreSQL<br/>Application data"]
User -->|"Request report<br/>HTTPS"| WebApp
WebApp -->|"POST /reports<br/>JSON/HTTPS"| APIServer
APIServer -->|"Enqueue job<br/>Redis protocol"| JobQueue
JobQueue -->|"Fetch jobs<br/>Redis protocol"| Worker
Worker -->|"Read/Write data<br/>SQL"| Database
APIServer -->|"Read/Write data<br/>SQL"| Database
style User fill:#CC78BC,stroke:#000,color:#fff
style WebApp fill:#0173B2,stroke:#000,color:#fff
style APIServer fill:#0173B2,stroke:#000,color:#fff
style JobQueue fill:#DE8F05,stroke:#000,color:#fff
style Worker fill:#0173B2,stroke:#000,color:#fff
style Database fill:#DE8F05,stroke:#000,color:#fff
Key Elements:
- Job Queue (orange): Redis-backed message queue for async tasks
- Background Worker: Separate container processing queued jobs
- Async flow: API enqueues job, worker processes separately
- Shared database: Both API and Worker access database
- Decoupled execution: Web request returns immediately, job runs later
Design Rationale: Background jobs prevent timeout errors on long tasks (report generation, email sending, image processing). Separate worker containers enable independent scaling based on queue depth.
Key Takeaway: Show background job processing as separate container. Include job queue as intermediary. Label async flows clearly (enqueue vs fetch).
Why It Matters: Synchronous long-running tasks destroy user experience. Container diagrams revealing time-intensive operations blocking API responses drive architectural decisions to move these tasks to background workers. Asynchronous processing reduces user-facing response times while often improving job quality since workers can perform thorough processing without timeout constraints. Visualizing the separation between synchronous and asynchronous workloads helps teams optimize for both responsiveness and thoroughness.
Example 16: Web App with Caching
Caching dramatically improves performance and reduces database load. This example shows cache integration in Container diagrams.
graph TD
User["[Person]<br/>User<br/>Views products"]
WebApp["[Container: Web Application]<br/>Next.js SSR<br/>Server-rendered UI"]
APIServer["[Container: API Server]<br/>Go/Gin<br/>REST API"]
Cache["[Container: Cache]<br/>Redis<br/>In-memory cache"]
Database["[Container: Database]<br/>PostgreSQL<br/>Product catalog"]
User -->|"GET /products<br/>HTTPS"| WebApp
WebApp -->|"GET /api/products<br/>JSON/HTTPS"| APIServer
APIServer -->|"1. Check cache<br/>Redis protocol"| Cache
Cache -.->|"Cache miss"| APIServer
APIServer -->|"2. Query DB<br/>SQL"| Database
Database -->|"Product data"| APIServer
APIServer -->|"3. Update cache<br/>Redis protocol"| Cache
style User fill:#CC78BC,stroke:#000,color:#fff
style WebApp fill:#0173B2,stroke:#000,color:#fff
style APIServer fill:#0173B2,stroke:#000,color:#fff
style Cache fill:#DE8F05,stroke:#000,color:#fff
style Database fill:#DE8F05,stroke:#000,color:#fff
Key Elements:
- Cache container (orange): Redis for in-memory caching
- Numbered flow: Shows cache-aside pattern (1. check cache, 2. query DB on miss, 3. update cache)
- Dotted line: Cache miss indicates conditional flow
- Performance optimization: Cache reduces database load
Design Rationale: Cache-aside pattern (application manages cache) gives API Server control over cache invalidation. Redis positioned between API and database reveals it’s a performance optimization, not a required dependency.
Key Takeaway: Use numbered steps to show cache access patterns. Use dotted lines for conditional flows (cache miss). Position cache visually between API and database to show its role in data flow.
Why It Matters: Caching strategy affects cost and performance at scale. Container diagrams showing data access patterns help identify queries for infrequently changing data that could benefit from caching. Implementing appropriate cache layers with TTLs matching data change frequency can dramatically reduce database load, potentially deferring expensive infrastructure upgrades. Visualizing cache position and data flow in Container diagrams drives these cost-saving optimization decisions.
Example 17: Web App with CDN
Content Delivery Networks accelerate static asset delivery. This example shows CDN integration at Container level.
graph TD
User["[Person]<br/>User<br/>Browses website"]
CDN["[Container: CDN]<br/>CloudFront/Cloudflare<br/>Global edge network"]
WebApp["[Container: Web Application]<br/>Static site (React build)<br/>Hosted on S3"]
APIServer["[Container: API Server]<br/>Node.js<br/>REST API"]
Database["[Container: Database]<br/>MongoDB<br/>Application data"]
User -->|"GET /assets/*<br/>HTTPS"| CDN
CDN -->|"Origin fetch<br/>(cache miss)"| WebApp
User -->|"GET /api/*<br/>HTTPS"| APIServer
APIServer -->|"Queries<br/>MongoDB protocol"| Database
style User fill:#CC78BC,stroke:#000,color:#fff
style CDN fill:#029E73,stroke:#000,color:#fff
style WebApp fill:#0173B2,stroke:#000,color:#fff
style APIServer fill:#0173B2,stroke:#000,color:#fff
style Database fill:#DE8F05,stroke:#000,color:#fff
Key Elements:
- CDN (teal): External service providing edge caching
- Path-based routing: /assets/to CDN, /api/ to API Server
- Origin fetch: CDN pulls from WebApp on cache miss
- Separation of concerns: Static assets (CDN/S3) vs dynamic API
Design Rationale: CDN handles static assets (HTML, CSS, JS, images) reducing origin server load and improving latency via geographic distribution. API calls bypass CDN and hit API Server directly because they’re dynamic.
Key Takeaway: Show CDN as external system (teal) even though it’s part of your infrastructure. Use path patterns (/assets/, /api/) to clarify routing logic.
Why It Matters: CDN architecture dramatically affects infrastructure costs and performance. Container diagrams showing routing patterns help identify static assets unnecessarily hitting origin servers. Proper CDN configuration with appropriate cache headers can reduce origin server load significantly, allowing systems to handle traffic spikes without infrastructure scaling. Visualizing CDN routing and cache patterns in Container diagrams drives performance optimization decisions and reveals opportunities to offload traffic to edge networks.
Container Diagrams - With Databases (Examples 18-22)
Example 18: Web App with Read Replicas
Database read replicas improve read performance and availability. This example shows primary-replica database architecture.
graph TD
User["[Person]<br/>User"]
WebApp["[Container: Web Application]<br/>Angular SPA"]
APIServer["[Container: API Server]<br/>Java/Spring Boot"]
PrimaryDB["[Container: Database - Primary]<br/>PostgreSQL Primary<br/>Writes + Reads"]
ReplicaDB1["[Container: Database - Replica]<br/>PostgreSQL Replica 1<br/>Reads only"]
ReplicaDB2["[Container: Database - Replica]<br/>PostgreSQL Replica 2<br/>Reads only"]
User -->|HTTPS| WebApp
WebApp -->|JSON/HTTPS| APIServer
APIServer -->|"Writes<br/>SQL"| PrimaryDB
APIServer -->|"Reads<br/>SQL"| ReplicaDB1
APIServer -->|"Reads<br/>SQL"| ReplicaDB2
PrimaryDB -.->|"Replication"| ReplicaDB1
PrimaryDB -.->|"Replication"| ReplicaDB2
style User fill:#CC78BC,stroke:#000,color:#fff
style WebApp fill:#0173B2,stroke:#000,color:#fff
style APIServer fill:#0173B2,stroke:#000,color:#fff
style PrimaryDB fill:#DE8F05,stroke:#000,color:#fff
style ReplicaDB1 fill:#CA9161,stroke:#000,color:#fff
style ReplicaDB2 fill:#CA9161,stroke:#000,color:#fff
Key Elements:
- Primary database (orange): Handles all writes and some reads
- Replica databases (brown): Handle read-only queries
- Replication flow (dotted): Data replicates from primary to replicas
- Read/Write separation: API writes to primary, reads from replicas
- Multiple replicas: Load balancing across replicas
Design Rationale: Read replicas scale read-heavy workloads by distributing queries across multiple database instances. Primary handles writes (requiring strong consistency), replicas handle reads (allowing eventual consistency).
Key Takeaway: Use different colors for primary (orange) and replicas (brown) to distinguish roles. Show replication with dotted lines. Label read vs write flows explicitly.
Why It Matters: Read scaling is a common database bottleneck. Container diagrams showing read/write patterns help identify workloads dominated by read queries. Adding read replicas and routing read traffic appropriately can dramatically reduce query latency with minimal application code changes—purely infrastructure optimization. Visualizing database access patterns in Container diagrams drives decisions about when replica scaling provides the most value versus other optimization strategies.
Example 19: Web App with Database Sharding
Sharding distributes data across multiple databases by partition key. This example shows horizontal database scaling via sharding.
graph TD
User["[Person]<br/>User"]
WebApp["[Container: Web Application]<br/>React SPA"]
APIServer["[Container: API Server]<br/>Python/Django"]
ShardRouter["[Container: Shard Router]<br/>Vitess/ProxySQL<br/>Query routing"]
Shard1["[Container: Database Shard 1]<br/>PostgreSQL<br/>User IDs 0-999999"]
Shard2["[Container: Database Shard 2]<br/>PostgreSQL<br/>User IDs 1000000-1999999"]
Shard3["[Container: Database Shard 3]<br/>PostgreSQL<br/>User IDs 2000000+"]
User -->|HTTPS| WebApp
WebApp -->|JSON/HTTPS| APIServer
APIServer -->|"SQL queries"| ShardRouter
ShardRouter -->|"Shard 1 queries"| Shard1
ShardRouter -->|"Shard 2 queries"| Shard2
ShardRouter -->|"Shard 3 queries"| Shard3
style User fill:#CC78BC,stroke:#000,color:#fff
style WebApp fill:#0173B2,stroke:#000,color:#fff
style APIServer fill:#0173B2,stroke:#000,color:#fff
style ShardRouter fill:#DE8F05,stroke:#000,color:#fff
style Shard1 fill:#CA9161,stroke:#000,color:#fff
style Shard2 fill:#CA9161,stroke:#000,color:#fff
style Shard3 fill:#CA9161,stroke:#000,color:#fff
Key Elements:
- Shard Router (orange): Routes queries to appropriate shard based on partition key
- Three shards (brown): Each handles a range of user IDs
- Partition strategy: User ID ranges define shard boundaries
- Horizontal scaling: Add more shards to increase capacity
- Technology: Vitess/ProxySQL handles routing complexity
Design Rationale: Sharding distributes data to overcome single-database limits (storage, throughput, connections). Router hides sharding complexity from application code, allowing transparent scaling.
Key Takeaway: Show shard router as separate container. Label each shard with its partition range. Use consistent color for shards to show they’re equivalent.
Why It Matters: Sharding enables growth beyond single-database limits but adds operational complexity. Container diagrams showing sharding architecture reveal the multiplication of operational concerns—each shard requires backups, migrations, monitoring, and failure handling. This visibility drives investment in automation for shard provisioning, rebalancing, and management. Without proper tooling, operational overhead grows linearly with shard count; with automation, complexity increase becomes sub-linear even as data scales horizontally.
Example 20: Web App with Separate Read/Write Databases (CQRS)
Command Query Responsibility Segregation (CQRS) uses separate databases for writes and reads. This example shows CQRS at Container level.
graph TD
User["[Person]<br/>User"]
WebApp["[Container: Web Application]<br/>Vue.js SPA"]
WriteAPI["[Container: Write API]<br/>Node.js<br/>Command handlers"]
ReadAPI["[Container: Read API]<br/>Node.js<br/>Query handlers"]
WriteDB["[Container: Write Database]<br/>PostgreSQL<br/>Normalized schema"]
ReadDB["[Container: Read Database]<br/>MongoDB<br/>Denormalized views"]
EventBus["[Container: Event Bus]<br/>Kafka<br/>Change data capture"]
User -->|"Commands<br/>POST/PUT/DELETE"| WebApp
User -->|"Queries<br/>GET"| WebApp
WebApp -->|"POST /api/commands"| WriteAPI
WebApp -->|"GET /api/queries"| ReadAPI
WriteAPI -->|"SQL Writes"| WriteDB
ReadAPI -->|"MongoDB Queries"| ReadDB
WriteDB -->|"Change events"| EventBus
EventBus -->|"Update views"| ReadDB
style User fill:#CC78BC,stroke:#000,color:#fff
style WebApp fill:#0173B2,stroke:#000,color:#fff
style WriteAPI fill:#0173B2,stroke:#000,color:#fff
style ReadAPI fill:#0173B2,stroke:#000,color:#fff
style WriteDB fill:#DE8F05,stroke:#000,color:#fff
style ReadDB fill:#CA9161,stroke:#000,color:#fff
style EventBus fill:#029E73,stroke:#000,color:#fff
Key Elements:
- Separate APIs: Write API handles commands, Read API handles queries
- Different databases: PostgreSQL (normalized) for writes, MongoDB (denormalized) for reads
- Event Bus (teal): Kafka propagates changes from write to read database
- Command/Query split: POST/PUT/DELETE vs GET segregated at API level
- Eventual consistency: Read database updated asynchronously via events
Design Rationale: CQRS optimizes write and read paths independently. Write database uses normalized schema for data integrity; read database uses denormalized schema for query performance. Event bus decouples the two.
Key Takeaway: Show write and read paths as completely separate flows. Use different colors for write database (orange) and read database (brown). Include event bus to show synchronization mechanism.
Why It Matters: CQRS handles extreme read/write ratio imbalances. Container diagrams quantifying read versus write traffic help identify when separate optimization paths make sense. Building separate read infrastructure (denormalized, heavily cached, geographically distributed) can dramatically improve query latency while maintaining write consistency guarantees. CQRS architecture in Container diagrams makes read/write ratios and optimization opportunities visible, helping teams decide when the complexity tradeoff is justified.
Example 21: Multi-Tenant Web App with Database Isolation
Multi-tenant systems require data isolation between customers. This example shows database-per-tenant architecture.
graph TD
TenantA["[Person]<br/>Tenant A User"]
TenantB["[Person]<br/>Tenant B User"]
TenantC["[Person]<br/>Tenant C User"]
WebApp["[Container: Web Application]<br/>React SPA<br/>Multi-tenant UI"]
APIServer["[Container: API Server]<br/>Ruby on Rails<br/>Tenant routing"]
DBTenantA["[Container: Tenant A Database]<br/>PostgreSQL<br/>Tenant A data"]
DBTenantB["[Container: Tenant B Database]<br/>PostgreSQL<br/>Tenant B data"]
DBTenantC["[Container: Tenant C Database]<br/>PostgreSQL<br/>Tenant C data"]
TenantA -->|"HTTPS + Tenant ID"| WebApp
TenantB -->|"HTTPS + Tenant ID"| WebApp
TenantC -->|"HTTPS + Tenant ID"| WebApp
WebApp -->|"API calls + Tenant ID"| APIServer
APIServer -->|"Tenant A queries"| DBTenantA
APIServer -->|"Tenant B queries"| DBTenantB
APIServer -->|"Tenant C queries"| DBTenantC
style TenantA fill:#CC78BC,stroke:#000,color:#fff
style TenantB fill:#CC78BC,stroke:#000,color:#fff
style TenantC fill:#CC78BC,stroke:#000,color:#fff
style WebApp fill:#0173B2,stroke:#000,color:#fff
style APIServer fill:#0173B2,stroke:#000,color:#fff
style DBTenantA fill:#CA9161,stroke:#000,color:#fff
style DBTenantB fill:#CA9161,stroke:#000,color:#fff
style DBTenantC fill:#CA9161,stroke:#000,color:#fff
Key Elements:
- Three tenant users: Each tenant isolated logically and physically
- Tenant routing: API server routes to correct database based on tenant ID
- Database-per-tenant: Complete data isolation between tenants
- Shared application tier: Single web app and API server serve all tenants
- Tenant ID propagation: Tenant identifier flows through all layers
Design Rationale: Database-per-tenant provides strongest data isolation (regulatory compliance, customer-specific SLAs) at cost of operational complexity (N databases to manage). Alternative is shared database with row-level tenant ID filtering.
Key Takeaway: Show each tenant database separately when using database-per-tenant architecture. Use tenant ID labels on connections to show routing logic.
Why It Matters: Tenant isolation strategy affects compliance, costs, and blast radius. Container diagrams help teams evaluate different multi-tenancy approaches based on customer requirements. Hybrid architectures (database-per-tenant for compliance-sensitive customers, shared database for standard tiers) can optimize costs while meeting diverse regulatory needs. Visualizing tenant architecture in Container diagrams helps teams make business-critical tradeoffs between isolation guarantees, operational complexity, and infrastructure costs.
Example 22: Web App with Time-Series Database
Time-series data (metrics, logs, sensor data) requires specialized databases. This example shows time-series database integration.
graph TD
User["[Person]<br/>User<br/>Views dashboards"]
WebApp["[Container: Web Application]<br/>Grafana<br/>Visualization dashboard"]
APIServer["[Container: API Server]<br/>Go/Gin<br/>Data collection API"]
TransactionalDB["[Container: Database]<br/>PostgreSQL<br/>Users and config"]
TimeSeriesDB["[Container: Time-Series Database]<br/>InfluxDB/TimescaleDB<br/>Metrics and events"]
User -->|"View metrics<br/>HTTPS"| WebApp
WebApp -->|"Query time-series<br/>InfluxQL"| TimeSeriesDB
WebApp -->|"Query config<br/>SQL"| TransactionalDB
APIServer -->|"Write metrics<br/>InfluxDB line protocol"| TimeSeriesDB
APIServer -->|"Read/Write config<br/>SQL"| TransactionalDB
style User fill:#CC78BC,stroke:#000,color:#fff
style WebApp fill:#0173B2,stroke:#000,color:#fff
style APIServer fill:#0173B2,stroke:#000,color:#fff
style TransactionalDB fill:#DE8F05,stroke:#000,color:#fff
style TimeSeriesDB fill:#CA9161,stroke:#000,color:#fff
Key Elements:
- Two database types: PostgreSQL (relational) for config, InfluxDB (time-series) for metrics
- Specialized query languages: SQL vs InfluxQL
- Write-optimized time-series: InfluxDB handles high-volume metric writes
- Separate concerns: User config in relational DB, time-series data in specialized DB
- Purpose-built storage: Time-series databases optimize for time-based queries
Design Rationale: Time-series databases provide compression, efficient time-range queries, and downsampling/retention policies that relational databases can’t match. Separating transactional data (users, config) from time-series data (metrics, logs) optimizes each.
Key Takeaway: Show time-series databases separately from transactional databases. Use different colors (brown for time-series) to distinguish. Note specialized protocols (InfluxDB line protocol, InfluxQL).
Why It Matters: Time-series data volume overwhelms general-purpose databases. Container diagrams showing specialized database types help teams recognize when purpose-built storage provides significant advantages. Time-series databases offer superior compression and query performance for metrics and logs compared to relational databases. Visualizing database specialization drives technology selection—relational for transactions, time-series for metrics/logs/events, graph for relationships—matching storage technology to data access patterns.
Component Diagrams - Basic (Examples 23-27)
Example 23: API Server Internal Components
Component diagrams zoom into a single container (from Container diagram) and show its internal structure. This example demonstrates a typical API server component organization.
graph TD
APIServer["API Server Container"]
AuthController["[Component]<br/>Auth Controller<br/>Handles login/logout"]
UserController["[Component]<br/>User Controller<br/>User CRUD operations"]
ProductController["[Component]<br/>Product Controller<br/>Product management"]
AuthService["[Component]<br/>Auth Service<br/>Token validation"]
UserService["[Component]<br/>User Service<br/>Business logic"]
ProductService["[Component]<br/>Product Service<br/>Business logic"]
UserRepository["[Component]<br/>User Repository<br/>Data access"]
ProductRepository["[Component]<br/>Product Repository<br/>Data access"]
AuthController -->|Uses| AuthService
UserController -->|Uses| AuthService
UserController -->|Uses| UserService
ProductController -->|Uses| AuthService
ProductController -->|Uses| ProductService
UserService -->|Uses| UserRepository
ProductService -->|Uses| ProductRepository
style APIServer fill:#0173B2,stroke:#000,color:#fff
style AuthController fill:#DE8F05,stroke:#000,color:#fff
style UserController fill:#DE8F05,stroke:#000,color:#fff
style ProductController fill:#DE8F05,stroke:#000,color:#fff
style AuthService fill:#029E73,stroke:#000,color:#fff
style UserService fill:#029E73,stroke:#000,color:#fff
style ProductService fill:#029E73,stroke:#000,color:#fff
style UserRepository fill:#CC78BC,stroke:#000,color:#fff
style ProductRepository fill:#CC78BC,stroke:#000,color:#fff
Key Elements:
- Three layers: Controllers (orange), Services (teal), Repositories (purple)
- Controller responsibility: HTTP request handling and routing
- Service responsibility: Business logic and orchestration
- Repository responsibility: Database access and data mapping
- Cross-cutting concerns: AuthService used by all controllers
- Dependencies flow downward: Controllers → Services → Repositories
Design Rationale: Layered architecture separates HTTP concerns (controllers) from business logic (services) from data access (repositories). This enables testing (mock services/repositories), technology changes (swap databases), and code reuse (multiple controllers using same service).
Key Takeaway: Use color coding to show layers. Orange for controllers, teal for services, purple for repositories. Show dependencies with “Uses” relationships.
Why It Matters: Component organization affects testability and maintenance. Component diagrams revealing architectural violations—such as controllers bypassing service layers to call repositories directly—help teams identify where business logic duplication and inconsistent rule enforcement occur. Enforcing proper layered architecture (controllers → services → repositories) reduces duplicate logic, improves testability through clear boundaries, and enables easier service extraction when migrating to microservices.
Example 24: Web Application Components (Frontend)
Frontend applications have internal structure that Component diagrams reveal. This example shows React application component organization.
graph TD
WebApp["Web Application Container"]
AppShell["[Component]<br/>App Shell<br/>Layout and routing"]
AuthModule["[Component]<br/>Auth Module<br/>Login/logout UI"]
DashboardModule["[Component]<br/>Dashboard Module<br/>Dashboard pages"]
SettingsModule["[Component]<br/>Settings Module<br/>Settings pages"]
APIClient["[Component]<br/>API Client<br/>HTTP communication"]
AuthStore["[Component]<br/>Auth Store<br/>User state management"]
DataStore["[Component]<br/>Data Store<br/>Application state"]
AppShell -->|Renders| AuthModule
AppShell -->|Renders| DashboardModule
AppShell -->|Renders| SettingsModule
AuthModule -->|Uses| AuthStore
AuthModule -->|Uses| APIClient
DashboardModule -->|Uses| DataStore
DashboardModule -->|Uses| APIClient
SettingsModule -->|Uses| DataStore
SettingsModule -->|Uses| APIClient
AuthStore -->|Uses| APIClient
DataStore -->|Uses| APIClient
style WebApp fill:#0173B2,stroke:#000,color:#fff
style AppShell fill:#DE8F05,stroke:#000,color:#fff
style AuthModule fill:#029E73,stroke:#000,color:#fff
style DashboardModule fill:#029E73,stroke:#000,color:#fff
style SettingsModule fill:#029E73,stroke:#000,color:#fff
style APIClient fill:#CC78BC,stroke:#000,color:#fff
style AuthStore fill:#CA9161,stroke:#000,color:#fff
style DataStore fill:#CA9161,stroke:#000,color:#fff
Key Elements:
- App Shell (orange): Core layout and routing logic
- Feature modules (teal): Auth, Dashboard, Settings - each encapsulates related UI
- API Client (purple): Centralized HTTP communication
- State stores (brown): AuthStore for user state, DataStore for application data
- Module independence: Modules don’t call each other, only shared services
- Centralized state: Stores manage state, modules consume via subscriptions
Design Rationale: Feature modules encapsulate related UI components (pages, forms, widgets) enabling code splitting and lazy loading. Centralized API client and state stores prevent duplicate fetch logic and ensure consistency.
Key Takeaway: Show frontend structure with modules (feature areas), shared services (API client), and state management (stores). Use color to distinguish layers.
Why It Matters: Frontend component organization affects bundle size and initial load time. Component diagrams showing feature module boundaries help identify opportunities for code splitting. Implementing lazy loading—where modules load on demand rather than upfront—can dramatically reduce initial bundle size and improve time-to-interactive, especially on slower network connections. Visualizing module dependencies drives decisions about what to load immediately versus defer until needed.
Example 25: Background Worker Components
Background workers process queued jobs. This example shows internal organization of a worker container.
graph TD
Worker["Background Worker Container"]
JobDispatcher["[Component]<br/>Job Dispatcher<br/>Fetches jobs from queue"]
EmailHandler["[Component]<br/>Email Handler<br/>Processes email jobs"]
ReportHandler["[Component]<br/>Report Handler<br/>Generates reports"]
ImageHandler["[Component]<br/>Image Handler<br/>Processes images"]
EmailService["[Component]<br/>Email Service<br/>SMTP client"]
PDFService["[Component]<br/>PDF Service<br/>Report generation"]
ImageService["[Component]<br/>Image Service<br/>Resize/optimize"]
Logger["[Component]<br/>Logger<br/>Job logging"]
JobDispatcher -->|Routes| EmailHandler
JobDispatcher -->|Routes| ReportHandler
JobDispatcher -->|Routes| ImageHandler
EmailHandler -->|Uses| EmailService
EmailHandler -->|Uses| Logger
ReportHandler -->|Uses| PDFService
ReportHandler -->|Uses| Logger
ImageHandler -->|Uses| ImageService
ImageHandler -->|Uses| Logger
style Worker fill:#0173B2,stroke:#000,color:#fff
style JobDispatcher fill:#DE8F05,stroke:#000,color:#fff
style EmailHandler fill:#029E73,stroke:#000,color:#fff
style ReportHandler fill:#029E73,stroke:#000,color:#fff
style ImageHandler fill:#029E73,stroke:#000,color:#fff
style EmailService fill:#CC78BC,stroke:#000,color:#fff
style PDFService fill:#CC78BC,stroke:#000,color:#fff
style ImageService fill:#CC78BC,stroke:#000,color:#fff
style Logger fill:#CA9161,stroke:#000,color:#fff
Key Elements:
- Job Dispatcher (orange): Routes incoming jobs to appropriate handlers
- Job Handlers (teal): EmailHandler, ReportHandler, ImageHandler - each handles one job type
- Services (purple): External integrations (SMTP, PDF, Image processing)
- Logger (brown): Cross-cutting logging used by all handlers
- Routing logic: Dispatcher determines handler based on job type
Design Rationale: Separating job routing (dispatcher) from job processing (handlers) enables independent handler scaling and makes it easy to add new job types. Handlers don’t call each other, preventing complex dependencies.
Key Takeaway: Show job routing (dispatcher), job handlers (one per job type), and shared services (logging, monitoring). Use consistent color for handlers to show they’re equivalent.
Why It Matters: Worker component organization affects fault isolation and scalability. Component diagrams showing shared job dispatchers help identify single points of failure where one slow job type can block all others. Separating into dedicated worker pools per job type (email, image processing, report generation) isolates failures and enables independent scaling. This architectural separation ensures critical fast jobs continue processing even when resource-intensive jobs slow down under load.
Example 26: Microservice Internal Components
Microservices are containers with focused responsibilities. This example shows typical microservice internal organization.
graph TD
OrderService["Order Service Container"]
OrderAPI["[Component]<br/>Order API<br/>REST endpoints"]
OrderEventHandler["[Component]<br/>Order Event Handler<br/>Consumes events"]
OrderBusinessLogic["[Component]<br/>Order Business Logic<br/>Validation and processing"]
OrderRepository["[Component]<br/>Order Repository<br/>Database access"]
EventPublisher["[Component]<br/>Event Publisher<br/>Publishes order events"]
PaymentClient["[Component]<br/>Payment Client<br/>Calls Payment Service"]
OrderAPI -->|Uses| OrderBusinessLogic
OrderEventHandler -->|Uses| OrderBusinessLogic
OrderBusinessLogic -->|Uses| OrderRepository
OrderBusinessLogic -->|Uses| EventPublisher
OrderBusinessLogic -->|Uses| PaymentClient
style OrderService fill:#0173B2,stroke:#000,color:#fff
style OrderAPI fill:#DE8F05,stroke:#000,color:#fff
style OrderEventHandler fill:#DE8F05,stroke:#000,color:#fff
style OrderBusinessLogic fill:#029E73,stroke:#000,color:#fff
style OrderRepository fill:#CC78BC,stroke:#000,color:#fff
style EventPublisher fill:#CA9161,stroke:#000,color:#fff
style PaymentClient fill:#CA9161,stroke:#000,color:#fff
Key Elements:
- Two entry points (orange): OrderAPI (synchronous) and OrderEventHandler (asynchronous)
- Business logic (teal): Shared by both entry points
- Repository (purple): Database access layer
- Event Publisher (brown): Publishes domain events to message bus
- Service Client (brown): Calls other microservices (Payment Service)
- Hexagonal architecture: Business logic at center, entry points and infrastructure at edges
Design Rationale: Multiple entry points (API and event handler) enable both request/response and event-driven interactions. Shared business logic ensures consistency regardless of entry point. Repository and clients are pluggable infrastructure.
Key Takeaway: Show entry points (API, event handlers), business logic, repository, and external integrations (event publisher, service clients). Use colors to distinguish layers.
Why It Matters: Microservice component organization affects maintainability and testability. Component diagrams revealing business logic scattered across API handlers help identify duplication and testing challenges. Refactoring to hexagonal architecture (business logic at center, infrastructure at edges) consolidates logic, reduces duplication, and improves test coverage by isolating business rules from HTTP and database dependencies. This separation makes business logic independently testable without infrastructure concerns.
Example 27: Plugin Architecture Components
Plugin systems enable extensibility through dynamically loaded components. This example shows plugin architecture at Component level.
graph TD
CoreApp["Core Application Container"]
PluginRegistry["[Component]<br/>Plugin Registry<br/>Manages plugins"]
CoreLogic["[Component]<br/>Core Logic<br/>Main application logic"]
PluginLoader["[Component]<br/>Plugin Loader<br/>Dynamic loading"]
PaymentPlugin["[Component]<br/>Payment Plugin<br/>Payment processing"]
ShippingPlugin["[Component]<br/>Shipping Plugin<br/>Shipping calculations"]
TaxPlugin["[Component]<br/>Tax Plugin<br/>Tax calculations"]
PluginInterface["[Component]<br/>Plugin Interface<br/>Contract definition"]
CoreLogic -->|Uses| PluginRegistry
PluginRegistry -->|Manages| PaymentPlugin
PluginRegistry -->|Manages| ShippingPlugin
PluginRegistry -->|Manages| TaxPlugin
PluginLoader -->|Loads| PaymentPlugin
PluginLoader -->|Loads| ShippingPlugin
PluginLoader -->|Loads| TaxPlugin
PaymentPlugin -.->|Implements| PluginInterface
ShippingPlugin -.->|Implements| PluginInterface
TaxPlugin -.->|Implements| PluginInterface
style CoreApp fill:#0173B2,stroke:#000,color:#fff
style PluginRegistry fill:#DE8F05,stroke:#000,color:#fff
style CoreLogic fill:#DE8F05,stroke:#000,color:#fff
style PluginLoader fill:#DE8F05,stroke:#000,color:#fff
style PaymentPlugin fill:#029E73,stroke:#000,color:#fff
style ShippingPlugin fill:#029E73,stroke:#000,color:#fff
style TaxPlugin fill:#029E73,stroke:#000,color:#fff
style PluginInterface fill:#CC78BC,stroke:#000,color:#fff
Key Elements:
- Core components (orange): Registry, Logic, Loader - stable core
- Plugins (teal): Payment, Shipping, Tax - interchangeable implementations
- Plugin Interface (purple): Contract that plugins must implement
- Dotted lines: “Implements” relationship (interface conformance)
- Dynamic loading: PluginLoader enables runtime plugin addition
- Registry pattern: PluginRegistry provides plugin discovery
Design Rationale: Plugin architecture separates stable core from variable extensions. Core logic uses PluginRegistry (not plugins directly) to maintain loose coupling. PluginInterface defines contract enabling third-party plugins.
Key Takeaway: Show core (orange), plugins (teal), and interface (purple). Use dotted lines for interface implementation. Label registry and loader to show plugin management.
Why It Matters: Plugin architectures enable ecosystem growth but add complexity. Component diagrams showing plugin interfaces and registry patterns help teams design extensibility points that enable third-party contributions while maintaining system stability. Clear interface definitions prevent plugin conflicts and ensure consistent behavior. Visualizing plugin architecture drives extensibility decisions—identifying where to allow plugins (payment gateways, shipping providers) versus where to maintain strict control (core business logic).
Integration Basics (Examples 28-30)
Example 28: REST API Integration
REST APIs are the most common integration pattern. This example shows RESTful service integration at Container level.
graph TD
MobileApp["[Container: Mobile App]<br/>iOS/Android<br/>Native app"]
APIGateway["[Container: API Gateway]<br/>Kong/Nginx<br/>Request routing"]
UserService["[Container: User Service]<br/>Java/Spring<br/>User management"]
OrderService["[Container: Order Service]<br/>Go<br/>Order processing"]
MobileApp -->|"GET /users/:id<br/>JSON/HTTPS"| APIGateway
MobileApp -->|"POST /orders<br/>JSON/HTTPS"| APIGateway
APIGateway -->|"Route to<br/>UserService"| UserService
APIGateway -->|"Route to<br/>OrderService"| OrderService
style MobileApp fill:#0173B2,stroke:#000,color:#fff
style APIGateway fill:#DE8F05,stroke:#000,color:#fff
style UserService fill:#029E73,stroke:#000,color:#fff
style OrderService fill:#029E73,stroke:#000,color:#fff
Key Elements:
- Mobile App: Client making HTTP requests
- API Gateway: Routes requests to appropriate backend service
- RESTful endpoints: GET /users/:id, POST /orders
- JSON payload: Data format for requests/responses
- HTTPS protocol: Secure transport
- Microservices: UserService and OrderService behind gateway
Design Rationale: API Gateway centralizes cross-cutting concerns (authentication, rate limiting, logging) while microservices behind it remain focused on domain logic. Clients call one endpoint (gateway), gateway routes to many services.
Key Takeaway: Show REST integration with HTTP methods (GET, POST), URLs, and protocol (HTTPS). Use API Gateway to abstract backend complexity from clients.
Why It Matters: API Gateway patterns reduce client-server coupling. Container diagrams showing many backend services reveal the complexity clients would face without an aggregation layer. API Gateway provides a single endpoint for clients, handling routing, retry, and circuit breaking centrally. This reduces mobile app complexity and enables backend service changes without requiring client updates, maintaining a stable contract while allowing backend evolution.
Example 29: GraphQL Integration
GraphQL provides flexible querying capabilities. This example shows GraphQL integration compared to REST.
graph TD
WebApp["[Container: Web Application]<br/>React SPA<br/>GraphQL client"]
GraphQLServer["[Container: GraphQL Server]<br/>Apollo Server<br/>GraphQL API"]
UserService["[Container: User Service]<br/>REST API<br/>User data"]
ProductService["[Container: Product Service]<br/>REST API<br/>Product data"]
OrderService["[Container: Order Service]<br/>REST API<br/>Order data"]
WebApp -->|"GraphQL query<br/>Single endpoint<br/>POST /graphql"| GraphQLServer
GraphQLServer -->|"GET /users/:id"| UserService
GraphQLServer -->|"GET /products/:id"| ProductService
GraphQLServer -->|"GET /orders/:id"| OrderService
style WebApp fill:#0173B2,stroke:#000,color:#fff
style GraphQLServer fill:#DE8F05,stroke:#000,color:#fff
style UserService fill:#029E73,stroke:#000,color:#fff
style ProductService fill:#029E73,stroke:#000,color:#fff
style OrderService fill:#029E73,stroke:#000,color:#fff
Key Elements:
- GraphQL Server: Aggregation layer translating GraphQL to REST
- Single endpoint: POST /graphql (unlike REST’s multiple endpoints)
- Client flexibility: Clients specify exactly what data they need
- Service aggregation: GraphQL server calls multiple backend services
- REST backends: Existing REST services remain unchanged
- N+1 prevention: GraphQL server batches requests to backends
Design Rationale: GraphQL server acts as Backend-for-Frontend (BFF) aggregating multiple REST services into single flexible query interface. This reduces client-server round trips and prevents over-fetching.
Key Takeaway: Show GraphQL server as aggregation layer between client and REST services. Label with “Single endpoint POST /graphql” to highlight GraphQL’s unified interface.
Why It Matters: GraphQL solves over-fetching and under-fetching problems. Container diagrams showing multiple REST calls to render single views reveal opportunities for query aggregation. GraphQL enables clients to request exactly the data they need in one query, reducing network round trips and data transfer. This is particularly valuable for mobile clients on constrained networks, where minimizing requests and payload size directly improves user experience.
Example 30: Event-Driven Integration
Event-driven architectures enable loose coupling between services. This example shows pub/sub integration pattern.
graph TD
OrderService["[Container: Order Service]<br/>Creates orders"]
InventoryService["[Container: Inventory Service]<br/>Manages stock"]
EmailService["[Container: Email Service]<br/>Sends notifications"]
AnalyticsService["[Container: Analytics Service]<br/>Tracks metrics"]
EventBus["[Container: Event Bus]<br/>Kafka/RabbitMQ<br/>Message broker"]
OrderService -->|"Publish:<br/>order.created"| EventBus
EventBus -->|"Subscribe:<br/>order.created"| InventoryService
EventBus -->|"Subscribe:<br/>order.created"| EmailService
EventBus -->|"Subscribe:<br/>order.created"| AnalyticsService
style OrderService fill:#0173B2,stroke:#000,color:#fff
style InventoryService fill:#029E73,stroke:#000,color:#fff
style EmailService fill:#029E73,stroke:#000,color:#fff
style AnalyticsService fill:#029E73,stroke:#000,color:#fff
style EventBus fill:#DE8F05,stroke:#000,color:#fff
Key Elements:
- Publisher: OrderService publishes events (doesn’t know subscribers)
- Event Bus: Kafka/RabbitMQ distributes events to subscribers
- Subscribers: Inventory, Email, Analytics - each reacts independently
- Event schema: “order.created” - well-defined event type
- Temporal decoupling: Publisher and subscribers operate asynchronously
- Subscriber independence: Adding new subscriber doesn’t change publisher
Design Rationale: Event-driven integration enables one-to-many communication without coupling. OrderService doesn’t call Inventory/Email/Analytics directly; it publishes event and subscribers react independently. This enables adding new subscribers (e.g., FraudDetectionService) without modifying publisher.
Key Takeaway: Show publisher publishing to event bus, subscribers subscribing to event bus. Label events with schema names (order.created). Use arrows to show data flow direction.
Why It Matters: Event-driven architectures prevent cascade failures. Container diagrams showing synchronous calls to many downstream services reveal tight coupling and fragility—any slow or failing service can block critical operations. Switching to event-driven patterns (publish events, services subscribe) decouples operations, allowing core workflows to complete successfully even when downstream services are unavailable. This temporal decoupling significantly improves system resilience and reduces failure propagation.
This completes the beginner-level C4 Model by-example tutorial with 30 comprehensive examples covering introductory concepts, System Context diagrams, Container diagrams, Component diagrams, and basic integration patterns (0-40% coverage).