Intermediate
This intermediate-level tutorial builds on beginner foundations with 30 examples covering detailed component organization, deployment strategies, dynamic interaction flows, advanced integration patterns, and production-ready multi-container architectures.
Detailed Component Diagrams (Examples 31-38)
Example 31: Modular Monolith Components
Modular monoliths organize code into domain modules within a single deployment unit. This example shows internal boundaries that enable future microservices extraction.
graph TD
MonolithApp["Modular Monolith Container"]
APIGatewayModule["[Component]<br/>API Gateway Module<br/>Routing and auth"]
UserModule["[Component]<br/>User Module<br/>User domain logic"]
OrderModule["[Component]<br/>Order Module<br/>Order domain logic"]
ProductModule["[Component]<br/>Product Module<br/>Product domain logic"]
SharedKernel["[Component]<br/>Shared Kernel<br/>Common utilities"]
UserDB["[Component]<br/>User Schema<br/>User tables"]
OrderDB["[Component]<br/>Order Schema<br/>Order tables"]
ProductDB["[Component]<br/>Product Schema<br/>Product tables"]
APIGatewayModule -->|Routes to| UserModule
APIGatewayModule -->|Routes to| OrderModule
APIGatewayModule -->|Routes to| ProductModule
UserModule -->|Uses| SharedKernel
OrderModule -->|Uses| SharedKernel
ProductModule -->|Uses| SharedKernel
UserModule -->|Accesses| UserDB
OrderModule -->|Accesses| OrderDB
ProductModule -->|Accesses| ProductDB
OrderModule -.->|"Anti-corruption layer"| UserModule
style MonolithApp fill:#0173B2,stroke:#000,color:#fff
style APIGatewayModule fill:#DE8F05,stroke:#000,color:#fff
style UserModule fill:#029E73,stroke:#000,color:#fff
style OrderModule fill:#029E73,stroke:#000,color:#fff
style ProductModule fill:#029E73,stroke:#000,color:#fff
style SharedKernel fill:#CC78BC,stroke:#000,color:#fff
style UserDB fill:#CA9161,stroke:#000,color:#fff
style OrderDB fill:#CA9161,stroke:#000,color:#fff
style ProductDB fill:#CA9161,stroke:#000,color:#fff
Key Elements:
- Domain modules (teal): User, Order, Product - each encapsulates domain logic
- Separate schemas (brown): Database organized by domain boundaries (not one shared schema)
- Shared Kernel (purple): Common utilities used across modules
- Anti-corruption layer (dotted): OrderModule calls UserModule through adapter preventing direct coupling
- API Gateway Module (orange): Single entry point routing to appropriate module
- Module independence: Each module could become microservice with minimal changes
Design Rationale: Modular monolith provides microservices benefits (domain boundaries, team autonomy) without microservices complexity (network calls, distributed transactions). Separate database schemas enforce boundaries while single deployment unit simplifies operations.
Key Takeaway: Organize monoliths by domain modules with separate database schemas. Use anti-corruption layers for cross-module communication. This architecture enables incremental extraction to microservices when scaling demands it.
Why It Matters: Modular monoliths prevent “big ball of mud” while deferring microservices complexity. Component diagrams showing tangled dependencies (every module calling every other module directly) reveal when refactoring to clearer boundaries is needed before attempting service extraction. Implementing modular architecture with anti-corruption layers enables safe, incremental extraction to microservices when scaling demands it. This approach allows teams to scale critical paths independently while maintaining overall system stability and avoiding risky big-bang migrations.
Example 32: Event Sourcing Components
Event sourcing stores state changes as events rather than current state. This example shows event sourcing architecture at Component level.
graph TD
OrderService["Order Service Container"]
CommandHandler["[Component]<br/>Command Handler<br/>Processes commands"]
EventStore["[Component]<br/>Event Store<br/>Append-only event log"]
EventHandler["[Component]<br/>Event Handler<br/>Applies events"]
Aggregate["[Component]<br/>Order Aggregate<br/>Business logic"]
ReadModel["[Component]<br/>Read Model<br/>Current state projection"]
QueryHandler["[Component]<br/>Query Handler<br/>Handles queries"]
CommandHandler -->|"1. Load events"| EventStore
CommandHandler -->|"2. Rebuild state"| Aggregate
Aggregate -->|"3. Generate event"| CommandHandler
CommandHandler -->|"4. Append event"| EventStore
EventStore -->|"Event stream"| EventHandler
EventHandler -->|"5. Update projection"| ReadModel
QueryHandler -->|"Reads from"| ReadModel
style OrderService fill:#0173B2,stroke:#000,color:#fff
style CommandHandler fill:#DE8F05,stroke:#000,color:#fff
style EventStore fill:#029E73,stroke:#000,color:#fff
style EventHandler fill:#DE8F05,stroke:#000,color:#fff
style Aggregate fill:#CC78BC,stroke:#000,color:#fff
style ReadModel fill:#CA9161,stroke:#000,color:#fff
style QueryHandler fill:#DE8F05,stroke:#000,color:#fff
Key Elements:
- Event Store (teal): Immutable append-only log of all state changes
- Command Handler (orange): Processes write operations (CreateOrder, CancelOrder)
- Aggregate (purple): Rebuilds current state from events, enforces business rules
- Event Handler (orange): Consumes events to update read models
- Read Model (brown): Denormalized view optimized for queries
- Query Handler (orange): Handles read operations from read model
- Numbered flow: Shows event sourcing write path (1-5)
Design Rationale: Event sourcing provides complete audit trail and time travel capabilities. Separating write path (event store) from read path (read models) enables CQRS benefits. Events are source of truth; read models are derived projections.
Key Takeaway: Store events (state changes) rather than current state. Rebuild state by replaying events. Maintain separate read models for query performance. This enables temporal queries (“what was state at time T?”) and complete audit trails.
Why It Matters: Event sourcing is valuable for domains requiring audit trails and temporal queries. Component diagrams showing event flow help teams understand how complete history of state changes enables faster dispute resolution. Event replay capabilities allow teams to prove system state at any point in time, significantly reducing manual investigation efforts. This makes event sourcing particularly valuable in financial and compliance-heavy domains.
Example 33: Hexagonal Architecture (Ports and Adapters)
Hexagonal architecture isolates business logic from infrastructure concerns. This example shows ports and adapters pattern at Component level.
graph TD
ServiceCore["Service Container"]
RESTAdapter["[Component]<br/>REST Adapter<br/>HTTP interface"]
GraphQLAdapter["[Component]<br/>GraphQL Adapter<br/>GraphQL interface"]
MessageAdapter["[Component]<br/>Message Adapter<br/>Event consumer"]
InputPort["[Component]<br/>Input Port<br/>Use case interface"]
BusinessLogic["[Component]<br/>Business Logic<br/>Domain model and rules"]
OutputPort["[Component]<br/>Output Port<br/>Repository interface"]
PostgreSQLAdapter["[Component]<br/>PostgreSQL Adapter<br/>SQL implementation"]
MongoDBAdapter["[Component]<br/>MongoDB Adapter<br/>NoSQL implementation"]
EventPublisher["[Component]<br/>Event Publisher<br/>Kafka adapter"]
RESTAdapter -->|Calls| InputPort
GraphQLAdapter -->|Calls| InputPort
MessageAdapter -->|Calls| InputPort
InputPort -->|Invokes| BusinessLogic
BusinessLogic -->|Uses| OutputPort
OutputPort -->|Implemented by| PostgreSQLAdapter
OutputPort -->|Implemented by| MongoDBAdapter
OutputPort -->|Implemented by| EventPublisher
style ServiceCore fill:#0173B2,stroke:#000,color:#fff
style RESTAdapter fill:#029E73,stroke:#000,color:#fff
style GraphQLAdapter fill:#029E73,stroke:#000,color:#fff
style MessageAdapter fill:#029E73,stroke:#000,color:#fff
style InputPort fill:#DE8F05,stroke:#000,color:#fff
style BusinessLogic fill:#CC78BC,stroke:#000,color:#fff
style OutputPort fill:#DE8F05,stroke:#000,color:#fff
style PostgreSQLAdapter fill:#CA9161,stroke:#000,color:#fff
style MongoDBAdapter fill:#CA9161,stroke:#000,color:#fff
style EventPublisher fill:#CA9161,stroke:#000,color:#fff
Key Elements:
- Business Logic (purple): Domain model at center—independent of infrastructure
- Input Port (orange): Interface defining use cases (application boundary)
- Input Adapters (teal): REST, GraphQL, Message—multiple ways to invoke business logic
- Output Port (orange): Interface for infrastructure dependencies (repository, events)
- Output Adapters (brown): PostgreSQL, MongoDB, Kafka—pluggable implementations
- Dependency inversion: Business logic depends on abstractions (ports), not concrete implementations (adapters)
- Testability: Mock output adapters to test business logic in isolation
Design Rationale: Hexagonal architecture makes business logic the center, isolated from delivery mechanisms (HTTP, GraphQL, messaging) and infrastructure (databases, message queues). This enables technology changes without modifying business logic and comprehensive testing without infrastructure dependencies.
Key Takeaway: Define input ports (use cases) and output ports (infrastructure interfaces). Implement adapters for each technology. Business logic depends only on ports (abstractions), not adapters (implementations). This achieves true technology independence.
Why It Matters: Hexagonal architecture enables technology migration without rewriting business logic. Component diagrams showing ports and adapters help teams understand isolation boundaries. When business logic depends only on abstractions (ports), teams can add new adapters for different technologies while maintaining backward compatibility. This significantly reduces migration effort compared to traditional layered architectures where business logic is tightly coupled to specific delivery mechanisms.
Example 34: Microservice with Circuit Breaker
Circuit breakers prevent cascading failures in distributed systems. This example shows circuit breaker integration at Component level.
graph TD
ServiceA["Service A Container"]
APIController["[Component]<br/>API Controller<br/>HTTP endpoints"]
BusinessLogic["[Component]<br/>Business Logic<br/>Core logic"]
CircuitBreaker["[Component]<br/>Circuit Breaker<br/>Hystrix/Resilience4j"]
ServiceBClient["[Component]<br/>Service B Client<br/>HTTP client"]
FallbackHandler["[Component]<br/>Fallback Handler<br/>Degraded mode logic"]
Cache["[Component]<br/>Local Cache<br/>Fallback data"]
MetricsCollector["[Component]<br/>Metrics Collector<br/>Failure tracking"]
APIController -->|Calls| BusinessLogic
BusinessLogic -->|Calls| CircuitBreaker
CircuitBreaker -->|"Protected call"| ServiceBClient
CircuitBreaker -.->|"Circuit OPEN"| FallbackHandler
FallbackHandler -->|"Returns stale data"| Cache
CircuitBreaker -->|"Records metrics"| MetricsCollector
style ServiceA fill:#0173B2,stroke:#000,color:#fff
style APIController fill:#DE8F05,stroke:#000,color:#fff
style BusinessLogic fill:#029E73,stroke:#000,color:#fff
style CircuitBreaker fill:#CC78BC,stroke:#000,color:#fff
style ServiceBClient fill:#CA9161,stroke:#000,color:#fff
style FallbackHandler fill:#029E73,stroke:#000,color:#fff
style Cache fill:#CA9161,stroke:#000,color:#fff
style MetricsCollector fill:#CA9161,stroke:#000,color:#fff
Key Elements:
- Circuit Breaker (purple): Hystrix/Resilience4j wrapper around service calls
- Three states: CLOSED (normal), OPEN (failing), HALF-OPEN (testing recovery)
- Service B Client (brown): HTTP client for calling Service B
- Fallback Handler (teal): Executes when circuit is OPEN
- Local Cache (brown): Stores stale data for degraded mode
- Metrics Collector (brown): Tracks failure rates and circuit state
- Dotted line: Conditional fallback path when circuit opens
Design Rationale: Circuit breaker prevents cascading failures by failing fast when downstream service is unhealthy. After threshold failures, circuit opens and immediately returns fallback response without attempting call. This reduces load on failing service, giving it time to recover.
Key Takeaway: Wrap external service calls with circuit breakers. Implement fallback logic returning cached or degraded data when circuit opens. Monitor circuit state metrics to detect failures. This prevents cascade failures and maintains partial functionality.
Why It Matters: Circuit breakers contain failure blast radius in distributed systems. Component diagrams showing circuit breakers help teams understand how fallback mechanisms enable graceful degradation. When properly implemented, circuit breakers allow systems to maintain partial functionality using cached or degraded data instead of experiencing complete outages. Services without circuit breakers risk cascading failures where one service timeout propagates to all dependent services. Circuit breakers significantly reduce customer-facing impact by preventing failure amplification.
Example 35: Saga Pattern for Distributed Transactions
Sagas coordinate distributed transactions across microservices using compensating actions. This example shows saga orchestration at Component level.
graph TD
OrderService["Order Service Container"]
SagaOrchestrator["[Component]<br/>Saga Orchestrator<br/>Coordinates transaction"]
SagaLog["[Component]<br/>Saga Log<br/>Tracks saga state"]
OrderManager["[Component]<br/>Order Manager<br/>Creates order"]
PaymentClient["[Component]<br/>Payment Client<br/>Calls Payment Service"]
InventoryClient["[Component]<br/>Inventory Client<br/>Calls Inventory Service"]
ShippingClient["[Component]<br/>Shipping Client<br/>Calls Shipping Service"]
CompensationHandler["[Component]<br/>Compensation Handler<br/>Rollback logic"]
SagaOrchestrator -->|"1. Create order"| OrderManager
SagaOrchestrator -->|"2. Reserve payment"| PaymentClient
SagaOrchestrator -->|"3. Reserve inventory"| InventoryClient
SagaOrchestrator -->|"4. Schedule shipping"| ShippingClient
SagaOrchestrator -->|"Log each step"| SagaLog
SagaOrchestrator -.->|"On failure"| CompensationHandler
CompensationHandler -.->|"Cancel order"| OrderManager
CompensationHandler -.->|"Refund payment"| PaymentClient
CompensationHandler -.->|"Release inventory"| InventoryClient
style OrderService fill:#0173B2,stroke:#000,color:#fff
style SagaOrchestrator fill:#DE8F05,stroke:#000,color:#fff
style SagaLog fill:#029E73,stroke:#000,color:#fff
style OrderManager fill:#CC78BC,stroke:#000,color:#fff
style PaymentClient fill:#CA9161,stroke:#000,color:#fff
style InventoryClient fill:#CA9161,stroke:#000,color:#fff
style ShippingClient fill:#CA9161,stroke:#000,color:#fff
style CompensationHandler fill:#DE8F05,stroke:#000,color:#fff
Key Elements:
- Saga Orchestrator (orange): Coordinates multi-step transaction across services
- Saga Log (teal): Persists saga state for crash recovery
- Service clients (brown): Payment, Inventory, Shipping integrations
- Numbered sequence: 1-4 shows happy path execution order
- Compensation Handler (orange): Executes rollback compensating actions
- Dotted lines: Compensation path when any step fails
- Forward recovery: Each step logged before execution enables retry
Design Rationale: Distributed transactions require coordination without 2-phase commit (which doesn’t scale). Saga pattern breaks transaction into local transactions with compensating actions for rollback. Orchestrator-based saga (vs choreography) centralizes coordination logic.
Key Takeaway: Implement sagas for multi-service transactions. Log each step before execution. Define compensating actions for rollback. On failure, execute compensations in reverse order. This achieves eventual consistency without distributed locking.
Why It Matters: Sagas enable complex business processes across microservices while maintaining data consistency. Component diagrams showing saga orchestration help teams understand how multi-step transactions achieve high success rates despite individual service failures. Without sagas, partial failures leave systems in inconsistent states where some operations complete while others fail. Compensation logic ensures systems can roll back completed operations when later steps fail, significantly reducing inconsistent state occurrences. This makes sagas essential for distributed transaction management.
Example 36: API Gateway with Rate Limiting
Rate limiting prevents abuse and ensures fair resource allocation. This example shows rate limiting implementation at Component level.
graph TD
Gateway["API Gateway Container"]
RequestRouter["[Component]<br/>Request Router<br/>Route to backend"]
RateLimiter["[Component]<br/>Rate Limiter<br/>Token bucket algorithm"]
RateLimitStore["[Component]<br/>Rate Limit Store<br/>Redis - token counts"]
AuthMiddleware["[Component]<br/>Auth Middleware<br/>Extract user/API key"]
QuotaManager["[Component]<br/>Quota Manager<br/>Per-user limits"]
ResponseCache["[Component]<br/>Response Cache<br/>Cached responses"]
BackendPool["[Component]<br/>Backend Pool<br/>Upstream services"]
RequestRouter -->|"1. Authenticate"| AuthMiddleware
AuthMiddleware -->|"2. Check quota"| QuotaManager
QuotaManager -->|"3. Get user limits"| RateLimitStore
QuotaManager -->|"4. Apply rate limit"| RateLimiter
RateLimiter -.->|"Rate limit exceeded"| RequestRouter
RateLimiter -->|"5. Check cache"| ResponseCache
ResponseCache -->|"6. Cache miss"| BackendPool
style Gateway fill:#0173B2,stroke:#000,color:#fff
style RequestRouter fill:#DE8F05,stroke:#000,color:#fff
style RateLimiter fill:#029E73,stroke:#000,color:#fff
style RateLimitStore fill:#CA9161,stroke:#000,color:#fff
style AuthMiddleware fill:#CC78BC,stroke:#000,color:#fff
style QuotaManager fill:#029E73,stroke:#000,color:#fff
style ResponseCache fill:#CA9161,stroke:#000,color:#fff
style BackendPool fill:#CA9161,stroke:#000,color:#fff
Key Elements:
- Rate Limiter (teal): Implements token bucket algorithm for rate limiting
- Rate Limit Store (brown): Redis stores per-user token counts
- Auth Middleware (purple): Extracts user identity for quota lookup
- Quota Manager (teal): Enforces per-user rate limits (free vs paid tiers)
- Response Cache (brown): Reduces backend load for repeated requests
- Numbered flow: 1-6 shows request processing sequence
- Dotted line: Rate limit exceeded returns 429 Too Many Requests
Design Rationale: Rate limiting at gateway protects backend services from overload and ensures fair resource allocation among users. Redis-backed counters enable distributed rate limiting across gateway instances. Multi-tier limits (per-second, per-minute, per-day) prevent both burst attacks and sustained abuse.
Key Takeaway: Implement rate limiting at API gateway layer. Use distributed store (Redis) for counters. Enforce per-user quotas with different limits for free vs paid tiers. Return 429 status with Retry-After header when limit exceeded.
Why It Matters: Rate limiting prevents denial-of-service scenarios and enables tiered business models. Component diagrams showing quota enforcement help teams understand how per-user limits protect infrastructure from abuse. Rate limiting allows systems to differentiate service tiers (free vs paid) with different request quotas, creating monetization opportunities while maintaining system stability. Without rate limiting, single users can exhaust resources affecting all users. Proper rate limiting implementation eliminates outages caused by API abuse and resource exhaustion.
Example 37: Cache Layers and Invalidation
Multi-level caching improves performance but requires invalidation strategy. This example shows cache hierarchy at Component level.
graph TD
Service["Application Service Container"]
APIHandler["[Component]<br/>API Handler<br/>HTTP endpoints"]
L1Cache["[Component]<br/>L1 Cache<br/>In-memory cache"]
L2Cache["[Component]<br/>L2 Cache<br/>Redis cache"]
CacheInvalidator["[Component]<br/>Cache Invalidator<br/>Invalidation logic"]
EventListener["[Component]<br/>Event Listener<br/>Listens for updates"]
BusinessLogic["[Component]<br/>Business Logic<br/>Core service logic"]
Repository["[Component]<br/>Repository<br/>Database access"]
APIHandler -->|"1. Check L1"| L1Cache
L1Cache -.->|"L1 miss"| APIHandler
APIHandler -->|"2. Check L2"| L2Cache
L2Cache -.->|"L2 miss"| APIHandler
APIHandler -->|"3. Fetch from DB"| BusinessLogic
BusinessLogic -->|"Query"| Repository
Repository -->|"Data"| BusinessLogic
BusinessLogic -->|"4. Populate L2"| L2Cache
BusinessLogic -->|"5. Populate L1"| L1Cache
EventListener -->|"Data changed event"| CacheInvalidator
CacheInvalidator -->|"Invalidate L1"| L1Cache
CacheInvalidator -->|"Invalidate L2"| L2Cache
style Service fill:#0173B2,stroke:#000,color:#fff
style APIHandler fill:#DE8F05,stroke:#000,color:#fff
style L1Cache fill:#029E73,stroke:#000,color:#fff
style L2Cache fill:#CA9161,stroke:#000,color:#fff
style CacheInvalidator fill:#CC78BC,stroke:#000,color:#fff
style EventListener fill:#DE8F05,stroke:#000,color:#fff
style BusinessLogic fill:#029E73,stroke:#000,color:#fff
style Repository fill:#CA9161,stroke:#000,color:#fff
Key Elements:
- L1 Cache (teal): In-process memory cache (fastest, smallest capacity)
- L2 Cache (brown): Redis distributed cache (slower than L1, larger capacity)
- Cache hierarchy: Check L1 → L2 → Database in order
- Cache invalidation: Event-driven invalidation when data changes
- Event Listener (orange): Subscribes to data change events
- Cache Invalidator (purple): Removes stale entries from both cache levels
- Numbered flow: Shows cache-aside pattern with multi-level lookup
Design Rationale: Multi-level caching balances speed, capacity, and consistency. L1 cache eliminates network calls, L2 cache eliminates database queries. Event-driven invalidation maintains consistency by clearing caches when data changes.
Key Takeaway: Implement cache hierarchy (in-memory → distributed → database). Check each level in order, populate higher levels on miss. Use event-driven invalidation to remove stale data. This optimizes both read performance and consistency.
Why It Matters: Cache invalidation is notoriously difficult but critical for correctness. Component diagrams showing invalidation paths help teams understand how data changes must propagate through all cache levels. When invalidation events don’t reach all cache levels, stale data persists causing consistency bugs where updates aren’t reflected in the user interface. Implementing comprehensive multi-level invalidation (clearing both in-memory and distributed caches on data changes) dramatically reduces stale data percentages, fixing bugs where recent changes don’t appear to users. Proper invalidation strategy is essential for maintaining data consistency in cached systems.
Example 38: Multi-Tenancy Component Organization
Multi-tenant systems require tenant isolation at component level. This example shows tenant-aware component organization.
graph TD
MultiTenantApp["Multi-Tenant Application Container"]
TenantResolver["[Component]<br/>Tenant Resolver<br/>Extracts tenant ID"]
TenantContext["[Component]<br/>Tenant Context<br/>Thread-local storage"]
APIController["[Component]<br/>API Controller<br/>HTTP endpoints"]
TenantValidator["[Component]<br/>Tenant Validator<br/>Validates tenant access"]
BusinessLogic["[Component]<br/>Business Logic<br/>Core logic"]
TenantAwareRepository["[Component]<br/>Tenant-Aware Repository<br/>Filters by tenant ID"]
ConnectionPool["[Component]<br/>Connection Pool<br/>Database connections"]
TenantResolver -->|"Sets tenant ID"| TenantContext
APIController -->|"Reads tenant ID"| TenantContext
APIController -->|"Validates access"| TenantValidator
APIController -->|"Calls"| BusinessLogic
BusinessLogic -->|"Reads tenant ID"| TenantContext
BusinessLogic -->|"Calls"| TenantAwareRepository
TenantAwareRepository -->|"Appends WHERE tenant_id"| ConnectionPool
style MultiTenantApp fill:#0173B2,stroke:#000,color:#fff
style TenantResolver fill:#DE8F05,stroke:#000,color:#fff
style TenantContext fill:#CC78BC,stroke:#000,color:#fff
style APIController fill:#029E73,stroke:#000,color:#fff
style TenantValidator fill:#DE8F05,stroke:#000,color:#fff
style BusinessLogic fill:#029E73,stroke:#000,color:#fff
style TenantAwareRepository fill:#CA9161,stroke:#000,color:#fff
style ConnectionPool fill:#CA9161,stroke:#000,color:#fff
Key Elements:
- Tenant Resolver (orange): Extracts tenant ID from request (header, subdomain, JWT claim)
- Tenant Context (purple): Thread-local storage propagating tenant ID through call stack
- Tenant Validator (orange): Ensures user has access to requested tenant
- Tenant-Aware Repository (brown): Automatically appends tenant ID filter to all queries
- Shared database: All tenants share tables with tenant_id column (vs database-per-tenant)
- Automatic isolation: Repository prevents cross-tenant data leaks
Design Rationale: Tenant context propagation ensures tenant ID flows through entire request without explicit parameter passing. Tenant-aware repository acts as safety net—even if business logic forgets to filter by tenant, repository enforces isolation. This prevents catastrophic cross-tenant data leaks.
Key Takeaway: Extract tenant ID at entry point, store in thread-local context. Implement tenant-aware repository automatically filtering all queries by tenant ID. Validate tenant access early. This prevents cross-tenant data leaks and simplifies business logic.
Why It Matters: Multi-tenancy bugs cause catastrophic data breaches. Component diagrams showing tenant isolation help teams understand how automatic filtering prevents cross-tenant data leaks. When business logic queries databases without tenant filters, users can access other tenants’ data. Implementing tenant-aware repositories that automatically apply tenant ID filters to all queries creates an architectural safeguard—even when developers forget to filter explicitly, the repository layer enforces isolation. This pattern prevents entire classes of security vulnerabilities and data breach scenarios in multi-tenant systems.
Deployment Diagrams (Examples 39-45)
Example 39: Simple Cloud Deployment (AWS)
Deployment diagrams show how containers map to infrastructure. This example demonstrates basic AWS deployment with EC2, RDS, and S3.
graph TD
Internet["Internet"]
CloudFront["[AWS CloudFront]<br/>CDN<br/>Edge caching"]
ALB["[AWS ALB]<br/>Application Load Balancer<br/>Traffic distribution"]
EC2_1["[AWS EC2 Instance 1]<br/>t3.large<br/>US-East-1a"]
EC2_2["[AWS EC2 Instance 2]<br/>t3.large<br/>US-East-1b"]
WebApp1["[Container: Web App]<br/>React build"]
WebApp2["[Container: Web App]<br/>React build"]
APIServer1["[Container: API Server]<br/>Node.js:14"]
APIServer2["[Container: API Server]<br/>Node.js:14"]
RDS["[AWS RDS]<br/>PostgreSQL 14<br/>db.t3.medium"]
RDSReplica["[AWS RDS Replica]<br/>PostgreSQL 14<br/>db.t3.medium"]
S3["[AWS S3]<br/>File storage"]
Internet -->|HTTPS| CloudFront
CloudFront -->|Static assets| S3
Internet -->|HTTPS| ALB
ALB -->|Routes traffic| EC2_1
ALB -->|Routes traffic| EC2_2
EC2_1 -->|Hosts| WebApp1
EC2_1 -->|Hosts| APIServer1
EC2_2 -->|Hosts| WebApp2
EC2_2 -->|Hosts| APIServer2
APIServer1 -->|Writes| RDS
APIServer1 -->|Reads| RDSReplica
APIServer2 -->|Writes| RDS
APIServer2 -->|Reads| RDSReplica
RDS -.->|Replication| RDSReplica
style Internet fill:#CC78BC,stroke:#000,color:#fff
style CloudFront fill:#DE8F05,stroke:#000,color:#fff
style ALB fill:#DE8F05,stroke:#000,color:#fff
style EC2_1 fill:#029E73,stroke:#000,color:#fff
style EC2_2 fill:#029E73,stroke:#000,color:#fff
style WebApp1 fill:#0173B2,stroke:#000,color:#fff
style WebApp2 fill:#0173B2,stroke:#000,color:#fff
style APIServer1 fill:#0173B2,stroke:#000,color:#fff
style APIServer2 fill:#0173B2,stroke:#000,color:#fff
style RDS fill:#CA9161,stroke:#000,color:#fff
style RDSReplica fill:#CA9161,stroke:#000,color:#fff
style S3 fill:#CA9161,stroke:#000,color:#fff
Key Elements:
- CloudFront (orange): CDN serving static assets from S3
- Application Load Balancer (orange): Distributes traffic across EC2 instances
- EC2 instances (teal): Two instances across availability zones (1a, 1b)
- Container deployment: WebApp and APIServer containers on same EC2 instances
- RDS Primary/Replica (brown): PostgreSQL with read replica for scaling
- S3 storage (brown): Blob storage for uploaded files
- High availability: Multi-AZ deployment (instances in different zones)
- Instance types: t3.large for compute, db.t3.medium for database
Design Rationale: Multi-AZ deployment ensures high availability—if one availability zone fails, other zone continues serving traffic. Read replica scales read-heavy workloads. CloudFront reduces origin load and improves latency for global users.
Key Takeaway: Deploy containers across multiple availability zones for high availability. Use managed services (RDS, S3, CloudFront) to reduce operational overhead. Specify instance types to communicate capacity planning.
Why It Matters: Deployment topology directly impacts availability and cost trade-offs. Component diagrams showing single availability zone deployments help teams recognize infrastructure failure risks. When all instances run in one zone, zone failures cause complete outages. Multi-AZ deployment introduces modest cost increases but significantly improves availability by distributing instances across independent failure domains. This architecture reduces revenue loss from outages and improves overall system resilience. The availability-cost trade-off makes multi-AZ deployment essential for production systems.
Example 40: Kubernetes Deployment
Kubernetes provides container orchestration. This example shows Kubernetes deployment architecture.
graph TD
Users["Users"]
Ingress["[K8s Ingress]<br/>NGINX Ingress Controller<br/>L7 routing"]
ServiceMesh["[K8s Service Mesh]<br/>Istio<br/>Traffic management"]
FrontendService["[K8s Service]<br/>Frontend Service<br/>ClusterIP"]
FrontendPods["[K8s Pods]<br/>Frontend Pods x3<br/>2 CPU, 4GB RAM"]
APIService["[K8s Service]<br/>API Service<br/>ClusterIP"]
APIPods["[K8s Pods]<br/>API Pods x5<br/>4 CPU, 8GB RAM"]
WorkerService["[K8s Service]<br/>Worker Service<br/>ClusterIP"]
WorkerPods["[K8s Pods]<br/>Worker Pods x2<br/>8 CPU, 16GB RAM"]
PVC["[K8s PVC]<br/>Persistent Volume<br/>100GB SSD"]
ExternalDB["[External]<br/>Managed PostgreSQL<br/>Outside cluster"]
Users -->|HTTPS| Ingress
Ingress -->|Routes| ServiceMesh
ServiceMesh -->|Frontend traffic| FrontendService
ServiceMesh -->|API traffic| APIService
FrontendService -->|Load balances| FrontendPods
APIService -->|Load balances| APIPods
WorkerService -->|Load balances| WorkerPods
APIPods -->|Reads/Writes| ExternalDB
WorkerPods -->|Reads/Writes| PVC
WorkerPods -->|Queries| ExternalDB
style Users fill:#CC78BC,stroke:#000,color:#fff
style Ingress fill:#DE8F05,stroke:#000,color:#fff
style ServiceMesh fill:#DE8F05,stroke:#000,color:#fff
style FrontendService fill:#029E73,stroke:#000,color:#fff
style FrontendPods fill:#0173B2,stroke:#000,color:#fff
style APIService fill:#029E73,stroke:#000,color:#fff
style APIPods fill:#0173B2,stroke:#000,color:#fff
style WorkerService fill:#029E73,stroke:#000,color:#fff
style WorkerPods fill:#0173B2,stroke:#000,color:#fff
style PVC fill:#CA9161,stroke:#000,color:#fff
style ExternalDB fill:#CA9161,stroke:#000,color:#fff
Key Elements:
- Ingress Controller (orange): NGINX handles external traffic routing
- Service Mesh (orange): Istio provides traffic management, observability, security
- K8s Services (teal): ClusterIP services for internal load balancing
- Pods (blue): Running containers with resource specifications (CPU/RAM)
- Replica counts: Frontend x3, API x5, Worker x2
- Persistent Volume Claim (brown): Shared storage for workers
- External Database (brown): Managed PostgreSQL outside Kubernetes
Design Rationale: Kubernetes abstracts infrastructure—services provide stable endpoints, pods are ephemeral and auto-healing. Ingress handles external routing, service mesh handles internal routing. Managed database outside cluster simplifies operations and backups.
Key Takeaway: Deploy containers as Kubernetes pods with replica counts. Use services for load balancing. Specify resource requests (CPU/RAM). Use ingress for external traffic, service mesh for internal. Keep stateful services (databases) outside cluster.
Why It Matters: Kubernetes deployment architecture affects both reliability and infrastructure costs. Deployment diagrams showing pod replica counts help teams right-size infrastructure by revealing actual capacity needs. Over-provisioning wastes resources and increases costs, while under-provisioning causes availability issues. Proper replica sizing based on actual load patterns reduces unnecessary infrastructure spending while improving system availability. This balance between cost efficiency and reliability makes capacity planning critical in Kubernetes environments.
Example 41: Blue-Green Deployment
Blue-green deployment enables zero-downtime releases. This example shows blue-green infrastructure pattern.
graph TD
Users["Users"]
Router["[Router/Load Balancer]<br/>Route53 or ALB<br/>Traffic switcher"]
BlueEnv["Blue Environment<br/>Version 1.0 (Current)"]
BlueInstances["[EC2 Instances]<br/>3x t3.large<br/>Version 1.0"]
BlueDB["[Database]<br/>RDS Primary<br/>Shared"]
GreenEnv["Green Environment<br/>Version 2.0 (Staging)"]
GreenInstances["[EC2 Instances]<br/>3x t3.large<br/>Version 2.0"]
Users -->|"100% traffic"| Router
Router -->|"Active"| BlueEnv
Router -.->|"0% traffic (idle)"| GreenEnv
BlueEnv -->|Contains| BlueInstances
BlueInstances -->|Connects| BlueDB
GreenEnv -->|Contains| GreenInstances
GreenInstances -->|Connects| BlueDB
style Users fill:#CC78BC,stroke:#000,color:#fff
style Router fill:#DE8F05,stroke:#000,color:#fff
style BlueEnv fill:#0173B2,stroke:#000,color:#fff
style BlueInstances fill:#029E73,stroke:#000,color:#fff
style BlueDB fill:#CA9161,stroke:#000,color:#fff
style GreenEnv fill:#029E73,stroke:#000,color:#fff
style GreenInstances fill:#029E73,stroke:#000,color:#fff
Deployment Process:
- Blue environment serves 100% production traffic (Version 1.0)
- Green environment deployed with new version (Version 2.0) using same database
- Test green environment with smoke tests and validation
- Switch router from blue to green (instantly routes 100% traffic to Version 2.0)
- Monitor green for errors—if found, switch back to blue (instant rollback)
- Decommission blue after green proven stable
Key Elements:
- Two identical environments: Blue (current), Green (new version)
- Shared database: Both environments connect to same database (requires backward-compatible schema changes)
- Router switching: Single change redirects all traffic (Route53 DNS or ALB target group)
- Instant rollback: Switch back to blue if issues detected
- Zero downtime: No service interruption during switch
Design Rationale: Blue-green deployment eliminates deployment downtime and enables instant rollback. Running both environments simultaneously costs 2x infrastructure during deployment but reduces risk. Shared database requires careful migration strategy (backward-compatible changes only).
Key Takeaway: Maintain two production-like environments. Deploy new version to idle environment. Switch traffic atomically. Keep old environment for instant rollback. This achieves zero-downtime deployments with low risk.
Why It Matters: Deployment downtime and risky rollbacks impact both revenue and user trust. Deployment diagrams showing blue-green architectures help teams understand how dual environments enable frequent deployments with minimal risk. Instant rollback capabilities (switching traffic back to the previous environment) dramatically reduce mean time to recovery from deployment issues. While infrastructure costs increase temporarily during deployment windows, this overhead is minimal compared to the business value of increased deployment velocity and reduced outage risk. Blue-green deployments make continuous delivery practical and safe.
Example 42: Canary Deployment
Canary deployment gradually shifts traffic to new version. This example shows canary release pattern.
graph TD
Users["Users"]
TrafficSplitter["[Traffic Splitter]<br/>Istio/NGINX<br/>Weighted routing"]
StableVersion["[Stable Version]<br/>Version 1.0<br/>Pods x10"]
CanaryVersion["[Canary Version]<br/>Version 2.0<br/>Pods x1"]
MetricsCollector["[Metrics Collector]<br/>Prometheus<br/>Error rates, latency"]
AutomatedPromotion["[Promotion Controller]<br/>Flagger/Argo Rollouts<br/>Traffic shift automation"]
Users -->|100% traffic| TrafficSplitter
TrafficSplitter -->|"90% traffic"| StableVersion
TrafficSplitter -->|"10% traffic"| CanaryVersion
StableVersion -->|Metrics| MetricsCollector
CanaryVersion -->|Metrics| MetricsCollector
MetricsCollector -->|"Analysis"| AutomatedPromotion
AutomatedPromotion -->|"Adjust weights"| TrafficSplitter
style Users fill:#CC78BC,stroke:#000,color:#fff
style TrafficSplitter fill:#DE8F05,stroke:#000,color:#fff
style StableVersion fill:#0173B2,stroke:#000,color:#fff
style CanaryVersion fill:#029E73,stroke:#000,color:#fff
style MetricsCollector fill:#CA9161,stroke:#000,color:#fff
style AutomatedPromotion fill:#DE8F05,stroke:#000,color:#fff
Canary Progression:
- Initial state: Stable version handles 100% traffic
- Deploy canary: New version deployed with 1 pod (10% capacity)
- 10% traffic: Route 10% traffic to canary, 90% to stable
- Monitor metrics: Compare error rates, latency between stable and canary
- Automated promotion: If canary healthy, increase to 25%, then 50%, then 100%
- Rollback: If canary unhealthy (elevated errors), route 100% back to stable
- Complete: Canary becomes new stable, old version decommissioned
Key Elements:
- Weighted traffic splitting: 90% stable, 10% canary (adjustable)
- Metrics-driven: Prometheus tracks error rates and latency
- Automated promotion: Flagger/Argo Rollouts automate traffic shift based on metrics
- Gradual rollout: 10% → 25% → 50% → 100% over 30-60 minutes
- Limited blast radius: Only 10% users affected if canary has issues
Design Rationale: Canary deployment detects production issues with minimal user impact. Automated promotion based on metrics (not manual decision) enables continuous delivery. Gradual rollout increases confidence before full deployment.
Key Takeaway: Deploy new version alongside stable version. Route small percentage of traffic to canary (10%). Monitor metrics and automate promotion if healthy, rollback if unhealthy. Gradually increase traffic percentage until full rollout.
Why It Matters: Canary deployments catch production-only bugs before they affect entire user bases. Deployment diagrams showing gradual rollout percentages help teams understand how incremental traffic routing limits blast radius. When bugs surface in canary environments, only a small percentage of users experience issues before automatic rollback occurs. Canary deployments with automated monitoring and rollback dramatically reduce the impact of production bugs compared to full deployments. This progressive delivery approach makes continuous deployment safer at scale.
Example 43: Multi-Region Active-Active Deployment
Active-active deployment across regions provides disaster recovery and low latency. This example shows global multi-region architecture.
graph TD
GlobalUsers["Global Users"]
Route53["[AWS Route53]<br/>GeoDNS<br/>Latency-based routing"]
USEast["US-East Region"]
USEastALB["[ALB]<br/>US-East"]
USEastApp["[App Instances]<br/>3x EC2"]
USEastDB["[Aurora Primary]<br/>US-East"]
EUWest["EU-West Region"]
EUWestALB["[ALB]<br/>EU-West"]
EUWestApp["[App Instances]<br/>3x EC2"]
EUWestDB["[Aurora Primary]<br/>EU-West"]
APSoutheast["AP-Southeast Region"]
APSoutheastALB["[ALB]<br/>AP-Southeast"]
APSoutheastApp["[App Instances]<br/>3x EC2"]
APSoutheastDB["[Aurora Primary]<br/>AP-Southeast"]
GlobalCache["[CloudFront + ElastiCache Global Datastore]<br/>Distributed cache"]
GlobalUsers -->|GeoDNS routing| Route53
Route53 -->|"US traffic"| USEast
Route53 -->|"EU traffic"| EUWest
Route53 -->|"Asia traffic"| APSoutheast
USEast -->|Contains| USEastALB
USEastALB -->|Routes| USEastApp
USEastApp -->|Reads/Writes| USEastDB
EUWest -->|Contains| EUWestALB
EUWestALB -->|Routes| EUWestApp
EUWestApp -->|Reads/Writes| EUWestDB
APSoutheast -->|Contains| APSoutheastALB
APSoutheastALB -->|Routes| APSoutheastApp
APSoutheastApp -->|Reads/Writes| APSoutheastDB
USEastDB -.->|"Global database replication"| EUWestDB
EUWestDB -.->|"Global database replication"| APSoutheastDB
APSoutheastDB -.->|"Global database replication"| USEastDB
USEastApp -->|Cache| GlobalCache
EUWestApp -->|Cache| GlobalCache
APSoutheastApp -->|Cache| GlobalCache
style GlobalUsers fill:#CC78BC,stroke:#000,color:#fff
style Route53 fill:#DE8F05,stroke:#000,color:#fff
style USEast fill:#0173B2,stroke:#000,color:#fff
style EUWest fill:#0173B2,stroke:#000,color:#fff
style APSoutheast fill:#0173B2,stroke:#000,color:#fff
style USEastALB fill:#029E73,stroke:#000,color:#fff
style EUWestALB fill:#029E73,stroke:#000,color:#fff
style APSoutheastALB fill:#029E73,stroke:#000,color:#fff
style USEastApp fill:#029E73,stroke:#000,color:#fff
style EUWestApp fill:#029E73,stroke:#000,color:#fff
style APSoutheastApp fill:#029E73,stroke:#000,color:#fff
style USEastDB fill:#CA9161,stroke:#000,color:#fff
style EUWestDB fill:#CA9161,stroke:#000,color:#fff
style APSoutheastDB fill:#CA9161,stroke:#000,color:#fff
style GlobalCache fill:#DE8F05,stroke:#000,color:#fff
Key Elements:
- Three regions: US-East, EU-West, AP-Southeast (all active)
- GeoDNS routing: Route53 directs users to nearest region based on latency
- Regional databases: Each region has Aurora primary database
- Global database replication: Bi-directional replication across regions (eventually consistent)
- Global cache: CloudFront + ElastiCache Global Datastore for shared cache
- Identical stacks: Each region runs complete application stack
- Disaster recovery: If one region fails, traffic routes to healthy regions
Design Rationale: Active-active multi-region provides lowest latency (users route to nearest region) and highest availability (survive entire region failure). Global database replication enables eventual consistency. Cache layer reduces cross-region database queries.
Key Takeaway: Deploy identical application stacks in multiple regions. Use GeoDNS to route users to nearest region. Implement global database replication for data availability. Use distributed cache to minimize cross-region latency.
Why It Matters: Multi-region active-active architecture provides both performance improvements and disaster recovery capabilities. Deployment diagrams showing geographic distribution help teams understand how regional redundancy maintains service availability during infrastructure failures. When one region experiences outages, traffic automatically routes to healthy regions, preventing complete service disruption. Multi-region deployments introduce infrastructure cost multipliers but deliver significantly higher availability levels. The trade-off between infrastructure cost and availability makes multi-region deployment essential for services requiring high uptime guarantees and global performance.
Example 44: Serverless Deployment (AWS Lambda)
Serverless architectures eliminate server management. This example shows AWS Lambda-based deployment.
graph TD
Users["Users"]
CloudFront["[CloudFront]<br/>CDN + S3 Origin<br/>Static content"]
APIGateway["[API Gateway]<br/>HTTP API<br/>Lambda proxy"]
AuthFunction["[Lambda Function]<br/>Auth Handler<br/>Node.js 18<br/>512MB RAM"]
UserFunction["[Lambda Function]<br/>User Handler<br/>Python 3.11<br/>1GB RAM"]
OrderFunction["[Lambda Function]<br/>Order Handler<br/>Go 1.21<br/>2GB RAM"]
EventBridge["[EventBridge]<br/>Event bus<br/>Async processing"]
EmailFunction["[Lambda Function]<br/>Email Worker<br/>Node.js 18<br/>256MB RAM"]
DynamoDB["[DynamoDB]<br/>NoSQL database<br/>On-demand capacity"]
S3["[S3]<br/>File storage"]
Users -->|Static content| CloudFront
Users -->|API requests| APIGateway
APIGateway -->|POST /auth| AuthFunction
APIGateway -->|GET /users| UserFunction
APIGateway -->|POST /orders| OrderFunction
AuthFunction -->|Reads/Writes| DynamoDB
UserFunction -->|Reads/Writes| DynamoDB
OrderFunction -->|Reads/Writes| DynamoDB
OrderFunction -->|Publishes events| EventBridge
EventBridge -->|order.created| EmailFunction
EmailFunction -->|Reads templates| S3
style Users fill:#CC78BC,stroke:#000,color:#fff
style CloudFront fill:#DE8F05,stroke:#000,color:#fff
style APIGateway fill:#DE8F05,stroke:#000,color:#fff
style AuthFunction fill:#0173B2,stroke:#000,color:#fff
style UserFunction fill:#0173B2,stroke:#000,color:#fff
style OrderFunction fill:#0173B2,stroke:#000,color:#fff
style EventBridge fill:#029E73,stroke:#000,color:#fff
style EmailFunction fill:#0173B2,stroke:#000,color:#fff
style DynamoDB fill:#CA9161,stroke:#000,color:#fff
style S3 fill:#CA9161,stroke:#000,color:#fff
Key Elements:
- Lambda Functions (blue): Containerized business logic (Auth, User, Order, Email)
- API Gateway (orange): HTTP endpoints invoking Lambda functions
- EventBridge (teal): Event bus for async Lambda invocations
- DynamoDB (brown): NoSQL database with on-demand capacity
- S3 (brown): Static content and file storage
- Resource specifications: Runtime (Node.js, Python, Go), Memory (256MB-2GB)
- Auto-scaling: Lambda scales automatically, no instance management
Design Rationale: Serverless eliminates infrastructure management—no servers to provision, patch, or scale. Pay per request (not per hour) reduces costs for variable workloads. Event-driven architecture (EventBridge → Lambda) enables async processing.
Key Takeaway: Deploy business logic as Lambda functions. Use API Gateway for HTTP endpoints. Use EventBridge for event-driven invocations. Specify runtime and memory for each function. Leverage auto-scaling—no capacity planning needed.
Why It Matters: Serverless architecture fundamentally changes cost structures from fixed to variable. Deployment diagrams showing serverless functions help teams understand how automatic scaling handles traffic spikes without manual provisioning. Serverless costs scale with actual usage rather than provisioned capacity, dramatically reducing costs for workloads with variable traffic patterns. Reserved infrastructure remains idle during low-traffic periods, wasting resources. Serverless eliminates this waste by charging only for actual execution time, reducing costs for bursty workloads while improving spike handling through automatic scaling.
Example 45: Hybrid Cloud Deployment
Hybrid cloud deploys across on-premises and cloud infrastructure. This example shows hybrid architecture.
graph TD
OnPremises["On-Premises Data Center"]
OnPremApp["[VM Cluster]<br/>Legacy Application<br/>VMware vSphere"]
OnPremDB["[Database]<br/>Oracle RAC<br/>Customer data"]
VPN["[VPN Gateway]<br/>Site-to-Site VPN<br/>Encrypted tunnel"]
AWSCloud["AWS Cloud"]
AWSVPC["[VPC]<br/>Private network"]
APIGateway["[EC2 Instances]<br/>API Gateway<br/>Integration layer"]
NewApps["[ECS Cluster]<br/>New Microservices<br/>Docker containers"]
AuroraDB["[Aurora]<br/>PostgreSQL<br/>New data"]
S3["[S3]<br/>Data lake<br/>Analytics"]
OnPremises -->|Contains| OnPremApp
OnPremises -->|Contains| OnPremDB
OnPremApp -->|Reads/Writes| OnPremDB
OnPremises <-->|Site-to-Site VPN| VPN
VPN <-->|Encrypted| AWSCloud
AWSCloud -->|Contains| AWSVPC
AWSVPC -->|Contains| APIGateway
AWSVPC -->|Contains| NewApps
APIGateway -->|Calls via VPN| OnPremApp
APIGateway <-->|Reads/Writes| AuroraDB
NewApps -->|Calls| APIGateway
OnPremDB -.->|"Daily sync"| S3
AuroraDB -.->|"Replication"| S3
style OnPremises fill:#CA9161,stroke:#000,color:#fff
style OnPremApp fill:#029E73,stroke:#000,color:#fff
style OnPremDB fill:#DE8F05,stroke:#000,color:#fff
style VPN fill:#CC78BC,stroke:#000,color:#fff
style AWSCloud fill:#0173B2,stroke:#000,color:#fff
style AWSVPC fill:#0173B2,stroke:#000,color:#fff
style APIGateway fill:#029E73,stroke:#000,color:#fff
style NewApps fill:#029E73,stroke:#000,color:#fff
style AuroraDB fill:#DE8F05,stroke:#000,color:#fff
style S3 fill:#DE8F05,stroke:#000,color:#fff
Key Elements:
- On-premises (brown): Legacy application on VMware, Oracle database
- AWS Cloud (blue): New microservices on ECS, Aurora database
- VPN Gateway (purple): Site-to-site VPN connecting on-premises to AWS
- API Gateway (teal): Integration layer mediating between old and new systems
- Data replication: On-premises DB syncs to S3 data lake daily
- Hybrid workload: New apps in cloud call legacy systems via VPN
Design Rationale: Hybrid cloud enables gradual migration from on-premises to cloud. API Gateway abstracts deployment location—new microservices don’t care whether they call on-premises or cloud services. VPN provides secure connectivity. Data lake (S3) aggregates data from both environments for analytics.
Key Takeaway: Connect on-premises and cloud via VPN. Deploy integration layer (API Gateway) mediating between environments. Migrate workloads incrementally. Aggregate data in cloud data lake. This enables cloud benefits while preserving on-premises investments.
Why It Matters: Hybrid cloud architecture reduces migration risk while enabling incremental cloud adoption. Deployment diagrams showing hybrid topologies help teams understand how to migrate workloads incrementally while maintaining legacy system integration. New workloads deployed to cloud benefit from reduced infrastructure costs and improved scalability, while core systems remain on-premises to satisfy regulatory or technical constraints. Integration layers enable cloud and on-premises systems to communicate efficiently. Gradual migration approaches reduce risk and maintain business continuity compared to “big bang” migrations that introduce significant operational risk.
Dynamic Diagrams - Sequence Flows (Examples 46-50)
Example 46: User Authentication Flow (OAuth2)
Dynamic diagrams show temporal sequences. This example demonstrates OAuth2 authentication flow.
sequenceDiagram
participant User
participant WebApp
participant AuthServer
participant ResourceAPI
User->>WebApp: 1. Click "Login"
WebApp->>AuthServer: 2. Redirect to /authorize<br/>(client_id, redirect_uri, scope)
AuthServer->>User: 3. Show login form
User->>AuthServer: 4. Submit credentials
AuthServer->>AuthServer: 5. Validate credentials
AuthServer->>WebApp: 6. Redirect with code<br/>(redirect_uri?code=abc123)
WebApp->>AuthServer: 7. POST /token<br/>(code, client_id, client_secret)
AuthServer->>AuthServer: 8. Validate code
AuthServer->>WebApp: 9. Return access_token + refresh_token
WebApp->>ResourceAPI: 10. GET /api/user<br/>Authorization: Bearer <access_token>
ResourceAPI->>ResourceAPI: 11. Validate token
ResourceAPI->>WebApp: 12. Return user data
WebApp->>User: 13. Show dashboard
Key Steps:
1-2. Initiate flow: User clicks login, WebApp redirects to AuthServer 3-4. User authentication: AuthServer shows form, user submits credentials 5. Credential validation: AuthServer verifies username/password 6. Authorization code: AuthServer redirects back with temporary code 7-8. Token exchange: WebApp exchanges code for tokens (proves it’s legitimate client) 9. Tokens issued: AuthServer returns access_token (for API calls) and refresh_token (for renewal) 10-11. API call: WebApp calls Resource API with access_token in header 12-13. Data returned: User sees dashboard with their data
Design Rationale: OAuth2 authorization code flow prevents access_token exposure to browser. WebApp receives code (not token) via redirect, then exchanges code for token server-to-server (using client_secret). This protects tokens from XSS attacks.
Key Takeaway: Use sequence diagrams to show authentication flows. Number steps sequentially. Show redirects, token exchanges, and API calls. OAuth2 authorization code flow is most secure for web apps.
Why It Matters: Authentication flow design directly impacts account security. Sequence diagrams showing OAuth2 flows help teams understand the security implications of different token exchange patterns. Sending access tokens via browser redirects exposes them to browser history and cross-site scripting attacks. Server-side code exchange (authorization code flow) keeps access tokens away from browsers, significantly improving security. Proper authentication flow selection dramatically reduces account compromise rates by eliminating token exposure vectors.
Example 47: Microservice Saga Execution Flow
Sagas coordinate distributed transactions. This example shows saga execution with compensation.
sequenceDiagram
participant Client
participant OrderService
participant PaymentService
participant InventoryService
participant ShippingService
Client->>OrderService: 1. POST /orders
OrderService->>OrderService: 2. Create order (status: pending)
OrderService->>PaymentService: 3. POST /payments/reserve
PaymentService->>PaymentService: 4. Reserve \$100
PaymentService->>OrderService: 5. Payment reserved (payment_id: 123)
OrderService->>InventoryService: 6. POST /inventory/reserve
InventoryService->>InventoryService: 7. Check stock (OUT OF STOCK)
InventoryService->>OrderService: 8. Error: Item unavailable
Note over OrderService: Saga failure - begin compensation
OrderService->>PaymentService: 9. POST /payments/123/cancel
PaymentService->>PaymentService: 10. Release \$100 reservation
PaymentService->>OrderService: 11. Payment cancelled
OrderService->>OrderService: 12. Update order (status: failed)
OrderService->>Client: 13. Error: Order failed - out of stock
Key Steps:
1-2. Order creation: Client creates order, OrderService persists with “pending” status 3-5. Payment reservation: OrderService reserves payment, receives payment_id for later capture 6-8. Inventory check: OrderService attempts inventory reservation, fails (out of stock) 9-11. Compensation: OrderService cancels payment reservation (rollback) 12-13. Saga failure: Order marked failed, error returned to client
Design Rationale: Saga pattern breaks distributed transaction into local transactions with compensating actions. When any step fails, saga executes compensations in reverse order to rollback. This achieves eventual consistency without distributed locking.
Key Takeaway: Show saga happy path and compensation path in sequence diagrams. Number steps to show execution order. Highlight compensation logic when failures occur. Sagas enable distributed transactions across microservices.
Why It Matters: Saga compensation logic is critical for preventing inconsistent distributed state. Sequence diagrams showing compensation paths help teams understand how failures at intermediate steps require rollback operations. Without proper compensation, partial saga failures leave systems in inconsistent states where some operations completed while others failed. Adding compensation actions for each saga step dramatically reduces inconsistent state occurrences by ensuring failed transactions roll back all completed operations. This makes saga compensation essential for distributed transaction integrity.
Example 48: Event-Driven Message Flow
Event-driven systems use async messaging. This example shows pub/sub message flow with multiple consumers.
sequenceDiagram
participant OrderService
participant EventBus as Event Bus (Kafka)
participant InventoryService
participant EmailService
participant AnalyticsService
OrderService->>OrderService: 1. Create order (order_id: 456)
OrderService->>EventBus: 2. Publish event<br/>Topic: orders<br/>Event: order.created<br/>Payload: {order_id: 456, user_id: 789, total: \$50}
EventBus->>InventoryService: 3. Deliver event (offset: 12345)
EventBus->>EmailService: 4. Deliver event (offset: 12345)
EventBus->>AnalyticsService: 5. Deliver event (offset: 12345)
InventoryService->>InventoryService: 6. Decrement stock<br/>(order_id: 456)
InventoryService->>EventBus: 7. Commit offset 12345
EmailService->>EmailService: 8. Send confirmation email<br/>(user_id: 789)
EmailService->>EventBus: 9. Commit offset 12345
AnalyticsService->>AnalyticsService: 10. Update metrics<br/>(total: \$50)
AnalyticsService->>EventBus: 11. Commit offset 12345
Note over EventBus: All consumers processed event successfully
Key Steps:
1-2. Event publication: OrderService creates order, publishes event to Kafka topic 3-5. Fan-out delivery: Event bus delivers same event to three consumers (parallel) 6-7. Inventory processing: InventoryService decrements stock, commits offset (marks event processed) 8-9. Email processing: EmailService sends confirmation, commits offset 10-11. Analytics processing: AnalyticsService updates metrics, commits offset
Design Rationale: Event-driven architecture decouples services—OrderService doesn’t know about consumers. Adding new consumer (FraudService) doesn’t require OrderService changes. Kafka retains events enabling replay and new consumers to catch up.
Key Takeaway: Show event publication and parallel consumption in sequence diagrams. Include offset commits to show guaranteed processing. Event-driven architecture enables loose coupling and independent scaling.
Why It Matters: Event-driven architecture prevents tight coupling and enables system extensibility. Sequence diagrams showing event publication and consumption help teams understand how new consumers can be added without modifying producers. Event-driven systems allow new functionality to subscribe to existing event streams, replaying historical events to build initial state before processing real-time events. This enables zero-downtime feature deployment without changing existing services. Event-driven patterns make systems significantly more extensible than direct service-to-service coupling.
Example 49: Database Transaction with Rollback
Database transactions ensure ACID properties. This example shows transaction lifecycle with rollback scenario.
sequenceDiagram
participant Client
participant AppServer
participant DatabasePool
participant Transaction
participant Database
Client->>AppServer: 1. POST /transfer<br/>{from: A, to: B, amount: \$100}
AppServer->>DatabasePool: 2. Get connection
DatabasePool->>AppServer: 3. Connection acquired
AppServer->>Transaction: 4. BEGIN TRANSACTION
Transaction->>Database: 5. Start transaction<br/>(isolation: READ COMMITTED)
AppServer->>Transaction: 6. SELECT balance FROM accounts WHERE id=A FOR UPDATE
Transaction->>Database: 7. Lock row A, read balance (\$150)
Database->>AppServer: 8. balance: \$150
AppServer->>Transaction: 9. UPDATE accounts SET balance=50 WHERE id=A
Transaction->>Database: 10. Write to transaction log (uncommitted)
AppServer->>Transaction: 11. SELECT balance FROM accounts WHERE id=B FOR UPDATE
Transaction->>Database: 12. Attempt lock row B
Database->>AppServer: 13. TIMEOUT - row locked by another transaction
Note over AppServer: Deadlock detected - rollback
AppServer->>Transaction: 14. ROLLBACK
Transaction->>Database: 15. Discard transaction log<br/>Release locks
AppServer->>DatabasePool: 16. Release connection
AppServer->>Client: 17. Error: Transfer failed - try again
Key Steps:
1-3. Connection management: Client requests transfer, AppServer gets database connection 4-5. Transaction start: BEGIN TRANSACTION starts transaction with READ COMMITTED isolation 6-8. Row locking: SELECT FOR UPDATE locks account A row, reads balance ($150) 9-10. Tentative update: UPDATE modifies balance to $50 (uncommitted, in transaction log) 11-13. Deadlock: Attempt to lock account B times out (another transaction holds lock) 14-15. Rollback: ROLLBACK discards transaction log, releases locks, restores balance to $150 16-17. Error handling: Connection returned to pool, error returned to client
Design Rationale: SELECT FOR UPDATE pessimistically locks rows preventing concurrent modifications. Transaction log enables rollback—uncommitted changes exist only in log, not database. Deadlock detection prevents infinite waits.
Key Takeaway: Show transaction lifecycle (BEGIN, operations, COMMIT/ROLLBACK) in sequence diagrams. Include locking (SELECT FOR UPDATE), rollback scenarios, and connection pool management. Transactions ensure atomicity.
Why It Matters: Transaction boundary design directly impacts data correctness and prevents race conditions. Sequence diagrams showing transaction flows help teams understand where locking mechanisms are required. Without proper locking, concurrent transactions can read stale data, leading to lost updates and data inconsistencies. Implementing row-level locks (SELECT FOR UPDATE) prevents race conditions by ensuring only one transaction modifies a row at a time. Proper transaction design with appropriate locking eliminates entire classes of concurrency bugs in financial and other critical systems.
Example 50: API Rate Limiting with Backoff
Rate limiting requires retry logic. This example shows rate limit enforcement with exponential backoff.
sequenceDiagram
participant Client
participant APIGateway
participant RateLimiter
participant BackendAPI
Client->>APIGateway: 1. GET /api/data (request 1)
APIGateway->>RateLimiter: 2. Check rate limit<br/>(user: alice, limit: 10/min)
RateLimiter->>RateLimiter: 3. Increment counter (1/10)
RateLimiter->>APIGateway: 4. OK - within limit
APIGateway->>BackendAPI: 5. Forward request
BackendAPI->>Client: 6. 200 OK + data
Note over Client: ... 9 more requests (total 10/10)
Client->>APIGateway: 11. GET /api/data (request 11)
APIGateway->>RateLimiter: 12. Check rate limit
RateLimiter->>RateLimiter: 13. Counter at 10/10 (limit exceeded)
RateLimiter->>APIGateway: 14. REJECT
APIGateway->>Client: 15. 429 Too Many Requests<br/>Retry-After: 45 seconds
Client->>Client: 16. Wait 2^1 = 2 seconds (exponential backoff attempt 1)
Client->>APIGateway: 17. GET /api/data (retry 1)
APIGateway->>RateLimiter: 18. Check rate limit (still 10/10)
RateLimiter->>APIGateway: 19. REJECT
APIGateway->>Client: 20. 429 Too Many Requests<br/>Retry-After: 43 seconds
Client->>Client: 21. Wait 2^2 = 4 seconds (exponential backoff attempt 2)
Client->>APIGateway: 22. GET /api/data (retry 2)
Note over RateLimiter: Window elapsed (60 seconds), counter reset to 0/10
APIGateway->>RateLimiter: 23. Check rate limit (0/10)
RateLimiter->>RateLimiter: 24. Increment counter (1/10)
RateLimiter->>APIGateway: 25. OK - within limit
APIGateway->>BackendAPI: 26. Forward request
BackendAPI->>Client: 27. 200 OK + data
Key Steps:
1-6. Normal request: Within rate limit (1/10), request forwarded to backend 11-15. Rate limit exceeded: 11th request rejected with 429 status and Retry-After header 16-20. First retry: Client waits 2 seconds (2^1), retries, still rate limited 21-27. Successful retry: After 4 more seconds (2^2), rate limit window elapsed, request succeeds
Design Rationale: Exponential backoff prevents thundering herd—instead of all clients retrying immediately, exponential delays spread retries over time. Retry-After header communicates exact time to wait (better than guessing).
Key Takeaway: Show rate limit enforcement, 429 responses with Retry-After, and client exponential backoff in sequence diagrams. Exponential backoff: wait 2^attempt seconds between retries.
Why It Matters: Exponential backoff is critical for preventing retry storms in distributed systems. Sequence diagrams showing retry patterns help teams understand how linear backoff can cause cascading failures. When clients retry too quickly, they generate additional load before rate limit windows reset, amplifying the problem. Exponential backoff with jitter spreads retries over time, allowing systems to recover while preventing retry amplification. This pattern dramatically reduces rate limit error rates and prevents retry storms from overwhelming API gateways and backend services.
Advanced Integration Patterns (Examples 51-55)
Example 51: Backend for Frontend (BFF) Pattern
BFF pattern creates specialized backends for different frontend clients. This example shows mobile and web BFFs.
graph TD
MobileApp["[Mobile App]<br/>iOS/Android<br/>Native clients"]
WebApp["[Web App]<br/>React SPA<br/>Browser client"]
MobileBFF["[Mobile BFF]<br/>GraphQL API<br/>Mobile-optimized"]
WebBFF["[Web BFF]<br/>REST API<br/>Web-optimized"]
UserService["[User Service]<br/>User management"]
ProductService["[Product Service]<br/>Product catalog"]
OrderService["[Order Service]<br/>Order processing"]
RecommendationService["[Recommendation Service]<br/>ML-based recommendations"]
MobileApp -->|"GraphQL queries"| MobileBFF
WebApp -->|"REST calls"| WebBFF
MobileBFF -->|"Aggregates"| UserService
MobileBFF -->|"Aggregates"| ProductService
MobileBFF -->|"Aggregates"| OrderService
MobileBFF -.->|"Omits (save bandwidth)"| RecommendationService
WebBFF -->|"Aggregates"| UserService
WebBFF -->|"Aggregates"| ProductService
WebBFF -->|"Aggregates"| OrderService
WebBFF -->|"Includes (more screen space)"| RecommendationService
style MobileApp fill:#CC78BC,stroke:#000,color:#fff
style WebApp fill:#CC78BC,stroke:#000,color:#fff
style MobileBFF fill:#0173B2,stroke:#000,color:#fff
style WebBFF fill:#0173B2,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
style RecommendationService fill:#CA9161,stroke:#000,color:#fff
Key Elements:
- Two BFFs: MobileBFF (GraphQL) and WebBFF (REST)—separate backends for different frontends
- Mobile optimizations: GraphQL enables clients to request exact data needed (reduce bandwidth)
- Web optimizations: REST with richer responses (browsers have more bandwidth)
- Selective aggregation: MobileBFF omits recommendations (save 3G bandwidth), WebBFF includes them
- Service aggregation: Both BFFs call multiple backend services, aggregate responses
- Frontend-specific logic: BFFs contain logic specific to mobile vs web UX
Design Rationale: BFF pattern prevents “one-size-fits-all” API that serves all clients poorly. Mobile clients need minimal bandwidth (3G/4G), web clients can handle richer data. BFFs optimize for each client’s constraints and capabilities.
Key Takeaway: Create separate Backend-for-Frontend APIs for mobile, web, desktop. Optimize each BFF for its client (data format, payload size, features). Aggregate multiple backend services in BFF layer. This enables client-specific optimizations.
Why It Matters: Backend-for-Frontend pattern enables client-specific optimizations that dramatically improve performance. Component diagrams showing BFF layers help teams understand how different clients have different data requirements. Mobile clients benefit from reduced payload sizes and optimized data formats, while desktop clients can receive full-resolution assets. Generic APIs force all clients to receive the same data, causing mobile clients to download unnecessary large payloads. BFF pattern allows each client type to receive optimally sized data, significantly improving mobile performance on bandwidth-constrained networks.
Example 52: Strangler Fig Pattern for Legacy Migration
Strangler fig pattern gradually replaces legacy systems. This example shows incremental migration strategy.
graph TD
Users["Users"]
Proxy["[Routing Proxy]<br/>NGINX/Envoy<br/>Route by feature"]
LegacyMonolith["[Legacy Monolith]<br/>Rails application<br/>All features (80%)"]
NewUserService["[New User Service]<br/>User management<br/>Microservice (10%)"]
NewOrderService["[New Order Service]<br/>Order processing<br/>Microservice (10%)"]
LegacyDB["[Legacy Database]<br/>PostgreSQL<br/>Shared (read-only for new services)"]
NewUserDB["[New User Database]<br/>PostgreSQL<br/>User service owned"]
NewOrderDB["[New Order Database]<br/>PostgreSQL<br/>Order service owned"]
Users -->|All requests| Proxy
Proxy -->|"/users/* → new service (10%)"| NewUserService
Proxy -->|"/orders/* → new service (10%)"| NewOrderService
Proxy -->|"All other routes (80%)"| LegacyMonolith
LegacyMonolith -->|"Reads/Writes"| LegacyDB
NewUserService -->|"Writes"| NewUserDB
NewUserService -.->|"Reads (during migration)"| LegacyDB
NewOrderService -->|"Writes"| NewOrderDB
NewOrderService -.->|"Reads (during migration)"| LegacyDB
LegacyDB -.->|"Data sync"| NewUserDB
LegacyDB -.->|"Data sync"| NewOrderDB
style Users fill:#CC78BC,stroke:#000,color:#fff
style Proxy fill:#DE8F05,stroke:#000,color:#fff
style LegacyMonolith fill:#CA9161,stroke:#000,color:#fff
style NewUserService fill:#0173B2,stroke:#000,color:#fff
style NewOrderService fill:#0173B2,stroke:#000,color:#fff
style LegacyDB fill:#029E73,stroke:#000,color:#fff
style NewUserDB fill:#029E73,stroke:#000,color:#fff
style NewOrderDB fill:#029E73,stroke:#000,color:#fff
Migration Phases:
- Phase 1 (Current): Proxy routes 10% traffic (users, orders) to new microservices, 80% to legacy monolith
- Phase 2: Extract more domains (products, billing) incrementally, route 40% to microservices, 60% to legacy
- Phase 3: Continue until 90%+ traffic routed to microservices, legacy handles 10%
- Phase 4: Decommission legacy monolith completely
Key Elements:
- Routing Proxy (orange): Routes traffic based on URL path to new services or legacy
- Legacy Monolith (brown): Existing system gradually being replaced
- New Microservices (blue): User and Order services extracted so far (20% of features)
- Dual-write pattern: New services write to their databases, read from legacy (during transition)
- Data synchronization: Legacy DB syncs data to new databases incrementally
- Progressive migration: 10% → 40% → 90% → 100% microservices over 2-3 years
Design Rationale: Strangler fig pattern avoids risky “big bang” rewrites. Incremental extraction reduces risk—if new service fails, route traffic back to legacy. Dual-write and data sync maintain consistency during migration.
Key Takeaway: Route traffic through proxy that directs requests to new services or legacy based on feature. Extract features incrementally (10% at a time). Maintain dual-write or data sync during transition. Decommission legacy only after 90%+ migrated.
Why It Matters: Strangler fig pattern dramatically reduces migration risk compared to big bang rewrites. Complete system rewrites often fail to deliver, causing organizations to lose competitive position during multi-year rewrite efforts. Component diagrams showing incremental extraction help teams understand how to migrate functionality piece by piece while maintaining existing services. Strangler fig enables organizations to migrate systems gradually while shipping features continuously, achieving zero-downtime migrations over extended periods. This incremental approach eliminates the catastrophic “rewrite failed” scenarios common with big bang migrations.
Example 53: Service Mesh (Istio) Architecture
Service mesh provides observability, security, and reliability for microservices. This example shows Istio service mesh.
graph TD
Users["Users"]
IngressGateway["[Istio Ingress Gateway]<br/>Entry point<br/>TLS termination"]
ServiceA["Service A Pod"]
ServiceAApp["[Container]<br/>Service A<br/>Business logic"]
ServiceAProxy["[Envoy Sidecar]<br/>Service A Proxy<br/>Traffic management"]
ServiceB["Service B Pod"]
ServiceBApp["[Container]<br/>Service B<br/>Business logic"]
ServiceBProxy["[Envoy Sidecar]<br/>Service B Proxy<br/>Traffic management"]
ControlPlane["[Istio Control Plane]<br/>Pilot + Citadel<br/>Config + Certificates"]
Telemetry["[Observability]<br/>Prometheus + Grafana<br/>Metrics + Tracing"]
Users -->|HTTPS| IngressGateway
IngressGateway -->|Routes| ServiceAProxy
ServiceA -->|Contains| ServiceAApp
ServiceA -->|Contains| ServiceAProxy
ServiceAProxy <-->|Intercepts traffic| ServiceAApp
ServiceAProxy -->|mTLS| ServiceBProxy
ServiceB -->|Contains| ServiceBApp
ServiceB -->|Contains| ServiceBProxy
ServiceBProxy <-->|Intercepts traffic| ServiceBApp
ControlPlane -.->|"Config + Certs"| ServiceAProxy
ControlPlane -.->|"Config + Certs"| ServiceBProxy
ServiceAProxy -.->|Metrics| Telemetry
ServiceBProxy -.->|Metrics| Telemetry
style Users fill:#CC78BC,stroke:#000,color:#fff
style IngressGateway fill:#DE8F05,stroke:#000,color:#fff
style ServiceA fill:#029E73,stroke:#000,color:#fff
style ServiceAApp fill:#0173B2,stroke:#000,color:#fff
style ServiceAProxy fill:#CC78BC,stroke:#000,color:#fff
style ServiceB fill:#029E73,stroke:#000,color:#fff
style ServiceBApp fill:#0173B2,stroke:#000,color:#fff
style ServiceBProxy fill:#CC78BC,stroke:#000,color:#fff
style ControlPlane fill:#DE8F05,stroke:#000,color:#fff
style Telemetry fill:#CA9161,stroke:#000,color:#fff
Key Elements:
- Envoy sidecar proxies (purple): Injected into every pod, intercept all traffic
- Service containers (blue): Business logic unaware of service mesh
- Mutual TLS (mTLS): Service A → Service B traffic encrypted and authenticated automatically
- Control Plane (orange): Pilot distributes routing config, Citadel manages certificates
- Telemetry (brown): Prometheus collects metrics from sidecars, Grafana visualizes
- Traffic interception: All service-to-service calls go through sidecars (transparent to app)
Design Rationale: Service mesh moves cross-cutting concerns (observability, security, reliability) from application code to infrastructure layer. Sidecar pattern enables mesh features without application changes. mTLS provides zero-trust security automatically.
Key Takeaway: Deploy service mesh with sidecar proxies intercepting all service traffic. Enable mTLS for service-to-service encryption. Use control plane for config distribution. Collect metrics from sidecars for observability. This provides security and observability without application code changes.
Why It Matters: Service mesh architecture solves cross-cutting concerns at scale without requiring application code changes. Component diagrams showing sidecar proxies help teams understand how mesh infrastructure can implement security and observability uniformly across all services. Implementing mutual TLS across hundreds of microservices through application code changes requires substantial engineering effort. Service mesh provides automatic mTLS via sidecar proxies, dramatically reducing implementation time while preventing unencrypted service-to-service communication vulnerabilities. Centralized observability through mesh sidecars significantly improves mean time to detect issues by providing uniform telemetry across all services.
Example 54: Event Sourcing with CQRS
Event Sourcing + CQRS provides audit trails and optimized reads. This example shows complete CQRS architecture.
graph TD
Users["Users"]
CommandAPI["[Command API]<br/>Write operations<br/>POST/PUT/DELETE"]
QueryAPI["[Query API]<br/>Read operations<br/>GET"]
CommandHandler["[Command Handler]<br/>Validates commands"]
EventStore["[Event Store]<br/>Immutable event log<br/>EventStoreDB"]
EventBus["[Event Bus]<br/>Kafka<br/>Event distribution"]
Projector1["[Projector 1]<br/>Order summary view"]
Projector2["[Projector 2]<br/>User dashboard view"]
ReadDB1["[Read Database 1]<br/>PostgreSQL<br/>Order summaries"]
ReadDB2["[Read Database 2]<br/>MongoDB<br/>User dashboards"]
Users -->|"Commands"| CommandAPI
Users -->|"Queries"| QueryAPI
CommandAPI -->|Validate| CommandHandler
CommandHandler -->|Append events| EventStore
EventStore -->|Publish events| EventBus
EventBus -->|order.* events| Projector1
EventBus -->|user.* events| Projector2
Projector1 -->|Updates| ReadDB1
Projector2 -->|Updates| ReadDB2
QueryAPI -->|Reads| ReadDB1
QueryAPI -->|Reads| ReadDB2
style Users fill:#CC78BC,stroke:#000,color:#fff
style CommandAPI fill:#0173B2,stroke:#000,color:#fff
style QueryAPI fill:#0173B2,stroke:#000,color:#fff
style CommandHandler fill:#029E73,stroke:#000,color:#fff
style EventStore fill:#DE8F05,stroke:#000,color:#fff
style EventBus fill:#029E73,stroke:#000,color:#fff
style Projector1 fill:#CA9161,stroke:#000,color:#fff
style Projector2 fill:#CA9161,stroke:#000,color:#fff
style ReadDB1 fill:#CA9161,stroke:#000,color:#fff
style ReadDB2 fill:#CA9161,stroke:#000,color:#fff
Key Elements:
- Separate APIs: CommandAPI for writes, QueryAPI for reads (complete CQRS separation)
- Event Store (orange): Single source of truth storing all events immutably
- Event Bus (teal): Distributes events to projectors
- Projectors (brown): Consume events, update read models (denormalized views)
- Multiple Read DBs: PostgreSQL for relational views, MongoDB for document views
- Optimized reads: Each read model optimized for specific query pattern
- Eventual consistency: Writes commit to event store immediately, read models update asynchronously
Design Rationale: Event Sourcing provides complete audit trail and time-travel queries. CQRS optimizes write path (event store) and read path (denormalized views) independently. Multiple read models enable query-specific optimizations.
Key Takeaway: Separate write operations (commands → event store) from read operations (queries → read models). Use projectors to build read models from events. Optimize each read model for its query pattern. Accept eventual consistency between writes and reads.
Why It Matters: Event Sourcing combined with CQRS enables independent optimization of write and read paths for complex domains. Component diagrams showing separate paths help teams understand how to optimize write operations for low latency while optimizing read operations for complex query patterns. Without separation, complex reporting queries competing for the same database resources degrade write operation latency. CQRS allows writes to use event stores optimized for append-only operations while reads use materialized views optimized for specific query patterns. This architectural separation achieves both low-latency writes and efficient complex queries simultaneously.
Example 55: GraphQL Federation
GraphQL federation unifies multiple GraphQL services. This example shows federated GraphQL architecture.
graph TD
Clients["Clients"]
ApolloGateway["[Apollo Gateway]<br/>GraphQL Federation<br/>Query orchestration"]
UserSubgraph["[User Subgraph]<br/>GraphQL Service<br/>User domain"]
ProductSubgraph["[Product Subgraph]<br/>GraphQL Service<br/>Product domain"]
OrderSubgraph["[Order Subgraph]<br/>GraphQL Service<br/>Order domain"]
UserDB["[User Database]<br/>PostgreSQL"]
ProductDB["[Product Database]<br/>MongoDB"]
OrderDB["[Order Database]<br/>PostgreSQL"]
Clients -->|"Unified GraphQL query"| ApolloGateway
ApolloGateway -->|"Fetches User fields"| UserSubgraph
ApolloGateway -->|"Fetches Product fields"| ProductSubgraph
ApolloGateway -->|"Fetches Order fields"| OrderSubgraph
UserSubgraph -->|Queries| UserDB
ProductSubgraph -->|Queries| ProductDB
OrderSubgraph -->|Queries| OrderDB
ApolloGateway -.->|"Stitches responses"| Clients
style Clients fill:#CC78BC,stroke:#000,color:#fff
style ApolloGateway fill:#DE8F05,stroke:#000,color:#fff
style UserSubgraph fill:#0173B2,stroke:#000,color:#fff
style ProductSubgraph fill:#0173B2,stroke:#000,color:#fff
style OrderSubgraph fill:#0173B2,stroke:#000,color:#fff
style UserDB fill:#CA9161,stroke:#000,color:#fff
style ProductDB fill:#CA9161,stroke:#000,color:#fff
style OrderDB fill:#CA9161,stroke:#000,color:#fff
Example Federated Query:
# Client sends single query to Gateway
query {
user(id: "123") {
# Gateway routes to User Subgraph
name
email
orders {
# Gateway routes to Order Subgraph (references User)
id
total
items {
# Gateway routes to Product Subgraph (references Order)
product {
name
price
}
}
}
}
}
# Gateway orchestrates:
# 1. Call UserSubgraph for user(id: "123") → returns user data
# 2. Call OrderSubgraph with user reference → returns orders
# 3. Call ProductSubgraph with order.items references → returns products
# 4. Stitch all responses into single JSON responseKey Elements:
- Apollo Gateway (orange): Federation gateway stitching subgraph responses
- Subgraphs (blue): User, Product, Order—each owns portion of GraphQL schema
- Unified schema: Clients query one schema, gateway splits query across subgraphs
- Entity references: Subgraphs reference entities from other subgraphs (User in Order)
- Automatic stitching: Gateway combines responses into single JSON
- Independent deployment: Each subgraph deployed independently
Design Rationale: GraphQL federation enables microservices architecture while maintaining single GraphQL schema for clients. Each team owns their subgraph schema and implementation. Gateway provides unified API hiding microservices complexity.
Key Takeaway: Use GraphQL federation to unify multiple GraphQL services. Each service owns its subgraph schema. Gateway orchestrates queries across subgraphs and stitches responses. Clients see unified schema hiding distributed architecture.
Why It Matters: GraphQL federation dramatically reduces client-side complexity in distributed systems. Component diagrams showing gateway orchestration help teams understand how unified schemas simplify client implementations. Without federation, clients must manage connections to dozens of individual GraphQL services, each requiring separate configuration, error handling, and retry logic. Federation allows clients to interact with a single gateway endpoint that orchestrates queries across multiple backend services. This significantly reduces client application size and complexity while simplifying mobile development by consolidating all backend communication through one unified interface.
Multi-Container Architectures (Examples 56-60)
Example 56: Microservices with Service Discovery
Service discovery enables dynamic service location. This example shows Consul-based service discovery.
graph TD
APIGateway["[API Gateway]<br/>Entry point"]
ServiceRegistry["[Service Registry]<br/>Consul<br/>Service discovery"]
ServiceA1["[Service A Instance 1]<br/>10.0.1.10:8080"]
ServiceA2["[Service A Instance 2]<br/>10.0.1.11:8080"]
ServiceA3["[Service A Instance 3]<br/>10.0.1.12:8080"]
ServiceB1["[Service B Instance 1]<br/>10.0.2.10:8081"]
ServiceB2["[Service B Instance 2]<br/>10.0.2.11:8081"]
HealthCheck["[Health Check]<br/>Periodic polling"]
APIGateway -->|"1. Query: Where is Service A?"| ServiceRegistry
ServiceRegistry -->|"2. Return healthy instances"| APIGateway
APIGateway -->|"3. Load balance across instances"| ServiceA1
APIGateway -->|"3. Load balance across instances"| ServiceA2
APIGateway -->|"3. Load balance across instances"| ServiceA3
ServiceA1 -->|"4. Register on startup"| ServiceRegistry
ServiceA2 -->|"4. Register on startup"| ServiceRegistry
ServiceA3 -->|"4. Register on startup"| ServiceRegistry
ServiceB1 -->|"4. Register on startup"| ServiceRegistry
ServiceB2 -->|"4. Register on startup"| ServiceRegistry
HealthCheck -.->|"Periodic checks"| ServiceA1
HealthCheck -.->|"Periodic checks"| ServiceA2
HealthCheck -.->|"Periodic checks"| ServiceA3
ServiceA3 -.->|"Unhealthy"| HealthCheck
HealthCheck -.->|"Deregister Service A3"| ServiceRegistry
style APIGateway fill:#DE8F05,stroke:#000,color:#fff
style ServiceRegistry fill:#CC78BC,stroke:#000,color:#fff
style ServiceA1 fill:#0173B2,stroke:#000,color:#fff
style ServiceA2 fill:#0173B2,stroke:#000,color:#fff
style ServiceA3 fill:#CA9161,stroke:#000,color:#fff
style ServiceB1 fill:#029E73,stroke:#000,color:#fff
style ServiceB2 fill:#029E73,stroke:#000,color:#fff
style HealthCheck fill:#DE8F05,stroke:#000,color:#fff
Key Elements:
- Service Registry (purple): Consul maintains map of service names to instance IPs
- Dynamic registration: Services register themselves on startup (IP + port)
- Health checks: Periodic polling detects unhealthy instances (Service A3 failing)
- Automatic deregistration: Unhealthy instances removed from registry
- Client-side discovery: API Gateway queries registry, gets healthy instance list
- Load balancing: API Gateway distributes requests across healthy instances
Design Rationale: Service discovery enables dynamic scaling and fault tolerance. New instances auto-register, failed instances auto-deregister. Clients always query healthy instances without manual configuration.
Key Takeaway: Use service registry (Consul, Eureka, etcd) for dynamic service discovery. Services register on startup, health checks detect failures. Clients query registry for current healthy instances. This enables auto-scaling and self-healing.
Why It Matters: Service discovery is essential for elastic scaling in dynamic cloud environments. Component diagrams showing service registry patterns help teams understand how automatic registration eliminates manual configuration overhead. When services scale elastically based on demand, manually managing service instance configurations becomes impractical. Service discovery allows instances to self-register on startup and deregister on shutdown, eliminating configuration deployment overhead. Health checks automatically detect and remove unhealthy instances, enabling rapid automatic failover. This makes elastic scaling practical while maintaining service reliability.
Example 57: Distributed Tracing Architecture
Distributed tracing tracks requests across microservices. This example shows Jaeger tracing architecture.
graph TD
User["User"]
Frontend["[Frontend]<br/>React SPA"]
Gateway["[API Gateway]<br/>Trace ID: abc123"]
ServiceA["[Service A]<br/>Trace ID: abc123<br/>Span ID: span-1"]
ServiceB["[Service B]<br/>Trace ID: abc123<br/>Span ID: span-2"]
ServiceC["[Service C]<br/>Trace ID: abc123<br/>Span ID: span-3"]
JaegerAgent["[Jaeger Agent]<br/>Sidecar collector"]
JaegerCollector["[Jaeger Collector]<br/>Aggregates spans"]
TracingDB["[Tracing Database]<br/>Elasticsearch<br/>Span storage"]
JaegerUI["[Jaeger UI]<br/>Trace visualization"]
User -->|"1. Request"| Frontend
Frontend -->|"2. Generate Trace ID: abc123"| Gateway
Gateway -->|"3. Call with Trace ID"| ServiceA
ServiceA -->|"4. Call with Trace ID"| ServiceB
ServiceB -->|"5. Call with Trace ID"| ServiceC
ServiceA -.->|"Span 1: Gateway→ServiceA (150ms)"| JaegerAgent
ServiceB -.->|"Span 2: ServiceA→ServiceB (200ms)"| JaegerAgent
ServiceC -.->|"Span 3: ServiceB→ServiceC (50ms)"| JaegerAgent
JaegerAgent -.->|Batched spans| JaegerCollector
JaegerCollector -.->|Stores| TracingDB
JaegerUI -.->|Queries| TracingDB
style User fill:#CC78BC,stroke:#000,color:#fff
style Frontend fill:#0173B2,stroke:#000,color:#fff
style Gateway fill:#DE8F05,stroke:#000,color:#fff
style ServiceA fill:#029E73,stroke:#000,color:#fff
style ServiceB fill:#029E73,stroke:#000,color:#fff
style ServiceC fill:#029E73,stroke:#000,color:#fff
style JaegerAgent fill:#CA9161,stroke:#000,color:#fff
style JaegerCollector fill:#CA9161,stroke:#000,color:#fff
style TracingDB fill:#CA9161,stroke:#000,color:#fff
style JaegerUI fill:#DE8F05,stroke:#000,color:#fff
Trace Visualization:
Trace ID: abc123 (Total: 400ms)
├─ [Gateway] 150ms
│ └─ [Service A] 200ms
│ └─ [Service B] 50ms
│ └─ [Service C] 50ms
Timeline:
|--Gateway:150ms--|
|--ServiceA:200ms--|
|--ServiceB:50ms--|
|--ServiceC:50ms--|
0ms 150ms 350ms 400msKey Elements:
- Trace ID (abc123): Unique identifier propagated across all services for one request
- Span IDs: Each service creates span recording its processing time
- Context propagation: Trace ID passed in HTTP headers (X-B3-TraceId, X-B3-SpanId)
- Jaeger Agent: Sidecar collecting spans, batching for efficiency
- Jaeger Collector: Aggregates spans from all agents into complete traces
- Tracing Database: Elasticsearch stores spans for querying
- Jaeger UI: Visualizes traces showing request flow and latency breakdown
Design Rationale: Distributed tracing provides observability into request flow across microservices. Trace ID correlation enables reconstructing full request path. Span timing reveals bottlenecks (ServiceA: 200ms vs ServiceC: 50ms).
Key Takeaway: Generate trace ID at entry point, propagate through all services. Each service creates span with timing. Collect spans via agents, aggregate in collector. Store for querying and visualization. This enables request-level debugging in distributed systems.
Why It Matters: Distributed tracing is essential for performance debugging in microservice architectures. Component diagrams showing trace propagation help teams understand how to identify performance bottlenecks across service boundaries. In distributed systems, slow requests may result from any service in the call chain, making bottleneck identification challenging without tracing. Distributed traces reveal which services contribute most latency, allowing teams to focus optimization efforts. Without tracing, teams risk optimizing services that contribute minimal latency while ignoring actual bottlenecks. Tracing ensures performance optimization targets the right components.
Example 58: Asynchronous Processing Architecture
Asynchronous processing improves responsiveness. This example shows job queue architecture with multiple worker types.
graph TD
Users["Users"]
WebAPI["[Web API]<br/>Accepts requests<br/>Returns immediately"]
JobQueue["[Job Queue]<br/>Redis/RabbitMQ<br/>Persistent queue"]
HighPriorityWorkers["[High Priority Workers]<br/>3x instances<br/>Critical jobs"]
NormalPriorityWorkers["[Normal Priority Workers]<br/>5x instances<br/>Standard jobs"]
LowPriorityWorkers["[Low Priority Workers]<br/>2x instances<br/>Batch jobs"]
ResultCache["[Result Cache]<br/>Redis<br/>Job results"]
NotificationService["[Notification Service]<br/>WebSocket/SSE<br/>Real-time updates"]
Database["[Database]<br/>PostgreSQL<br/>Persistent storage"]
Users -->|"1. POST /jobs"| WebAPI
WebAPI -->|"2. Enqueue job"| JobQueue
WebAPI -->|"3. Return job_id: xyz789"| Users
JobQueue -->|"High priority"| HighPriorityWorkers
JobQueue -->|"Normal priority"| NormalPriorityWorkers
JobQueue -->|"Low priority"| LowPriorityWorkers
HighPriorityWorkers -->|"Process job"| Database
NormalPriorityWorkers -->|"Process job"| Database
LowPriorityWorkers -->|"Process job"| Database
HighPriorityWorkers -->|"Store result"| ResultCache
NormalPriorityWorkers -->|"Store result"| ResultCache
LowPriorityWorkers -->|"Store result"| ResultCache
ResultCache -->|"Job completed"| NotificationService
NotificationService -.->|"WebSocket push"| Users
Users -->|"4. GET /jobs/xyz789/status"| WebAPI
WebAPI -->|"Query result"| ResultCache
WebAPI -->|"5. Return completed/pending"| Users
style Users fill:#CC78BC,stroke:#000,color:#fff
style WebAPI fill:#0173B2,stroke:#000,color:#fff
style JobQueue fill:#DE8F05,stroke:#000,color:#fff
style HighPriorityWorkers fill:#029E73,stroke:#000,color:#fff
style NormalPriorityWorkers fill:#029E73,stroke:#000,color:#fff
style LowPriorityWorkers fill:#CA9161,stroke:#000,color:#fff
style ResultCache fill:#DE8F05,stroke:#000,color:#fff
style NotificationService fill:#CC78BC,stroke:#000,color:#fff
style Database fill:#CA9161,stroke:#000,color:#fff
Key Elements:
- Job Queue (orange): Persistent queue storing pending jobs (survives worker crashes)
- Priority-based workers: High (3x, critical), Normal (5x, standard), Low (2x, batch)
- Immediate response: WebAPI returns job_id instantly without waiting for completion
- Result Cache (orange): Redis stores job results for fast status queries
- Real-time notifications: WebSocket pushes completion notifications to users
- Status polling: Users can poll GET /jobs/:id/status for job state
- Resource allocation: More workers for high-priority jobs (3x vs 2x low-priority)
Design Rationale: Async processing prevents timeout errors on long-running tasks. Priority-based workers ensure critical jobs process first. Result cache enables fast status checks without database queries.
Key Takeaway: Enqueue long-running jobs instead of processing synchronously. Return job_id immediately. Use priority queues with dedicated workers per priority. Store results in cache. Push completion notifications via WebSocket. This improves responsiveness and enables priority management.
Why It Matters: Asynchronous processing fundamentally transforms user experience by decoupling request handling from background work. Component diagrams showing job queue architectures help teams understand how to improve perceived responsiveness. Synchronous processing forces users to wait for all operations to complete before receiving responses. Asynchronous processing immediately returns control to users while enqueueing background work. Priority queues ensure critical tasks complete quickly while less important tasks process in background. This dramatically improves perceived application performance and enables better resource allocation for different task priorities.
Example 59: Multi-Tier Caching Strategy
Multi-tier caching optimizes performance at different layers. This example shows comprehensive caching architecture.
graph TD
Users["Users"]
CDN["[CDN Layer]<br/>CloudFront<br/>Edge caching (static)"]
ApplicationServer["Application Server"]
L1Cache["[L1: In-Memory Cache]<br/>Local cache<br/>100ms TTL"]
L2Cache["[L2: Distributed Cache]<br/>Redis Cluster<br/>5min TTL"]
L3Cache["[L3: Database Query Cache]<br/>PostgreSQL<br/>Materialized views"]
PrimaryDB["[Primary Database]<br/>PostgreSQL<br/>Source of truth"]
CacheWarmer["[Cache Warmer]<br/>Background job<br/>Preload hot data"]
Users -->|"1. GET /products"| CDN
CDN -.->|"CDN miss"| ApplicationServer
ApplicationServer -->|"2. Check L1"| L1Cache
L1Cache -.->|"L1 miss"| ApplicationServer
ApplicationServer -->|"3. Check L2"| L2Cache
L2Cache -.->|"L2 miss"| ApplicationServer
ApplicationServer -->|"4. Check L3"| L3Cache
L3Cache -.->|"L3 miss"| ApplicationServer
ApplicationServer -->|"5. Query database"| PrimaryDB
PrimaryDB -->|"Data"| ApplicationServer
ApplicationServer -->|"6. Populate L3"| L3Cache
ApplicationServer -->|"7. Populate L2"| L2Cache
ApplicationServer -->|"8. Populate L1"| L1Cache
CacheWarmer -.->|"Preload popular items"| L2Cache
style Users fill:#CC78BC,stroke:#000,color:#fff
style CDN fill:#DE8F05,stroke:#000,color:#fff
style ApplicationServer fill:#0173B2,stroke:#000,color:#fff
style L1Cache fill:#029E73,stroke:#000,color:#fff
style L2Cache fill:#029E73,stroke:#000,color:#fff
style L3Cache fill:#029E73,stroke:#000,color:#fff
style PrimaryDB fill:#CA9161,stroke:#000,color:#fff
style CacheWarmer fill:#CC78BC,stroke:#000,color:#fff
Cache Hierarchy Performance:
CDN: 1-50ms (edge locations globally, static content)
L1 (In-Memory): 1-5ms (local process memory, smallest capacity)
L2 (Redis): 5-20ms (network call, distributed, larger capacity)
L3 (DB Cache): 20-50ms (database query, materialized views)
Database: 50-500ms (disk I/O, query execution)Key Elements:
- Four cache tiers: CDN (static), L1 (in-memory), L2 (Redis), L3 (DB materialized views)
- Graduated TTLs: L1 (100ms), L2 (5min), L3 (daily refresh)
- Cache-aside pattern: Check L1 → L2 → L3 → DB, populate on miss
- Cache warmer: Background job preloading frequently accessed data into L2
- Different optimization targets: CDN (global latency), L1 (eliminate network), L2 (shared cache), L3 (complex queries)
Design Rationale: Multi-tier caching balances latency, capacity, and consistency. L1 eliminates network calls (fastest, smallest). L2 provides shared cache across app servers (moderate speed, large capacity). L3 caches complex query results (slower than L2, avoids expensive computations). CDN serves static content from edge locations.
Key Takeaway: Implement multiple cache tiers with different TTLs. Check caches in order of speed (L1 → L2 → L3 → DB). Populate higher tiers on lower-tier hits. Use cache warmer for predictable hot data. This optimizes both read latency and database load.
Why It Matters: Multi-tier caching architectures dramatically reduce infrastructure costs at scale. Component diagrams showing cache hierarchies help teams understand how layered caching prevents database load. When high percentages of requests are served from cache, database query volumes decrease dramatically, deferring expensive database scaling investments. In-memory caches (L1) eliminate even distributed cache network calls, further reducing infrastructure costs. Multi-tier caching makes high-traffic applications economically viable by converting expensive database queries into cheap memory lookups. This cost reduction becomes increasingly significant at scale.
Example 60: Zero-Downtime Deployment Pipeline
Zero-downtime deployment requires orchestrated updates. This example shows complete deployment pipeline with health checks.
graph TD
LoadBalancer["[Load Balancer]<br/>Routes traffic<br/>Health check enabled"]
BlueCluster["Blue Cluster (Current v1.0)"]
BlueInstance1["[Instance 1]<br/>v1.0 Healthy"]
BlueInstance2["[Instance 2]<br/>v1.0 Healthy"]
BlueInstance3["[Instance 3]<br/>v1.0 Healthy"]
GreenCluster["Green Cluster (Deploying v2.0)"]
GreenInstance1["[Instance 1]<br/>v2.0 Deploying"]
GreenInstance2["[Instance 2]<br/>v2.0 Idle"]
GreenInstance3["[Instance 3]<br/>v2.0 Idle"]
HealthCheck["[Health Check]<br/>Periodic polling<br/>/health endpoint"]
DeployOrchestrator["[Deploy Orchestrator]<br/>Rolling update<br/>One instance at a time"]
LoadBalancer -->|"100% traffic"| BlueCluster
LoadBalancer -.->|"0% traffic (warming up)"| GreenCluster
BlueCluster -->|Contains| BlueInstance1
BlueCluster -->|Contains| BlueInstance2
BlueCluster -->|Contains| BlueInstance3
GreenCluster -->|Contains| GreenInstance1
GreenCluster -->|Contains| GreenInstance2
GreenCluster -->|Contains| GreenInstance3
HealthCheck -.->|"GET /health"| BlueInstance1
HealthCheck -.->|"GET /health"| BlueInstance2
HealthCheck -.->|"GET /health"| BlueInstance3
HealthCheck -.->|"GET /health"| GreenInstance1
DeployOrchestrator -.->|"1. Deploy to Green 1"| GreenInstance1
DeployOrchestrator -.->|"2. Wait for healthy"| HealthCheck
HealthCheck -.->|"v2.0 healthy"| DeployOrchestrator
DeployOrchestrator -.->|"3. Route 10% traffic"| LoadBalancer
DeployOrchestrator -.->|"4. Monitor errors"| LoadBalancer
DeployOrchestrator -.->|"5. Deploy Green 2 & 3"| GreenInstance2
style LoadBalancer fill:#DE8F05,stroke:#000,color:#fff
style BlueCluster fill:#0173B2,stroke:#000,color:#fff
style BlueInstance1 fill:#029E73,stroke:#000,color:#fff
style BlueInstance2 fill:#029E73,stroke:#000,color:#fff
style BlueInstance3 fill:#029E73,stroke:#000,color:#fff
style GreenCluster fill:#0173B2,stroke:#000,color:#fff
style GreenInstance1 fill:#029E73,stroke:#000,color:#fff
style GreenInstance2 fill:#CA9161,stroke:#000,color:#fff
style GreenInstance3 fill:#CA9161,stroke:#000,color:#fff
style HealthCheck fill:#CC78BC,stroke:#000,color:#fff
style DeployOrchestrator fill:#DE8F05,stroke:#000,color:#fff
Deployment Phases:
Phase 1: Deploy Canary
- Deploy v2.0 to Green Instance 1
- Wait for health check (GET /health returns 200 OK)
- Route 10% traffic to Green Instance 1
Phase 2: Monitor Canary
- Monitor error rates on Green Instance 1 for 10 minutes
- Compare error rate: Blue (v1.0) vs Green canary (v2.0)
- If Green errors < Blue errors + 5%: proceed
- If Green errors ≥ Blue errors + 5%: rollback (route 0% to Green)
Phase 3: Full Rollout
- Deploy v2.0 to Green Instances 2 & 3
- Gradually shift traffic: 10% → 25% → 50% → 100% Green
- Decommission Blue cluster after 100% traffic on Green
Key Elements:
- Load Balancer (orange): Routes traffic with health-check-based routing
- Blue cluster: Current production version (v1.0) serving 100% traffic
- Green cluster: New version (v2.0) being deployed incrementally
- Health Check (purple): Polls /health endpoint every 5 seconds
- Deploy Orchestrator (orange): Automates rolling update sequence
- Canary deployment: Deploy one instance first, monitor before full rollout
- Automated rollback: If canary shows elevated errors, route traffic back to blue
Design Rationale: Zero-downtime deployment requires keeping old version running while new version deploys. Health checks ensure only healthy instances receive traffic. Canary deployment detects production issues before full rollout.
Key Takeaway: Deploy new version to separate cluster while old version serves traffic. Use health checks to validate new instances. Route small percentage of traffic to canary. Monitor error rates. Gradually shift traffic if canary healthy. This achieves zero downtime with automated rollback.
Why It Matters: Zero-downtime deployment strategies enable high-velocity continuous delivery. Deployment diagrams showing rolling updates with automated health checks help teams understand how to detect and rollback problematic deployments rapidly. Automated health checks detect issues early and remove unhealthy instances from load balancers before users are impacted. This dramatically reduces mean time to recovery compared to manual rollback processes. Zero-downtime deployments with automatic rollback enable organizations to deploy frequently while maintaining high availability, making aggressive deployment schedules practical and safe.
This completes the intermediate-level C4 Model by-example tutorial with 30 comprehensive examples covering detailed component diagrams, deployment strategies, dynamic sequence flows, advanced integration patterns, and production-ready multi-container architectures (40-75% coverage).