This chapter explains what system design is really about, why it matters in real-world software engineering, and why it has become such an important part of technical interviews.
We'll use one example throughout: a photo-sharing app. It starts as a single server and grows into a system with load balancers, caches, queues, and a CDN, and each step shows a different part of what system design involves.
Suppose you build a photo-sharing app. Users can sign up, upload photos, follow other people, and scroll through a feed of photos from the people they follow.
You write the code, deploy everything on a single server, and it works perfectly. In fact, one server might be enough to handle your first few thousand users and store all their data and photos.
Now imagine ten million people start using the app every day. The code for uploading a photo might not change, but the problems you need to solve are completely different:
These are no longer problems you solve inside a single function. They depend on how the entire system is designed and how its different parts work together.
System design is the process of deciding what components a system needs, what each one is responsible for, how they communicate, and how data flows between them so the system can meet its requirements.
It operates at a higher level than writing code. When you write code, you think about functions, classes, data structures, and algorithms. When you design a system, you think about servers, databases, caches, queues, storage, and the network connecting them.
You also have to think about what happens when one of these components slows down or fails, and how the system behaves as traffic and data grow. A design that works for one server on a quiet day has to keep working when a database is slow, a server crashes, or traffic doubles overnight.
Every system design starts with requirements, because you can't evaluate a design until you know what the system needs to do. Requirements usually fall into two groups.
Functional requirements describe what the system should do. For our photo-sharing app, users should be able to:
Non-functional requirements describe how well the system should do those things. For example:
The two groups influence the design in different ways. Functional requirements help you decide which features, APIs, and components the system needs. Each feature usually maps to an API, and each API touches specific parts of the system.
Non-functional requirements shape how those components are designed, how much capacity you need, how failures are handled, and where the system should run. The 200 ms feed target might push you toward a cache. The availability target means you need more than one server. The durability target affects how and where photos are stored.
A photo app for a hundred users and one for a hundred million users may support the same features, but their system designs can look completely different.
A common pitfall is starting from technologies: "Use Kafka, Redis, Kubernetes, and Cassandra."
A better approach is to start from requirements: "We need to handle high write volume, absorb traffic spikes, and process events asynchronously. That points toward a queue or log-based system."
Most large systems are built from a relatively small set of common building blocks. Here is how they fit together in the photo-sharing app.
Much of system design is deciding which of these building blocks you need and how they should work together.
Let's see how these pieces fit together in our photo-sharing app, starting with an upload.
When a user wants to upload a photo, the request first goes through the load balancer to one of the application servers. The server stores metadata such as the owner, caption, and timestamp in the database, then generates a pre-signed URL that allows the app to upload the image directly to object storage. The image bytes never pass through the application server, which keeps large uploads from tying it up.
Once the upload is complete, the system places a job on a message queue for any additional processing. Background workers pick up that job and handle slower tasks, such as generating thumbnails or updating followers' feeds.
Reading the feed takes a different path.
When another user opens their feed, the application server first checks the cache for the list of posts. If the data is not there, it reads it from the database and can store the result in the cache for future requests.
The images themselves are served through a CDN, so users can download them from a server closer to their location instead of fetching every image from the original storage system.
Each of these components exists for a reason:
When we evaluate a system design, we usually look at a few common qualities.
Cost matters too. A good design should meet its requirements without using more resources than necessary.
Improving one of these qualities often means making a trade-off somewhere else.
For example, adding a cache can reduce database load and make reads much faster, but the cached data may become stale and stay out of date until it is refreshed or expires.
Similarly, adding more replicas and redundancy can improve performance and availability, but it also increases cost and makes the system more complex to operate.
There is rarely one design that is best at everything. A large part of system design is understanding these trade-offs, deciding which qualities matter most for your requirements, and being able to explain why you chose one approach over another.
Real systems are rarely built in their final form from day one. Our photo-sharing app might start with a single application server and one database. For the first few thousand users, that may be more than enough.
As usage grows, different bottlenecks start to appear. Maybe the database becomes overloaded. Maybe serving images becomes too slow. Maybe background work starts delaying user requests.
That is when you add the component that solves the specific problem:
Adding all of these too early usually creates more complexity, more things to debug, and higher cost without providing much value.
A good system design meets the requirements you have today, while leaving room to scale as those requirements grow.
If you work on backend systems, you make system design decisions all the time, even when building relatively small features.
Take the Like feature in our photo app. You might need to decide:
Each answer comes with its own trade-off:
Understanding system design helps you reason through these choices, understand the trade-offs, and explain why one approach makes more sense than another.
System design also plays a big role in software engineering interviews. At many tech companies, a system design round is a standard part of the interview process for mid-level and senior engineers, and some companies also include a simpler version for less experienced candidates. Your performance in this round can also influence the level you are hired at, which can directly affect your compensation.
The format is usually open-ended. You might be asked to design a URL shortener, a chat application, or a photo-sharing app like the one used in this chapter. You typically get around 45 minutes to an hour to work through the problem.
There is usually no single correct answer, and you are not expected to write code that passes test cases. Instead, you follow a process similar to what we covered in this chapter.
As you go, you explain the decisions you are making and the trade-offs behind them.
You do not need to memorize common designs to do well in these interviews. If you understand the core building blocks, the problems they solve, and the trade-offs they introduce, you can reason through systems you have never seen before.
So, that's system design at a high level. It's about taking a set of requirements, turning them into a system of components, deciding how those components work together, and making the right trade-offs for the problem you're trying to solve.
In the rest of this course, we will go through these building blocks and concepts one by one, understand what problems they solve, and learn when and why to use them.