Your startup's app works perfectly with 100 users. But at 10,000 users, everything breaks. The database is overloaded, the single server can't handle requests, and users are frustrated. This is the moment backend architecture becomes critical. Great architecture scales from 1 to 1 million users without rewriting everything. In this comprehensive guide, you'll learn the architectural patterns that power companies like Netflix, Uber, and Amazon.
1. Learning Objectives
By the end of this lesson, you will be able to:
- Understand different architectural patterns (monolithic, microservices, serverless)
- Design scalable database architectures (read replicas, sharding)
- Implement caching strategies (Redis, CDN, application cache)
- Use message queues for async processing (RabbitMQ, Kafka, SQS)
- Design fault-tolerant and highly available systems
- Make informed trade-offs between consistency, availability, and performance
- Architect a complete scalable backend system
2. Why Backend Architecture Matters
Real-world scenario: Black Friday. Your e-commerce site is getting 100x normal traffic. Without proper architecture, the site crashes, you lose millions in revenue, and customers go to competitors. With proper architecture, the site stays up, scales automatically, and handles the load seamlessly.
Poor architecture leads to:
- ❌ Slow response times (users leave)
- ❌ Downtime during traffic spikes (lost revenue)
- ❌ Expensive scaling (over-provisioning resources)
- ❌ Difficult maintenance (spaghetti code)
- ❌ Team bottlenecks (can't work in parallel)
Good architecture enables:
- ✅ Handle millions of concurrent users
- ✅ 99.99% uptime (minutes of downtime per year)
- ✅ Scale cost-effectively
- ✅ Multiple teams working independently
- ✅ Fast feature development
3. Core Concepts
Architectural Patterns
Monolithic Architecture
┌─────────────────────────────────────┐
│ MONOLITHIC APP │
├─────────────────────────────────────┤
│ ┌─────────┐ ┌─────────┐ ┌─────┐ │
│ │ User │ │ Product │ │Order│ │
│ │ Module │ │ Module │ │Mod │ │
│ └─────────┘ └─────────┘ └─────┘ │
│ ┌─────────────────────────────┐ │
│ │ Shared Database │ │
│ └─────────────────────────────┘ │
└─────────────────────────────────────┘
Pros: Simple to develop, easy to deploy, fewer network calls
Cons: Hard to scale individual components, single point of failure, large codebase
Best for: Small teams, startups, simple applications (first 6-12 months)
Microservices Architecture
┌─────────┐ ┌─────────┐ ┌─────────┐
│ User │ │Product │ │ Order │
│ Service │ │Service │ │ Service │
└────┬────┘ └────┬────┘ └────┬────┘
│ │ │
└──────────────┼──────────────┘
│
┌─────┴─────┐
│ API │
│ Gateway │
└─────┬─────┘
│
┌─────┴─────┐
│ Client │
└───────────┘
User DB Product DB Order DB
(Postgres) (MongoDB) (Postgres)
Pros: Independent scaling, technology diversity, team autonomy, faster deployments
Cons: Distributed system complexity, network latency, data consistency challenges
Best for: Large teams, complex domains, need for independent scaling
Serverless Architecture
Client Request
│
▼
┌─────────────┐
│ API Gateway │
└──────┬──────┘
│
▼
┌─────────────┐ ┌─────────────┐
│ Lambda │────▶│ S3 │
│ Function │ │ (Static) │
└──────┬──────┘ └─────────────┘
│
▼
┌─────────────┐ ┌─────────────┐
│ DynamoDB │ │ SQS │
│ (NoSQL) │ │ (Queue) │
└─────────────┘ └─────────────┘
Pros: No infrastructure management, auto-scaling, pay only for usage
Cons: Cold starts, vendor lock-in, execution time limits
Best for: Spiky workloads, event-driven processing, startups
Database Architecture Patterns
Read Replicas (Scale Reads)
┌─────────────┐
Write ──────────│ Primary │
│ Database │
└──────┬──────┘
│ Async Replication
┌──────────────┼──────────────┐
│ │ │
▼ ▼ ▼
┌───────────┐ ┌───────────┐ ┌───────────┐
│ Replica 1 │ │ Replica 2 │ │ Replica 3 │
│ (Read) │ │ (Read) │ │ (Read) │
└───────────┘ └───────────┘ └───────────┘
▲ ▲ ▲
│ │ │
└──────────────┼──────────────┘
│
Read Load Balancer
│
┌──────┴──────┐
│ App Reads │
└─────────────┘
# Application code for read/write splitting
class DatabaseRouter:
def __init__(self):
self.write_db = "primary.postgres.example.com"
self.read_replicas = [
"replica1.postgres.example.com",
"replica2.postgres.example.com",
"replica3.postgres.example.com"
]
self.replica_index = 0
def get_connection(self, operation):
if operation in ['INSERT', 'UPDATE', 'DELETE', 'CREATE']:
# Write operations go to primary
return self.write_db
else:
# Read operations go to replicas (round-robin)
replica = self.read_replicas[self.replica_index % len(self.read_replicas)]
self.replica_index += 1
return replica
# Usage
router = DatabaseRouter()
# Write goes to primary
db = router.get_connection('INSERT')
db.execute("INSERT INTO users VALUES ('alex', 'alex@example.com')")
# Reads go to replicas
for i in range(100):
db = router.get_connection('SELECT')
result = db.query("SELECT * FROM products")
Database Sharding (Scale Writes)
┌─────────────────────────────┐
│ Application │
└─────────────┬───────────────┘
│
┌─────────────▼───────────────┐
│ Sharding Layer │
│ (Hash user_id % 4 = shard)│
└─────────────┬───────────────┘
│
┌─────────────┬───────────┼───────────┬─────────────┐
│ │ │ │ │
▼ ▼ ▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐
│Shard 0 │ │Shard 1 │ │Shard 2 │ │Shard 3 │
│Users │ │Users │ │Users │ │Users │
│0-25% │ │26-50% │ │51-75% │ │76-100% │
└────────┘ └────────┘ └────────┘ └────────┘
# Sharding implementation
import hashlib
class ShardingRouter:
def __init__(self, num_shards=4):
self.num_shards = num_shards
self.shards = [f"shard_{i}.db.example.com" for i in range(num_shards)]
def get_shard(self, user_id):
# Consistent hashing
hash_value = int(hashlib.md5(str(user_id).encode()).hexdigest(), 16)
shard_index = hash_value % self.num_shards
return self.shards[shard_index]
def get_user(self, user_id):
shard = self.get_shard(user_id)
return shard.query(f"SELECT * FROM users WHERE id = {user_id}")
def create_user(self, user_id, data):
shard = self.get_shard(user_id)
return shard.execute(f"INSERT INTO users VALUES ({user_id}, '{data}')")
# Range-based sharding (for time-series data)
def get_time_shard(timestamp):
if timestamp < '2024-01-01':
return 'historical_db'
elif timestamp < '2025-01-01':
return 'archive_db'
else:
return 'current_db'
Caching Strategies
Caching Layers
┌─────────────────────────────────────────────────────────────┐
│ USER REQUEST │
└─────────────────────────────┬───────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Layer 1: Browser Cache (CDN) │
│ - Static assets (images, CSS, JS) │
│ - Cache-Control: max-age=31536000 │
└─────────────────────────────┬───────────────────────────────┘
│ (Cache Miss)
▼
┌─────────────────────────────────────────────────────────────┐
│ Layer 2: CDN Cache (CloudFront, CloudFlare) │
│ - API responses, pages │
│ - TTL: 5-60 minutes │
└─────────────────────────────┬───────────────────────────────┘
│ (Cache Miss)
▼
┌─────────────────────────────────────────────────────────────┐
│ Layer 3: Redis/Memcached (Application Cache) │
│ - Database query results │
│ - Session data │
│ - TTL: 1-30 minutes │
│ - Eviction: LRU (Least Recently Used) │
└─────────────────────────────┬───────────────────────────────┘
│ (Cache Miss)
▼
┌─────────────────────────────────────────────────────────────┐
│ Layer 4: Database │
│ - Source of truth │
│ - Write-through caching │
└─────────────────────────────────────────────────────────────┘
# Redis caching implementation
import redis
import json
from functools import wraps
redis_client = redis.Redis(host='cache.example.com', port=6379, decode_responses=True)
# Cache decorator
def cache(ttl=300):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
# Generate cache key
key = f"{func.__name__}:{args}:{kwargs}"
# Try to get from cache
cached = redis_client.get(key)
if cached:
return json.loads(cached)
# Execute function
result = func(*args, **kwargs)
# Store in cache
redis_client.setex(key, ttl, json.dumps(result))
return result
return wrapper
return decorator
# Cache aside pattern
@cache(ttl=300)
def get_user_profile(user_id):
# Expensive database query
return db.query(f"SELECT * FROM users WHERE id = {user_id}")
# Write-through caching
def update_user_profile(user_id, data):
# Update database first
db.execute(f"UPDATE users SET profile = '{data}' WHERE id = {user_id}")
# Then update cache
cache_key = f"get_user_profile:{user_id}"
redis_client.setex(cache_key, 300, json.dumps(data))
# Cache invalidation (the hardest problem)
def invalidate_user_cache(user_id):
pattern = f"*:{user_id}*"
for key in redis_client.scan_iter(match=pattern):
redis_client.delete(key)
# Popular cache strategies
# 1. Cache-Aside (Lazy Loading)
# 2. Write-Through (Update cache on write)
# 3. Write-Behind (Async write to DB)
# 4. Write-Around (Write to DB only, read populates cache)
Message Queues (Async Processing)
┌─────────────────────────────────────────────────────────────┐
│ PRODUCER (Fast) │
│ - Receives HTTP request │
│ - Publishes message to queue (milliseconds) │
│ - Returns 202 Accepted immediately │
└─────────────────────────────┬───────────────────────────────┘
│
▼
┌─────────────────┐
│ Message Queue │
│ (RabbitMQ/ │
│ Kafka/SQS) │
└─────────────────┘
│
│
┌─────────┴─────────┐
│ │
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ Consumer 1 │ │ Consumer 2 │
│ (Slow) │ │ (Slow) │
│ - Processes │ │ - Sends email │
│ - Resizes │ │ - Updates │
│ images │ │ analytics │
└─────────────────┘ └─────────────────┘
# Producer - Fast HTTP endpoint
from flask import Flask, request, jsonify
import pika
import json
app = Flask(__name__)
# Connect to RabbitMQ
connection = pika.BlockingConnection(pika.ConnectionParameters('rabbitmq.example.com'))
channel = connection.channel()
channel.queue_declare(queue='order_processing', durable=True)
@app.route('/orders', methods=['POST'])
def create_order():
order_data = request.json
# Publish to queue (fast - milliseconds)
channel.basic_publish(
exchange='',
routing_key='order_processing',
body=json.dumps(order_data),
properties=pika.BasicProperties(delivery_mode=2) # Persistent
)
# Return immediately
return jsonify({
'status': 'accepted',
'message': 'Order received and processing'
}), 202
# Consumer - Background worker
import time
def process_order(ch, method, properties, body):
order = json.loads(body)
# Slow operations - no HTTP timeout worries!
time.sleep(5) # Process payment
time.sleep(3) # Update inventory
time.sleep(2) # Send confirmation email
print(f"Order {order['id']} completed!")
# Acknowledge message (remove from queue)
ch.basic_ack(delivery_tag=method.delivery_tag)
# Start consumer
channel.basic_consume(queue='order_processing', on_message_callback=process_order)
print('Waiting for orders...')
channel.start_consuming()
Load Balancing Strategies
# Nginx load balancer configuration
upstream backend_servers {
# Round Robin (default)
server backend1.example.com weight=3;
server backend2.example.com weight=2;
server backend3.example.com weight=1;
# Least Connections (send to server with fewest active connections)
least_conn;
# IP Hash (stickiness - same user to same server)
ip_hash;
# Health checks
server backend1.example.com max_fails=3 fail_timeout=30s;
server backend2.example.com backup; # Backup server
}
server {
listen 80;
location / {
proxy_pass http://backend_servers;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
# Buffering for slow clients
proxy_buffering on;
proxy_buffer_size 4k;
proxy_buffers 8 4k;
# Timeouts
proxy_connect_timeout 5s;
proxy_send_timeout 10s;
proxy_read_timeout 30s;
}
# Health check endpoint
location /health {
return 200 'OK';
add_header Content-Type text/plain;
}
}
4. Complete Architecture: E-Commerce Platform
┌─────────────────────────────────────────────────────────────────────────────┐
│ USERS │
└─────────────────────────────────┬───────────────────────────────────────────┘
│
▼
┌─────────────────────────┐
│ CloudFlare CDN │
│ - DDoS protection │
│ - SSL termination │
│ - Global caching │
└────────────┬────────────┘
│
▼
┌─────────────────────────┐
│ AWS ALB │
│ - Load balancing │
│ - Path-based routing │
└────────────┬────────────┘
│
┌─────────────────────┼─────────────────────┐
│ │ │
▼ ▼ ▼
┌───────────────────┐ ┌───────────────────┐ ┌───────────────────┐
│ API Gateway │ │ Web Servers │ │ Static Assets │
│ (Auth/Rate │ │ (Next.js) │ │ (S3 + CloudFront)│
│ limiting) │ │ 3-10 instances │ │ │
└─────────┬─────────┘ └─────────┬─────────┘ └───────────────────┘
│ │
▼ ▼
┌───────────────────┐ ┌───────────────────┐
│ Microservices │ │ │
│ (ECS/K8s) │ │ │
└─────────┬─────────┘ └───────────────────┘
│
┌─────┼─────┬─────────────┬─────────────┐
│ │ │ │ │
▼ ▼ ▼ ▼ ▼
┌────┐ ┌────┐ ┌────┐ ┌────────┐ ┌─────────┐
│User│ │Prod│ │Order│ │ Redis │ │ SQS │
│Svc │ │Svc │ │Svc │ │ Cache │ │ Queue │
└──┬─┘ └──┬─┘ └──┬──┘ └────────┘ └────┬────┘
│ │ │ │
▼ ▼ ▼ ▼
┌────┐ ┌────┐ ┌────┐ ┌─────────┐
│User│ │Prod│ │Order│ │Worker │
│DB │ │DB │ │DB │ │Services │
└────┘ └────┘ └────┘ └─────────┘
│ │ │ │
└──────┼──────┘ │
│ │
┌────┴────┐ ┌────┴────┐
│ Read │ │ Email │
│Replicas │ │ Service │
└─────────┘ └─────────┘
Monitoring & Observability:
- Prometheus (metrics)
- Grafana (dashboards)
- Loki (logs)
- Jaeger (tracing)
# docker-compose-architecture.yml
version: '3.8'
services:
# API Gateway
gateway:
image: nginx:alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf
depends_on:
- user-service
- product-service
- order-service
# Microservices
user-service:
build: ./services/user
environment:
- DB_HOST=user-db
- REDIS_HOST=cache
depends_on:
- user-db
- cache
deploy:
replicas: 3
product-service:
build: ./services/product
environment:
- DB_HOST=product-db
- REDIS_HOST=cache
depends_on:
- product-db
- cache
deploy:
replicas: 5 # Read-heavy
order-service:
build: ./services/order
environment:
- DB_HOST=order-db
- QUEUE_HOST=rabbitmq
depends_on:
- order-db
- rabbitmq
deploy:
replicas: 2
# Databases (with read replicas)
user-db:
image: postgres:15
volumes:
- user-data:/var/lib/postgresql/data
user-db-replica:
image: postgres:15
command: postgres -c hot_standby=on
depends_on:
- user-db
product-db:
image: mongodb:7
volumes:
- product-data:/data/db
order-db:
image: postgres:15
volumes:
- order-data:/var/lib/postgresql/data
# Caching
cache:
image: redis:7-alpine
command: redis-server --maxmemory 2gb --maxmemory-policy allkeys-lru
volumes:
- cache-data:/data
# Message Queue
rabbitmq:
image: rabbitmq:3-management
environment:
- RABBITMQ_DEFAULT_USER=admin
- RABBITMQ_DEFAULT_PASS=password
# Workers
email-worker:
build: ./workers/email
depends_on:
- rabbitmq
deploy:
replicas: 2
inventory-worker:
build: ./workers/inventory
depends_on:
- rabbitmq
deploy:
replicas: 3
# Monitoring
prometheus:
image: prom/prometheus
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
ports:
- "9090:9090"
grafana:
image: grafana/grafana
depends_on:
- prometheus
ports:
- "3000:3000"
volumes:
user-data:
product-data:
order-data:
cache-data:
5. Common Architecture Trade-offs
6. Common Errors & Solutions
7. Summary Checklist
8. Next Steps
Advanced backend architecture topics to learn next:
- Event-Driven Architecture: Event sourcing, CQRS, Kafka Streams
- Service Mesh: Istio, Linkerd for advanced service communication
- GraphQL Federation: Combining multiple GraphQL services
- Real-time Systems: WebSockets, Server-Sent Events, gRPC streaming
- Disaster Recovery: Multi-region deployment, backup strategies
Practice resources:
- System Design Primer - The best resource
- High Scalability - Real-world architectures
- AWS Architecture Center - Reference architectures
- ByteByteGo - System design videos
Recommended books:
- "Designing Data-Intensive Applications" by Martin Kleppmann
- "Building Microservices" by Sam Newman
- "System Design Interview" by Alex Xu
Comments (0)
This is exactly what I needed! The initContainer approach solved our migration issues completely. Thanks for the detailed guide!
ReplyLeave a Comment