Learn system design as a sequence of decisions, using a small learning platform as the running example.
System design is deciding how parts of a product work together under real constraints. The goal is not the most complicated architecture. The goal is a design that meets today's needs and has a credible path for tomorrow.
flowchart TD
R[Clarify users and functional requirements] --> Q[Define quality goals]
Q --> E[Estimate traffic, storage, and failure impact]
E --> H[Draw the smallest high-level design]
H --> D[Choose data and communication patterns]
D --> F[Find bottlenecks and failure modes]
F --> T[State trade-offs and test assumptions]
T --> H
For a learning platform, functional requirements might be: read an article, search by topic, sign in, and save progress. Non-functional requirements describe qualities such as response time, availability, privacy, accessibility, cost, and recovery time.
“It must scale” is not measurable. “Support 10,000 daily readers while 95% of article requests finish within 300 ms” guides design and testing.
High-level design shows major components and data flow. Low-level design zooms into modules, interfaces, classes, and algorithms.
flowchart LR
User --> CDN[CDN / edge cache]
CDN --> Web[Web application]
Web --> API[Progress API]
API --> DB[(Relational database)]
API --> Queue[Event queue]
Queue --> Analytics[Analytics worker]
Start with a modular monolith unless independent deployment or scaling is a proven need. Microservices can isolate ownership and scaling, but introduce network failures, distributed data, operational overhead, and harder debugging.
Vertical scaling gives one machine more resources. Horizontal scaling adds instances and requires traffic distribution, usually through a load balancer. Stateless request handlers are easier to scale because another instance can handle the next request.
Caching reduces repeated work, but introduces stale data and invalidation rules. A CDN caches content near readers. A database index accelerates selected queries. Measure where time is spent before adding either.
Reliability asks whether the system keeps doing the correct work. Availability asks whether it can be reached. Fault tolerance describes how it continues or degrades when a component fails. These overlap, but they are not identical.
Use transactions when changes must succeed or fail together. Replication can improve read capacity and availability. Partitioning and sharding divide data, but complicate queries and operations.
Synchronous calls give an immediate result and are easy to follow. Queues and events decouple work and absorb bursts, but require idempotency, retries, ordering decisions, and observability. Rate limiting protects finite capacity and should return a clear response to clients.
Show trust boundaries and sensitive data on the diagram. Decide authentication, authorization, encryption in transit, secret storage, backup, and recovery. Add logs, metrics, and traces that answer what failed, for whom, where, and since when.
Test at several levels: component behavior, integration contracts, realistic load, failure recovery, and the deployment pipeline itself.
Keep the first version to one page:
A good design makes uncertainty visible. It does not pretend every future problem is already solved.
Functional requirements describe behavior users need; non-functional requirements define qualities and constraints such as latency, accessibility, security, capacity, and recovery.
High-level design explains system boundaries, data flow, dependencies, and major tradeoffs; low-level design specifies components, interfaces, data models, algorithms, and failure handling.
A capacity estimate converts product assumptions into requests per second, concurrency, stored bytes, and network throughput. Ranges and peak factors are more honest than false precision.