A Practical System Design Framework
I am starting a new series about system design. I would like to study the systems I deal with every day, or I am interested in. My goal is to understand how these systems work, why they are designed the way day are, and what trade-offs their creators had to make. In this first post, I am discussing what makes system design a durable skill, and presenting a simple method to get started with design.
Even though it is a senior engineering skill built by years of practicing the craft, there is an underlying framework that can be quickly internalized by any junior engineer.
Why care about system design?
For a long time, system design has been one of the senior engineering skills. Even before AI-assisted coding, senior engineers spent less time writing code and more time deciding what code should be written.
As you gained experience, you moved toward decision-making about architecture, trade-offs, and coordination. You delegated portions of your coding tasks to more junior developers. Needed to expose backend function to the frontend? You designed an API, and had your intern write the code. You created a component that queried a database? You figured out what queries you needed, and let your a junior colleague handle the rest.
Now that shift is happening much earlier.
In a sense, every engineer has access to an unlimited number of interns. You can generate code all day long. Now you must evaluate what should exist, how the pieces fit together, and which compromises you're willing to make. Even though you can make anything now, it does not mean that your app is needed or designed correctly.
Our tools are constantly becoming more capable. So using them wisely and designing the right systems are becoming more important than ever. The earlier you can develop the judgement, the faster you will progress in your career.
How to design a system?
Whenever I start thinking about a new system, I usually follow the same recipe:
- Understand what you are building
- Estimate resource usage
- Design the interfaces
- Model the data
- Plan for growth
- Pick your trade-offs
In each step I ask myself several questions to explore the territory. The questions I am sharing below are the foundation. As you go deeper into every aspect of the design, more and more questions pop up.
Now let's dive into each step.
Understand what you are building
You must clarify the requirements. As engineers we have to deal with ambiguity in features and problem definitions. One step toward dealing with it is trying to clarify as many requirements as possible.
First you need to understand what the user is trying to achieve. I guarantee you cannot get everything the first time, because no one knows what they want exactly. Thus, they cannot describe it to you. As you go through the design steps, more and more requirements are going to show up. That is normal. In real life, this process is not a straight line, but rather a spiral that converges on the solution.
Requirements come in two flavors: functional and non-functional.
Functional requirements
These tell you what your system must do.
For example, you need to build a service that loads terrabytes of data into your database. Requirements can be things like the user wants to load 1 TB of relational data into a database instance, and it must be available to query immediately. The source database is publicly available, so you do not need to worry about VPC peering, proxies or anything else. However, users need to pause data loading on demand. This prevents heavy database loads from degrading their primary services. This means you need to capture the progress and store it somewhere.
Also, consider how the user can make mistakes in the configuration or misuse your service. What scenarios are forbidden, what guidance do you need to provide so your system is easy to use correctly.
Questions
- What does the system do?
- What is the end goal of the user?
- What user errors do we need to handle?
Non-functional requirements
These are further requirements that make sure that your system can work without degrading the service. Functional requirements determine what the system does, non-functional requirements describe how it fulfills the requirements.
You must consider how the system behaves in specific situations like heavy load or network connectivity issues. Furthermore, as users are going to interact with it, you need to know what data they are going to provide, how to store it properly.
You need to define the deployment strategy for future fixes in advance. Does deploying code changes require a full restart? Should the system reload configs dynamically via files or environment variables? You must assess whether the business can afford multi-day downtime for database schema migrations, and plan how to update services independently. Since you will inevitably need maintenance windows for dependency updates and configuration tweaks, you need to schedule them during low-traffic periods and notify users in time.
Questions
- What conditions could degrade the user experience?
- Is the system storing confidential and private data?
- How much downtime can we afford?
Estimate resource usage
Once you narrowed down the requirements, you can move on to estimating resource usage for the desired daily active users. These estimations are going to drive some aspects of your design decisions.
You need to estimate:
- storage usage
- memory usage
- network usage
Let's say that the requirement is to serve 1000 requests in parallel, and each request takes 2 MB of memory. You need a machine, or a job that has at least 1000 * 2 MB memory. Or you are registering users, and every user can upload a profile picture. You cap the file size at 5 MB, so you will need 5 MB * number of users of storage. These numbers does not have to be perfect, just good enough to size your system in the beginning.
Questions
- Is the system write heavy or read heavy?
- Do we need to shard the data?
- What resources do the runner machines have?
Interface design
Given that your system will interact with other systems, you need to define a clear interface between them. Designing a good API is about more than just defining a few endpoints. No matter what protocol you choose (HTTP, RPC, etc.), it should be intuitive, consistent, and difficult to misuse.
You want the API to guide developers toward the right way of using it and make common mistakes harder to make. It should also have safeguards in place, such as authentication, input validation, and rate limiting, so that one client cannot overwhelm the system with too many requests. A well-designed API makes integration easier for everyone while also protecting the system behind it. Even if you are developing an internal system you still need to be careful about misuse. Other systems might overwhelm it due to programming errors, or sudden unexpected failures.
Do not forget to provide specification or documentation for the users of your API.
Questions
- Who initiates the request? Is it a person or some automation?
- How to protect the interface from misuse?
- What data is going to flow between the sender and the receiver?
Model the data
Now that you know how others are going to interact with our application, you can start considering the data and the access model and the storage behind your system.
It is highly likely that you need to persist things to the disk. You need to consider whether the application needs a read-heavy database or a read-heavy database. Or maybe it needs both. Maybe most of you users are reading data, with only a small number of writes. Or perhaps your system is constantly ingesting new data and needs to handle a large number of writes. You may even need separate storage systems optimized for different workloads!
How you structure your data will affect how efficiently your users can read and write it, how easily you can scale the system, and how difficult it will be to maintain consistency.
Think about the queries your application needs to support and design the data model around those access patterns. A database schema that looks elegant might not be what you need. If user requests require lots of JOINs, you need to sacrifice the elegance of the schema.
You also need to choose the right storage software. You can choose from relational databases, NoSQL databases, caches, etc.
A relational database might be the right choice when you need strong consistency, transactions, and relationships. A NoSQL database might make more sense when you need flexible schemas or horizontal scalability. In some systems, we usually not to pick just one. You might use different databases for different parts of the application, choosing each one based on the workloads.
Questions
- What is your data model? Is is structured or unstructured data?
- How are users going to access this data?
- Do we need strong consistency or high availability?
Plan for growth
In the beginning, no one is going to use your system. But once it gets deployed and becomes part of a bigger whole, it is going to serve the incoming requests. So be prepared for the possible growth that success could bring.
Your application will not be under a constant load either. Traffic can vary throughout the day, and sometimes a sudden event can cause a huge spike in requests. This is why you need to think about scalability. Vertical scalability and horizontal scalability are both important tools in your toolbox.
Vertical scaling means giving a single machine more resources: more CPU, memory, or storage. It is usually the simplest way to handle growth, but there is a limit to how beefy machines you can get.
Horizontal scaling means adding more machines and distributing the workload between them. This can give us much more room to grow, but it also introduces additional complexity, such as load balancing, data consistency, and coordination between instances.
Avoid the temptation to build for infinite scale from day one. Instead, understand where your bottlenecks are likely to appear and make sure your architecture can evolve as your system grows.
The best design is often one that is simple enough to start with but leaves you a path to scale when you actually need it. As I said in the beginning, you can never know in advance what to will happen exactly. Try to stay flexible and be prepared for the next steps in the evolution of your system.
Questions
- Is the load constant or changing?
- Do I need scale the system horizontally and/or vertically?
- What decisions do I need to postpone to keep my system flexible?
Pick your trade-offs
Probably the most important part of designing a system, is detailing the trade-off of each decisions. Do not worry, all systems have trade-off. We strive for perfection, but in practice, you are usually choosing the next best option given the constraints. Maybe you have the financial resources, so you can throw at the problem. You can run multiple instances of the same service to scale with the growing traffic. You pay more money for running more instances, but you save developer effort by not improving the performance of the services. So in the end you have more time to develop features.
Maybe we choose simplicity over flexibility, consistency over availability, or lower cost over higher performance. The important thing is not to pretend the solution is perfect, rather understand what we are giving up in exchange for what we are getting. System design is less about finding the "right" answer and more about making conscious choices, understanding consequences, and knowing why you chose one option over the other.
Questions
- What are we gaining? What are we losing?
- Does this trade-off make sense for the system we are building?
- Can we reverse any of these decisions?
Outro
One of the reasons many of us became engineers is simple: we like understanding how things work.
The good news is that system design isn't some mysterious skill reserved for staff engineers and architects. You can start figuring out how things work right now. It's a way of thinking: asking questions, identifying constraints, and reasoning about trade-offs.
The next time you use a product, try looking beyond the UI. Ask yourself how it might be built. What data does it store? What happens when it gets a million users? What compromises did its creators make?
That's exactly what we'll do throughout this series.
We'll take real-world systems, break them apart, and explore the decisions behind them. If you're early in your career, my hope is that these posts help you build the intuition that usually takes years to develop.
See you in the next one.