Complete Guide to Software System Design: From Architecture to Production
By Jagadeesh Katla
Complete Guide to
Software System Design
A comprehensive deep-dive from fundamentals to advanced architecture strategies. Master the art of building scalable, maintainable, and reliable software systems that can serve millions of users.
Introduction
Software system design is the discipline of building scalable, maintainable, reliable, and secure software systems. It represents the bridge between business requirements and technical implementation, transforming abstract ideas into concrete, deployable architectures that can serve millions of users while maintaining high performance, availability, and user satisfaction.
In today's digital landscape, where applications need to handle massive scale, process petabytes of data, and provide real-time responses across the globe, understanding system design has become not just important—it's absolutely critical. Whether you're building a simple web application or architecting the next social media platform, the principles of good system design apply universally.
Why System Design Matters
- Scalability: Handle growth from 100 to 100 million users without complete rewrites
- Reliability: Ensure your system stays available even when components fail
- Performance: Deliver sub-second response times to users worldwide
- Maintainability: Make changes and add features without breaking existing functionality
- Cost Efficiency: Optimize resource usage to reduce operational expenses
- Security: Protect user data and prevent security breaches
"If coding builds a product, system design builds the ecosystem in which that product thrives. It's the difference between a working prototype and a production-ready system that serves millions."
Complete Table of Contents
Types of System Design
Functional, non-functional, architectural, data & storage, and deployment design
Functional vs Non-Functional
Understanding requirements, constraints, and quality attributes
System Architecture Basics
Layers, patterns, logical and physical architecture
High-Level vs Low-Level Design
HLD components, LLD implementation details
Monolith vs Microservices vs Serverless
Architectural patterns, pros, cons, and when to use each
API Design & Communication
REST, GraphQL, gRPC, WebSockets, sync/async patterns
Databases & Data Models
SQL, NoSQL types, indexing, normalization, sharding
Storage Concepts
Block, object, file storage, CDN, distributed systems
Caching & Performance
Caching strategies, Redis, Memcached, CDN caching
Scalability Strategies
Horizontal, vertical scaling, replication, partitioning
Load Balancing
Algorithms, L4/L7 balancing, health checks, failover
Message Queues & Streaming
RabbitMQ, Kafka, event-driven architecture
CAP Theorem & Consistency
Consistency, availability, partition tolerance, BASE vs ACID
Security & Authentication
OAuth, JWT, encryption, zero-trust architecture
CI/CD & DevOps
Pipelines, Docker, Kubernetes, infrastructure as code
Monitoring & Observability
Logs, metrics, traces, Prometheus, Grafana, ELK
Design Patterns
MVC, Repository, Circuit Breaker, CQRS, Saga
UML Diagrams
Use case, class, sequence, deployment diagrams
Cloud Architecture
VPC, subnets, auto-scaling, multi-region deployment
Cost Optimization
Resource optimization, spot instances, caching strategies
Real-World System Design
YouTube, WhatsApp, Uber, Twitter architecture case studies
Types of Software System Design
System design is a multi-faceted discipline that requires understanding various design types, each focusing on different aspects of building robust software systems. Let's explore the five primary categories in comprehensive detail.
1. Functional Design
Functional design defines "What the system should do" - the features, behaviors, and business logic that deliver value to users. This encompasses user requirements, use cases, business workflows, and feature specifications.
Core Components:
-
User Stories: Narrative descriptions from user perspective (As a [role], I want [feature] so that [benefit])
-
Use Cases: Detailed scenarios with preconditions, steps, and postconditions
-
Business Rules: Logic governing system behavior and data processing
-
Data Flow: How information moves through the system
Example: E-commerce Cart
- ✓ Users can add/remove items to cart
- ✓ Cart persists across sessions
- ✓ Apply discount codes and coupons
- ✓ Calculate total with taxes and shipping
- ✓ Show real-time inventory status
- ✓ Save items for later purchase
- ✓ Share cart with others via link
- ✓ Suggest related products
Functional Requirements Example
Requirement ID: FR-001
Title: User Authentication System
Description:
The system shall provide secure user authentication with multiple methods
Functional Requirements:
1. Email/Password Authentication
- Users can register with email and password
- Password must be 8+ characters with special chars
- Email verification required before login
2. Social Authentication
- Support Google OAuth 2.0
- Support Facebook Login
- Auto-create user profile from social data
3. Two-Factor Authentication (Optional)
- SMS-based OTP
- Authenticator app support (TOTP)
- Backup codes generation
4. Session Management
- JWT tokens with 24-hour expiry
- Refresh token mechanism
- Logout from all devices option
5. Password Recovery
- Email-based reset link (valid 1 hour)
- Security questions as backup
- Lock account after 5 failed attempts
2. Non-Functional Design
Non-functional design specifies "How well the system performs" - the quality attributes that determine user satisfaction, system reliability, and operational excellence.
| Category | Attributes | Specific Requirements | Measurement Metrics |
|---|---|---|---|
| Performance | Response time, throughput, latency | API responds in <200ms (p95), Page load <2s | Percentiles (p50, p95, p99), RPS, TPS |
| Scalability | Horizontal/vertical scaling, elasticity | Support 1M concurrent users, 10K RPS | Users/second, Data volume, Auto-scale time |
| Availability | Uptime, fault tolerance, redundancy | 99.99% SLA (52min downtime/year) | Uptime %, MTBF, MTTR |
| Reliability | Error rate, data integrity, recovery | Error rate <0.1%, Zero data loss, RPO 1hr | Error rate %, RPO, RTO |
| Security | Authentication, encryption, compliance | OAuth 2.0, TLS 1.3, GDPR compliant | Vulnerabilities, Compliance score |
| Maintainability | Code quality, modularity, documentation | 80% test coverage, Modular architecture | Code complexity, Test coverage % |
| Usability | User experience, accessibility | WCAG 2.1 AA, Mobile responsive | Task completion time, Error rate |
| Cost Efficiency | Resource optimization, cloud costs | Monthly cloud cost < $10K for 100K users | Cost per user, Infrastructure cost |
Trade-off Example: Performance vs Consistency
Scenario: Social media "likes" counter
- ✓ Ultra-fast response (<10ms)
- ✓ Handle millions of likes/second
- ✗ Count may be slightly inaccurate temporarily
- Best for: Social media, analytics
- ✓ Always accurate count
- ✓ Reliable for critical operations
- ✗ Slower response (50-100ms)
- Best for: Banking, inventory
3. Architectural Design
Architectural design determines the overall structure, components, and their interactions. It's the blueprint that guides implementation and ensures all requirements are met efficiently.
Layered Architecture
Organizes system into horizontal layers (Presentation → Business Logic → Data Access → Database)
Client-Server
Separates concerns: clients handle UI, servers manage data and business logic
Event-Driven
Components communicate via events; producers emit, consumers react asynchronously
Microservices
Independent services organized around business capabilities, each with own database
Service-Oriented
Services communicate via protocols (SOAP, REST), with ESB for integration
Hexagonal (Ports & Adapters)
Core business logic isolated from external concerns via ports and adapters
4. Data & Storage Design
Data design focuses on how data is structured, stored, accessed, and maintained. Poor data design leads to performance bottlenecks, data inconsistencies, and scalability issues.
Key Considerations:
-
Schema Design: Normalize for consistency or denormalize for performance
-
Indexing Strategy: B-tree, hash, full-text indexes for query optimization
-
Partitioning: Horizontal (sharding) or vertical data splitting
-
Replication: Master-slave, multi-master for high availability
-
Data Migration: ETL processes for data movement
Database Selection:
- Relational (PostgreSQL, MySQL): Structured data, ACID transactions
- Document (MongoDB): JSON documents, flexible schema
- Key-Value (Redis): Fast caching, session storage
- Column-Family (Cassandra): Time-series, write-heavy workloads
- Graph (Neo4j): Social networks, relationships
- Search (Elasticsearch): Full-text search, analytics
5. Deployment & Infrastructure Design
Deployment design defines how the system is packaged, deployed, and operated in production. Modern deployment strategies enable rapid releases with minimal downtime.
| Component | Purpose | Technologies | Best Practices |
|---|---|---|---|
| CI/CD Pipeline | Automate build, test, deploy | Jenkins, GitLab CI, GitHub Actions | Automated tests, staged rollouts |
| Containerization | Package with dependencies | Docker, containerd | Multi-stage builds, small images |
| Orchestration | Manage container lifecycle | Kubernetes, Docker Swarm | Auto-scaling, health checks |
| Infrastructure as Code | Version control infrastructure | Terraform, CloudFormation, Ansible | Declarative configs, state management |
| Monitoring | Observe system health | Prometheus, Datadog, New Relic | SLIs, SLOs, alerts |
Functional vs Non-Functional Requirements
Requirements form the foundation of system design. Functional requirements define what the system does, while non-functional requirements specify how well it does it. Both are equally critical for project success.
Functional Requirements
Define specific behaviors, features, and functions the system must provide to satisfy business needs and user goals.
Example: Video Streaming Platform
-
1
Video Upload: Users can upload videos (MP4, AVI, MOV) up to 5GB. System supports drag-and-drop and multi-file selection.
-
2
Transcoding: Automatic conversion to multiple resolutions (2160p, 1080p, 720p, 480p, 360p) with adaptive bitrate streaming.
-
3
Thumbnail Generation: System creates 10 thumbnails at different timestamps; creator selects default.
-
4
Playback Controls: Play, pause, seek, volume, speed (0.25x to 2x), quality selector, fullscreen, picture-in-picture.
-
5
Social Features: Like, dislike, comment, share, subscribe, create playlists, save to watch later.
-
6
Search & Discovery: Full-text search, filters (duration, upload date, views), recommendations based on history.
-
7
Analytics: Creators view watch time, demographics, traffic sources, revenue reports.
-
8
Content Moderation: AI-based flagging of inappropriate content; manual review queue for admins.
Characteristics of Good Functional Requirements:
- ✓ Clear and unambiguous
- ✓ Testable and verifiable
- ✓ Traceable to business goals
- ✓ Feasible within constraints
- ✓ Prioritized by importance
Non-Functional Requirements
Specify quality attributes, performance characteristics, and constraints that determine system success and user satisfaction.
Example: Video Streaming Platform
-
1
Performance: Video buffering starts <2 seconds. Seek operations <500ms. Upload speed 10MB/s minimum.
-
2
Scalability: Support 50M concurrent viewers. Handle 100K uploads/hour. Store 500PB of video content.
-
3
Availability: 99.99% uptime SLA (52.6 min downtime/year). Multi-region deployment with automatic failover.
-
4
Reliability: Zero data loss guarantee. Automatic retry for failed uploads. Self-healing infrastructure.
-
5
Security: AES-256 encryption for stored videos. TLS 1.3 for transmission. DRM for premium content.
-
6
Usability: Support 25 languages. WCAG 2.1 AA accessibility. Works on iOS 14+, Android 10+, modern browsers.
-
7
Cost Efficiency: CDN bandwidth cost <$0.02/GB. Storage costs optimized with tiered storage.
-
8
Compliance: COPPA compliance for users under 13. GDPR for EU users. Content ID system for copyright.
Characteristics of Good Non-Functional Requirements:
- ✓ Quantifiable and measurable
- ✓ Realistic and achievable
- ✓ Aligned with business priorities
- ✓ Trade-offs documented
- ✓ Testable through benchmarks
Understanding Trade-offs in Requirements
Real-world system design requires balancing conflicting requirements. Here are common trade-offs you'll encounter:
Performance vs Security
Conflict: Encryption, authentication checks, and security validations add latency
Solution: Use hardware acceleration for encryption, implement smart caching for auth tokens, optimize security checks on critical paths
Consistency vs Availability
Conflict: CAP theorem - can't have perfect consistency and availability during network partitions
Solution: Use eventual consistency for non-critical data (social feeds), strong consistency for financial transactions
Scalability vs Complexity
Conflict: Microservices enable infinite scaling but increase operational complexity
Solution: Start with modular monolith, extract services when specific scaling needs emerge
Cost vs Performance
Conflict: High performance requires premium infrastructure (faster CPUs, more RAM, global CDN)
Solution: Implement intelligent caching, auto-scaling, database query optimization, use CDN strategically
Time-to-Market vs Quality
Conflict: Rushing features can compromise code quality, testing, and architecture
Solution: MVP approach with quality gates, technical debt tracking, regular refactoring sprints
Flexibility vs Security
Conflict: Open APIs and integrations increase attack surface
Solution: API gateway with rate limiting, OAuth 2.0, input validation, regular security audits
System Architecture Basics
System architecture is the fundamental organization of a software system, including its components, their relationships, and the principles governing their design and evolution. A well-designed architecture enables scalability, maintainability, and resilience.
System Layers Visualization
┌────────────────────────────────────────────────────────┐
│ [CLIENT LAYER] │
│ Web Browser | Mobile App | Desktop │
│ (React, Vue, Angular, Flutter, SwiftUI) │
└────────────────────────────────────────────────────────┘
↓ HTTPS/WSS
┌────────────────────────────────────────────────────────┐
│ [CDN / EDGE LAYER] │
│ Cloudflare, AWS CloudFront, Akamai │
│ (Static Assets, DDoS Protection, Caching) │
└────────────────────────────────────────────────────────┘
↓
┌────────────────────────────────────────────────────────┐
│ [LOAD BALANCER] │
│ NGINX, HAProxy, AWS ALB/NLB, F5 │
│ (Traffic Distribution, SSL Termination, Health) │
└────────────────────────────────────────────────────────┘
↓
┌────────────────────────────────────────────────────────┐
│ [APPLICATION LAYER - API TIER] │
│ Node.js | Django | Spring Boot | Go │
│ Multiple Instances (Horizontal Scaling) │
│ Stateless for Easy Scaling │
└────────────────────────────────────────────────────────┘
↓ ↓ ↓
┌──────────────────┐ ┌────────────┐ ┌─────────────────┐
│ [CACHE LAYER] │ │ [MESSAGE] │ │ [SEARCH INDEX] │
│ Redis/Memcached │ │ QUEUE │ │ Elasticsearch │
│ (Session, Data) │ │ Kafka/RMQ │ │ (Full-text) │
└──────────────────┘ └────────────┘ └─────────────────┘
↓
┌────────────────────────────────────────────────────────┐
│ [BUSINESS LOGIC LAYER - SERVICES] │
│ Microservices / Service-Oriented Architecture │
│ User Service | Payment | Notification | Analytics │
└────────────────────────────────────────────────────────┘
↓
┌────────────────────────────────────────────────────────┐
│ [DATA ACCESS LAYER - DAL] │
│ ORM (Hibernate, TypeORM, SQLAlchemy) │
│ Repository Pattern, Query Builders │
└────────────────────────────────────────────────────────┘
↓
┌────────────────────────────────────────────────────────┐
│ [DATABASE / STORAGE LAYER] │
│ Primary DB: PostgreSQL/MySQL (Write Operations) │
│ Read Replicas: Multiple copies (Read Operations) │
│ NoSQL: MongoDB, Cassandra (Specific Use Cases) │
│ Object Storage: S3, GCS (Files, Images, Videos) │
└────────────────────────────────────────────────────────┘
| Layer | Responsibilities | Technologies | Scaling Strategy |
|---|---|---|---|
| Presentation | UI rendering, user input, client-side validation, responsive design | React, Vue, Angular, Flutter | CDN caching, lazy loading |
| Application | Request routing, authentication, rate limiting, API endpoints | Express, FastAPI, Spring Boot | Horizontal scaling, stateless |
| Business Logic | Core functionality, business rules, workflows, orchestration | Domain models, Services | Microservices, async processing |
| Data Access | CRUD operations, transactions, query optimization | Repository, DAO patterns | Connection pooling, caching |
| Database | Persistent storage, data integrity, consistency | PostgreSQL, MongoDB, Redis | Replication, sharding |
Architectural Principles
- Separation of Concerns: Each layer has distinct responsibilities
- Loose Coupling: Minimize dependencies between components
- High Cohesion: Related functionality grouped together
- Abstraction: Hide implementation details behind interfaces
- Scalability: Design for horizontal and vertical scaling
- Resilience: Graceful degradation and fault tolerance
High-Level Design vs Low-Level Design
System design is typically divided into two phases: High-Level Design (HLD) focuses on the overall architecture and component relationships, while Low-Level Design (LLD) dives into implementation details, algorithms, and data structures.
HLD vs LLD: Understanding the Difference
High-Level Design (HLD)
Focus: System architecture, component interaction, data flow
What HLD Covers:
- System architecture overview
- Component identification
- Database design (ER diagrams)
- API contracts
- Technology stack selection
- Scalability strategy
- Security architecture
Audience:
Architects, Product Managers, Stakeholders, Engineering Teams
Deliverables:
- • Architecture diagrams
- • Component diagrams
- • Data flow diagrams
- • Infrastructure layout
Low-Level Design (LLD)
Focus: Implementation details, algorithms, class design
What LLD Covers:
- Class diagrams
- Sequence diagrams
- Database schema (tables, indexes)
- Algorithms and data structures
- Method signatures
- Error handling logic
- Design patterns
Audience:
Developers, QA Engineers, Technical Leads
Deliverables:
- • Class diagrams
- • Sequence diagrams
- • Pseudocode
- • Database schemas
| Aspect | High-Level Design (HLD) | Low-Level Design (LLD) |
|---|---|---|
| Abstraction Level | High (30,000 ft view) | Low (ground level view) |
| Created By | Solution Architects, System Designers | Software Engineers, Developers |
| Phase | Early design phase | Implementation phase |
| Questions Answered | "What components?", "How do they interact?" | "How to implement?", "What algorithm?" |
| Time Investment | Days to weeks | Weeks to months |
| Changes Impact | High (affects entire system) | Low (affects specific modules) |
Example: URL Shortener (like bit.ly)
High-Level Design
Requirements Gathering:
Functional:
- • Generate short URL from long URL
- • Redirect short URL to original URL
- • Custom short URLs (optional)
- • Analytics (click count, geography)
- • URL expiration
Non-Functional:
- • High availability (99.99%)
- • Low latency (< 100ms redirect)
- • Scale: 100M URLs created/month
- • Read heavy (100:1 read/write ratio)
- • Prevent collisions
Capacity Estimation:
// Traffic Estimates
Write requests: 100M URLs/month
= 100M / (30 days × 24 hrs × 3600 sec)
≈ 40 URLs/second
Read requests: 100:1 ratio
= 40 × 100 = 4,000 reads/second
// Storage Estimates
Average URL size: 500 bytes
100M URLs/month × 500 bytes = 50 GB/month
Over 5 years: 50 GB × 12 × 5 = 3 TB
// Bandwidth
Write: 40 req/s × 500 bytes = 20 KB/s
Read: 4000 req/s × 500 bytes = 2 MB/s
HLD Architecture Diagram:
┌─────────────────────────────────────────────────────────────────┐
│ URL Shortener System │
└─────────────────────────────────────────────────────────────────┘
┌──────────────┐
│ Client │
│ (Browser) │
└──────┬───────┘
│
HTTPS │
↓
┌───────────────────────┐
│ Load Balancer │
│ (NGINX/HAProxy) │
└───────────┬───────────┘
│
┌───────────────┼───────────────┐
↓ ↓ ↓
┌───────────┐ ┌───────────┐ ┌───────────┐
│ App │ │ App │ │ App │
│ Server 1 │ │ Server 2 │ │ Server 3 │
└─────┬─────┘ └─────┬─────┘ └─────┬─────┘
│ │ │
└───────────────┼───────────────┘
↓
┌─────────────────┐
│ Cache (Redis) │
│ - URL mappings │
│ - Hot data │
└────────┬────────┘
│ Cache miss
↓
┌──────────────────────────┐
│ Database (PostgreSQL) │
│ Primary + Read Replicas│
│ │
│ Tables: │
│ - urls (id, short, │
│ long, created) │
│ - analytics (clicks, │
│ location) │
└──────────────────────────┘
API Endpoints:
POST /api/shorten
Body: { "url": "https://example.com/very-long-url" }
Response: { "short_url": "https://short.ly/abc123" }
GET /{short_code}
Redirect to original URL (302)
GET /api/stats/{short_code}
Returns analytics data
Technology Stack (HLD Level):
Frontend:
React.js, Next.js
Backend:
Node.js / Go / Python
Database:
PostgreSQL + Redis
Load Balancer:
AWS ALB / NGINX
Hosting:
AWS EC2 / Kubernetes
Monitoring:
Prometheus + Grafana
Low-Level Design
URL Encoding Algorithm:
// Approach 1: Base62 Encoding
class URLShortener {
private static BASE62 = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
// Convert auto-incrementing ID to Base62
encodeID(id: number): string {
if (id === 0) return this.BASE62[0];
let shortURL = '';
while (id > 0) {
shortURL = this.BASE62[id % 62] + shortURL;
id = Math.floor(id / 62);
}
return shortURL;
}
// Decode short URL back to ID
decodeURL(shortURL: string): number {
let id = 0;
for (let char of shortURL) {
id = id * 62 + this.BASE62.indexOf(char);
}
return id;
}
async createShortURL(longURL: string): Promise {
// 1. Check if URL already exists (avoid duplicates)
const existing = await this.db.findByLongURL(longURL);
if (existing) return existing.shortCode;
// 2. Generate new ID (auto-increment or UUID)
const id = await this.db.getNextID();
// 3. Encode ID to Base62
const shortCode = this.encodeID(id);
// 4. Store mapping
await this.db.insert({
id: id,
shortCode: shortCode,
longURL: longURL,
createdAt: new Date(),
expiresAt: new Date(Date.now() + 365 * 24 * 60 * 60 * 1000) // 1 year
});
// 5. Cache for fast lookups
await this.cache.set(`short:${shortCode}`, longURL, 3600);
return `https://short.ly/${shortCode}`;
}
async redirect(shortCode: string): Promise {
// 1. Check cache first
let longURL = await this.cache.get(`short:${shortCode}`);
if (!longURL) {
// 2. Cache miss - query database
const record = await this.db.findByShortCode(shortCode);
if (!record) {
throw new Error('URL not found');
}
if (record.expiresAt < new Date()) {
throw new Error('URL expired');
}
longURL = record.longURL;
// 3. Update cache
await this.cache.set(`short:${shortCode}`, longURL, 3600);
}
// 4. Track analytics (async, non-blocking)
this.trackClick(shortCode).catch(err => console.error(err));
return longURL;
}
private async trackClick(shortCode: string): Promise {
await this.db.incrementClickCount(shortCode);
// Store detailed analytics in separate table
await this.analyticsDB.insert({
shortCode,
timestamp: new Date(),
// ip, userAgent, referer, etc.
});
}
}
// Example Usage:
const shortener = new URLShortener(db, cache);
// Shorten
const shortURL = await shortener.createShortURL(
'https://example.com/very-long-url-with-params?id=123&source=email'
);
// Returns: https://short.ly/a1B2c3
// Redirect
const originalURL = await shortener.redirect('a1B2c3');
// Returns: https://example.com/very-long-url-with-params?id=123&source=email
Database Schema (LLD):
-- PostgreSQL Schema
CREATE TABLE urls (
id BIGSERIAL PRIMARY KEY,
short_code VARCHAR(10) UNIQUE NOT NULL,
long_url TEXT NOT NULL,
user_id BIGINT,
created_at TIMESTAMP DEFAULT NOW(),
expires_at TIMESTAMP,
click_count INTEGER DEFAULT 0,
is_custom BOOLEAN DEFAULT FALSE,
INDEX idx_short_code (short_code),
INDEX idx_long_url_hash (MD5(long_url)),
INDEX idx_user_id (user_id),
INDEX idx_created_at (created_at)
);
CREATE TABLE clicks (
id BIGSERIAL PRIMARY KEY,
short_code VARCHAR(10) NOT NULL,
clicked_at TIMESTAMP DEFAULT NOW(),
ip_address INET,
user_agent TEXT,
referer TEXT,
country VARCHAR(2),
city VARCHAR(100),
FOREIGN KEY (short_code) REFERENCES urls(short_code),
INDEX idx_short_code_time (short_code, clicked_at)
);
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
email VARCHAR(255) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT NOW(),
plan VARCHAR(20) DEFAULT 'free'
);
-- Partitioning for analytics (optional, for scale)
CREATE TABLE clicks_2025_11 PARTITION OF clicks
FOR VALUES FROM ('2025-11-01') TO ('2025-12-01');
Class Diagram (LLD):
┌─────────────────────────────────────────┐
│ URLShortenerService │
├─────────────────────────────────────────┤
│ - db: Database │
│ - cache: CacheService │
│ - analytics: AnalyticsService │
├─────────────────────────────────────────┤
│ + createShortURL(longURL): string │
│ + redirect(shortCode): string │
│ + getAnalytics(shortCode): Stats │
│ - encodeID(id): string │
│ - decodeURL(shortURL): number │
│ - trackClick(shortCode): void │
└─────────────────────────────────────────┘
│
│ uses
↓
┌─────────────────────────────────────────┐
│ URLRepository │
├─────────────────────────────────────────┤
│ - connection: DatabaseConnection │
├─────────────────────────────────────────┤
│ + findByShortCode(code): URL │
│ + findByLongURL(url): URL │
│ + insert(url: URL): boolean │
│ + incrementClickCount(code): void │
│ + getNextID(): number │
└─────────────────────────────────────────┘
┌─────────────────────────────────────────┐
│ CacheService │
├─────────────────────────────────────────┤
│ - redis: RedisClient │
├─────────────────────────────────────────┤
│ + get(key): Promise │
│ + set(key, value, ttl): Promise │
│ + delete(key): Promise │
└─────────────────────────────────────────┘
┌─────────────────────────────────────────┐
│ URL (Model) │
├─────────────────────────────────────────┤
│ + id: number │
│ + shortCode: string │
│ + longURL: string │
│ + userId: number │
│ + createdAt: Date │
│ + expiresAt: Date │
│ + clickCount: number │
└─────────────────────────────────────────┘
When to Focus on HLD vs LLD
Focus on HLD When:
-
Starting a new project
Define architecture before coding -
System design interviews
45-60 min focus on HLD, not implementation -
Scaling existing system
Rearchitect for performance/scale -
Communication with stakeholders
Explain system without technical jargon
Focus on LLD When:
-
Ready to implement
HLD approved, time to write code -
Optimizing algorithms
Improve time/space complexity -
Bug fixing
Debug specific implementation issues -
Code reviews
Ensure clean, maintainable code
Monolith vs Microservices vs Serverless
Choosing the right architectural pattern is one of the most important decisions in system design. Each approach has its own trade-offs in terms of complexity, scalability, deployment, and team structure. Let's dive deep into each.
Architectural Patterns Comparison
| Aspect | Monolith | Microservices | Serverless |
|---|---|---|---|
| Deployment | Single unit | Independent services | Functions |
| Scalability | Scale entire app | Scale services independently | Auto-scale per function |
| Development Speed | Fast initially | Slower (coordination) | Fast (isolated functions) |
| Complexity | Low | High | Medium |
| Technology Stack | Single stack | Polyglot (multiple languages) | Limited (platform-specific) |
| Team Structure | Single team | Multiple teams per service | Small teams |
| Cost (Small Scale) | Low (1-2 servers) | High (many services) | Very low (pay per use) |
| Cost (Large Scale) | High (scale everything) | Optimized (scale what's needed) | Can be expensive |
| Best For | MVPs, small teams, simple apps | Large teams, complex apps, scale | Event-driven, variable load |
Monolithic Architecture
All components of the application run in a single process. Simpler to develop, test, and deploy initially.
Architecture Diagram
┌─────────────────────────────────────┐
│ Monolithic Application │
├─────────────────────────────────────┤
│ │
│ ┌───────────────────────────────┐ │
│ │ Presentation Layer │ │
│ │ (UI/Controllers) │ │
│ └───────────────────────────────┘ │
│ │
│ ┌───────────────────────────────┐ │
│ │ Business Logic Layer │ │
│ │ • User Management │ │
│ │ • Order Processing │ │
│ │ • Payment Handling │ │
│ │ • Inventory Management │ │
│ │ • Shipping Logic │ │
│ └───────────────────────────────┘ │
│ │
│ ┌───────────────────────────────┐ │
│ │ Data Access Layer │ │
│ │ (Database Access) │ │
│ └───────────────────────────────┘ │
│ │
└──────────────┬──────────────────────┘
↓
┌──────────────┐
│ Database │
│ (Postgres) │
└──────────────┘
Deployed as: Single JAR/WAR/executable
Scaled by: Running multiple instances
behind load balancer
Code Example
// Express.js Monolith Structure
project/
├── src/
│ ├── controllers/
│ │ ├── userController.js
│ │ ├── orderController.js
│ │ ├── productController.js
│ │ └── paymentController.js
│ ├── services/
│ │ ├── userService.js
│ │ ├── orderService.js
│ │ └── paymentService.js
│ ├── models/
│ │ ├── User.js
│ │ ├── Order.js
│ │ └── Product.js
│ ├── routes/
│ │ └── index.js
│ └── app.js
└── package.json
// app.js - Single entry point
const express = require('express');
const app = express();
app.use('/api/users', userRoutes);
app.use('/api/orders', orderRoutes);
app.use('/api/products', productRoutes);
app.use('/api/payments', paymentRoutes);
app.listen(3000, () => {
console.log('Monolith running on port 3000');
});
// All features share same:
// - Codebase
// - Database connection
// - Dependencies
// - Deployment pipeline
Advantages
- Simple Development: One codebase, easy to understand
- Easy Testing: End-to-end tests straightforward
- Simple Deployment: One deployment artifact
- Performance: In-process calls (no network overhead)
- Easy Debugging: Single application to debug
- Transaction Management: ACID transactions easy
- Lower Operational Cost: Few servers initially
Disadvantages
- Scaling: Must scale entire app (wasteful)
- Deployment Risk: Small change requires full redeploy
- Technology Lock-in: Stuck with initial tech choice
- Large Codebase: Becomes hard to maintain
- Long Build Times: As app grows, builds slow down
- Team Coordination: Merge conflicts, blocking each other
- Single Point of Failure: Bug can crash entire app
When to Choose Monolith
- ✓ MVP/Startup: Need to move fast, validate idea
- ✓ Small Team: < 10 developers
- ✓ Simple Domain: Not too many business capabilities
- ✓ Limited Traffic: Can scale vertically initially
- ✓ Cost-Conscious: Keep infrastructure simple
Pro tip: Start with a well-structured monolith. You can always extract microservices later if needed (modular monolith approach).
Microservices Architecture
Application broken into small, independent services that communicate via APIs. Each service owns its data and can be deployed independently.
Microservices Architecture:
┌──────────────────┐
│ API Gateway │
│ (Authentication,│
│ Rate Limiting) │
└────────┬─────────┘
│
┌────────────────────┼────────────────────┐
│ │ │
↓ ↓ ↓
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ User Service │ │ Order Service │ │ Product Service │
├──────────────────┤ ├──────────────────┤ ├──────────────────┤
│ • Registration │ │ • Create Order │ │ • Catalog │
│ • Authentication │ │ • Order Status │ │ • Search │
│ • Profile │ │ • Order History │ │ • Inventory │
├──────────────────┤ ├──────────────────┤ ├──────────────────┤
│ REST/gRPC API │ │ REST/gRPC API │ │ REST/gRPC API │
└────────┬─────────┘ └────────┬─────────┘ └────────┬─────────┘
│ │ │
↓ ↓ ↓
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ User DB │ │ Order DB │ │ Product DB │
│ (PostgreSQL) │ │ (MongoDB) │ │ (Elasticsearch) │
└─────────────────┘ └─────────────────┘ └─────────────────┘
↓ ↓ ↓
┌──────────────────────────────────────────────────────────┐
│ Message Bus (Kafka / RabbitMQ) │
│ Events: UserCreated, OrderPlaced, InventoryUpdated │
└──────────────────────────────────────────────────────────┘
↑ ↑ ↑
│ │ │
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ Payment Service │ │ Shipping Service │ │ Notification Svc │
└──────────────────┘ └──────────────────┘ └──────────────────┘
Each service:
• Independent deployment
• Own database (database per service pattern)
• Technology freedom (polyglot)
• Team ownership
Service Structure
// Order Service (Node.js)
order-service/
├── src/
│ ├── api/
│ │ └── orderController.js
│ ├── services/
│ │ └── orderService.js
│ ├── models/
│ │ └── Order.js
│ ├── events/
│ │ ├── publisher.js
│ │ └── subscriber.js
│ └── server.js
├── package.json
├── Dockerfile
└── kubernetes/
└── deployment.yaml
// orderService.js
class OrderService {
async createOrder(orderData) {
// 1. Validate order
const validationResult = await this.validateOrder(orderData);
// 2. Save order
const order = await Order.create(orderData);
// 3. Publish event (async)
await eventBus.publish('OrderCreated', {
orderId: order.id,
userId: order.userId,
items: order.items,
total: order.total
});
return order;
}
}
// Event Listener in Inventory Service
eventBus.subscribe('OrderCreated', async (event) => {
// Reduce inventory for ordered items
for (const item of event.items) {
await Inventory.decrement(item.productId, item.quantity);
}
});
Inter-Service Communication
Synchronous (REST/gRPC)
// Order service calls User service
async function createOrder(userId, items) {
// Sync call to verify user
const user = await fetch(
`http://user-service/api/users/${userId}`
);
if (!user.ok) {
throw new Error('User not found');
}
// Continue with order creation
}
Use when: Need immediate response, simple request-reply
Asynchronous (Events)
// Order service publishes event
await kafka.send({
topic: 'order-events',
messages: [{
key: orderId,
value: JSON.stringify({
type: 'OrderCreated',
orderId,
userId,
items
})
}]
});
// Multiple services react
// - Inventory: Reduce stock
// - Payment: Charge card
// - Shipping: Create shipment
// - Notification: Send email
Use when: Broadcast to multiple services, eventual consistency OK
Advantages
- Independent Scaling: Scale services based on load
- Technology Diversity: Use best tool for each job
- Fault Isolation: Service failure doesn't crash all
- Independent Deployment: Deploy services separately
- Team Autonomy: Teams own services end-to-end
- Easier Understanding: Smaller, focused codebases
- Reusability: Services can be reused across apps
Disadvantages
- Complexity: Distributed system challenges
- Network Latency: Inter-service calls are slower
- Data Consistency: Eventual consistency, no ACID
- Testing: Integration testing is complex
- Monitoring: Need distributed tracing
- DevOps Overhead: More services to manage
- Higher Cost: More infrastructure initially
When to Choose Microservices
- ✓ Large Team: > 20 developers, need team autonomy
- ✓ Complex Domain: Many distinct business capabilities
- ✓ Different Scaling Needs: Some services need more resources
- ✓ Technology Diversity: Need different languages/frameworks
- ✓ High Availability: Can't afford full system downtime
Warning: Don't start with microservices! The complexity is not worth it until you have the scale and team size to justify it.
Serverless Architecture
Run code without managing servers. Functions execute in response to events, auto-scale, and you pay only for execution time.
Serverless Architecture (AWS Lambda Example):
┌─────────────────────────────────────────────────────────────┐
│ Event Sources │
├─────────────────────────────────────────────────────────────┤
│ API Gateway │ S3 Upload │ DynamoDB │ SNS/SQS │ CloudWatch │
└──────┬───────┴─────┬─────┴────┬─────┴────┬────┴──────┬─────┘
│ │ │ │ │
↓ ↓ ↓ ↓ ↓
┌──────────────────────────────────────────────────────────────┐
│ AWS Lambda Functions (Auto-scale) │
├──────────────────────────────────────────────────────────────┤
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Create │ │ Process │ │ Generate │ │ Send │ │
│ │ User │ │ Image │ │ Report │ │ Email │ │
│ │ Function │ │ Function │ │ Function │ │ Function │ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
└──────┬───────────────┬──────────────┬──────────────┬────────┘
│ │ │ │
↓ ↓ ↓ ↓
┌──────────────────────────────────────────────────────────────┐
│ Backend Services │
├──────────────────────────────────────────────────────────────┤
│ DynamoDB │ S3 Storage │ SES (Email) │ CloudWatch │
└──────────────────────────────────────────────────────────────┘
Characteristics:
• No server management
• Auto-scaling (0 to 1000s of instances)
• Pay per request (not per hour)
• Event-driven execution
• Stateless functions
Lambda Function Example
// Image Processing Lambda (Node.js)
const AWS = require('aws-sdk');
const sharp = require('sharp');
const s3 = new AWS.S3();
exports.handler = async (event) => {
// Triggered by S3 upload
const bucket = event.Records[0].s3.bucket.name;
const key = event.Records[0].s3.object.key;
// Download image from S3
const image = await s3.getObject({
Bucket: bucket,
Key: key
}).promise();
// Resize image (3 sizes)
const sizes = [
{ name: 'thumbnail', width: 150 },
{ name: 'medium', width: 500 },
{ name: 'large', width: 1200 }
];
for (const size of sizes) {
const resized = await sharp(image.Body)
.resize(size.width)
.toBuffer();
// Upload resized image
await s3.putObject({
Bucket: bucket,
Key: `${size.name}/${key}`,
Body: resized,
ContentType: 'image/jpeg'
}).promise();
}
return {
statusCode: 200,
body: 'Images processed'
};
};
// Auto-scales: 1 upload = 1 execution
// 1000 uploads = 1000 parallel executions
// Cost: Pay only for execution time
API Endpoint Example
// API Gateway + Lambda
// GET /api/users/{id}
exports.handler = async (event) => {
const userId = event.pathParameters.id;
// Query DynamoDB
const params = {
TableName: 'Users',
Key: { userId }
};
const result = await dynamodb.get(params).promise();
if (!result.Item) {
return {
statusCode: 404,
body: JSON.stringify({ error: 'User not found' })
};
}
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
},
body: JSON.stringify(result.Item)
};
};
// Serverless Framework config
// serverless.yml
service: user-api
provider:
name: aws
runtime: nodejs18.x
region: us-east-1
functions:
getUser:
handler: handler.getUser
events:
- http:
path: users/{id}
method: get
cors: true
Advantages
- No Server Management: Provider handles infrastructure
- Auto-Scaling: Scales to 0 and to 1000s automatically
- Cost-Effective: Pay per execution (not idle time)
- Fast Development: Focus on code, not infrastructure
- Event-Driven: Perfect for async workflows
- High Availability: Built-in redundancy
- Quick Deployments: Deploy functions in seconds
Disadvantages
- Cold Start: First request slower (100-1000ms)
- Vendor Lock-in: AWS Lambda != Azure Functions
- Execution Limits: 15min timeout, memory limits
- Local Testing: Hard to replicate cloud environment
- Debugging: Harder than traditional apps
- Cost at Scale: Can be expensive for high-traffic
- Stateless: Can't maintain connections
When to Choose Serverless
- ✓ Variable Load: Traffic spikes unpredictably
- ✓ Event-Driven: Triggered by uploads, messages, schedules
- ✓ Rapid Prototyping: Want to move fast without ops
- ✓ Background Jobs: Image processing, data transforms
- ✓ Low Traffic: Don't want to pay for idle servers
Best Practice: Hybrid approach - Use serverless for specific workloads (APIs, background jobs) alongside traditional servers for core services.
Decision Matrix: Which Architecture?
Choose Monolith If:
- ✓ Building MVP
- ✓ Team < 10 people
- ✓ Simple domain
- ✓ Need speed to market
- ✓ Budget constraints
- ✓ Predictable load
Examples:
Basecamp, StackOverflow (started monolith)
Choose Microservices If:
- ✓ Team > 20 people
- ✓ Complex domain
- ✓ Different scaling needs
- ✓ Need tech diversity
- ✓ Independent deployments
- ✓ High availability critical
Examples:
Netflix, Uber, Amazon
Choose Serverless If:
- ✓ Variable/unpredictable load
- ✓ Event-driven workflows
- ✓ Want zero ops
- ✓ Short-running tasks
- ✓ Cost optimization
- ✓ Rapid experimentation
Examples:
Coca-Cola, iRobot, Nordstrom
Golden Rule
"Start simple (monolith), evolve as needed. Architecture should solve real problems, not theoretical ones."
Most successful companies started with monoliths and evolved. Premature microservices is a common mistake.
API Design & Communication
APIs (Application Programming Interfaces) are the contracts that define how different components of your system communicate. Well-designed APIs enable seamless integration, scalability, and maintainability. Poor API design leads to technical debt, integration issues, and frustrated developers.
API Communication Paradigms
| API Type | Protocol | Data Format | Best Use Cases | Performance |
|---|---|---|---|---|
| REST | HTTP/HTTPS | JSON, XML | Web APIs, CRUD operations, public APIs | Good (stateless, cacheable) |
| GraphQL | HTTP/HTTPS | JSON | Complex queries, mobile apps, flexible data needs | Excellent (fetch exactly what you need) |
| gRPC | HTTP/2 | Protocol Buffers | Microservices, high-performance, real-time | Excellent (binary, multiplexing) |
| WebSocket | WS/WSS | JSON, Binary | Real-time chat, gaming, live updates | Excellent (persistent connection) |
| SOAP | HTTP, SMTP | XML | Enterprise, banking, legacy systems | Lower (verbose XML) |
REST API Design Principles
Resource-Based URLs
✅ Good REST URLs:
GET /api/v1/users
GET /api/v1/users/123
POST /api/v1/users
PUT /api/v1/users/123
DELETE /api/v1/users/123
GET /api/v1/users/123/orders
❌ Bad REST URLs:
GET /api/v1/getAllUsers
POST /api/v1/createUser
GET /api/v1/user?action=delete&id=123
HTTP Methods & Status Codes
- GET: Retrieve resources (200 OK, 404 Not Found)
- POST: Create resources (201 Created, 400 Bad Request)
- PUT: Update/Replace (200 OK, 204 No Content)
- PATCH: Partial update (200 OK)
- DELETE: Remove resources (204 No Content)
- 401: Unauthorized (missing/invalid auth)
- 403: Forbidden (authenticated but no permission)
- 429: Too Many Requests (rate limit exceeded)
- 500: Internal Server Error
- 503: Service Unavailable
REST API Best Practices
- Versioning: Use URL versioning (/api/v1/) or header versioning
- Pagination: Use limit/offset or cursor-based pagination
- Filtering & Sorting: /users?status=active&sort=created_at:desc
- HATEOAS: Include links to related resources in responses
- Rate Limiting: Return X-RateLimit headers
- Idempotency: PUT and DELETE should be idempotent
GraphQL: Query Exactly What You Need
GraphQL Query
query {
user(id: "123") {
name
email
posts(limit: 5) {
title
createdAt
comments {
author
text
}
}
}
}
Advantage: Fetch nested data in one request, no over-fetching
Equivalent REST Calls
// Multiple requests needed:
GET /api/users/123
GET /api/users/123/posts?limit=5
GET /api/posts/1/comments
GET /api/posts/2/comments
GET /api/posts/3/comments
...
// 5+ API calls vs 1 GraphQL query
// More network overhead
// Higher latency
Disadvantage: Can cause N+1 query problems, harder to cache
gRPC: High-Performance Microservices Communication
Protocol Buffer Definition (.proto)
syntax = "proto3";
service UserService {
rpc GetUser (GetUserRequest) returns (User);
rpc ListUsers (ListUsersRequest) returns (stream User);
rpc CreateUser (CreateUserRequest) returns (User);
}
message User {
int32 id = 1;
string name = 2;
string email = 3;
int64 created_at = 4;
}
message GetUserRequest {
int32 id = 1;
}
Advantages
- • Binary protocol (faster)
- • HTTP/2 multiplexing
- • Strongly typed
- • Bidirectional streaming
- • Auto code generation
Disadvantages
- • Not human-readable
- • Limited browser support
- • Steeper learning curve
- • Debugging harder
Best For
- • Internal microservices
- • Real-time systems
- • High-throughput APIs
- • Mobile backends
Synchronous vs Asynchronous Communication
Synchronous (Request-Response)
Client waits for server response before proceeding. Direct, immediate feedback.
Client Server
| |
|-------- Request ------->|
| |
| (Processing)
| |
|<------- Response -------|
| |
(Continue execution)
Use Cases:
- • User authentication
- • Payment processing
- • Data retrieval (CRUD)
- • Real-time validation
Technologies:
HTTP/REST, gRPC, GraphQL
Asynchronous (Event-Driven)
Client sends message and continues. Server processes independently and may respond later.
Producer Queue Consumer
| | |
|---- Publish Event ->| |
| | |
(Continue immediately) |---- Deliver ----->|
| | |
| | (Processing)
| | |
|<------- (Optional Callback) ------------|
Use Cases:
- • Email notifications
- • Order processing
- • Video transcoding
- • Background jobs
Technologies:
RabbitMQ, Kafka, AWS SQS/SNS
API Security Best Practices
1. Authentication & Authorization
- ✓ Use OAuth 2.0 for third-party access
- ✓ JWT tokens with short expiry (15-30 min)
- ✓ Refresh token rotation
- ✓ API keys for server-to-server
2. Rate Limiting
- ✓ Token bucket algorithm
- ✓ Per-user and per-IP limits
- ✓ Return 429 with Retry-After header
- ✓ Different tiers (free: 100/hr, paid: 10k/hr)
3. Input Validation
- ✓ Validate all input parameters
- ✓ Use schema validation (JSON Schema)
- ✓ Sanitize SQL/NoSQL queries
- ✓ Prevent injection attacks
4. HTTPS & Encryption
- ✓ TLS 1.3 for all communications
- ✓ Certificate pinning for mobile apps
- ✓ Encrypt sensitive data at rest
- ✓ HSTS headers
Databases & Data Models
Choosing the right database and designing an efficient data model are among the most critical decisions in system design. The wrong choice can lead to performance bottlenecks, scalability issues, and costly migrations later.
SQL vs NoSQL: When to Use Each
| Aspect | SQL (Relational) | NoSQL (Non-Relational) |
|---|---|---|
| Data Model | Tables with rows and columns, fixed schema | Documents, key-value, column-family, graph |
| Schema | Strict, predefined schema (DDL) | Flexible, schema-less or dynamic schema |
| Transactions | ACID compliant (strong consistency) | BASE model (eventual consistency) |
| Scaling | Vertical scaling (scale up) | Horizontal scaling (scale out) |
| Query Language | SQL (standardized) | Varies by database (MongoDB Query, CQL) |
| Best For | Complex queries, joins, transactions, financial data | Flexible data, high write throughput, rapid development |
| Examples | PostgreSQL, MySQL, Oracle, SQL Server | MongoDB, Cassandra, Redis, Neo4j, DynamoDB |
Relational Databases (SQL)
Normalization Forms
1NF (First Normal Form)
Each column contains atomic values, no repeating groups
❌ orders: customer_phones: "123, 456"
✅ phone_numbers: table with customer_id
2NF (Second Normal Form)
1NF + no partial dependencies on composite keys
❌ order_items: (order_id, product_id, customer_name)
✅ customer_name in orders table only
3NF (Third Normal Form)
2NF + no transitive dependencies
❌ orders: (id, customer_id, customer_city, city_zip)
✅ city_zip in separate cities table
When to Denormalize
While normalization reduces redundancy, denormalization can improve read performance:
- Read-heavy workloads where joins are expensive
- Reporting and analytics databases
- Frequently accessed data that rarely changes
- When caching isn't sufficient
Database Indexing Strategies
| Index Type | How It Works | Best For | Trade-offs |
|---|---|---|---|
| B-Tree Index | Balanced tree structure, sorted data | Range queries, sorting, unique constraints | Write overhead, storage space |
| Hash Index | Hash function maps keys to locations | Exact match lookups (WHERE id = 123) | No range queries, no sorting |
| Full-Text Index | Inverted index for text search | Search in text fields, fuzzy matching | Large storage, complex queries |
| Composite Index | Index on multiple columns (a, b, c) | Queries filtering on multiple columns | Column order matters for performance |
| Covering Index | Index includes all queried columns | Avoid table lookups, fast reads | Larger index size |
| Partial Index | Index only subset of rows (WHERE clause) | Frequent queries on specific data | Smaller size, faster writes |
-- Creating indexes in PostgreSQL
-- Simple B-tree index
CREATE INDEX idx_users_email ON users(email);
-- Composite index (order matters!)
CREATE INDEX idx_orders_customer_date ON orders(customer_id, created_at);
-- Partial index (only active users)
CREATE INDEX idx_active_users ON users(email) WHERE status = 'active';
-- Covering index (includes columns in SELECT)
CREATE INDEX idx_users_covering ON users(email) INCLUDE (name, created_at);
-- Full-text search index
CREATE INDEX idx_posts_search ON posts USING GIN(to_tsvector('english', content));
-- Unique index
CREATE UNIQUE INDEX idx_users_email_unique ON users(email);
NoSQL Database Categories
Document Databases
MongoDB, CouchDB, Firestore
Store data as JSON-like documents with flexible schemas.
{
"_id": "user123",
"name": "John Doe",
"email": "john@example.com",
"addresses": [
{
"type": "home",
"street": "123 Main St",
"city": "NYC"
}
],
"preferences": {
"theme": "dark",
"notifications": true
}
}
Best For:
- • Content management systems
- • User profiles and catalogs
- • Real-time analytics
- • Rapid prototyping
Key-Value Stores
Redis, Memcached, DynamoDB
Simple key-value pairs, extremely fast lookups.
// Redis commands
SET user:123:session "abc-def-789"
GET user:123:session
// Returns: "abc-def-789"
HSET user:123 name "John" email "j@example.com"
HGET user:123 name
// Returns: "John"
EXPIRE user:123:session 3600 // TTL: 1 hour
Best For:
- • Caching layer
- • Session management
- • Real-time leaderboards
- • Rate limiting counters
Column-Family Stores
Cassandra, HBase, ScyllaDB
Store data in column families, optimized for write-heavy workloads.
-- CQL (Cassandra Query Language)
CREATE TABLE user_activity (
user_id UUID,
timestamp TIMESTAMP,
action TEXT,
metadata MAP,
PRIMARY KEY (user_id, timestamp)
) WITH CLUSTERING ORDER BY (timestamp DESC);
-- Write-optimized: append-only log structure
Best For:
- • Time-series data
- • IoT sensor data
- • Event logging
- • High write throughput
Graph Databases
Neo4j, ArangoDB, Amazon Neptune
Store nodes and relationships, optimized for connected data.
// Cypher Query (Neo4j)
MATCH (user:User {name: "John"})-[:FRIENDS_WITH]->(friend)
-[:LIKES]->(post:Post)
WHERE post.created_at > date('2025-01-01')
RETURN friend.name, post.title
// Find friends' posts efficiently
Best For:
- • Social networks
- • Recommendation engines
- • Fraud detection
- • Knowledge graphs
Database Sharding & Partitioning
Sharding splits your database horizontally across multiple servers to handle massive scale.
Hash-Based Sharding
hash(user_id) % num_shards = shard_id
Pro: Even distribution
Con: Resharding is hard
Range-Based Sharding
user_id 1-1M → Shard1, 1M-2M → Shard2
Pro: Range queries fast
Con: Hotspots possible
Geo-Based Sharding
US users → US shard, EU → EU shard
Pro: Low latency, compliance
Con: Uneven loads
Sharding Challenges:
- • Cross-shard queries are expensive
- • No distributed transactions (need eventual consistency)
- • Resharding requires downtime or complex migration
- • Application logic must be shard-aware
Storage Concepts
Modern applications require different storage solutions for different types of data. Understanding the characteristics, performance, and cost implications of each storage type is crucial for system design.
Storage Types Comparison
| Storage Type | Characteristics | Use Cases | Examples | Cost |
|---|---|---|---|---|
| Block Storage | Raw storage volumes, low latency, high IOPS | Databases, boot volumes, transactional data | AWS EBS, Azure Disk, SAN | $$$$ |
| Object Storage | Unlimited scale, HTTP access, metadata-rich | Media files, backups, static assets, archives | AWS S3, Google Cloud Storage, Azure Blob | $ |
| File Storage | Hierarchical, shared access, POSIX compliant | Shared documents, home directories, CMS | AWS EFS, Azure Files, NFS, SMB | $$$ |
| Cold Storage | Archival, rare access, retrieval delays | Compliance archives, backups, logs | AWS Glacier, Google Coldline | ¢ |
Block Storage
Provides raw storage blocks that can be formatted with any file system. Attached to VMs/containers as volumes.
Characteristics:
- Low Latency: 1-5ms read/write
- High IOPS: 16,000-64,000 IOPS for SSDs
- Snapshots: Point-in-time backups
- Encryption: At-rest and in-transit
AWS EBS Volume Types:
- gp3 (General Purpose SSD): 3,000-16,000 IOPS
- io2 (Provisioned IOPS SSD): Up to 64,000 IOPS
- st1 (Throughput Optimized HDD): Big data, logs
- sc1 (Cold HDD): Infrequent access
Object Storage
Stores data as objects with unique identifiers, accessible via HTTP/HTTPS APIs. Highly scalable and durable.
// AWS S3 Structure
s3://my-bucket/users/avatars/user-123.jpg
Object Components:
- Key: users/avatars/user-123.jpg
- Value: Binary data
- Metadata: Content-Type, Cache-Control, custom tags
- Version ID: (if versioning enabled)
- Access Control: IAM policies, bucket policies
// Upload with metadata
PUT /users/avatars/user-123.jpg HTTP/1.1
Host: my-bucket.s3.amazonaws.com
Content-Type: image/jpeg
x-amz-storage-class: INTELLIGENT_TIERING
x-amz-server-side-encryption: AES256
S3 Storage Classes
- S3 Standard: $0.023/GB, frequent access
- S3 Intelligent-Tiering: Auto-moves between tiers
- S3 Infrequent Access: $0.0125/GB, monthly access
- S3 Glacier: $0.004/GB, archival (hours retrieval)
- S3 Glacier Deep Archive: $0.00099/GB (12hrs retrieval)
Best Practices
- ✓ Use CloudFront CDN for static assets
- ✓ Enable versioning for critical data
- ✓ Lifecycle policies for cost optimization
- ✓ Multipart upload for files > 100MB
- ✓ Use presigned URLs for temporary access
- ✓ Enable server access logging
Content Delivery Network (CDN)
CDNs distribute content to edge locations worldwide, reducing latency and improving user experience.
User in Japan User in USA User in Europe
| | |
| | |
[Tokyo Edge] [LA Edge] [London Edge]
| | |
↓ ↓ ↓
←------ Cache Miss ----→ [Origin Server (US-East)] ←----- Cache Miss -----→
(Your application/S3)
Cache Hit: ~20-50ms latency
Origin Request: 200-500ms latency
CDN Benefits:
✓ 80-95% requests served from edge (cache hits)
✓ Reduced origin server load
✓ DDoS protection (CloudFlare, Akamai)
✓ SSL/TLS termination at edge
✓ Image optimization & compression
Cache Strategies
- Cache-Control: max-age=3600
Browser caches for 1 hour - ETag / If-None-Match
Conditional requests - Vary: Accept-Encoding
Separate cache for gzip - Immutable assets
main.abc123.js (versioned)
Popular CDNs
- Cloudflare: 275+ locations, free tier
- AWS CloudFront: 450+ PoPs, AWS integration
- Akamai: Enterprise, 4000+ servers
- Fastly: Real-time purging, VCL
What to Cache
- ✓ Static assets (JS, CSS, images)
- ✓ Product catalog pages
- ✓ API responses (with short TTL)
- ✗ User-specific content
- ✗ Checkout/payment pages
Distributed File Systems
HDFS (Hadoop Distributed File System)
Store massive datasets across clusters, fault-tolerant with replication.
- • Block size: 128MB default
- • Replication factor: 3x
- • Write once, read many
- • Best for batch processing
GFS (Google File System)
Inspiration for HDFS, powers Google's infrastructure.
- • Chunk size: 64MB
- • Master-slave architecture
- • Optimized for large files
- • Used by BigTable, MapReduce
Amazon EFS (Elastic File System)
Managed NFS for AWS, scales automatically.
- • POSIX-compliant
- • Multi-AZ replication
- • Pay for what you use
- • Works with EC2, Lambda, ECS
Ceph
Open-source, unified storage (object + block + file).
- • CRUSH algorithm for data placement
- • Self-healing and self-managing
- • Used by OpenStack
- • Highly scalable
Caching & Performance
Caching is one of the most powerful techniques to improve system performance. By storing frequently accessed data in fast storage, you can reduce database load, lower latency, and handle more traffic with the same infrastructure.
Multi-Layer Caching Architecture
┌─────────────────────────────────────────────────────────┐
│ Layer 1: Browser Cache │
│ (localStorage, sessionStorage, Cache API) │
│ Latency: ~1ms | Hit Rate: 60-70% │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ Layer 2: CDN / Edge Cache │
│ (CloudFlare, CloudFront, Akamai) │
│ Latency: ~20-50ms | Hit Rate: 80-90% │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ Layer 3: Application Cache (In-Memory) │
│ (Node cache, Guava, Caffeine) │
│ Latency: <1ms | Hit Rate: 70-85% │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ Layer 4: Distributed Cache (Redis) │
│ (Redis, Memcached, Hazelcast) │
│ Latency: 1-5ms | Hit Rate: 85-95% │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ Layer 5: Database Query Cache │
│ (MySQL Query Cache, PostgreSQL) │
│ Latency: 5-20ms | Hit Rate: 60-80% │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ Database (Disk Storage) │
│ Latency: 50-200ms (Cache Miss) │
└─────────────────────────────────────────────────────────┘
Cache Hit Rate Impact
Example: Database query takes 100ms, cache lookup takes 1ms
- 90% hit rate: Average: (0.9 × 1ms) + (0.1 × 100ms) = 10.9ms
- 95% hit rate: Average: (0.95 × 1ms) + (0.05 × 100ms) = 5.95ms
- 99% hit rate: Average: (0.99 × 1ms) + (0.01 × 100ms) = 1.99ms
Takeaway: Going from 90% to 99% hit rate gives you 5x improvement!
Caching Strategies & Patterns
Cache-Aside (Lazy Loading)
Application Logic:
1. Check cache
2. If HIT → return cached data
3. If MISS:
a. Query database
b. Store in cache
c. Return data
[App] → [Cache] (miss)
↓ ↓
└──→ [Database]
↓
Store in cache
Return to app
// Cache-Aside Pattern
async function getUser(userId) {
// 1. Try cache first
const cached = await redis.get(`user:${userId}`);
if (cached) {
return JSON.parse(cached);
}
// 2. Cache miss - query DB
const user = await db.query(
'SELECT * FROM users WHERE id = ?',
[userId]
);
// 3. Store in cache (TTL: 1 hour)
await redis.setex(
`user:${userId}`,
3600,
JSON.stringify(user)
);
return user;
}
Pros & Cons:
✓ Only cache what's needed
✓ Cache failures don't break app
✗ Initial request is slow (cache miss)
✗ Cache stampede risk
Write-Through
Write Operation:
1. Write to cache
2. Cache writes to DB
3. Return success
[App] → [Cache] → [Database]
↓
Always in sync
Read Operation:
[App] → [Cache] (always HIT)
// Write-Through Pattern
async function updateUser(userId, data) {
// 1. Update cache
await redis.set(
`user:${userId}`,
JSON.stringify(data)
);
// 2. Write to database
await db.query(
'UPDATE users SET ? WHERE id = ?',
[data, userId]
);
return data;
}
// Reads are always fast
async function getUser(userId) {
const cached = await redis.get(`user:${userId}`);
return JSON.parse(cached);
}
Pros & Cons:
✓ Cache always fresh
✓ Predictable read performance
✗ Write latency increased
✗ Unused data cached
Write-Behind (Write-Back)
Write Operation:
1. Write to cache (fast)
2. Return success immediately
3. Async batch write to DB
[App] → [Cache] → Queue
↓ ↓
Return fast [Worker]
↓
[Database]
// Write-Behind Pattern
async function updateUser(userId, data) {
// 1. Write to cache immediately
await redis.set(`user:${userId}`, JSON.stringify(data));
// 2. Add to write queue (async)
await redis.lpush('db_write_queue', JSON.stringify({
type: 'UPDATE_USER',
userId,
data,
timestamp: Date.now()
}));
return data; // Return immediately
}
// Background worker processes queue
setInterval(async () => {
const writes = await redis.lrange('db_write_queue', 0, 99);
// Batch write to DB
await db.batchUpdate(writes);
await redis.ltrim('db_write_queue', 100, -1);
}, 5000); // Every 5 seconds
Pros & Cons:
✓ Fastest write performance
✓ Can batch DB operations
✗ Risk of data loss
✗ Complex consistency
Read-Through
Read Operation:
1. App requests from cache
2. Cache fetches from DB if miss
3. Cache stores and returns
[App] → [Cache]
↓ (miss)
[Database]
↓
Auto-populate cache
// Cache library handles DB fetch
const cacheConfig = {
store: redisStore,
ttl: 3600,
// Cache fetches from DB automatically
refresh: async (key) => {
const userId = key.split(':')[1];
return await db.query(
'SELECT * FROM users WHERE id = ?',
[userId]
);
}
};
// Application code is simple
const user = await cache.get(`user:${userId}`);
Pros & Cons:
✓ Simplified app logic
✓ Automatic cache population
✗ Initial request slow
✗ Less control
Redis: Advanced Caching Techniques
Redis Data Structures
// 1. Strings (simple cache)
SET user:123 "John Doe" EX 3600
GET user:123
// 2. Hash (structured data)
HSET user:123 name "John" email "j@ex.com"
HGETALL user:123
// 3. Lists (queues, activity feeds)
LPUSH notifications:user:123 "New message"
LRANGE notifications:user:123 0 9
// 4. Sets (unique items, tags)
SADD user:123:tags "developer" "golang"
SISMEMBER user:123:tags "developer"
// 5. Sorted Sets (leaderboards)
ZADD leaderboard 1000 "player1" 950 "player2"
ZREVRANGE leaderboard 0 9 WITHSCORES
// 6. HyperLogLog (count unique)
PFADD unique_visitors user1 user2 user3
PFCOUNT unique_visitors
// 7. Bitmap (boolean flags)
SETBIT active_users:2025-11-08 123 1
BITCOUNT active_users:2025-11-08
Redis Use Cases
-
Session Store:
SETEX session:abc123 1800 '{"user_id":123}' -
Rate Limiting:
INCR api:user:123:count EXPIRE api:user:123:count 3600 // Allow 1000 requests per hour -
Real-time Leaderboard:
ZINCRBY game:scores 10 "player123" ZREVRANK game:scores "player123" -
Pub/Sub Messaging:
PUBLISH notifications "New order #123" SUBSCRIBE notifications -
Distributed Lock:
SET lock:resource:123 "uuid" NX EX 30
Cache Stampede Problem & Solution
Problem: Cache Stampede
When cache expires, multiple requests simultaneously hit the database, causing overload.
Cache expires at 10:00:00
10:00:00.001 - Request 1 → DB
10:00:00.002 - Request 2 → DB
10:00:00.003 - Request 3 → DB
... 1000 requests hit DB!
Solution: Locking
async function getWithLock(key) {
let data = await redis.get(key);
if (data) return data;
// Try to acquire lock
const lockKey = `lock:${key}`;
const acquired = await redis.set(
lockKey, '1', 'NX', 'EX', 10
);
if (acquired) {
// This request fetches from DB
data = await db.query(...);
await redis.setex(key, 3600, data);
await redis.del(lockKey);
} else {
// Wait and retry
await sleep(100);
return getWithLock(key);
}
return data;
}
Cache Eviction Policies
| Policy | Description | Best For | Implementation |
|---|---|---|---|
| LRU (Least Recently Used) |
Evict items not accessed for longest time | General purpose, access patterns vary | Doubly linked list + hash map |
| LFU (Least Frequently Used) |
Evict items with lowest access count | When popularity matters (trending content) | Min-heap + hash map |
| FIFO (First In First Out) |
Evict oldest items first | Time-based data, logs, simple scenarios | Queue |
| TTL (Time To Live) |
Items expire after set duration | Session data, temporary data | Expiration timestamps |
| Random | Evict random items | Fast, when access patterns unpredictable | Random selection |
Scalability Strategies
Scalability is the ability of a system to handle increased load by adding resources. It's not just about handling more users—it's about doing so efficiently while maintaining performance and reliability.
Vertical vs Horizontal Scaling
Vertical Scaling (Scale Up)
┌─────────────────┐ ┌─────────────────┐
│ 4 CPU Cores │ │ 16 CPU Cores │
│ 16 GB RAM │ → │ 64 GB RAM │
│ 500 GB SSD │ │ 2 TB NVMe │
└─────────────────┘ └─────────────────┘
Current Server Upgraded Server
Advantages:
- ✓ Simple - no code changes needed
- ✓ No distributed system complexity
- ✓ Better for single-threaded apps
- ✓ ACID transactions easier
- ✓ Lower network latency
Disadvantages:
- ✗ Hardware limits (max CPU/RAM)
- ✗ Single point of failure
- ✗ Downtime during upgrades
- ✗ Expensive (non-linear cost)
- ✗ Limited by single machine
Best For:
Databases, monolithic apps, early-stage startups, quick wins
Horizontal Scaling (Scale Out)
┌──────────┐ ┌──────────┐
│ Server 1 │ │ Server 1 │
│ 4 CPU │ → │ 4 CPU │
│ 16 GB │ │ 16 GB │
└──────────┘ └──────────┘
┌──────────┐
│ Server 2 │
│ 4 CPU │
│ 16 GB │
└──────────┘
┌──────────┐
│ Server 3 │
│ 4 CPU │
│ 16 GB │
└──────────┘
Advantages:
- ✓ Infinite scalability (add more servers)
- ✓ High availability (redundancy)
- ✓ No downtime for scaling
- ✓ Cost-effective (commodity hardware)
- ✓ Geographic distribution
Disadvantages:
- ✗ Complex architecture
- ✗ Data consistency challenges
- ✗ Network latency between nodes
- ✗ Load balancing needed
- ✗ More operational overhead
Best For:
Web servers, microservices, stateless applications, cloud-native apps
When to Use Each Strategy
Start with Vertical Scaling: It's simpler and faster to implement. Vertical scaling is perfect when you're below the limit of a single powerful machine.
Move to Horizontal Scaling: When you hit hardware limits, need high availability, or when cost becomes prohibitive. Most modern systems eventually require horizontal scaling.
Database Scaling Techniques
Database Replication
Master-Slave (Primary-Replica)
┌─────────────┐
│ MASTER │
│ (Writes) │
└──────┬──────┘
│ Replicate
┌───────────┼───────────┐
↓ ↓ ↓
┌────────┐ ┌────────┐ ┌────────┐
│ SLAVE1 │ │ SLAVE2 │ │ SLAVE3 │
│(Reads) │ │(Reads) │ │(Reads) │
└────────┘ └────────┘ └────────┘
Write: 1 server | Read: 3+ servers
Use Case: Read-heavy workloads (90% reads, 10% writes)
Replication Lag: Async replication may cause 0.1-1s delay
Example: E-commerce product catalog
Multi-Master (Active-Active)
┌──────────┐ ←→ ┌──────────┐
│ MASTER 1 │ ←→ │ MASTER 2 │
│ (R/W) │ ←→ │ (R/W) │
└──────────┘ └──────────┘
↕ ↕
Bi-directional Replication
Both accept writes simultaneously
Use Case: Multi-region, high availability
Challenge: Conflict resolution needed
Example: Global CRM system
// MySQL Master-Slave Configuration
// Master (my.cnf):
server-id = 1
log-bin = /var/log/mysql/mysql-bin.log
binlog_do_db = production_db
// Slave (my.cnf):
server-id = 2
relay-log = /var/log/mysql/relay-bin
read_only = 1
// Application Code (using read replicas)
const dbConfig = {
master: { host: 'master.db.com', port: 3306 },
slaves: [
{ host: 'slave1.db.com', port: 3306 },
{ host: 'slave2.db.com', port: 3306 }
]
};
// Write to master
await masterDB.query('INSERT INTO users VALUES (...)');
// Read from slaves (round-robin)
const randomSlave = slaves[Math.floor(Math.random() * slaves.length)];
await slaveDB.query('SELECT * FROM users WHERE ...');
Database Sharding (Horizontal Partitioning)
Split data across multiple databases based on a shard key. Each shard is a complete database with subset of data.
Hash-Based
shard = hash(user_id) % num_shards
user_id: 123
hash(123) = 8472
8472 % 4 = 0 → Shard 0
Pro: Even distribution
Con: Hard to add shards
Range-Based
user_id 1-1M → Shard 0
user_id 1M-2M → Shard 1
user_id 2M-3M → Shard 2
Pro: Easy range queries
Con: Hotspots
Directory-Based
Lookup table:
user_id: 123 → Shard 2
user_id: 456 → Shard 1
Pro: Flexible
Con: Extra lookup
Sharding Challenges
- ❌ No JOINs across shards: Need application-level joins
- ❌ No foreign keys across shards: Referential integrity at app level
- ❌ Resharding is complex: Requires data migration
- ❌ Uneven data distribution: Some shards may be larger
Table Partitioning (Vertical Partitioning)
Split table columns into multiple tables based on access patterns.
Before Partitioning
users table:
┌────────┬────────┬──────────┬──────────┬──────────┐
│ id │ name │ email │ avatar │ bio │
├────────┼────────┼──────────┼──────────┼──────────┤
│ 123 │ John │ j@ex.com │ (1MB) │ (text) │
└────────┴────────┴──────────┴──────────┴──────────┘
Problem: Large rows, slow queries
After Partitioning
users_core (frequently accessed):
┌────────┬────────┬──────────┐
│ id │ name │ email │
└────────┴────────┴──────────┘
users_profile (rarely accessed):
┌────────┬──────────┬──────────┐
│ id │ avatar │ bio │
└────────┴──────────┴──────────┘
Benefit: Faster queries, better caching
Auto-Scaling Strategies
Reactive Scaling
Scale based on current metrics (CPU, memory, request rate)
If CPU > 70% for 5 minutes → Add 2 instances
If CPU < 30% for 10 minutes → Remove 1 instance
Scheduled Scaling
Scale based on predictable patterns
8 AM - 10 AM: Scale to 20 instances
10 PM - 6 AM: Scale to 5 instances
Black Friday: Scale to 100 instances
Predictive Scaling
Use ML to predict load and scale proactively
Analyze 30 days of traffic
Predict: Monday 3 PM will have 50% more traffic
Scale up 15 minutes before
// AWS Auto Scaling Configuration Example
{
"MinSize": 2,
"MaxSize": 50,
"DesiredCapacity": 5,
"TargetTrackingConfiguration": {
"PredefinedMetricSpecification": {
"PredefinedMetricType": "ASGAverageCPUUtilization",
"TargetValue": 70.0
},
"ScaleOutCooldown": 300, // Wait 5 min before scaling out again
"ScaleInCooldown": 600 // Wait 10 min before scaling in
},
"StepScalingPolicy": {
"AdjustmentType": "PercentChangeInCapacity",
"StepAdjustments": [
{ "MetricIntervalLowerBound": 0, "MetricIntervalUpperBound": 10, "ScalingAdjustment": 10 },
{ "MetricIntervalLowerBound": 10, "MetricIntervalUpperBound": 20, "ScalingAdjustment": 20 },
{ "MetricIntervalLowerBound": 20, "ScalingAdjustment": 30 }
]
}
}
Load Balancing
Load balancers distribute incoming traffic across multiple servers, ensuring no single server becomes overwhelmed. They're critical for high availability, scalability, and performance.
Load Balancing Algorithms
Round Robin
Request 1 → Server A
Request 2 → Server B
Request 3 → Server C
Request 4 → Server A
Request 5 → Server B
Distributes requests sequentially to each server in turn.
Pros: Simple, fair distribution
Cons: Ignores server capacity
Best for: Uniform server specs
Weighted Round Robin
Server A (weight: 3)
Server B (weight: 2)
Server C (weight: 1)
Pattern:
A, A, A, B, B, C (repeat)
Assigns more requests to servers with higher weights.
Pros: Handles different capacities
Cons: Static weights
Best for: Mixed server specs
Least Connections
Server A: 5 connections
Server B: 8 connections
Server C: 3 connections
New request → Server C
(has least connections)
Routes to server with fewest active connections.
Pros: Accounts for load
Cons: Overhead tracking
Best for: Long connections
Least Response Time
Server A: 50ms avg
Server B: 120ms avg
Server C: 30ms avg
New request → Server C
(fastest response)
Routes to server with lowest response time.
Pros: Best user experience
Cons: Complex calculation
Best for: Performance-critical
IP Hash
Client IP: 192.168.1.100
hash(192.168.1.100) % 3 = 1
Always routes to Server B
(session persistence)
Routes based on client IP hash.
Pros: Session persistence
Cons: Uneven distribution
Best for: Stateful apps
Random
Request 1 → Server B
Request 2 → Server A
Request 3 → Server B
Request 4 → Server C
(randomly selected)
Selects server randomly.
Pros: Simple, fast
Cons: May be uneven
Best for: Stateless, uniform load
Layer 4 vs Layer 7 Load Balancing
Layer 4 (Transport Layer)
Routes based on IP address and TCP/UDP port. Doesn't inspect packet content.
Client Request
↓
[L4 Load Balancer]
│
├→ Looks at: IP + Port
├→ Decision: TCP 192.168.1.100:443
↓
Routes to Server B
Characteristics:
- ✓ Very fast (no content inspection)
- ✓ Low latency (1-2ms overhead)
- ✓ Handles any protocol (HTTP, WebSocket, database)
- ✓ High throughput (millions of connections)
- ✗ No content-based routing
- ✗ No SSL termination
Examples:
AWS NLB, HAProxy (TCP mode), NGINX stream module
Use Case: Database load balancing, TCP applications, ultra-low latency
Layer 7 (Application Layer)
Routes based on HTTP headers, URLs, cookies. Full content inspection.
Client Request: GET /api/users
↓
[L7 Load Balancer]
│
├→ Looks at: URL, Headers, Cookies
├→ Decision: /api/* → API servers
↓
Routes to API Server Pool
Characteristics:
- ✓ Content-based routing
- ✓ SSL/TLS termination
- ✓ URL rewriting, redirects
- ✓ Authentication, rate limiting
- ✗ Higher latency (10-50ms)
- ✗ More resource intensive
Examples:
AWS ALB, NGINX, HAProxy (HTTP mode), Traefik
Use Case: Web applications, microservices routing, API gateways
// NGINX Layer 7 Configuration
http {
upstream api_servers {
server api1.example.com:8080;
server api2.example.com:8080;
server api3.example.com:8080;
}
upstream web_servers {
server web1.example.com:3000;
server web2.example.com:3000;
}
server {
listen 80;
# Route API requests to API servers
location /api/ {
proxy_pass http://api_servers;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host;
}
# Route static content to web servers
location / {
proxy_pass http://web_servers;
proxy_cache my_cache;
proxy_cache_valid 200 1h;
}
# Route admin to specific server
location /admin {
proxy_pass http://admin.example.com:9000;
auth_basic "Admin Area";
auth_basic_user_file /etc/nginx/.htpasswd;
}
}
}
Health Checks & Failover
Health checks ensure traffic only goes to healthy servers. When a server fails, the load balancer automatically routes traffic elsewhere.
Types of Health Checks
1. TCP Health Check
Check if server accepts TCP connections
Every 10 seconds:
Try to connect to server:80
If connection successful → Healthy
If 3 consecutive failures → Unhealthy
2. HTTP Health Check
Send HTTP request, check response code
GET /health HTTP/1.1
Expected: 200 OK
If 2xx/3xx → Healthy
If 4xx/5xx or timeout → Unhealthy
3. Custom Health Check
Check specific conditions (DB connection, disk space)
// /health endpoint
{
"status": "healthy",
"database": "connected",
"disk_space": "85%",
"response_time": "50ms"
}
Health Check Configuration
// AWS ALB Health Check
{
"HealthCheckProtocol": "HTTP",
"HealthCheckPath": "/health",
"HealthCheckIntervalSeconds": 30,
"HealthCheckTimeoutSeconds": 5,
"HealthyThresholdCount": 2,
"UnhealthyThresholdCount": 3,
"Matcher": {
"HttpCode": "200-299"
}
}
// Scenario:
// Check every 30 seconds
// Wait 5 seconds for response
// 2 consecutive successes → Healthy
// 3 consecutive failures → Unhealthy
Timeline:
00:00 - Check failed (1/3)
00:30 - Check failed (2/3)
01:00 - Check failed (3/3) → UNHEALTHY
(Remove from pool)
01:30 - Check success (1/2)
02:00 - Check success (2/2) → HEALTHY
(Add back to pool)
Failover Strategies
Active-Active
All servers handle traffic simultaneously
- ✓ Maximum resource utilization
- ✓ No idle servers
- ✓ Better performance
Active-Passive
Standby servers activated only on failure
- ✓ Simple failover
- ✓ Clear primary/backup
- ✗ Resource waste (idle servers)
Global Load Balancing (GSLB)
Route users to the nearest data center based on geography, reducing latency.
User in Japan User in USA User in Europe
↓ ↓ ↓
[DNS Query: api.example.com]
↓ ↓ ↓
[Global Load Balancer / GeoDNS]
↓ ↓ ↓
Returns: Returns: Returns:
Tokyo DC Virginia DC London DC
13.230.10.50 54.88.45.20 18.130.70.30
↓ ↓ ↓
Latency: 20ms Latency: 30ms Latency: 25ms
GeoDNS Routing
Route based on user's geographic location
AWS Route 53, Cloudflare, NS1
Latency-Based Routing
Route to region with lowest latency
Measure actual latency, not just distance
Failover Routing
Redirect to backup region if primary fails
Automatic disaster recovery
Message Queues & Streaming
Message queues and streaming platforms enable asynchronous communication between services, decoupling components and improving system resilience. They're essential for building scalable, event-driven architectures.
Message Queue vs Event Streaming
| Aspect | Message Queue | Event Streaming |
|---|---|---|
| Purpose | Task distribution, job processing | Real-time data pipelines, event sourcing |
| Message Retention | Deleted after consumption | Retained for days/weeks (configurable) |
| Consumption Model | Competitive (one consumer per message) | Publish-subscribe (multiple consumers) |
| Ordering | FIFO within queue (optional) | Strict ordering within partition |
| Performance | Thousands of messages/second | Millions of messages/second |
| Examples | RabbitMQ, AWS SQS, Azure Service Bus | Apache Kafka, AWS Kinesis, Pulsar |
| Use Cases | Email sending, image processing, task queue | Activity tracking, log aggregation, analytics |
RabbitMQ: Message Queue Deep Dive
┌────────────┐ ┌──────────────┐ ┌────────────┐ ┌────────────┐
│ Producer │────────→│ Exchange │────────→│ Queue │────────→│ Consumer │
│ (App) │ Publish│ (Routing) │ Bind │ (Buffer) │ Consume│ (Worker) │
└────────────┘ └──────────────┘ └────────────┘ └────────────┘
Exchange Types:
1. Direct - Route by exact routing key match
2. Fanout - Broadcast to all queues
3. Topic - Route by pattern matching (*.error, logs.#)
4. Headers - Route by message headers
RabbitMQ Example: Task Queue
// Producer (Send tasks)
const amqp = require('amqplib');
async function sendTask(taskData) {
const connection = await amqp.connect('amqp://localhost');
const channel = await connection.createChannel();
const queue = 'task_queue';
await channel.assertQueue(queue, {
durable: true // Survive broker restart
});
channel.sendToQueue(
queue,
Buffer.from(JSON.stringify(taskData)),
{ persistent: true } // Survive queue restart
);
console.log('Task sent:', taskData);
await channel.close();
await connection.close();
}
// Usage
await sendTask({
type: 'send_email',
to: 'user@example.com',
subject: 'Welcome'
});
Worker (Process tasks)
// Consumer (Process tasks)
async function startWorker() {
const connection = await amqp.connect('amqp://localhost');
const channel = await connection.createChannel();
const queue = 'task_queue';
await channel.assertQueue(queue, { durable: true });
// Fair dispatch: don't give worker more than 1 task
channel.prefetch(1);
console.log('Worker waiting for tasks...');
channel.consume(queue, async (msg) => {
const task = JSON.parse(msg.content.toString());
console.log('Processing:', task);
try {
// Process task (e.g., send email)
await processTask(task);
// Acknowledge success
channel.ack(msg);
} catch (error) {
// Reject and requeue if failed
channel.nack(msg, false, true);
}
});
}
startWorker();
RabbitMQ Patterns
Work Queue
Distribute tasks among workers
Use: Background jobs, image processing
Pub/Sub
Broadcast messages to subscribers
Use: Notifications, logging
RPC
Request-reply pattern
Use: Async API calls
Apache Kafka: Event Streaming Platform
Kafka Cluster Architecture:
┌─────────────────────────────────────────────────────────────────┐
│ Kafka Cluster │
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ │
│ │ Broker 1 │ │ Broker 2 │ │ Broker 3 │ │
│ │ │ │ │ │ │ │
│ │ Topic: logs│ │ Topic: logs│ │ Topic: logs│ │
│ │ Partition 0│ │ Partition 1│ │ Partition 2│ │
│ │ (Leader) │ │ (Replica) │ │ (Replica) │ │
│ └────────────┘ └────────────┘ └────────────┘ │
└─────────────────────────────────────────────────────────────────┘
↑ ↑ ↑
│ │ │
┌───────┴───────┐ ┌───────┴───────┐ ┌───────┴───────┐
│ Producer 1 │ │ Producer 2 │ │ Producer 3 │
│ (Web Server) │ │ (Mobile App) │ │ (IoT Device) │
└───────────────┘ └───────────────┘ └───────────────┘
↓ ↓ ↓
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
│ Consumer │ │ Consumer │ │ Consumer │
│ Group 1 │ │ Group 2 │ │ Group 3 │
│ (Analytics) │ │ (Monitoring) │ │ (Backup) │
└───────────────┘ └───────────────┘ └───────────────┘
Each consumer group independently reads all events
Kafka Producer
// Kafka Producer
const { Kafka } = require('kafkajs');
const kafka = new Kafka({
clientId: 'my-app',
brokers: ['kafka1:9092', 'kafka2:9092', 'kafka3:9092']
});
const producer = kafka.producer();
async function produceEvents() {
await producer.connect();
// Send single event
await producer.send({
topic: 'user-events',
messages: [
{
key: 'user-123', // Partition key
value: JSON.stringify({
event: 'user_login',
userId: 123,
timestamp: Date.now()
}),
headers: {
'correlation-id': 'abc-123'
}
}
]
});
// Batch send for better performance
await producer.sendBatch({
topicMessages: [
{
topic: 'user-events',
messages: Array.from({ length: 100 }, (_, i) => ({
key: `user-${i}`,
value: JSON.stringify({ event: 'page_view', userId: i })
}))
}
]
});
}
produceEvents();
Kafka Consumer
// Kafka Consumer
const consumer = kafka.consumer({
groupId: 'analytics-group',
// Start from earliest if new consumer
fromBeginning: true
});
async function consumeEvents() {
await consumer.connect();
await consumer.subscribe({
topics: ['user-events', 'order-events'],
// Listen to all partitions
fromBeginning: false
});
await consumer.run({
eachMessage: async ({ topic, partition, message }) => {
const event = JSON.parse(message.value.toString());
console.log({
topic,
partition,
offset: message.offset,
key: message.key.toString(),
event
});
// Process event
await handleEvent(event);
// Kafka auto-commits offset periodically
// Or manual: await consumer.commitOffsets([...])
}
});
}
consumeEvents();
Kafka Key Concepts
Topics & Partitions
- • Topic: Category of messages (e.g., "orders")
- • Partition: Ordered, immutable sequence of records
- • Offset: Unique ID of each record in partition
- • Replication: Each partition replicated across brokers
Consumer Groups
- • Each consumer in group reads from different partitions
- • Enables parallel processing
- • Automatic rebalancing on consumer add/remove
- • Each group maintains its own offset
Event-Driven Architecture Patterns
Event Sourcing
Store all changes as sequence of events instead of current state.
Example: Bank Account
// Traditional (store current state)
account_balance: $1000
// Event Sourcing (store events)
1. AccountCreated → $0
2. DepositMade → +$1500
3. WithdrawalMade → -$300
4. WithdrawalMade → -$200
Current Balance = $1000
Advantages:
✓ Full audit trail
✓ Replay events to rebuild state
✓ Time travel debugging
CQRS (Command Query Responsibility Segregation)
Separate read and write models for better scalability.
Commands (Write) Events Queries (Read)
↓ ↓ ↓
[Write Model] → [Event Store] → [Read Model]
(Postgres) (Kafka) (Elasticsearch)
(Redis cache)
Examples:
- Write optimized DB for commands
- Denormalized views for queries
- Event bus synchronizes them
Advantages:
✓ Scale reads/writes independently
✓ Optimize each model separately
Saga Pattern (Distributed Transactions)
Manage distributed transactions across microservices using compensating actions.
Choreography-Based Saga
// Order Processing Saga
1. Order Service
→ Create Order (PENDING)
→ Emit: OrderCreated event
2. Payment Service
← Listen: OrderCreated
→ Charge Payment
→ Emit: PaymentCompleted event
3. Inventory Service
← Listen: PaymentCompleted
→ Reserve Items
→ Emit: ItemsReserved event
4. Shipping Service
← Listen: ItemsReserved
→ Schedule Delivery
→ Emit: ShipmentScheduled
5. Order Service
← Listen: ShipmentScheduled
→ Update Order (CONFIRMED)
// If any step fails:
PaymentFailed event
→ Cancel Order (compensation)
Orchestration-Based Saga
// Saga Orchestrator
Orchestrator controls flow:
try {
// Step 1
order = await orderService.create();
// Step 2
payment = await paymentService.charge();
// Step 3
await inventoryService.reserve();
// Step 4
await shippingService.schedule();
// Complete
await orderService.confirm(order.id);
} catch (error) {
// Compensate in reverse order
await shippingService.cancel();
await inventoryService.release();
await paymentService.refund();
await orderService.cancel();
}
Real-World Use Cases
Email Notification System
Architecture: RabbitMQ + Worker Pool
[User Action] → [RabbitMQ Queue]
↓
┌──────────┼──────────┐
↓ ↓ ↓
[Worker 1] [Worker 2] [Worker 3]
↓ ↓ ↓
[SMTP Server (SendGrid)]
- Benefits:
- • Async - user doesn't wait for email
- • Retry failed emails automatically
- • Scale workers based on queue depth
- • Rate limiting to avoid spam filters
Real-Time Analytics Pipeline
Architecture: Kafka + Stream Processing
[Web/Mobile] → [Kafka: events topic]
↓
[Kafka Streams / Flink]
↓ ↓
[Aggregation] [Filtering]
↓ ↓
[Kafka: analytics] [Kafka: alerts]
↓ ↓
[Dashboard] [Alert Service]
- Benefits:
- • Real-time insights (seconds, not hours)
- • Handle millions of events/second
- • Replay for debugging/reprocessing
- • Multiple consumers for different purposes
Video Processing Pipeline
Architecture: SQS + Lambda + S3
[Upload] → [S3 Bucket]
↓ (trigger)
[SQS Queue]
↓
[Lambda: Transcode Job]
↓
[Generate: 1080p, 720p, 480p]
↓
[S3: Output Bucket]
↓
[Update: DB with URLs]
- Benefits:
- • Serverless - no server management
- • Auto-scaling based on uploads
- • Pay only for processing time
- • Handle spiky traffic (viral videos)
E-Commerce Order Processing
Architecture: Kafka + Microservices
[Order Created] → [Kafka]
↓
┌──────────────┼──────────────┐
↓ ↓ ↓
[Payment Svc] [Inventory Svc] [Email Svc]
↓ ↓ ↓
Charge Card Reserve Items Send Confirm
↓ ↓ ↓
└──────────────┼──────────────┘
↓
[Shipping Service]
- Benefits:
- • Loose coupling between services
- • Each service can scale independently
- • Failed steps can retry without affecting others
- • Easy to add new consumers (analytics, fraud detection)
CAP Theorem & Consistency Models
The CAP theorem is a fundamental principle in distributed systems that states you can only guarantee two out of three properties: Consistency, Availability, and Partition Tolerance. Understanding these trade-offs is crucial for designing distributed systems.
Understanding CAP Theorem
You Can Only Pick Two!
Consistency
Every read receives the most recent write or an error
Update user balance: $100 → $150
All nodes immediately see $150
No stale reads
Availability
Every request receives a response (success or failure)
System always responds
Even if some nodes are down
May return stale data
Partition Tolerance
System continues despite network partitions
Network splits cluster
Nodes can't communicate
System still operates
CP Systems
Consistency + Partition Tolerance
Network Partition Occurs:
Node A ←─X─→ Node B
Write to Node A → Success
Read from Node B → ERROR
(B can't confirm latest state)
System blocks until partition heals
Characteristics:
- • Strong consistency guaranteed
- • May become unavailable during partitions
- • Blocks operations until sync
- • Returns errors rather than stale data
Examples:
MongoDB, HBase, Redis (single instance), ZooKeeper
Use Cases:
Banking systems, inventory management, configuration management
AP Systems
Availability + Partition Tolerance
Network Partition Occurs:
Node A ←─X─→ Node B
Write to Node A → Success
Read from Node B → OLD DATA
(Returns last known state)
Both nodes accept requests
Characteristics:
- • Always available for reads/writes
- • May return stale data
- • Eventual consistency model
- • Continues during network issues
Examples:
Cassandra, DynamoDB, CouchDB, Riak
Use Cases:
Social media feeds, shopping carts, caching, analytics
CA Systems
Consistency + Availability
No Network Partition:
Node A ←───→ Node B
Write to Node A → Synced to B
Read from Node B → LATEST DATA
System fails if network partitions
Characteristics:
- • Strong consistency + high availability
- • Only works in single-node or LAN
- • Not viable for distributed systems
- • Network partitions will occur!
Examples:
Traditional RDBMS (single server), PostgreSQL (single node)
Reality:
In distributed systems, partitions are inevitable. CA is theoretical.
Important Reality: Partition Tolerance is Not Optional
In real distributed systems, network partitions will happen (hardware failures, network issues, datacenter problems). Therefore, the practical choice is between:
- CP: Sacrifice availability to maintain consistency during partitions
- AP: Sacrifice consistency to maintain availability during partitions
The real question: "When network fails, do you want your system to be unavailable but correct (CP), or available but potentially incorrect (AP)?"
ACID vs BASE: Transaction Models
ACID
Traditional database transaction model (CP systems)
AAtomicity
All operations in transaction succeed or all fail. No partial updates.
BEGIN TRANSACTION
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT;
If step 2 fails → step 1 is rolled back
All or nothing!
CConsistency
Database remains in valid state. All constraints, triggers, and rules are enforced.
// Example: Foreign key constraint
accounts (id, balance CHECK balance >= 0)
// Transaction that violates constraint fails
UPDATE accounts SET balance = -50 WHERE id = 1;
// ERROR: Check constraint violated
IIsolation
Concurrent transactions don't interfere. Appears as if executed serially.
Transaction 1: Read balance (100) → Write (150)
Transaction 2: Read balance (100) → Write (120)
With proper isolation:
T1 completes → balance = 150
T2 reads 150 → balance = 170
Without isolation:
Both read 100, both write → lost update!
DDurability
Committed transactions survive crashes. Persisted to non-volatile storage.
COMMIT TRANSACTION;
// Success response sent to client
[POWER FAILURE OCCURS]
After restart:
Data is still there (written to disk)
Write-ahead log ensures durability
Best For:
Financial systems, e-commerce orders, inventory management, any system where correctness is critical
Examples:
PostgreSQL, MySQL, Oracle, SQL Server
BASE
NoSQL/distributed system model (AP systems)
BBasically Available
System guarantees availability. May return stale data but always responds.
Request to read user profile:
Node A is down
↓
Redirect to Node B
↓
Return data (may be slightly stale)
↓
User gets response (not error)
SSoft State
State of system may change without input (due to eventual consistency).
Write to Node A: likes = 100
Read from Node B: likes = 95 (stale)
Wait a few seconds...
Read from Node B: likes = 100 (synced)
State changed without new writes!
EEventual Consistency
System will become consistent over time if no new updates are made.
Time 0: Write "status=active" to Node A
Time 1: Node B reads → "status=inactive" (stale)
Time 2: Replication completes
Time 3: Node B reads → "status=active" (consistent)
No strict timing guarantee
Eventually all nodes agree
Best For:
Social media, recommendations, analytics, shopping carts, any system where availability > consistency
Examples:
Cassandra, DynamoDB, Riak, CouchDB
Consistency Levels Spectrum
Different systems offer tunable consistency levels. You can choose the right trade-off for each operation.
Linearizable
Strongest guarantee. Appears as single copy.
Latency: Highest
Sequential
All operations in some total order.
Latency: High
Causal
Causally related ops ordered.
Latency: Medium
Read-your-writes
See own writes immediately.
Latency: Low
Eventual
Becomes consistent eventually.
Latency: Lowest
Tunable Consistency: Cassandra Example
Write Consistency Levels
// Replication Factor = 3
// ONE: Fast, low consistency
Write succeeds after 1 replica ACK
Latency: ~5ms
// QUORUM: Balanced (most common)
Write succeeds after 2/3 replicas ACK
Latency: ~15ms
// ALL: Strong consistency
Write succeeds after all 3 replicas ACK
Latency: ~50ms
(Fails if any replica down)
Read Consistency Levels
// ONE: Fastest, may be stale
Read from 1 replica
Latency: ~5ms
// QUORUM: Strong consistency
Read from 2/3 replicas, return latest
Latency: ~15ms
// ALL: Highest consistency
Read from all 3 replicas
Latency: ~50ms
Formula for strong consistency:
W + R > N (replication factor)
Example: Write QUORUM + Read QUORUM
2 + 2 > 3 ✓ (strong consistency)
Real-World Consistency Trade-offs
Banking System (CP - Strong Consistency)
Account balance must always be accurate. Correctness > Availability.
Design Choices:
- • Use ACID transactions (PostgreSQL, MySQL)
- • Synchronous replication
- • 2-phase commit for distributed transactions
- • Block operations during network partition
- • Better to show error than wrong balance
"I'd rather ATM says 'unavailable' than dispense $1000 when I only have $100"
Facebook News Feed (AP - Eventual Consistency)
Must always be available. Slight inconsistency acceptable. Availability > Consistency.
Design Choices:
- • Use Cassandra (AP system)
- • Eventual consistency model
- • Multiple data centers, async replication
- • Continue serving during network issues
- • OK if like count slightly delayed
"Better to show 99 likes instead of 100 than to show error page"
E-Commerce (Hybrid - Different Operations)
Mix strong and eventual consistency based on operation criticality.
Design Choices:
- Shopping Cart: Eventual (AP) - OK if items appear delayed
- Inventory: Strong (CP) - Prevent overselling
- Payment: ACID (CP) - Must be exact
- Product Reviews: Eventual (AP) - Slight delay OK
- Order Status: Eventual (AP) - Updates can lag
"Use the right consistency model for each use case"
Ticket Booking (CP - Strong Consistency)
Cannot oversell seats. Must prevent double booking. Correctness critical.
Design Choices:
- • Use distributed locks (Redis, ZooKeeper)
- • Optimistic locking with version numbers
- • Reserve seats with timeout (10 min hold)
- • Strong consistency for seat availability
- • Queue system for high demand events
"Better to queue users than to oversell concert tickets"
Security & Authentication
Security is not optional—it's fundamental. A single security breach can destroy user trust, result in massive fines, and damage your reputation permanently. Understanding authentication, authorization, encryption, and security best practices is crucial for every system architect.
Authentication vs Authorization
Authentication
"Who are you?"
Verifying the identity of a user or system. Proving you are who you claim to be.
Methods:
- Something you know: Password, PIN
- Something you have: Phone, security key
- Something you are: Fingerprint, face
- Multi-Factor Auth (MFA): Combine 2+ methods
// Authentication Example
POST /api/auth/login
{
"email": "user@example.com",
"password": "********"
}
Response:
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"user": { "id": 123, "email": "user@example.com" }
}
✓ Identity verified
Authorization
"What can you do?"
Determining what an authenticated user is allowed to access or perform.
Models:
- RBAC: Role-Based (admin, user, guest)
- ABAC: Attribute-Based (user.age > 18)
- ACL: Access Control Lists
- PBAC: Policy-Based Access Control
// Authorization Example
GET /api/admin/users
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Check: Does user have 'admin' role?
✓ Yes → Return data
✗ No → 403 Forbidden
Permission checked after authentication
Remember:
Authentication happens first (verify identity), then Authorization (check permissions). You can be authenticated but not authorized. Example: You're logged in (authenticated) but can't access admin panel (not authorized).
OAuth 2.0 & JWT Tokens
OAuth 2.0 Flow
OAuth 2.0 is an authorization framework that enables applications to obtain limited access to user accounts. Most common for "Login with Google/Facebook/GitHub".
Authorization Code Flow (Most Secure):
┌────────────┐ ┌──────────────┐
│ User │ │ Your App │
└─────┬──────┘ └──────┬───────┘
│ │
│ 1. Click "Login with Google" │
│───────────────────────────────────────────────────>│
│ │
│ 2. Redirect to Google │
│<───────────────────────────────────────────────────│
│ │
│ ┌─────────────────────┐ │
│────────>│ Google Auth Server │ │
│ └──────────┬──────────┘ │
│ 3. Enter credentials & authorize │
│ │ │
│ 4. Auth Code │ │
│<───────────────────┘ │
│ │
│ 5. Send auth code to Your App │
│───────────────────────────────────────────────────>│
│ │
│ ┌──────────────────────────────>│
│ │ 6. Exchange code for token │
│ ┌──────────┴──────────┐ │
│ │ Google Auth Server │ │
│ └──────────┬──────────┘ │
│ │ 7. Access Token │
│ │<──────────────────────────────│
│ │
│ 8. Access Token stored, user logged in │
│<───────────────────────────────────────────────────│
│ │
OAuth 2.0 Grant Types
Authorization Code
Most secure. For server-side apps. Uses secret key.
Use: Web applications
Implicit Flow
Token returned directly. Less secure. Deprecated.
Use: Legacy SPAs (don't use)
Client Credentials
Machine-to-machine auth. No user involved.
Use: API-to-API, microservices
PKCE (for mobile/SPA)
Secure for public clients. No client secret needed.
Use: Mobile apps, SPAs
Implementation Example
// Node.js with Passport.js
const passport = require('passport');
const GoogleStrategy = require('passport-google-oauth20');
passport.use(new GoogleStrategy({
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
callbackURL: "https://app.com/auth/google/callback"
},
function(accessToken, refreshToken, profile, done) {
// Find or create user in your database
User.findOrCreate({
googleId: profile.id,
email: profile.emails[0].value,
name: profile.displayName
}, function (err, user) {
return done(err, user);
});
}
));
// Routes
app.get('/auth/google',
passport.authenticate('google', {
scope: ['profile', 'email']
})
);
app.get('/auth/google/callback',
passport.authenticate('google', {
failureRedirect: '/login'
}),
function(req, res) {
res.redirect('/dashboard');
}
);
JWT (JSON Web Tokens)
JWT is a compact, self-contained way to securely transmit information between parties as a JSON object. Commonly used for authentication and information exchange.
JWT Structure: header.payload.signature
┌─────────────────────────────────────────────────────────────────┐
│ JWT TOKEN │
├─────────────────────────────────────────────────────────────────┤
│ eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9. │
│ eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ. │
│ SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c │
└─────────────────────────────────────────────────────────────────┘
│ │ │
▼ ▼ ▼
HEADER PAYLOAD SIGNATURE
HEADER (Base64): PAYLOAD (Base64): SIGNATURE:
{ { HMACSHA256(
"alg": "HS256", "sub": "1234567890", base64UrlEncode(header) + "." +
"typ": "JWT" "name": "John Doe", base64UrlEncode(payload),
} "iat": 1516239022, secret
"exp": 1516242622 )
}
Creating & Verifying JWT
// Creating JWT (Node.js)
const jwt = require('jsonwebtoken');
// Sign token
const token = jwt.sign(
{
userId: 123,
email: 'user@example.com',
role: 'admin'
},
process.env.JWT_SECRET, // Secret key
{
expiresIn: '24h', // Token expires in 24 hours
issuer: 'myapp.com',
audience: 'myapp-users'
}
);
// Token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
// Verify token
try {
const decoded = jwt.verify(
token,
process.env.JWT_SECRET
);
console.log(decoded);
// { userId: 123, email: '...', exp: ... }
} catch(error) {
// Invalid token, expired, or wrong signature
console.error('Invalid token:', error.message);
}
Middleware Protection
// Express.js authentication middleware
function authenticateToken(req, res, next) {
// Get token from header
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];
if (!token) {
return res.status(401).json({
error: 'Access token required'
});
}
jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
if (err) {
return res.status(403).json({
error: 'Invalid or expired token'
});
}
// Attach user to request
req.user = user;
next();
});
}
// Protected route
app.get('/api/profile',
authenticateToken, // Middleware
(req, res) => {
res.json({
user: req.user,
message: 'Protected data'
});
}
);
JWT Security Best Practices
- ✓ Use strong secret key (minimum 256 bits)
- ✓ Set short expiration times (15-60 minutes)
- ✓ Use refresh tokens for longer sessions
- ✓ Store tokens in httpOnly cookies (not localStorage)
- ✓ Validate token on every request
- ✓ Don't store sensitive data in payload (it's just Base64, not encrypted)
- ✓ Implement token blacklisting for logout
Encryption: Protecting Data
Encryption at Rest
Protecting data stored on disk (databases, files, backups)
Technologies:
- AES-256: Symmetric encryption standard
- Database TDE: Transparent Data Encryption
- AWS KMS: Key Management Service
- Disk encryption: LUKS, BitLocker
// Node.js - Encrypt sensitive data
const crypto = require('crypto');
const algorithm = 'aes-256-gcm';
const key = crypto.randomBytes(32);
const iv = crypto.randomBytes(16);
function encrypt(text) {
const cipher = crypto.createCipheriv(algorithm, key, iv);
let encrypted = cipher.update(text, 'utf8', 'hex');
encrypted += cipher.final('hex');
const authTag = cipher.getAuthTag();
return {
encrypted,
authTag: authTag.toString('hex'),
iv: iv.toString('hex')
};
}
// Encrypt password before storing
const sensitive = encrypt('user-password-123');
// Store: sensitive.encrypted + authTag + iv
Encryption in Transit
Protecting data moving between systems (HTTPS, VPN)
Technologies:
- TLS 1.3: Latest protocol for HTTPS
- SSL Certificates: Let's Encrypt, DigiCert
- VPN: WireGuard, OpenVPN
- mTLS: Mutual TLS for service-to-service
// NGINX TLS Configuration
server {
listen 443 ssl http2;
server_name api.example.com;
# TLS 1.3 only
ssl_protocols TLSv1.3;
ssl_certificate /etc/ssl/cert.pem;
ssl_certificate_key /etc/ssl/key.pem;
# Strong ciphers
ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256';
ssl_prefer_server_ciphers off;
# HSTS (force HTTPS)
add_header Strict-Transport-Security
"max-age=63072000" always;
# Certificate stapling
ssl_stapling on;
ssl_stapling_verify on;
}
Password Hashing (NOT Encryption)
Never encrypt passwords—hash them with one-way algorithms. Hashes cannot be reversed.
// ❌ WRONG - Never do this!
password = encrypt("mypassword123", secretKey);
// Can be decrypted if key is compromised
// ✅ CORRECT - Use bcrypt
const bcrypt = require('bcrypt');
// Hash password (with salt)
const saltRounds = 12;
const hash = await bcrypt.hash('mypassword123', saltRounds);
// $2b$12$K5Q9LwZJFqjPMZ3lA3vhO.rT7...
// Verify password
const match = await bcrypt.compare(
'user-input-password',
storedHash
);
if (match) {
console.log('Password correct!');
}
Hashing Algorithms:
- ✓ bcrypt: Adaptive, slow by design, industry standard
- ✓ Argon2: Modern, won password hashing competition
- ✓ scrypt: Memory-hard, resistant to ASICs
- ✗ MD5: Broken, never use
- ✗ SHA1: Deprecated, vulnerable
- ⚠ SHA256: Too fast for passwords (OK for data integrity)
Zero Trust Architecture
"Never trust, always verify" - Assume breach, verify everything, grant least privilege access.
Traditional Security (Perimeter-Based)
┌─────────────────────────────────┐
│ FIREWALL (Castle Wall) │
├─────────────────────────────────┤
│ ✓ Inside = Trusted │
│ ✗ Outside = Untrusted │
│ │
│ Problem: Once inside, │
│ lateral movement is easy │
│ (Attackers own the castle) │
└─────────────────────────────────┘
- ✗ Trust internal network
- ✗ VPN = full access
- ✗ Single point of failure
Zero Trust (Identity-Based)
┌─────────────────────────────────┐
│ Every Request Verified │
├─────────────────────────────────┤
│ 1. Authenticate (Who are you?) │
│ 2. Authorize (What can you do?) │
│ 3. Encrypt (Secure channel) │
│ 4. Monitor (Detect anomalies) │
│ │
│ Every service, every time │
│ No implicit trust │
└─────────────────────────────────┘
- ✓ Verify every request
- ✓ Least privilege access
- ✓ Micro-segmentation
Zero Trust Principles
1. Verify Explicitly
Authenticate and authorize based on all available data points (user, device, location, behavior)
2. Least Privilege
Limit user access with Just-In-Time and Just-Enough-Access (JIT/JEA)
3. Assume Breach
Minimize blast radius, segment access, verify end-to-end encryption, use analytics
4. Identity as Perimeter
User identity becomes the security boundary, not network location
5. Device Compliance
Verify device health before granting access (OS version, patches, antivirus)
6. Continuous Monitoring
Real-time threat detection, anomaly detection, automated response
CI/CD & DevOps Architecture
CI/CD (Continuous Integration/Continuous Deployment) and DevOps practices enable teams to deliver software faster, more reliably, and with higher quality. Automation, collaboration, and feedback loops are key to modern software delivery.
Complete CI/CD Pipeline
┌──────────────────────────────────────────────────────────────────────────────┐
│ CI/CD PIPELINE STAGES │
└──────────────────────────────────────────────────────────────────────────────┘
1. CODE 2. BUILD 3. TEST 4. DEPLOY
↓ ↓ ↓ ↓
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
│ Git │ Push │ Compile │ │ Unit │ │ Dev │
│ Commit │────────>│ Install │───────>│ Tests │───────>│ Env │
│ │ │ Deps │ │ │ │ │
└─────────┘ └─────────┘ └─────────┘ └─────────┘
↓ ↓ ↓
┌─────────┐ ┌─────────┐ ┌─────────┐
│ Docker │ │ Integr. │ │ Staging │
│ Build │ │ Tests │ │ Env │
└─────────┘ └─────────┘ └─────────┘
↓ ↓ ↓
┌─────────┐ ┌─────────┐ ┌─────────┐
│ Push to │ │ E2E │ │ Prod │
│Registry │ │ Tests │ │ Env │
└─────────┘ └─────────┘ └─────────┘
↓ ↓ ↓
┌─────────┐ ┌─────────┐ ┌─────────┐
│Security │ │ Perform.│ │ Monitor │
│ Scan │ │ Tests │ │ & Alert │
└─────────┘ └─────────┘ └─────────┘
If ANY stage fails → Pipeline stops, team notified
Continuous Integration (CI)
Automatically build and test code changes frequently (multiple times per day)
Key Practices:
- ✓ Commit code to main branch frequently
- ✓ Automated build on every commit
- ✓ Run automated tests
- ✓ Fast feedback (< 10 minutes)
- ✓ Fix broken builds immediately
Benefits:
• Catch bugs early • Reduce integration problems • Faster development • Higher code quality
Continuous Deployment (CD)
Automatically deploy every change that passes tests to production
Key Practices:
- ✓ Automated deployment pipeline
- ✓ Blue-green or canary deployments
- ✓ Automated rollback on failure
- ✓ Feature flags for risk mitigation
- ✓ Comprehensive monitoring
Benefits:
• Faster time to market • Reduced deployment risk • Smaller, safer changes • Quick feedback
GitHub Actions Pipeline Example
# .github/workflows/ci-cd.yml
name: CI/CD Pipeline
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run linter
run: npm run lint
- name: Run unit tests
run: npm test -- --coverage
- name: Run integration tests
run: npm run test:integration
- name: Build application
run: npm run build
- name: Security scan
run: npm audit --audit-level=high
- name: Build Docker image
run: |
docker build -t myapp:${{ github.sha }} .
docker tag myapp:${{ github.sha }} myapp:latest
- name: Run container security scan
uses: aquasecurity/trivy-action@master
with:
image-ref: 'myapp:latest'
deploy-staging:
needs: build-and-test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/develop'
steps:
- name: Deploy to staging
run: |
kubectl set image deployment/myapp \
myapp=myapp:${{ github.sha }} \
-n staging
- name: Run smoke tests
run: npm run test:smoke -- --env=staging
deploy-production:
needs: build-and-test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- name: Deploy to production (Blue-Green)
run: |
# Deploy to green environment
kubectl set image deployment/myapp-green \
myapp=myapp:${{ github.sha }} \
-n production
# Wait for deployment
kubectl rollout status deployment/myapp-green -n production
# Switch traffic (update service selector)
kubectl patch service myapp -n production \
-p '{"spec":{"selector":{"version":"green"}}}'
- name: Monitor deployment
run: |
# Check error rate for 10 minutes
npm run monitor:production
- name: Rollback on failure
if: failure()
run: |
kubectl patch service myapp -n production \
-p '{"spec":{"selector":{"version":"blue"}}}'
Docker: Containerization
Containers package application code with dependencies, ensuring consistent behavior across environments.
Dockerfile Example
# Multi-stage build for optimized image
FROM node:18-alpine AS builder
WORKDIR /app
# Copy package files
COPY package*.json ./
# Install dependencies
RUN npm ci --only=production
# Copy source code
COPY . .
# Build application
RUN npm run build
# Production stage
FROM node:18-alpine
WORKDIR /app
# Create non-root user
RUN addgroup -g 1001 -S nodejs && \
adduser -S nodejs -u 1001
# Copy built app from builder
COPY --from=builder --chown=nodejs:nodejs /app/dist ./dist
COPY --from=builder --chown=nodejs:nodejs /app/node_modules ./node_modules
COPY --from=builder --chown=nodejs:nodejs /app/package.json ./
# Switch to non-root user
USER nodejs
# Expose port
EXPOSE 3000
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=40s \
CMD node healthcheck.js || exit 1
# Start application
CMD ["node", "dist/server.js"]
Docker Compose (Local Development)
# docker-compose.yml
version: '3.8'
services:
app:
build: .
ports:
- "3000:3000"
environment:
- NODE_ENV=development
- DB_HOST=postgres
- REDIS_HOST=redis
depends_on:
- postgres
- redis
volumes:
- ./src:/app/src # Hot reload
networks:
- app-network
postgres:
image: postgres:15-alpine
environment:
- POSTGRES_DB=myapp
- POSTGRES_USER=user
- POSTGRES_PASSWORD=password
volumes:
- postgres-data:/var/lib/postgresql/data
networks:
- app-network
redis:
image: redis:7-alpine
networks:
- app-network
nginx:
image: nginx:alpine
ports:
- "80:80"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf
depends_on:
- app
networks:
- app-network
volumes:
postgres-data:
networks:
app-network:
driver: bridge
Docker Best Practices
- Use multi-stage builds (smaller images)
- Use specific tags, not :latest
- Run as non-root user
- Minimize layers (combine RUN commands)
- Use .dockerignore file
- Scan images for vulnerabilities
- Order layers by change frequency
- Use health checks
Kubernetes: Container Orchestration
Kubernetes automates deployment, scaling, and management of containerized applications across clusters of machines.
Kubernetes Cluster Architecture:
┌─────────────────────────────────────────────────────────────────┐
│ CONTROL PLANE │
│ ┌──────────────┐ ┌──────────────┐ ┌─────────────────────┐ │
│ │ API Server │ │ Scheduler │ │ Controller Manager │ │
│ │ (Entry point)│ │ (Pod placing)│ │ (Desired state) │ │
│ └──────────────┘ └──────────────┘ └─────────────────────┘ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ etcd (Cluster state storage) │ │
│ └──────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────┐
│ WORKER NODES │
├──────────────────────┬──────────────────────┬───────────────────┤
│ NODE 1 │ NODE 2 │ NODE 3 │
│ ┌────────────────┐ │ ┌────────────────┐ │ ┌──────────────┐│
│ │ kubelet │ │ │ kubelet │ │ │ kubelet ││
│ │ (Node agent) │ │ │ (Node agent) │ │ │ (Node agent) ││
│ └────────────────┘ │ └────────────────┘ │ └──────────────┘│
│ ┌────────────────┐ │ ┌────────────────┐ │ ┌──────────────┐│
│ │ POD 1 │ │ │ POD 3 │ │ │ POD 5 ││
│ │ ┌────────────┐ │ │ │ ┌────────────┐ │ │ │┌───────────┐││
│ │ │ Container │ │ │ │ │ Container │ │ │ ││Container │││
│ │ └────────────┘ │ │ │ └────────────┘ │ │ │└───────────┘││
│ └────────────────┘ │ └────────────────┘ │ └──────────────┘│
│ ┌────────────────┐ │ ┌────────────────┐ │ │
│ │ POD 2 │ │ │ POD 4 │ │ │
│ └────────────────┘ │ └────────────────┘ │ │
└──────────────────────┴──────────────────────┴───────────────────┘
Kubernetes Deployment
# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
namespace: production
spec:
replicas: 3 # Run 3 pods
selector:
matchLabels:
app: myapp
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
template:
metadata:
labels:
app: myapp
version: v1.0.0
spec:
containers:
- name: myapp
image: myapp:1.0.0
ports:
- containerPort: 3000
env:
- name: NODE_ENV
value: "production"
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: db-secret
key: password
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "512Mi"
cpu: "500m"
livenessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /ready
port: 3000
initialDelaySeconds: 5
periodSeconds: 5
Service & Ingress
# service.yaml
apiVersion: v1
kind: Service
metadata:
name: myapp-service
spec:
selector:
app: myapp
ports:
- protocol: TCP
port: 80
targetPort: 3000
type: ClusterIP
---
# ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: myapp-ingress
annotations:
cert-manager.io/cluster-issuer: "letsencrypt"
nginx.ingress.kubernetes.io/rate-limit: "100"
spec:
tls:
- hosts:
- api.example.com
secretName: myapp-tls
rules:
- host: api.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: myapp-service
port:
number: 80
---
# horizontal-pod-autoscaler.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: myapp-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: myapp
minReplicas: 3
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
Infrastructure as Code (IaC)
Manage infrastructure through code instead of manual processes. Version control, automation, and repeatability.
Terraform Example
# main.tf
provider "aws" {
region = "us-east-1"
}
# VPC
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
tags = {
Name = "production-vpc"
}
}
# EKS Cluster
resource "aws_eks_cluster" "main" {
name = "production-cluster"
role_arn = aws_iam_role.eks_cluster.arn
vpc_config {
subnet_ids = aws_subnet.private[*].id
}
}
# RDS Database
resource "aws_db_instance" "postgres" {
identifier = "production-db"
engine = "postgres"
engine_version = "15.3"
instance_class = "db.t3.medium"
allocated_storage = 100
db_name = "myapp"
username = "admin"
password = var.db_password
backup_retention_period = 7
multi_az = true
tags = {
Environment = "production"
}
}
# S3 Bucket
resource "aws_s3_bucket" "uploads" {
bucket = "myapp-uploads-production"
versioning {
enabled = true
}
server_side_encryption_configuration {
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "AES256"
}
}
}
}
Benefits & Tools
Benefits:
- ✓ Version control infrastructure
- ✓ Repeatable deployments
- ✓ Disaster recovery (rebuild from code)
- ✓ Documentation (code is docs)
- ✓ Cost tracking & optimization
Popular Tools:
- Terraform: Multi-cloud, declarative
- CloudFormation: AWS native
- Pulumi: Use real programming languages
- Ansible: Configuration management
- CDK: AWS Cloud Development Kit
Monitoring & Observability
"You can't improve what you can't measure." Monitoring and observability provide visibility into your system's health, performance, and behavior. The three pillars of observability are Logs, Metrics, and Traces.
The Three Pillars of Observability
Logs
Discrete events with timestamps describing what happened
[2025-11-08 23:30:15] INFO: User login
userId: 123
ip: 192.168.1.100
userAgent: Chrome/120
[2025-11-08 23:30:16] ERROR: DB query failed
query: SELECT * FROM users
error: Connection timeout
duration: 5000ms
Best for: Debugging, audit trails, error analysis
Tools: ELK Stack, Loki, Splunk, Papertrail
Metrics
Numerical measurements over time (counters, gauges, histograms)
http_requests_total 1547
http_request_duration_seconds 0.234
cpu_usage_percent 67.5
memory_used_bytes 2147483648
active_users 1250
db_connections_active 45
cache_hit_rate 0.95
Best for: Dashboards, alerts, trends, capacity planning
Tools: Prometheus, Grafana, Datadog, CloudWatch
Traces
Journey of a request through distributed system
TraceID: abc123
API Gateway [20ms]
↓
Auth Service [15ms]
↓
User Service [45ms]
├─ DB Query [30ms]
└─ Cache [5ms]
↓
Response [Total: 80ms]
Best for: Performance bottlenecks, latency analysis
Tools: Jaeger, Zipkin, OpenTelemetry, AWS X-Ray
When to Use Each
Logs:
"What error occurred at 3 AM?"
"Who accessed this resource?"
Metrics:
"Is CPU usage trending up?"
"What's our 99th percentile latency?"
Traces:
"Why is this request slow?"
"Which service is the bottleneck?"
Prometheus + Grafana Stack
The industry-standard open-source monitoring solution. Prometheus collects metrics, Grafana visualizes them.
Prometheus Configuration
# prometheus.yml
global:
scrape_interval: 15s
evaluation_interval: 15s
scrape_configs:
- job_name: 'api-servers'
static_configs:
- targets:
- 'api1:9090'
- 'api2:9090'
- 'api3:9090'
- job_name: 'databases'
static_configs:
- targets: ['postgres:9187']
- job_name: 'redis'
static_configs:
- targets: ['redis:9121']
- job_name: 'kubernetes-pods'
kubernetes_sd_configs:
- role: pod
relabel_configs:
- source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape]
action: keep
regex: true
# Alert rules
rule_files:
- 'alerts.yml'
alerting:
alertmanagers:
- static_configs:
- targets: ['alertmanager:9093']
Instrumenting Application
// Node.js with prom-client
const promClient = require('prom-client');
// Create metrics
const httpRequestDuration = new promClient.Histogram({
name: 'http_request_duration_seconds',
help: 'Duration of HTTP requests in seconds',
labelNames: ['method', 'route', 'status_code'],
buckets: [0.1, 0.5, 1, 2, 5]
});
const httpRequestTotal = new promClient.Counter({
name: 'http_requests_total',
help: 'Total number of HTTP requests',
labelNames: ['method', 'route', 'status_code']
});
const activeConnections = new promClient.Gauge({
name: 'active_connections',
help: 'Number of active connections'
});
// Middleware to track metrics
app.use((req, res, next) => {
const start = Date.now();
res.on('finish', () => {
const duration = (Date.now() - start) / 1000;
httpRequestDuration
.labels(req.method, req.route?.path || req.path, res.statusCode)
.observe(duration);
httpRequestTotal
.labels(req.method, req.route?.path || req.path, res.statusCode)
.inc();
});
next();
});
// Expose metrics endpoint
app.get('/metrics', async (req, res) => {
res.set('Content-Type', promClient.register.contentType);
res.end(await promClient.register.metrics());
});
PromQL Queries (Prometheus Query Language)
# Request rate (requests per second)
rate(http_requests_total[5m])
# 95th percentile latency
histogram_quantile(0.95,
rate(http_request_duration_seconds_bucket[5m])
)
# Error rate
rate(http_requests_total{status_code=~"5.."}[5m])
/
rate(http_requests_total[5m])
# CPU usage average across pods
avg(cpu_usage_percent) by (pod)
# Memory usage > 80%
memory_used_bytes / memory_total_bytes > 0.8
# Alert Rules (alerts.yml)
groups:
- name: example_alerts
rules:
- alert: HighErrorRate
expr: |
rate(http_requests_total{status_code=~"5.."}[5m])
/ rate(http_requests_total[5m]) > 0.05
for: 5m
labels:
severity: critical
annotations:
summary: "High error rate detected"
description: "Error rate is {{ $value }}%"
- alert: HighLatency
expr: |
histogram_quantile(0.95,
rate(http_request_duration_seconds_bucket[5m])
) > 1
for: 10m
labels:
severity: warning
annotations:
summary: "High latency detected"
ELK Stack (Elasticsearch, Logstash, Kibana)
Centralized logging solution for collecting, processing, storing, and visualizing log data.
Log Flow in ELK Stack:
┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Application │────>│ Filebeat │────>│ Logstash │────>│Elasticsearch │
│ Logs │ │ (Shipper) │ │ (Process) │ │ (Store) │
└──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘
│
↓
┌──────────────┐
│ Kibana │
│ (Visualize) │
└──────────────┘
Components:
- Filebeat: Lightweight log shipper
- Logstash: Parse, transform, enrich logs
- Elasticsearch: Store and index logs
- Kibana: Search, visualize, analyze
Logstash Configuration
# logstash.conf
input {
beats {
port => 5044
}
file {
path => "/var/log/app/*.log"
start_position => "beginning"
}
}
filter {
# Parse JSON logs
json {
source => "message"
}
# Parse timestamps
date {
match => ["timestamp", "ISO8601"]
target => "@timestamp"
}
# Add geo location from IP
geoip {
source => "client_ip"
}
# Extract fields
grok {
match => {
"message" => "%{COMBINEDAPACHELOG}"
}
}
# Remove sensitive data
mutate {
remove_field => ["password", "credit_card"]
}
}
output {
elasticsearch {
hosts => ["elasticsearch:9200"]
index => "app-logs-%{+YYYY.MM.dd}"
}
# Also output to stdout for debugging
stdout {
codec => rubydebug
}
}
Structured Logging Best Practices
// Bad logging
console.log('User logged in');
console.log('Error: ' + error);
// Good structured logging (winston)
const winston = require('winston');
const logger = winston.createLogger({
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
transports: [
new winston.transports.File({
filename: 'app.log'
})
]
});
logger.info('User logged in', {
userId: 123,
email: 'user@example.com',
ip: '192.168.1.100',
userAgent: req.headers['user-agent'],
timestamp: new Date().toISOString()
});
logger.error('Database query failed', {
error: error.message,
stack: error.stack,
query: 'SELECT * FROM users',
duration: 5000,
correlationId: req.id
});
Log Levels:
- ERROR: Something failed
- WARN: Something unexpected
- INFO: Important events
- DEBUG: Detailed info (dev only)
- TRACE: Very detailed (never in prod)
Distributed Tracing with Jaeger
Track requests as they flow through microservices, identifying bottlenecks and dependencies.
OpenTelemetry Implementation
// Node.js with OpenTelemetry
const { NodeTracerProvider } = require('@opentelemetry/sdk-trace-node');
const { JaegerExporter } = require('@opentelemetry/exporter-jaeger');
const { registerInstrumentations } = require('@opentelemetry/instrumentation');
const { HttpInstrumentation } = require('@opentelemetry/instrumentation-http');
const { ExpressInstrumentation } = require('@opentelemetry/instrumentation-express');
// Initialize tracer
const provider = new NodeTracerProvider();
// Configure Jaeger exporter
const exporter = new JaegerExporter({
endpoint: 'http://jaeger:14268/api/traces',
serviceName: 'api-service'
});
provider.addSpanProcessor(
new BatchSpanProcessor(exporter)
);
provider.register();
// Auto-instrument HTTP and Express
registerInstrumentations({
instrumentations: [
new HttpInstrumentation(),
new ExpressInstrumentation()
]
});
// Manual span creation
const tracer = provider.getTracer('api-service');
app.get('/api/users/:id', async (req, res) => {
const span = tracer.startSpan('get-user-handler');
try {
// Child span for DB query
const dbSpan = tracer.startSpan('db-query', {
parent: span
});
const user = await db.query('SELECT * FROM users WHERE id = ?', [req.params.id]);
dbSpan.end();
// Child span for cache
const cacheSpan = tracer.startSpan('cache-set');
await redis.set(`user:${req.params.id}`, JSON.stringify(user));
cacheSpan.end();
res.json(user);
} catch (error) {
span.setStatus({ code: SpanStatusCode.ERROR });
span.recordException(error);
res.status(500).json({ error: error.message });
} finally {
span.end();
}
});
Trace Visualization
Trace: Order Checkout (TraceID: abc123)
Total Duration: 856ms
API Gateway [15ms] ────────┐
↓
Auth Service [25ms] └─→ [40ms]
↓
Order Service [450ms] ←──────────┘
├─ Validate Items [50ms]
├─ Check Inventory [120ms]
│ └─ DB Query [110ms] ← SLOW!
├─ Calculate Tax [30ms]
└─ Create Order [250ms]
├─ DB Insert [200ms] ← SLOW!
└─ Cache Set [40ms]
↓
Payment Service [300ms]
├─ Stripe API [280ms]
└─ Update Order [20ms]
↓
Notification [51ms]
└─ Send Email [45ms]
Insights:
- DB queries are slow (need indexing?)
- Stripe API adds most latency
- Overall: 95% time in downstream services
Tracing Benefits:
- ✓ Identify slow services/queries
- ✓ Understand service dependencies
- ✓ Debug complex failures
- ✓ Optimize critical paths
- ✓ Measure end-to-end latency
Design Patterns
Design patterns are reusable solutions to common problems in software design. They represent best practices refined over years of experience and provide a shared vocabulary for developers to communicate complex ideas efficiently.
Architectural Design Patterns
MVC (Model-View-Controller)
Separates application into three interconnected components: Model (data), View (UI), Controller (logic).
MVC Architecture Flow:
┌─────────────────────────────────────┐
│ USER │
└───────────┬─────────────────────────┘
│ HTTP Request
↓
┌─────────────────────────────────────┐
│ CONTROLLER │
│ • Handles requests │
│ • Updates model │
│ • Selects view │
└────┬──────────────────────┬─────────┘
│ │
↓ ↓
┌──────────────────┐ ┌─────────────────┐
│ MODEL │ │ VIEW │
│ • Business │←───│ • Presentation │
│ logic │ │ • Templates │
│ • Data access │ │ • HTML │
│ • Validation │ └─────────────────┘
└──────────────────┘
│
↓
┌──────────────────┐
│ DATABASE │
└──────────────────┘
Express.js MVC Example
// Model (models/User.js)
class User {
constructor(db) {
this.db = db;
}
async findById(id) {
return await this.db.query(
'SELECT * FROM users WHERE id = ?',
[id]
);
}
async create(userData) {
const { name, email, password } = userData;
return await this.db.query(
'INSERT INTO users (name, email, password) VALUES (?, ?, ?)',
[name, email, password]
);
}
async update(id, data) {
return await this.db.query(
'UPDATE users SET ? WHERE id = ?',
[data, id]
);
}
}
// Controller (controllers/UserController.js)
class UserController {
constructor(userModel) {
this.userModel = userModel;
}
async getUser(req, res) {
try {
const user = await this.userModel.findById(req.params.id);
res.render('user/profile', { user });
} catch (error) {
res.status(500).render('error', { error });
}
}
async createUser(req, res) {
try {
const user = await this.userModel.create(req.body);
res.redirect(`/users/${user.id}`);
} catch (error) {
res.status(400).render('user/new', {
error: error.message
});
}
}
}
// View (views/user/profile.ejs)
<!-- HTML template with user data -->
<h1><%= user.name %></h1>
<p>Email: <%= user.email %></p>
// Routes (routes/users.js)
const router = express.Router();
const controller = new UserController(userModel);
router.get('/users/:id', (req, res) =>
controller.getUser(req, res)
);
router.post('/users', (req, res) =>
controller.createUser(req, res)
);
Benefits:
- ✓ Separation of concerns
- ✓ Easier to test
- ✓ Parallel development
- ✓ Code reusability
- ✓ Multiple views for same data
Use Cases:
- • Web applications
- • Desktop applications
- • Mobile apps
- • Any UI-driven application
Repository Pattern
Abstracts data access logic, providing a collection-like interface for accessing domain objects.
Without Repository (Direct DB Access)
// ❌ Tight coupling to database
class UserService {
async getUser(id) {
// Direct SQL in service
const result = await db.query(
'SELECT * FROM users WHERE id = ?',
[id]
);
return result[0];
}
async createUser(data) {
// Direct SQL
const result = await db.query(
'INSERT INTO users SET ?',
[data]
);
return result.insertId;
}
}
// Problems:
// - Hard to test
// - Hard to switch databases
// - SQL scattered everywhere
// - No abstraction
With Repository Pattern
// ✓ Clean abstraction
interface IUserRepository {
findById(id: number): Promise<User>;
findByEmail(email: string): Promise<User>;
create(user: User): Promise<User>;
update(id: number, user: User): Promise<User>;
delete(id: number): Promise<void>;
}
class UserRepository implements IUserRepository {
constructor(private db: Database) {}
async findById(id: number): Promise<User> {
const row = await this.db.query(
'SELECT * FROM users WHERE id = ?',
[id]
);
return this.mapToEntity(row);
}
async create(user: User): Promise<User> {
const result = await this.db.query(
'INSERT INTO users SET ?',
[user]
);
return { ...user, id: result.insertId };
}
private mapToEntity(row: any): User {
return new User(row.id, row.name, row.email);
}
}
// Service uses repository
class UserService {
constructor(private userRepo: IUserRepository) {}
async getUser(id: number) {
return await this.userRepo.findById(id);
}
}
// Benefits:
// - Easy to mock for testing
// - Can swap implementations
// - Centralized data access
// - Clean separation
CQRS (Command Query Responsibility Segregation)
Separates read and write operations into different models. Commands modify data, Queries retrieve data.
Traditional (Single Model):
┌──────────┐
│ Client │
└─────┬────┘
↓
┌─────────────┐
│ Service │ Both reads and writes
│ (CRUD) │ use same model
└──────┬──────┘
↓
┌──────────────┐
│ Database │
└──────────────┘
CQRS (Separate Models):
┌──────────┐ ┌──────────┐
│ Client │ │ Client │
└─────┬────┘ └─────┬────┘
│ Write │ Read
↓ ↓
┌─────────────┐ ┌─────────────┐
│ Command │ │ Query │
│ Handler │ │ Handler │
└──────┬──────┘ └──────┬──────┘
↓ ↓
┌──────────────┐ ┌──────────────┐
│ Write Model │ │ Read Model │
│ (Normalized) │─────→│(Denormalized)│
│ PostgreSQL │ Sync │ Elasticsearch│
└──────────────┘ └──────────────┘
Command Side (Write)
// Command
class CreateOrderCommand {
constructor(
public userId: number,
public items: OrderItem[],
public total: number
) {}
}
// Command Handler
class CreateOrderHandler {
constructor(
private orderRepo: OrderRepository,
private eventBus: EventBus
) {}
async handle(command: CreateOrderCommand) {
// Validate
this.validateOrder(command);
// Create order (write model)
const order = new Order({
userId: command.userId,
items: command.items,
total: command.total,
status: 'pending'
});
await this.orderRepo.save(order);
// Publish event
await this.eventBus.publish(
new OrderCreatedEvent(order)
);
return order.id;
}
private validateOrder(command: CreateOrderCommand) {
if (command.items.length === 0) {
throw new Error('Order must have items');
}
if (command.total <= 0) {
throw new Error('Invalid total');
}
}
}
Query Side (Read)
// Query
class GetOrderQuery {
constructor(public orderId: number) {}
}
// Query Handler
class GetOrderHandler {
constructor(private readDb: ReadDatabase) {}
async handle(query: GetOrderQuery) {
// Read from optimized read model
return await this.readDb.query(`
SELECT
o.id,
o.total,
o.status,
u.name as customer_name,
u.email,
GROUP_CONCAT(
CONCAT(i.name, ' x', oi.quantity)
) as items
FROM orders o
JOIN users u ON o.user_id = u.id
JOIN order_items oi ON o.id = oi.order_id
JOIN items i ON oi.item_id = i.id
WHERE o.id = ?
GROUP BY o.id
`, [query.orderId]);
}
}
// Event Handler (Sync read model)
class OrderCreatedEventHandler {
constructor(private readDb: ReadDatabase) {}
async handle(event: OrderCreatedEvent) {
// Update denormalized read model
await this.readDb.upsert('order_views', {
id: event.orderId,
customer_name: event.customerName,
total: event.total,
items_count: event.items.length,
created_at: event.timestamp
});
}
}
When to Use CQRS:
✓ Good For:
- • Complex business logic
- • Different read/write requirements
- • High read:write ratio
- • Need for multiple read models
- • Event sourcing architecture
✗ Overkill For:
- • Simple CRUD applications
- • Small systems
- • When consistency is critical
- • Limited team experience
- • Tight deadlines
Resilience Design Patterns
Circuit Breaker Pattern
Prevents cascading failures by stopping calls to a failing service, allowing it time to recover.
Circuit Breaker States:
┌─────────────────┐
│ CLOSED │ Normal operation
│ (Allow calls) │ Track failures
└────────┬────────┘
│ Threshold exceeded
│ (e.g., 5 failures)
↓
┌─────────────────┐
│ OPEN │ Block all calls
│ (Fail fast) │ Return error immediately
└────────┬────────┘
│ After timeout
│ (e.g., 30 seconds)
↓
┌─────────────────┐
│ HALF-OPEN │ Allow test request
│ (Try one call) │
└────────┬────────┘
│
┌────┴─────┐
│ │
Success? Failure?
│ │
↓ ↓
CLOSED OPEN
Implementation
class CircuitBreaker {
constructor(options = {}) {
this.failureThreshold = options.threshold || 5;
this.timeout = options.timeout || 30000;
this.resetTimeout = null;
this.state = 'CLOSED';
this.failureCount = 0;
this.nextAttempt = Date.now();
}
async call(fn) {
if (this.state === 'OPEN') {
if (Date.now() < this.nextAttempt) {
throw new Error('Circuit breaker is OPEN');
}
this.state = 'HALF-OPEN';
}
try {
const result = await fn();
this.onSuccess();
return result;
} catch (error) {
this.onFailure();
throw error;
}
}
onSuccess() {
this.failureCount = 0;
this.state = 'CLOSED';
}
onFailure() {
this.failureCount++;
if (this.failureCount >= this.failureThreshold) {
this.state = 'OPEN';
this.nextAttempt = Date.now() + this.timeout;
console.log('Circuit breaker opened');
}
}
}
// Usage
const breaker = new CircuitBreaker({
threshold: 5,
timeout: 30000
});
async function callExternalAPI() {
try {
return await breaker.call(async () => {
return await fetch('https://api.example.com/data');
});
} catch (error) {
console.log('Circuit breaker prevented call');
return getCachedData(); // Fallback
}
}
Benefits:
- ✓ Prevents cascade failures
- ✓ Fast fail (no waiting)
- ✓ Automatic recovery
- ✓ Protects downstream services
- ✓ Reduces resource waste
Saga Pattern
Manages distributed transactions across microservices using compensating transactions.
// Order Saga Implementation
class OrderSaga {
constructor(services) {
this.orderService = services.orderService;
this.paymentService = services.paymentService;
this.inventoryService = services.inventoryService;
this.shippingService = services.shippingService;
}
async execute(orderData) {
const saga = {
orderId: null,
paymentId: null,
reservationId: null,
shipmentId: null
};
try {
// Step 1: Create Order
saga.orderId = await this.orderService.create(orderData);
console.log('✓ Order created:', saga.orderId);
// Step 2: Process Payment
saga.paymentId = await this.paymentService.charge({
orderId: saga.orderId,
amount: orderData.total,
cardToken: orderData.paymentToken
});
console.log('✓ Payment processed:', saga.paymentId);
// Step 3: Reserve Inventory
saga.reservationId = await this.inventoryService.reserve({
orderId: saga.orderId,
items: orderData.items
});
console.log('✓ Inventory reserved:', saga.reservationId);
// Step 4: Schedule Shipping
saga.shipmentId = await this.shippingService.schedule({
orderId: saga.orderId,
address: orderData.shippingAddress
});
console.log('✓ Shipping scheduled:', saga.shipmentId);
// Success - Update order status
await this.orderService.confirm(saga.orderId);
return { success: true, orderId: saga.orderId };
} catch (error) {
console.error('Saga failed:', error.message);
// Compensate in reverse order
await this.compensate(saga, error);
throw new Error('Order processing failed: ' + error.message);
}
}
async compensate(saga, error) {
console.log('Starting compensation...');
// Reverse operations in opposite order
if (saga.shipmentId) {
await this.shippingService.cancel(saga.shipmentId);
console.log('✓ Shipping cancelled');
}
if (saga.reservationId) {
await this.inventoryService.release(saga.reservationId);
console.log('✓ Inventory released');
}
if (saga.paymentId) {
await this.paymentService.refund(saga.paymentId);
console.log('✓ Payment refunded');
}
if (saga.orderId) {
await this.orderService.cancel(saga.orderId, error.message);
console.log('✓ Order cancelled');
}
}
}
// Usage
const saga = new OrderSaga(services);
try {
const result = await saga.execute({
userId: 123,
items: [{ id: 1, quantity: 2 }],
total: 99.99,
paymentToken: 'tok_123',
shippingAddress: { /* ... */ }
});
console.log('Order completed:', result.orderId);
} catch (error) {
console.error('Order failed:', error.message);
}
Choreography-Based Saga:
- • Services listen to events
- • No central coordinator
- • More decoupled
- • Harder to track
Orchestration-Based Saga:
- • Central orchestrator
- • Explicit flow control
- • Easier to understand
- • Single point of failure
Other Essential Patterns
Retry Pattern
Automatically retry failed operations with exponential backoff
async function retry(fn, retries = 3) {
for (let i = 0; i < retries; i++) {
try {
return await fn();
} catch (error) {
if (i === retries - 1) throw error;
await sleep(Math.pow(2, i) * 1000);
}
}
}
Bulkhead Pattern
Isolate resources to prevent cascade failures
Example: Separate thread pools for different services
Adapter Pattern
Convert interface to match client expectations
Example: Wrapper for third-party APIs
Factory Pattern
Create objects without specifying exact class
Example: Database connection factory
Observer Pattern
Subscribe to events and get notified of changes
Example: Event emitters, pub/sub
Singleton Pattern
Ensure only one instance exists
Example: Database connection pool
UML Diagrams
UML (Unified Modeling Language) provides a standardized way to visualize system design. Different diagram types serve different purposes in documenting and communicating system architecture.
Use Case Diagram
Shows how users (actors) interact with the system. Focuses on functionality from user's perspective.
E-Commerce Use Case Example
┌─────────────────────────────────────────────────────┐
│ E-Commerce System │
│ │
│ ┌──────────────────────────────────────────┐ │
│ │ Browse Products │ │
│ └───────┬──────────────────────────────────┘ │
│ │ │
│ ┌───────▼──────────────────────────────────┐ │
│ │ Search Products │ │
│ └───────┬──────────────────────────────────┘ │
│ │ │
│ ┌───────▼──────────────────────────────────┐ │
│ │ Add to Cart │ │
│ └───────┬──────────────────────────────────┘ │
│ │ │
│ ┌───────▼──────────────────────────────────┐ │
│ │ Checkout │ │
│ │ ├─ Select Shipping │ │
│ │ ├─ Apply Coupon │ │
│ │ └─ Make Payment ──────┐ │ │
│ └──────────────────────────┬┘ │ │
│ │ │ │
│ ┌──────────────────────────▼──────────┐ │ │
│ │ Track Order │ │ │
│ └──────────────────────────┬──────────┘ │ │
│ │ │ │
│ ┌──────────────────────────▼──────────┐ │ │
│ │ Write Review │ │ │
│ └─────────────────────────────────────┘ │ │
│ │ │
└───────────────────────────────────────────────┘ │
↑ ↑
│ │
┌────┴────┐ ┌─────┴──────┐
│ Customer│ │ Payment │
│ (Actor) │ │ Gateway │
└─────────┘ │ (System) │
└────────────┘
Legend:
- Oval: Use Case
- Stick figure: Actor
- Line: Association
- Arrow: Include/Extend relationship
Key Components
Actors
- • Primary: Customer, Admin
- • Secondary: Payment gateway, Email service
- • System: Inventory management
Use Cases
- • Verb phrases (Browse, Search, Buy)
- • User goals (Complete order)
- • System functions (Process payment)
Relationships
- • Include: Always performed (Login ← Checkout)
- • Extend: Optional (Apply coupon → Checkout)
- • Generalization: Inheritance
When to Use
- ✓ Requirements gathering
- ✓ Stakeholder communication
- ✓ Feature planning
- ✓ Test case generation
Class Diagram
Shows system structure: classes, attributes, methods, and relationships between classes.
┌────────────────────────────────────────────────────────────────────┐
│ Order Management System │
└────────────────────────────────────────────────────────────────────┘
┌───────────────────┐ ┌───────────────────┐
│ User │ │ Order │
├───────────────────┤ ├───────────────────┤
│ - id: number │1 * │ - id: number │
│ - name: string │───────────────│ - userId: number │
│ - email: string │ places │ - total: decimal │
│ - password: string│ │ - status: string │
├───────────────────┤ │ - createdAt: Date │
│ + register() │ ├───────────────────┤
│ + login() │ │ + calculate() │
│ + updateProfile() │ │ + confirm() │
└───────────────────┘ │ + cancel() │
△ └─────────┬─────────┘
│ │
│ inherits │1
│ │
┌───────┴───────┐ │
│ │ │contains
│ │ │
┌───────────────┴─────┐ ┌─────────▼─────────┐
│ PremiumUser │ │ OrderItem │
├─────────────────────┤ * ├───────────────────┤
│ - membershipLevel │ │ - orderId: number │
│ - discountRate │ │ - productId: int │
├─────────────────────┤ │ - quantity: int │
│ + applyDiscount() │ │ - price: decimal │
└─────────────────────┘ ├───────────────────┤
│ + getSubtotal() │
└─────────┬─────────┘
│
│references
│
┌─────────▼─────────┐
│ Product │
├───────────────────┤
│ - id: number │
│ - name: string │
│ - price: decimal │
│ - stock: int │
├───────────────────┤
│ + updateStock() │
│ + checkAvailability()│
└───────────────────┘
Relationships:
─────── Association (uses)
───────── Composition (contains, strong ownership)
───────○ Aggregation (has-a, weak ownership)
───────△ Inheritance (is-a)
Visibility:
+ Public
- Private
# Protected
Relationships
- Association: User places Order (1 to many)
- Composition: Order contains OrderItems (strong)
- Aggregation: Shopping cart has Products (weak)
- Inheritance: PremiumUser extends User
- Dependency: Uses temporarily
Multiplicity
- 1: Exactly one
- 0..1: Zero or one
- *: Zero or more
- 1..*: One or more
- m..n: Range (e.g., 2..5)
Best Practices
- ✓ Show only relevant details
- ✓ Use clear naming
- ✓ Group related classes
- ✓ Show key relationships
- ✓ Include multiplicity
Sequence Diagram
Shows how objects interact over time. Focuses on message flow and order of operations.
User Login Sequence
User Browser API Server Auth Service Database
│ │ │ │ │
│ Enter │ │ │ │
│ Password │ │ │ │
├──────────>│ │ │ │
│ │ │ │ │
│ │POST /login │ │ │
│ ├───────────>│ │ │
│ │ │ │ │
│ │ │ Verify JWT │ │
│ │ ├────────────>│ │
│ │ │ │ │
│ │ │ │ Query user │
│ │ │ ├───────────>│
│ │ │ │ │
│ │ │ │ User data │
│ │ │ │<───────────┤
│ │ │ │ │
│ │ │ Check pwd │ │
│ │ │<────────────┤ │
│ │ │ │ │
│ │ │ Generate │ │
│ │ │ Token │ │
│ │ ├────────────>│ │
│ │ │ │ │
│ │ │ JWT │ │
│ │ │<────────────┤ │
│ │ │ │ │
│ │ 200 OK │ │ │
│ │ + Token │ │ │
│ │<───────────┤ │ │
│ │ │ │ │
│ Success │ │ │ │
│<──────────┤ │ │ │
│ │ │ │ │
Alternative flow (failure):
│ │ │ │ │
│ │ │ Invalid pwd │ │
│ │ │<────────────┤ │
│ │ │ │ │
│ │ 401 Error │ │ │
│ │<───────────┤ │ │
│ │ │ │ │
│ Error │ │ │ │
│<──────────┤ │ │ │
Components
Participants
- • Actors (User, Admin)
- • Systems (API Server, Database)
- • Objects (Controller, Service)
Messages
- Synchronous: ──────> (wait for response)
- Asynchronous: ─────-> (no wait)
- Return: <────── (dashed)
- Self-call: ↻ (to itself)
Control Structures
- alt: if-else conditions
- opt: optional flow
- loop: iteration
- par: parallel execution
Use Cases
- ✓ API flow documentation
- ✓ Debugging complex flows
- ✓ Understanding timing
- ✓ Integration testing
Deployment Diagram
Shows physical deployment of artifacts on hardware nodes. Represents infrastructure and deployment architecture.
┌──────────────────────────────────────────────────────────────────────┐
│ Production Deployment Architecture │
└──────────────────────────────────────────────────────────────────────┘
┌─────────────────┐
│ <> │
│ CloudFlare CDN │
│ (Global Edge) │
└────────┬────────┘
│ HTTPS
↓
┌─────────────────┐
│ <> │
│ Load Balancer │
│ (AWS ALB) │
└────────┬────────┘
│
┌──────────────────────┼──────────────────────┐
│ │ │
↓ ↓ ↓
┌───────────────────┐ ┌───────────────────┐ ┌───────────────────┐
│ <> │ │ <> │ │ <> │
│ Web Server 1 │ │ Web Server 2 │ │ Web Server 3 │
│ (EC2 t3.medium) │ │ (EC2 t3.medium) │ │ (EC2 t3.medium) │
├───────────────────┤ ├───────────────────┤ ├───────────────────┤
│ <> │ │ <> │ │ <> │
│ Node.js App │ │ Node.js App │ │ Node.js App │
│ Docker Container │ │ Docker Container │ │ Docker Container │
└─────────┬─────────┘ └─────────┬─────────┘ └─────────┬─────────┘
│ │ │
└───────────────────────┼───────────────────────┘
│
↓
┌───────────────────────┐
│ <> │
│ Redis Cluster │
│ (ElastiCache) │
│ - Cache │
│ - Session Store │
└───────────────────────┘
│
↓
┌───────────────────────┐
│ <> │
│ PostgreSQL Primary │
│ (RDS db.r5.large) │
│ - Multi-AZ │
└──────────┬────────────┘
│ Replication
┌──────────────┼──────────────┐
↓ ↓ ↓
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Read Replica │ │ Read Replica │ │ Read Replica │
│ (us-east) │ │ (us-west) │ │ (eu-west) │
└──────────────┘ └──────────────┘ └──────────────┘
┌───────────────────────┐
│ <> │
│ S3 Bucket │
│ - Static Assets │
│ - User Uploads │
│ - Backups │
└───────────────────────┘
Network Configuration:
- VPC: 10.0.0.0/16
- Public Subnet: 10.0.1.0/24 (Load Balancer)
- Private Subnet: 10.0.2.0/24 (App Servers)
- Private Subnet: 10.0.3.0/24 (Databases)
- Security Groups: Configured per tier
Nodes
- • Physical servers
- • Virtual machines
- • Containers
- • Cloud services
- • Network devices
Artifacts
- • Application code
- • Docker images
- • JAR/WAR files
- • Static files
- • Configuration
Communication
- • HTTP/HTTPS
- • TCP/UDP
- • Database protocols
- • Message queues
- • RPC calls
Cloud Architecture
Cloud architecture leverages cloud computing services to build scalable, resilient, and cost-effective systems. Understanding networking, compute, and storage services is essential for modern system design.
VPC & Network Architecture
Virtual Private Cloud (VPC) is your isolated network in the cloud. It provides complete control over networking configuration.
AWS VPC Architecture (Multi-AZ High Availability):
┌────────────────────────────────────────────────────────────────────────┐
│ VPC: 10.0.0.0/16 (us-east-1) │
│ │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ Internet Gateway │ │
│ └────────────────────────────┬─────────────────────────────────────┘ │
│ │ │
│ ┌────────────────────────────┴─────────────────────────────────────┐ │
│ │ NAT Gateway (Public Subnet) │ │
│ └────────────────────────────┬─────────────────────────────────────┘ │
│ │ │
├───────────────────────────────┼────────────────────────────────────────┤
│ Availability Zone A │ Availability Zone B │
├───────────────────────────────┼────────────────────────────────────────┤
│ ┌─────────────────────────┐ │ ┌─────────────────────────┐ │
│ │ Public Subnet │ │ │ Public Subnet │ │
│ │ 10.0.1.0/24 │ │ │ 10.0.2.0/24 │ │
│ ├─────────────────────────┤ │ ├─────────────────────────┤ │
│ │ • Load Balancer │ │ │ • Load Balancer │ │
│ │ • Bastion Host │ │ │ • NAT Gateway │ │
│ └──────────┬──────────────┘ │ └──────────┬──────────────┘ │
│ │ │ │ │
│ ┌──────────▼──────────────┐ │ ┌──────────▼──────────────┐ │
│ │ Private Subnet │ │ │ Private Subnet │ │
│ │ 10.0.3.0/24 │ │ │ 10.0.4.0/24 │ │
│ ├─────────────────────────┤ │ ├─────────────────────────┤ │
│ │ • EC2 (App Servers) │ │ │ • EC2 (App Servers) │ │
│ │ • ECS/EKS (Containers) │ │ │ • ECS/EKS (Containers) │ │
│ │ • Lambda Functions │ │ │ • Lambda Functions │ │
│ └──────────┬──────────────┘ │ └──────────┬──────────────┘ │
│ │ │ │ │
│ ┌──────────▼──────────────┐ │ ┌──────────▼──────────────┐ │
│ │ Private Subnet │ │ │ Private Subnet │ │
│ │ 10.0.5.0/24 │ │ │ 10.0.6.0/24 │ │
│ ├─────────────────────────┤ │ ├─────────────────────────┤ │
│ │ • RDS Primary │ │ │ • RDS Standby │ │
│ │ • ElastiCache │ │ │ • ElastiCache │ │
│ │ • Internal Load Balancer│ │ │ • Internal Load Balancer│ │
│ └─────────────────────────┘ │ └─────────────────────────┘ │
│ │ │
└───────────────────────────────┴────────────────────────────────────────┘
Security Groups:
- Web Tier: Allow 80/443 from Internet
- App Tier: Allow traffic from Web Tier only
- DB Tier: Allow 5432 from App Tier only
Network ACLs: Subnet-level firewall rules
Route Tables: Control traffic routing
Terraform VPC Configuration
# main.tf
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
enable_dns_hostnames = true
enable_dns_support = true
tags = {
Name = "production-vpc"
}
}
# Internet Gateway
resource "aws_internet_gateway" "main" {
vpc_id = aws_vpc.main.id
}
# Public Subnets
resource "aws_subnet" "public" {
count = 2
vpc_id = aws_vpc.main.id
cidr_block = "10.0.${count.index + 1}.0/24"
availability_zone = data.aws_availability_zones.available.names[count.index]
map_public_ip_on_launch = true
tags = {
Name = "public-subnet-${count.index + 1}"
Tier = "Public"
}
}
# Private Subnets (App Tier)
resource "aws_subnet" "private_app" {
count = 2
vpc_id = aws_vpc.main.id
cidr_block = "10.0.${count.index + 3}.0/24"
availability_zone = data.aws_availability_zones.available.names[count.index]
tags = {
Name = "private-app-subnet-${count.index + 1}"
Tier = "Application"
}
}
# Private Subnets (Data Tier)
resource "aws_subnet" "private_data" {
count = 2
vpc_id = aws_vpc.main.id
cidr_block = "10.0.${count.index + 5}.0/24"
availability_zone = data.aws_availability_zones.available.names[count.index]
tags = {
Name = "private-data-subnet-${count.index + 1}"
Tier = "Database"
}
}
# NAT Gateway
resource "aws_eip" "nat" {
count = 2
domain = "vpc"
}
resource "aws_nat_gateway" "main" {
count = 2
allocation_id = aws_eip.nat[count.index].id
subnet_id = aws_subnet.public[count.index].id
}
# Route Tables
resource "aws_route_table" "public" {
vpc_id = aws_vpc.main.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.main.id
}
}
resource "aws_route_table" "private" {
count = 2
vpc_id = aws_vpc.main.id
route {
cidr_block = "0.0.0.0/0"
nat_gateway_id = aws_nat_gateway.main[count.index].id
}
}
Security Groups
# security_groups.tf
# Load Balancer Security Group
resource "aws_security_group" "alb" {
name = "alb-sg"
description = "Security group for ALB"
vpc_id = aws_vpc.main.id
ingress {
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
description = "HTTPS from Internet"
}
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
description = "HTTP from Internet"
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
# Application Security Group
resource "aws_security_group" "app" {
name = "app-sg"
description = "Security group for app servers"
vpc_id = aws_vpc.main.id
ingress {
from_port = 3000
to_port = 3000
protocol = "tcp"
security_groups = [aws_security_group.alb.id]
description = "Allow from ALB"
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
# Database Security Group
resource "aws_security_group" "db" {
name = "db-sg"
description = "Security group for database"
vpc_id = aws_vpc.main.id
ingress {
from_port = 5432
to_port = 5432
protocol = "tcp"
security_groups = [aws_security_group.app.id]
description = "PostgreSQL from app tier"
}
}
Network Best Practices:
- ✓ Use multiple AZs for high availability
- ✓ Separate public/private subnets
- ✓ NAT Gateway for outbound internet from private subnets
- ✓ Security groups over NACLs (stateful vs stateless)
- ✓ VPC Flow Logs for network monitoring
- ✓ VPC Peering for multi-VPC communication
Auto Scaling Architecture
Automatically adjust compute capacity to maintain performance and optimize costs.
Auto Scaling Group Configuration
# auto_scaling.tf
resource "aws_launch_template" "app" {
name_prefix = "app-server-"
image_id = data.aws_ami.amazon_linux_2.id
instance_type = "t3.medium"
vpc_security_group_ids = [aws_security_group.app.id]
user_data = base64encode(<<-EOF
#!/bin/bash
yum update -y
yum install -y docker
systemctl start docker
systemctl enable docker
# Pull and run application
docker pull myapp:latest
docker run -d -p 3000:3000 \
-e DB_HOST=${aws_db_instance.main.address} \
-e REDIS_HOST=${aws_elasticache_cluster.main.cache_nodes[0].address} \
myapp:latest
EOF
)
iam_instance_profile {
name = aws_iam_instance_profile.app.name
}
monitoring {
enabled = true
}
}
resource "aws_autoscaling_group" "app" {
name = "app-asg"
min_size = 2
max_size = 10
desired_capacity = 3
health_check_type = "ELB"
health_check_grace_period = 300
vpc_zone_identifier = aws_subnet.private_app[*].id
target_group_arns = [aws_lb_target_group.app.arn]
launch_template {
id = aws_launch_template.app.id
version = "$Latest"
}
tag {
key = "Name"
value = "app-server"
propagate_at_launch = true
}
}
# Scale up policy
resource "aws_autoscaling_policy" "scale_up" {
name = "scale-up"
autoscaling_group_name = aws_autoscaling_group.app.name
adjustment_type = "ChangeInCapacity"
scaling_adjustment = 2
cooldown = 300
}
# Scale down policy
resource "aws_autoscaling_policy" "scale_down" {
name = "scale-down"
autoscaling_group_name = aws_autoscaling_group.app.name
adjustment_type = "ChangeInCapacity"
scaling_adjustment = -1
cooldown = 300
}
# CloudWatch Alarms
resource "aws_cloudwatch_metric_alarm" "high_cpu" {
alarm_name = "high-cpu"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = "2"
metric_name = "CPUUtilization"
namespace = "AWS/EC2"
period = "120"
statistic = "Average"
threshold = "70"
alarm_actions = [aws_autoscaling_policy.scale_up.arn]
dimensions = {
AutoScalingGroupName = aws_autoscaling_group.app.name
}
}
resource "aws_cloudwatch_metric_alarm" "low_cpu" {
alarm_name = "low-cpu"
comparison_operator = "LessThanThreshold"
evaluation_periods = "2"
metric_name = "CPUUtilization"
namespace = "AWS/EC2"
period = "120"
statistic = "Average"
threshold = "30"
alarm_actions = [aws_autoscaling_policy.scale_down.arn]
dimensions = {
AutoScalingGroupName = aws_autoscaling_group.app.name
}
}
Scaling Strategies
Target Tracking
Maintain a target metric value (e.g., 70% CPU)
resource "aws_autoscaling_policy" "target_tracking" {
name = "target-tracking-cpu"
autoscaling_group_name = aws_autoscaling_group.app.name
policy_type = "TargetTrackingScaling"
target_tracking_configuration {
predefined_metric_specification {
predefined_metric_type = "ASGAverageCPUUtilization"
}
target_value = 70.0
}
}
Step Scaling
Different scaling amounts based on alarm severity
- • CPU 50-70%: Add 1 instance
- • CPU 70-90%: Add 2 instances
- • CPU > 90%: Add 3 instances
Scheduled Scaling
Scale based on predictable patterns
resource "aws_autoscaling_schedule" "morning_scale_up" {
autoscaling_group_name = aws_autoscaling_group.app.name
scheduled_action_name = "morning-scale-up"
min_size = 5
max_size = 10
desired_capacity = 8
recurrence = "0 8 * * MON-FRI"
}
resource "aws_autoscaling_schedule" "evening_scale_down" {
autoscaling_group_name = aws_autoscaling_group.app.name
scheduled_action_name = "evening-scale-down"
min_size = 2
max_size = 10
desired_capacity = 3
recurrence = "0 20 * * *"
}
Multi-Region Deployment
Deploy across multiple geographic regions for global reach, disaster recovery, and compliance.
Multi-Region Active-Active Architecture:
┌──────────────────────┐
│ Route 53 (DNS) │
│ Latency-based │
│ Routing │
└────┬────────────┬────┘
│ │
┌─────────────┘ └─────────────┐
│ │
↓ ↓
┌────────────────────┐ ┌────────────────────┐
│ US-EAST-1 │ │ EU-WEST-1 │
│ (Primary) │ │ (Secondary) │
├────────────────────┤ ├────────────────────┤
│ • CloudFront CDN │ │ • CloudFront CDN │
│ • Load Balancer │ │ • Load Balancer │
│ • EC2 ASG (3-10) │ │ • EC2 ASG (3-10) │
│ • ElastiCache │ │ • ElastiCache │
│ • RDS Primary │◄───Replication───│ • RDS Read Replica │
│ • S3 (Primary) │◄───Replication───│ • S3 (Replica) │
└────────────────────┘ └────────────────────┘
│ │
└──────────────┬──────────────┬──────────┘
↓ ↓
┌───────────────────────────┐
│ Global DynamoDB Tables │
│ (Multi-region) │
└───────────────────────────┘
Benefits:
✓ Low latency (users routed to nearest region)
✓ Disaster recovery (automatic failover)
✓ Compliance (data residency requirements)
✓ Load distribution (reduce single region load)
Deployment Strategies
- Active-Active: All regions serve traffic simultaneously. Better utilization, more complex.
- Active-Passive: One region active, others on standby. Simpler, but wasted capacity.
- Hot-Warm-Cold: Primary (hot), backup ready (warm), cold backup (archives).
Data Replication
- Database: RDS cross-region replicas, DynamoDB global tables
- Storage: S3 cross-region replication (CRR)
- Cache: Redis Global Datastore
- Consistency: Eventual consistency trade-off
Challenges
- • Data consistency across regions
- • Increased infrastructure costs
- • Complex deployment pipelines
- • Cross-region network latency
- • Testing disaster recovery
Best Practices
- ✓ Use managed services (RDS, DynamoDB)
- ✓ Automate everything (IaC)
- ✓ Monitor all regions
- ✓ Regular DR drills
- ✓ Plan for region failure
Cost Optimization
Cloud costs can spiral out of control if not managed properly. A well-architected system balances performance, reliability, and cost. Smart optimization can reduce cloud bills by 40-70% without sacrificing quality.
Cloud Cost Optimization Strategies
Compute Optimization
Right-Sizing Instances
Match instance types to actual workload requirements
# Analyze CloudWatch metrics
aws cloudwatch get-metric-statistics \
--namespace AWS/EC2 \
--metric-name CPUUtilization \
--dimensions Name=InstanceId,Value=i-1234567890abcdef0 \
--start-time 2025-10-01T00:00:00Z \
--end-time 2025-11-01T00:00:00Z \
--period 3600 \
--statistics Average
# Results show average CPU: 15%
# Current: t3.xlarge (4 vCPU, $0.1664/hr)
# Optimized: t3.medium (2 vCPU, $0.0416/hr)
# Savings: 75% reduction = $1,050/month
Spot Instances
Use spare AWS capacity at up to 90% discount
Best For: Batch jobs, data processing, CI/CD, stateless apps
Not For: Databases, critical services with strict SLAs
resource "aws_spot_fleet_request" "batch" {
allocation_strategy = "lowestPrice"
target_capacity = 10
valid_until = "2025-12-31T23:59:59Z"
terminate_instances_with_expiration = true
launch_specification {
instance_type = "c5.large"
ami = "ami-12345678"
spot_price = "0.05" # Max price
}
# Fallback to on-demand if no spot
launch_specification {
instance_type = "c5.large"
ami = "ami-12345678"
}
}
# Savings: $0.085/hr (on-demand) → $0.025/hr (spot)
# 70% cost reduction!
Reserved Instances / Savings Plans
Commit to 1-3 years for predictable workloads
- • 1-year: 30-40% savings
- • 3-year: 50-72% savings
- • Mix: Reserved for baseline + Spot for peaks
Storage Optimization
S3 Storage Classes
| Class | Cost/GB | Use Case |
|---|---|---|
| Standard | $0.023 | Frequent access |
| IA | $0.0125 | Monthly access |
| Glacier | $0.004 | Archival |
| Deep Archive | $0.00099 | Long-term |
# S3 Lifecycle Policy
resource "aws_s3_bucket_lifecycle_configuration" "optimize" {
bucket = aws_s3_bucket.main.id
rule {
id = "move-to-ia"
status = "Enabled"
transition {
days = 30
storage_class = "STANDARD_IA"
}
transition {
days = 90
storage_class = "GLACIER"
}
transition {
days = 365
storage_class = "DEEP_ARCHIVE"
}
expiration {
days = 2555 # 7 years
}
}
}
# Example: 1TB data
# Month 1: $23 (Standard)
# Month 2-3: $12.50 (IA)
# Month 4-12: $4 (Glacier)
# Year 2+: $0.99 (Deep Archive)
# Total savings: 95% over 2 years
Database Optimization
- Read Replicas: Offload read traffic (cheaper than scaling primary)
- Aurora Serverless: Auto-scale based on load
- Compression: Reduce storage by 60-80%
- Archiving: Move old data to cheaper storage
Delete Unused Resources
# Find unattached EBS volumes
aws ec2 describe-volumes \
--filters Name=status,Values=available \
--query 'Volumes[*].[VolumeId,Size,VolumeType]'
# Find old snapshots (> 30 days)
aws ec2 describe-snapshots --owner-ids self \
--query 'Snapshots[?StartTime<=`2024-10-01`].[SnapshotId,StartTime]'
# Unused Elastic IPs (charges apply!)
aws ec2 describe-addresses \
--filters "Name=association-id,Values=''"
# Common waste: 30% of resources unused!
Network & Data Transfer Optimization
Data transfer costs are often overlooked but can be 10-20% of total cloud bill.
Data Transfer Pricing (AWS)
| Transfer Type | Cost |
|---|---|
| Data IN (Internet → AWS) | FREE |
| Data OUT (AWS → Internet) | $0.09/GB |
| Same AZ | FREE |
| Cross-AZ (same region) | $0.01/GB each way |
| Cross-Region | $0.02/GB |
| Via CloudFront | $0.085/GB (cheaper!) |
Cost Example:
API serving 10TB/month to users:
- ❌ Direct from EC2: 10,000 GB × $0.09 = $900/month
- ✅ Via CloudFront: 10,000 GB × $0.085 = $850/month
- ✅ With caching (90% hit): 1,000 GB × $0.085 = $85/month
- Savings: $815/month (90%!)
Optimization Strategies
1. Use CDN/CloudFront
- ✓ Cache static content at edge
- ✓ Reduce origin data transfer
- ✓ Lower latency for users
- ✓ Cheaper than direct transfer
2. Compress Data
// Enable compression in NGINX
gzip on;
gzip_types text/plain text/css application/json
application/javascript text/xml
application/xml image/svg+xml;
gzip_min_length 1000;
// Result: 1MB → 200KB (80% reduction)
// Transfer cost: $0.09 → $0.018 per file
3. Regional Architecture
- ✓ Deploy close to users
- ✓ Minimize cross-region transfers
- ✓ Use VPC endpoints (free transfer)
- ✓ Keep services in same AZ when possible
Cost Monitoring & Governance
AWS Cost Management Tools
AWS Cost Explorer
- • Visualize spending trends
- • Forecast future costs
- • Filter by service, tag, region
- • Identify cost spikes
AWS Budgets
resource "aws_budgets_budget" "monthly" {
name = "monthly-budget"
budget_type = "COST"
limit_amount = "1000"
limit_unit = "USD"
time_unit = "MONTHLY"
notification {
comparison_operator = "GREATER_THAN"
threshold = 80
threshold_type = "PERCENTAGE"
notification_type = "ACTUAL"
subscriber_email_addresses = [
"team@example.com"
]
}
}
Cost Allocation Tags
- • Tag by: Environment, Team, Project
- • Track costs per application
- • Chargeback to departments
- • Identify waste quickly
Cost Optimization Checklist
-
Right-size instances
Review CPU/memory usage monthly -
Use auto-scaling
Scale down during off-hours -
Implement caching
Redis/Memcached, CloudFront CDN -
Use Spot/Reserved instances
Mix for optimal cost-performance -
Delete unused resources
Old snapshots, unattached volumes, test servers -
Optimize storage
S3 lifecycle policies, database cleanup -
Monitor data transfer
Use VPC endpoints, regional deployment -
Set up alerts
Budget alerts, anomaly detection -
Review monthly
Cost reports, optimization opportunities
Real-World System Design Case Studies
Let's examine how tech giants architect their systems to serve billions of users. These case studies reveal practical applications of the concepts we've covered.
YouTube Architecture
2+ billion users, 500+ hours uploaded per minute
System Requirements
Functional:
- • Upload videos (any format, up to 256GB)
- • Stream videos (adaptive bitrate)
- • Search videos (full-text, filters)
- • User engagement (likes, comments, subscribe)
- • Recommendations (personalized)
Non-Functional:
- • 1 billion hours watched daily
- • Video starts in < 2 seconds
- • 99.9% availability
- • Global CDN for low latency
- • Handle viral videos (10M+ views)
High-Level Architecture
┌─────────────────────────────────────────┐
│ YouTube Architecture │
└─────────────────────────────────────────┘
User Upload Flow:
[User] → [Load Balancer] → [Upload Service]
↓
[Google Cloud Storage]
↓
[Transcoding Pipeline]
(Parallel workers)
↓
Multiple formats:
• 2160p (4K)
• 1080p (Full HD)
• 720p, 480p, 360p
• Various codecs (VP9, H.264)
↓
[CDN Distribution]
(1000+ edge locations)
Video Playback Flow:
[User] → [CDN Edge Server] → [HLS/DASH Stream]
↓ (Cache Miss)
[Origin Servers]
↓
[Video Metadata DB]
(MySQL/Bigtable)
Search & Recommendations:
[User Query] → [Search Service]
↓
[Elasticsearch]
↓
[ML Model]
(TensorFlow recommendations)
↓
[Result Ranking]
Storage Strategy
- Original Videos: Google Cloud Storage (cold storage for old videos)
- Processed Videos: Multiple formats cached at edge
- Metadata: MySQL + Bigtable (views, likes, comments)
- Total: Exabytes (millions of TB)
Scalability Techniques
- CDN: Cache popular videos at edge (90%+ hit rate)
- Sharding: Partition by video ID
- Read Replicas: Scale database reads
- Async Processing: Transcoding in background
Key Optimizations
- Adaptive Streaming: Adjust quality based on bandwidth
- Prefetching: Load next segments ahead
- Compression: VP9 codec (50% smaller)
- ML: Predict popular videos, pre-cache
WhatsApp Architecture
2+ billion users, 100+ billion messages daily
Architecture Highlights
WhatsApp Messaging Flow:
[User A]
↓ Send message
[WhatsApp Client]
↓ XMPP Protocol
[Load Balancer]
↓
[Chat Server (Erlang)]
↓
┌──────────────────────────┐
│ 1. Store message │
│ (Mnesia DB - in-memory)│
│ │
│ 2. Check User B online? │
│ (Redis presence) │
│ │
│ 3. If online: Push │
│ If offline: Queue │
└──────────────────────────┘
↓
[User B receives message]
↓
[Send ACK (double check)]
↓
[Delete from server]
Key: Messages deleted after delivery!
End-to-end encrypted (Signal Protocol)
Tech Stack:
- Language: Erlang (2M+ connections per server)
- Database: Mnesia (in-memory), RocksDB
- Caching: Redis (presence, online status)
- Media: S3-like object storage
- Push: FCM (Android), APNs (iOS)
Design Decisions
Why Erlang?
- ✓ Built for telecom (millions of concurrent connections)
- ✓ Hot code reloading (no downtime)
- ✓ Lightweight processes (millions per server)
- ✓ Built-in fault tolerance
Minimal Data Storage
- • Messages deleted after delivery
- • Only metadata stored (who sent what when)
- • Media stored temporarily (30 days)
- • Privacy-focused design
Scalability Approach
- Sharding: By phone number hash
- Load Balancing: Consistent hashing
- Presence: Redis pub/sub
- Team Size: ~50 engineers (incredibly efficient!)
End-to-End Encryption
WhatsApp uses Signal Protocol for E2EE (even WhatsApp can't read messages)
How it works:
- 1. Device generates key pair (public/private)
- 2. Public keys exchanged via server
- 3. Message encrypted with recipient's public key
- 4. Only recipient's private key can decrypt
Benefits:
- ✓ Complete privacy
- ✓ WhatsApp cannot access content
- ✓ Forward secrecy (keys rotate)
- ✓ Verification via QR code
Uber Architecture
Real-time ride matching, 10,000+ cities
Ride Matching Algorithm
Uber Ride Request Flow:
[User requests ride]
↓
[Location: lat=40.7589, lng=-73.9851]
↓
[Geohashing / QuadTree Indexing]
↓
Find nearby drivers:
┌─────────────────────────────┐
│ QuadTree (spatial index) │
│ │
│ Search radius: 2 miles │
│ Found: 15 drivers │
│ │
│ Filter: │
│ • Available │
│ • High rating (>4.7) │
│ • Car type matches │
│ Result: 8 drivers │
└─────────────────────────────┘
↓
[Score & Rank drivers]
Factors:
• Distance to pickup
• Driver rating
• Acceptance rate
• Estimated wait time
↓
[Send request to top driver]
↓
Driver accepts?
✓ Yes → Match confirmed
✗ No → Try next driver (15s timeout)
↓
[Real-time tracking]
WebSocket connection
Location updates every 4s
Technical Components
Geospatial Indexing
// Geohash encoding
Location: 40.7589, -73.9851
Geohash: dr5ru6
Precision: 6 chars = ~1.2km area
// QuadTree for efficient search
class QuadTree {
search(lat, lng, radius) {
// O(log n) instead of O(n)
// Check only relevant quadrants
return nearby_drivers;
}
}
// Redis Geo commands
GEOADD drivers -73.9851 40.7589 "driver123"
GEORADIUS drivers -73.9851 40.7589 2 km
Surge Pricing
- Algorithm: Supply vs Demand in area
- Real-time: Calculate every 1-2 minutes
- Factors: Request rate, driver availability, events
- Implementation: Kafka streams + ML
Schemaless Storage
Uber's custom MySQL-based storage (handles 100K+ writes/sec)
- • Stores trip data, driver info, payments
- • Auto-sharding by ID
- • Fallback to another shard on failure
Microservices
- • Dispatch Service
- • Pricing Service
- • Payment Service
- • Notification Service
- • Maps Service
- • ETA Service
- 2,200+ microservices!
Tech Stack
- Backend: Go, Python, Node.js
- DB: MySQL, PostgreSQL, Cassandra
- Cache: Redis
- Messaging: Kafka
- Orchestration: Kubernetes
Challenges Solved
- ✓ Real-time matching (< 1s)
- ✓ GPS accuracy issues
- ✓ Network reliability
- ✓ Peak hour scaling (10x load)
- ✓ Global consistency
Twitter (X) Architecture
500M tweets/day, real-time timeline delivery
Timeline Delivery Challenge
Problem:
Celebrity with 100M followers tweets. How to deliver to all followers' timelines instantly?
❌ Bad Approach: Pull on Read
When user opens app:
1. Find people they follow (1000 users)
2. Get latest tweets from each (1000 queries!)
3. Merge and sort by timestamp
4. Return top 50
Problem: Too slow! 1000+ DB queries
✓ Good Approach: Fan-out on Write
When celebrity tweets:
1. Insert tweet into timeline of all followers
(Async, background workers)
2. Pre-compute timelines
When user opens app:
1. Read from their pre-computed timeline (1 query!)
2. Fast! O(1) instead of O(n)
Implementation: Redis sorted sets
Hybrid Approach
For Normal Users (< 10K followers):
Fan-out on write - Push to all followers' timelines
- ✓ Fast reads
- ✓ Acceptable write cost
For Celebrities (100M+ followers):
Pull on read - Fetch their tweets when user reads timeline
- ✓ Avoid 100M writes per tweet
- ✓ Cache aggressively
Timeline Assembly:
// Merge timelines
timeline = get_precomputed_timeline(user_id);
celebrity_tweets = get_celebrity_tweets(user_id);
final_timeline = merge(timeline, celebrity_tweets);
return top(final_timeline, 50);
Tech Stack Highlights:
- Backend: Scala, Java
- Timeline: Manhattan (distributed DB)
- Cache: Redis, Memcached
- Search: Elasticsearch
- Messaging: Kafka
- Storage: MySQL, Manhattan
Conclusion & Final Notes
Congratulations!
You've completed an extensive journey through Software System Design. From fundamentals to advanced architecture patterns, you now have the knowledge to design scalable, reliable, and maintainable systems.
21 Chapters
Comprehensive coverage of all system design topics
100+ Examples
Real code and architecture diagrams
Production-Ready
Battle-tested patterns and best practices
"The best system design is one that solves real problems elegantly, scales gracefully, and can be maintained by your team."
Key Takeaways
Design Principles
- Start Simple: Don't over-engineer. Add complexity only when needed.
- Think Trade-offs: Every decision has pros and cons. Choose wisely.
- Design for Failure: Assume everything will fail. Plan accordingly.
- Measure Everything: You can't improve what you don't measure.
Best Practices
- Automate: CI/CD, testing, infrastructure, monitoring.
- Document: Architecture decisions, APIs, runbooks.
- Iterate: Build → Measure → Learn → Improve.
- Communicate: Share knowledge, review designs, collaborate.
Scalability Mindset
- Horizontal > Vertical: Scale out, not just up.
- Stateless Services: Easier to scale and deploy.
- Cache Aggressively: Every layer can benefit.
- Async When Possible: Don't block user requests.
Common Pitfalls to Avoid
- Premature Optimization: Build for now, not hypothetical scale.
- Single Points of Failure: Always have redundancy.
- Ignoring Monitoring: You're flying blind without observability.
- Technical Debt: Pay it down regularly or it compounds.
Continue Your Learning Journey
Books
- • Designing Data-Intensive Applications by Martin Kleppmann
- • System Design Interview by Alex Xu
- • Building Microservices by Sam Newman
- • Site Reliability Engineering by Google
- • Clean Architecture by Robert Martin
Online Courses
- • Grokking System Design (Educative)
- • System Design Primer (GitHub)
- • Microservices Architecture (Udemy)
- • AWS Solutions Architect
- • Kubernetes Deep Dive
Websites & Blogs
- • High Scalability (highscalability.com)
- • Martin Fowler's Blog
- • Engineering blogs: Netflix, Uber, Airbnb
- • AWS Architecture Blog
- • InfoQ, Hacker News
Communities
- • r/systemdesign (Reddit)
- • System Design Discord servers
- • Dev.to, Hashnode
- • Local meetups & conferences
- • Twitter: #systemdesign
Hands-On Practice
- • Build side projects
- • Contribute to open source
- • LeetCode system design problems
- • Mock interviews (Pramp, Interviewing.io)
- • Study real systems on GitHub
Certifications
- • AWS Solutions Architect
- • Google Cloud Architect
- • Azure Solutions Architect
- • Kubernetes (CKA, CKAD)
- • TOGAF (Enterprise Architecture)
Your Journey Starts Now
System design is not just about passing interviews—it's about building systems that solve real problems, delight users, and stand the test of time. Start small, iterate often, and never stop learning.
Remember:
"Build. Measure. Learn."
"The best time to start was yesterday. The second best time is now."
Set Goals
Practice Daily
Ship It!