Overview of System Design
System design is the process of planning the architecture, components, data and communication of a software system so that it meets its goals. It helps engineers build systems that are scalable, fast, reliable and easy to maintain, before a single line of production code is written.
This tutorial explains system design in simple terms: why it matters, the building blocks every system uses, the core ideas such as scalability and availability, a step-by-step design process, and a complete example of an online bookstore with Java code.
1. What Is System Design?
Writing code solves a single problem inside one program. System design looks at the whole picture: how many servers you need, where data is stored, how parts talk to each other, what happens when something fails, and how the system grows when more people use it.
A good system design answers questions like these:
- What should the system do, and for how many users?
- Which parts (services, databases, caches, queues) are needed, and how do they connect?
- How is data stored, read and kept safe?
- How does the system keep working when a server or network fails?
- How will it handle ten or a hundred times more traffic in the future?
2. Why Is System Design Important?
- Scalability: The system can handle growth in users, requests and data without a full rewrite.
- Performance: Pages and APIs respond quickly, even when many people use them at once.
- Reliability and Availability: The system stays up and keeps data safe, even when some parts fail.
- Maintainability: Clear boundaries make it easier to fix bugs and add features without breaking existing ones.
- Security: Sensitive data such as passwords and payment details is protected from the start.
- Cost Control: You use the right amount of infrastructure instead of over- or under-buying servers.
- Career Growth: System design is a core skill for senior engineers and a standard part of technical interviews.
3. Functional and Non-Functional Requirements
Every design starts with requirements. They come in two types:
| Type | Question It Answers | Bookstore Examples |
|---|---|---|
| Functional | What should the system do? | Search books, add to cart, place an order, write a review |
| Non-functional | How well should it do it? | Search results in under 200 ms, 99.9% availability, support 1 million daily users, secure payments |
Non-functional requirements usually drive the big design decisions. Learn more in Functional vs Non-Functional Requirements.
4. High-Level vs Low-Level Design
| Aspect | High-Level Design (HLD) | Low-Level Design (LLD) |
|---|---|---|
| Focus | The big picture: services, databases, caches, queues | The details inside one component: classes, methods, tables |
| Output | Architecture diagram and data flow | Class diagrams, API contracts, database schemas |
| Audience | Architects, team leads, stakeholders | Developers implementing the feature |
| Example | "Use a load balancer, three app servers, PostgreSQL and Redis" | "OrderService.placeOrder() validates stock, then saves an Order" |
See High-Level vs Low-Level Design for a deeper comparison.
5. Key Components of System Design
Most large systems are built from the same set of building blocks:
| Component | What It Does | Common Examples |
|---|---|---|
| Architecture | Defines how the parts of the system are organised and interact | Monolith, microservices, event-driven |
| Clients | The apps people use to reach the system | Web browser, mobile app |
| DNS | Turns a domain name into a server address | Route 53, Cloudflare DNS |
| Load Balancer | Spreads incoming traffic across many servers | NGINX, HAProxy, AWS ELB |
| Application Servers | Run the business logic | Spring Boot services |
| APIs | Let clients and services talk to each other | REST, gRPC, GraphQL |
| Data Storage | Stores data safely for later use | PostgreSQL, MySQL, MongoDB |
| Cache | Keeps frequently used data in fast memory | Redis, Caffeine |
| Message Queue | Lets services work asynchronously and absorb traffic spikes | Kafka, RabbitMQ |
| CDN | Serves images and static files from servers close to the user | CloudFront, Cloudflare |
| Object Storage | Stores large files such as images and videos | Amazon S3, Google Cloud Storage |
| Security | Protects data and allows only authorised access | HTTPS, OAuth 2.0, JWT |
| Monitoring | Shows health, errors and performance | Prometheus, Grafana, ELK |
6. Core Concepts
Scalability
Scalability is the ability to handle more load by adding resources. There are two ways to scale:
| Aspect | Vertical Scaling (Scale Up) | Horizontal Scaling (Scale Out) |
|---|---|---|
| How | Give one server more CPU, RAM or disk | Add more servers behind a load balancer |
| Simplicity | Very simple; no code changes | Needs stateless services and a load balancer |
| Limit | Stops at the biggest available machine | Can keep growing by adding machines |
| Failure | One server is a single point of failure | Other servers keep working if one fails |
Availability
Availability is the percentage of time a system is up and working. It is often written as a number of "nines":
| Availability | Allowed Downtime per Year | Allowed Downtime per Month |
|---|---|---|
| 99% ("two nines") | About 3.65 days | About 7.3 hours |
| 99.9% ("three nines") | About 8.77 hours | About 43.8 minutes |
| 99.99% ("four nines") | About 52.6 minutes | About 4.4 minutes |
| 99.999% ("five nines") | About 5.3 minutes | About 26 seconds |
Higher availability comes from redundancy (more than one copy of every important part) and failover (switching to a healthy copy automatically). Read more in Availability in System Design.
Latency and Throughput
- Latency is how long one request takes, for example 120 ms to load a page. Lower is better.
- Throughput is how many requests the system handles per second. Higher is better.
Caching, CDNs, efficient queries and fewer network hops reduce latency. See Latency in System Design.
Consistency and the CAP Theorem
Consistency means every user sees the same, latest data. In a distributed system, the CAP theorem says that when the network splits (a partition), you must choose between:
- Consistency: refuse some requests so nobody sees outdated data, or
- Availability: keep answering every request, even if some answers are slightly out of date.
A bank balance usually favours consistency; a social media like counter usually favours availability. See Consistency in System Design.
7. A Step-by-Step Design Process
Whether you are designing a real product or answering an interview question, these steps give you a clear path:
- Clarify requirements: List the main features and the non-functional goals (users, speed, availability).
- Estimate the scale: Roughly calculate requests per second, storage and bandwidth.
- Draw the high-level design: Clients, load balancer, services, databases, caches and queues.
- Design the data model: Choose databases and define the main tables or documents.
- Define the APIs: Decide the endpoints, inputs and outputs.
- Deep dive into key parts: Search, payments or whatever is hardest or most important.
- Find bottlenecks and failures: Ask "what breaks first?" and add caching, replicas or queues.
- Review trade-offs: Explain why you chose each option and what you gave up.
For more detail, see System Design Process and Methodologies.
8. Example: Designing an Online Bookstore
Imagine building an online bookstore. System design helps you decide:
- Which database will store books, users and orders?
- How will users search for books quickly?
- How will the site handle thousands of users at the same time?
- How will payment information stay secure?
- What happens if a server crashes during a big sale?
Step 1: Requirements
- Functional: browse and search books, view details, add to cart, place orders, pay online.
- Non-functional: 1 million daily active users, search in under 200 ms, 99.9% availability, secure payments.
Step 2: Back-of-the-Envelope Estimation
Daily active users = 1,000,000
Requests per user per day = 10
Requests per day = 10,000,000
Average requests/second = 10,000,000 / 86,400 ≈ 116 req/s
Peak (about 3x average) ≈ 350 req/s
Books in catalog = 1,000,000
Book details (2 KB each) ≈ 2 GB -> fits easily in one database
Cover images (200 KB each) ≈ 200 GB -> object storage + CDN
These rough numbers tell us that one database can hold the catalog, but images belong in object storage behind a CDN, and a few app servers behind a load balancer can comfortably handle the traffic.
Step 3: High-Level Architecture
Users (web / mobile)
|
|---------------------------> [ CDN ] <--- book cover images (object storage)
v
[ Load Balancer ]
|
+--> App Server 1 --+
+--> App Server 2 --+--> [ Redis Cache ] (popular books)
+--> App Server 3 --+
|
+--> [ PostgreSQL ] (books, users, orders)
+--> [ Search Engine ] (fast full-text search)
+--> [ Message Queue ] --> Email / Shipping workers
Step 4: Key Decisions
- Database: PostgreSQL for books, users and orders, because orders need reliable transactions.
- Fast search: a search engine such as Elasticsearch, kept in sync with the database.
- Handling many users: stateless app servers behind a load balancer, plus a Redis cache for popular books.
- Images: object storage with a CDN, so images load quickly worldwide.
- Slow tasks: confirmation emails and shipping requests go through a message queue, so checkout stays fast.
- Security: HTTPS everywhere, hashed passwords, and a trusted payment provider so card details never touch our servers.
- Reliability: at least two app servers and a database replica, so one failure does not stop the store.
By planning all these aspects up front, the bookstore can run smoothly, handle growth and give users a good experience.
9. System Design Ideas in Java
System design is mostly about architecture, but its ideas show up directly in code. Here are four small Java examples from the bookstore.
API Design: A Clean REST Endpoint
A versioned URL (/api/v1) and paging with a maximum page size keep the API stable and protect the server from huge requests.
package com.javabytechie.bookstore;
import java.util.List;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/v1/books")
public class BookController {
private static final int MAX_PAGE_SIZE = 100;
private final BookService bookService;
public BookController(BookService bookService) {
this.bookService = bookService;
}
@GetMapping("/{id}")
public BookResponse getBook(@PathVariable Long id) {
return bookService.findById(id);
}
@GetMapping
public List<BookResponse> search(@RequestParam String query,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size) {
// Never let a client ask for an unlimited number of results.
return bookService.search(query, page, Math.min(size, MAX_PAGE_SIZE));
}
}
Caching: Fast Reads for Popular Books
With the cache-aside approach, the first request loads a book from the database and stores it in the cache; later requests are served from memory. When data changes, the cached copy is removed.
package com.javabytechie.bookstore;
import java.math.BigDecimal;
import java.util.List;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.data.domain.PageRequest;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class BookService {
private final BookRepository bookRepository;
public BookService(BookRepository bookRepository) {
this.bookRepository = bookRepository;
}
@Cacheable(cacheNames = "books", key = "#id")
public BookResponse findById(Long id) {
// Runs only on a cache miss; the result is then stored in the "books" cache.
return bookRepository.findById(id)
.map(BookResponse::from)
.orElseThrow(() -> new BookNotFoundException(id));
}
@Transactional
@CacheEvict(cacheNames = "books", key = "#id")
public void updatePrice(Long id, BigDecimal newPrice) {
// Removing the cached copy makes the next read load the new price.
Book book = bookRepository.findById(id)
.orElseThrow(() -> new BookNotFoundException(id));
book.setPrice(newPrice);
}
public List<BookResponse> search(String query, int page, int size) {
return bookRepository.findByTitleContainingIgnoreCase(query, PageRequest.of(page, size))
.stream()
.map(BookResponse::from)
.toList();
}
}
package com.javabytechie.bookstore;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
@SpringBootApplication
@EnableCaching // switches on @Cacheable and @CacheEvict
public class BookstoreApplication {
public static void main(String[] args) {
SpringApplication.run(BookstoreApplication.class, args);
}
}
Adding spring-boot-starter-data-redis makes Spring Boot store this cache in Redis, so all app servers share it.
Load Balancing: Round Robin
A load balancer sends each new request to the next server in turn. This simplified version shows the idea in plain Java:
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
public class RoundRobinLoadBalancer {
private final List<String> servers;
private final AtomicInteger counter = new AtomicInteger(0);
public RoundRobinLoadBalancer(List<String> servers) {
if (servers.isEmpty()) {
throw new IllegalArgumentException("At least one server is required");
}
this.servers = List.copyOf(servers);
}
public String nextServer() {
// floorMod keeps the index valid even after the counter overflows.
int index = Math.floorMod(counter.getAndIncrement(), servers.size());
return servers.get(index);
}
public static void main(String[] args) {
RoundRobinLoadBalancer balancer = new RoundRobinLoadBalancer(
List.of("app-server-1", "app-server-2", "app-server-3"));
for (int request = 1; request <= 6; request++) {
System.out.println("Request " + request + " -> " + balancer.nextServer());
}
}
}
Request 1 -> app-server-1
Request 2 -> app-server-2
Request 3 -> app-server-3
Request 4 -> app-server-1
Request 5 -> app-server-2
Request 6 -> app-server-3
Protecting the System: Token Bucket Rate Limiter
Rate limiting stops one client from overloading the system. In the token bucket approach, a bucket refills with tokens at a fixed speed, and each request must take one token. When the bucket is empty, the request is rejected.
public class TokenBucketRateLimiter {
private final long capacity;
private final double refillTokensPerNano;
private double availableTokens;
private long lastRefillTime;
public TokenBucketRateLimiter(long capacity, long refillTokensPerSecond) {
this.capacity = capacity;
this.refillTokensPerNano = refillTokensPerSecond / 1_000_000_000.0;
this.availableTokens = capacity;
this.lastRefillTime = System.nanoTime();
}
public synchronized boolean tryAcquire() {
refill();
if (availableTokens >= 1) {
availableTokens -= 1;
return true; // request allowed
}
return false; // request rejected (HTTP 429 Too Many Requests)
}
private void refill() {
long now = System.nanoTime();
double newTokens = (now - lastRefillTime) * refillTokensPerNano;
availableTokens = Math.min(capacity, availableTokens + newTokens);
lastRefillTime = now;
}
}
For example, new TokenBucketRateLimiter(10, 5) allows a burst of up to 10 requests and then about 5 requests per second. Learn more in the Rate Limiting Pattern tutorial.
10. Common Trade-offs
There is no perfect design, only good trade-offs for a given situation:
| Trade-off | Option A | Option B |
|---|---|---|
| Consistency vs Availability | Always correct data, may reject requests during failures | Always responds, data may be briefly outdated |
| Latency vs Freshness | Cache data for speed | Read the database every time for the latest value |
| SQL vs NoSQL | Strong structure, joins and transactions | Flexible schema and easy horizontal scaling |
| Monolith vs Microservices | Simple to build and deploy | Independent teams and scaling, more complexity |
| Sync vs Async | Immediate answer, tighter coupling | Better resilience, eventual results |
| Cost vs Performance | Fewer, cheaper servers | More capacity and redundancy |
Explore architecture choices in Monolithic Architecture and Microservices Architecture.
11. Tips for Beginners
- Always start with requirements; never jump straight to technologies.
- Keep the first design simple, then improve it where the numbers say it is needed.
- Do rough math for traffic and storage; it guides every big decision.
- Remove single points of failure by having at least two of everything important.
- Keep application servers stateless so they are easy to scale out.
- Cache data that is read often and changes rarely.
- Move slow work, such as emails and reports, to background queues.
- Design for failure: use timeouts, retries and health checks.
- Plan monitoring and alerts from day one.
- Explain every choice with its trade-off; "it depends" is fine if you say on what.
12. Conclusion
System design is a critical skill for developers and engineers. It turns a list of features into a blueprint that is scalable, reliable, fast, secure and affordable. Start with clear requirements, estimate the scale, combine well-known building blocks such as load balancers, databases, caches and queues, and always explain your trade-offs. Learning system design helps you think bigger and plan smarter, and the rest of this System Design Tutorial will take you deeper into each topic.
Frequently Asked Questions (FAQ)
Q1. What is system design in simple words?
System design is planning how a software system will be built: which parts it has, how they connect, where data is stored, and how it will stay fast and reliable as more people use it. It is the blueprint created before and during development.
Q2. Who needs to learn system design?
Backend developers, full-stack developers, architects and anyone preparing for mid-level or senior engineering interviews. Even junior developers benefit, because it explains why code is structured the way it is.
Q3. What is the difference between system design and software architecture?
They overlap a lot. Software architecture usually means the overall structure and style of a system, such as microservices or event-driven. System design is broader and more practical: it also covers capacity estimates, databases, caching, scaling and failure handling for a specific problem.
Q4. What is the difference between functional and non-functional requirements?
Functional requirements describe what the system does, such as "users can place an order". Non-functional requirements describe how well it does it, such as "orders are confirmed in under one second with 99.9% availability".
Q5. What is the difference between HLD and LLD?
High-Level Design shows the big picture: services, databases, caches and how data flows between them. Low-Level Design zooms into one part and defines classes, methods, APIs and database tables.
Q6. What is scalability?
Scalability is a system's ability to handle more users, requests or data by adding resources, without a major redesign and without becoming slow.
Q7. What is the difference between vertical and horizontal scaling?
Vertical scaling makes one server more powerful. Horizontal scaling adds more servers that share the load. Horizontal scaling can grow further and survives single-server failures, but needs stateless services and a load balancer.
Q8. What does a load balancer do?
It receives incoming requests and spreads them across several servers, so no single server is overloaded. It also stops sending traffic to servers that fail their health checks. Common strategies are round robin, least connections and IP hash.
Q9. Why is caching important?
Reading from memory is much faster than reading from a database or calling another service. Caching popular data reduces response times and lowers the load on the database. The main challenge is keeping cached data up to date.
Q10. What is a CDN?
A Content Delivery Network is a group of servers spread around the world that store copies of static files such as images, CSS and videos. Users download these files from a nearby server, which makes pages load faster.
Q11. When should I use SQL and when NoSQL?
- Use SQL (PostgreSQL, MySQL) when data is structured and you need joins and reliable transactions, such as orders and payments.
- Use NoSQL (MongoDB, Cassandra, Redis) for flexible data, very high write volumes or simple key-based access at huge scale.
Many systems use both.
Q12. What is the CAP theorem?
It says that a distributed system facing a network partition must choose between Consistency (every read sees the latest data) and Availability (every request gets a response). Partition tolerance is required in practice, so the real choice is between C and A during failures.
Q13. What is the difference between latency and throughput?
Latency is the time a single request takes. Throughput is the number of requests handled per unit of time. A system can have high throughput but still feel slow if each request has high latency.
Q14. What does "99.9% availability" mean?
It means the system may be down for at most 0.1% of the time, which is about 8.77 hours per year or about 43.8 minutes per month.
Q15. What is a single point of failure?
It is any component whose failure stops the whole system, such as a single database server or a single load balancer. You remove it by adding redundancy, for example a replica that can take over automatically.
Q16. Why are message queues used in system design?
Queues let one part of the system hand off work and move on, while workers process it later. They smooth out traffic spikes, keep slow tasks such as emails out of the user's request, and keep work safe if a worker is temporarily down.
Q17. What is back-of-the-envelope estimation?
It is a quick, rough calculation of traffic, storage and bandwidth using simple assumptions. For example:
1,000,000 users x 10 requests/day = 10,000,000 requests/day
10,000,000 / 86,400 seconds ≈ 116 requests/second
These numbers guide decisions such as how many servers or what kind of database you need.
Q18. What does "stateless service" mean, and why does it help?
A stateless service does not keep user-specific data in its own memory between requests; session data lives in a shared store such as Redis or in a token. Any server can then handle any request, which makes horizontal scaling and failover easy.
Q19. How should I prepare for a system design interview?
- Learn the building blocks: load balancers, caches, databases, queues and CDNs.
- Practise a fixed process: requirements, estimation, high-level design, deep dive, trade-offs.
- Design common systems such as a URL shortener, chat app or news feed.
- Explain your reasoning out loud and always mention trade-offs.
Q20. Is there one correct answer in system design?
No. Different requirements lead to different designs, and every choice has pros and cons. A good design is one that meets the stated requirements and clearly explains its trade-offs.