Loading…
Loading…
Learn the core principles of system design including scalability, availability, and consistency. A beginner-friendly guide to thinking about large-scale systems.
Tags
This is Part 1 of the System Design from Zero to Hero series.
System design is the process of defining the architecture, components, and data flow of a system that satisfies a set of requirements. It starts not with technology choices, but with understanding trade-offs between scalability, availability, consistency, and latency. Before you pick a database or a message queue, you need to reason about what your system actually needs to do and what constraints it operates under.
Every application you use daily, whether it is a search engine, a ride-sharing app, or a messaging platform, is the result of deliberate system design decisions. When the system serves ten users, almost any architecture works. When it serves ten million, the wrong architecture collapses under its own weight.
System design is also the single most tested skill in senior engineering interviews. Companies want to see that you can reason about problems at scale, identify bottlenecks before they appear, and communicate trade-offs clearly. But beyond interviews, system design thinking is what separates engineers who build prototypes from engineers who build products that survive contact with real users.
At its simplest, every networked system follows the client-server model. A client makes a request, a server processes it and returns a response. Your browser is a client. The machine running your web application is a server.
┌──────────┐ HTTP Request ┌──────────┐
│ │ ──────────────────────────▶ │ │
│ Client │ │ Server │
│ (Browser)│ ◀──────────────────────── │ (App) │
│ │ HTTP Response │ │
└──────────┘ └──────────┘
This model extends to every layer of a distributed system. Your application server is a client to the database. Your database is a client to the filesystem. Understanding this recursive relationship is the foundation of system design thinking.
A monolithic architecture packages all functionality into a single deployable unit. One codebase, one process, one database. This is where most applications should start. It is simpler to develop, test, deploy, and debug.
A distributed system splits functionality across multiple services that communicate over a network. Each service can be developed, deployed, and scaled independently. But distribution introduces an entirely new class of problems: network failures, data consistency, partial outages, and increased operational complexity.
Monolith: Distributed:
┌─────────────────────┐ ┌──────────┐ ┌──────────┐
│ Auth + Users + │ │ Auth │ │ Users │
│ Orders + Payments │ │ Service │ │ Service │
│ + Notifications │ └────┬─────┘ └────┬─────┘
│ │ │ │
│ Single DB │ ┌────┴─────┐ ┌────┴─────┐
└─────────────────────┘ │ Orders │ │ Payments │
│ Service │ │ Service │
└──────────┘ └──────────┘
The common mistake is moving to microservices too early. If your team has fewer than 20 engineers and your product is still finding market fit, a well-structured monolith will serve you better than a distributed system. You can always decompose later; reassembling microservices into a monolith is far harder.
Every system has two types of requirements:
Functional requirements describe what the system does. "Users can upload photos." "The system sends email notifications." "Admins can generate monthly reports." These are features.
Non-functional requirements describe how well the system performs. These are the properties that matter at scale:
The critical insight is that you cannot maximize all of these simultaneously. The CAP theorem tells us that in a distributed system experiencing a network partition, you must choose between consistency and availability. Understanding these trade-offs is the core skill of system design.
Before designing a system, you need rough numbers to guide your decisions. Estimation is not about precision; it is about getting within an order of magnitude so you know whether you need one server or a thousand.
Key numbers every system designer should internalize:
Operation Time
─────────────────────────────────────────────
L1 cache reference 0.5 ns
L2 cache reference 7 ns
Main memory reference 100 ns
SSD random read 150 μs
HDD random read 10 ms
Round trip within same datacenter 0.5 ms
Round trip CA to Netherlands 150 ms
Example estimation: Suppose you are designing a photo-sharing service with 10 million daily active users. Each user uploads an average of 2 photos per day, and each photo is 2 MB.
Daily uploads: 10M users × 2 photos = 20M photos/day
Storage per day: 20M × 2 MB = 40 TB/day
Write throughput: 20M / 86,400 seconds ≈ 230 writes/sec
Peak (assume 3x average): ~700 writes/sec
Storage per year: 40 TB × 365 ≈ 14.6 PB/year
These numbers immediately tell you that you need an object storage system (not a relational database for photo blobs), you need horizontal scaling for writes, and your storage costs will be a primary budget concern.
Let us look at how these concepts manifest in a simple application. Consider a basic web API:
# A simple monolithic Flask application
from flask import Flask, jsonify, request
from datetime import datetime
import time
app = Flask(__name__)
# In-memory store (for demonstration; use a database in production)
users = {}
request_count = 0
@app.before_request
def track_metrics():
"""Non-functional requirement: observability"""
global request_count
request_count += 1
request.start_time = time.time()
@app.after_request
def log_latency(response):
"""Track latency for each request"""
latency_ms = (time.time() - request.start_time) * 1000
app.logger.info(f"Request completed in {latency_ms:.2f}ms")
response.headers['X-Response-Time'] = f"{latency_ms:.2f}ms"
return response
@app.route('/api/users', methods=['POST'])
def create_user():
"""Functional requirement: users can register"""
data = request.json
user_id = str(len(users) + 1)
users[user_id] = {
'id': user_id,
'name': data['name'],
'created_at': datetime.utcnow().isoformat()
}
return jsonify(users[user_id]), 201
@app.route('/api/health')
def health_check():
"""Non-functional requirement: availability monitoring"""
return jsonify({
'status': 'healthy',
'uptime': time.process_time(),
'total_requests': request_count
})This works for a small application. But notice the problems already present: in-memory storage means data loss on restart (durability failure), a single process means one crash takes down everything (availability failure), and the global counter with no locking would cause race conditions under concurrent load (consistency failure). System design is about recognizing these weaknesses and choosing the right mitigations based on your actual requirements.
When approaching any system design problem, use this framework:
| Decision | Choose When | Avoid When |
|---|---|---|
| Monolith | Small team, early product, rapid iteration | Large teams with independent deployment needs |
| Microservices | Clear domain boundaries, independent scaling needs | Unclear boundaries, small team, early stage |
| Strong consistency | Financial transactions, inventory counts | Social feeds, analytics, recommendations |
| Eventual consistency | High availability priority, geo-distribution | Banking, booking systems with overbooking risk |
Q: How would you design a URL shortener? Start with requirements: create short URLs, redirect to original, track click counts. Estimate scale (100M URLs, 10:1 read/write ratio). A single PostgreSQL instance with a base62-encoded auto-incrementing ID handles this comfortably. Add caching and read replicas only when you outgrow it.
Q: What is the difference between latency and throughput? Latency is the time for a single request to complete (milliseconds). Throughput is how many requests complete per unit of time (requests per second). You can have low latency but low throughput (a single fast server) or high latency but high throughput (a batch processing system). Optimizing one often comes at the cost of the other.
Q: How do you decide between SQL and NoSQL? Start with your access patterns and consistency needs. If you need complex joins, transactions, and strong consistency, use SQL. If you need flexible schemas, horizontal scaling, and can tolerate eventual consistency, consider NoSQL. We cover this in depth in Part 4: Databases.
Q: What does "five nines" availability mean practically? 99.999% availability allows only 5.26 minutes of downtime per year. Achieving this requires redundancy at every layer (multiple servers, multiple data centers, multiple regions), automated failover, and extensive monitoring. Most applications start by targeting 99.9% (8.7 hours/year downtime) which is significantly easier and cheaper to achieve.
Now that you understand the foundational concepts, the next step is learning how to handle growing traffic. Continue to Part 2: Scaling Strategies where we explore horizontal and vertical scaling, stateless service design, and auto-scaling patterns.
Every system designer should understand scalability (horizontal and vertical), availability (uptime guarantees), consistency models, latency optimization, and how to reason about trade-offs between them. These five concepts appear in every system design discussion, whether you are designing a chat application or a distributed database. Mastering the trade-offs between them is more valuable than memorizing specific technologies.
Basic programming knowledge helps, but system design is primarily about architectural thinking, trade-off analysis, and understanding how components interact at scale. You do not need to be an expert in any specific language. What matters more is understanding concepts like network communication, data storage trade-offs, and failure modes. That said, practical experience building and operating systems at scale is the single best way to develop system design intuition.
Software architecture focuses on code structure within an application — design patterns, module boundaries, dependency injection, and class hierarchies. System design addresses how multiple services, databases, and infrastructure components work together to serve millions of users. Software architecture asks "how should I organize this codebase?" while system design asks "how should I organize these servers, databases, caches, and queues so the system stays fast, reliable, and cost-effective at scale?" In practice, senior engineers need both skills.
Collaboration
I help startups and established companies design, build, and scale world-class digital products. From deep technical architecture to pixel-perfect UI: let's bring your vision to life.
Design a fault-tolerant e-commerce order system with inventory management, payment processing, saga pattern for transactions, and event-driven order fulfillment.
Build observable systems with structured logging, distributed tracing, and metrics dashboards. Learn SRE practices including SLOs, error budgets, and incident response.
Understand the CAP theorem, its practical implications, and distributed consensus algorithms like Raft and Paxos. Learn how real databases handle partition tolerance.