In our day-to-day lives, we're surrounded by systems—whether it's the social media app you check every morning, the payment system you use to buy your iced coffee (black), or the cloud infrastructure that stores your critical health or financial information.
A system is a collection of interconnected components that interact to perform specific tasks, enabling users to achieve a desired outcome efficiently and reliably.
The goal of Systems Simplified is to demystify system design by focusing on the essentials—concepts you can use no matter your level of experience. You don’t need a deep background in engineering to follow along! If you're just testing the waters, this discussion will hopefully spark an interest in exploring system design further-or at least give you the answer to "What happens when I go to Google.com
?"!
Note: If you're attending a live version of this discussion, you can following along here: Systems Simpified - Live!
The principles discussed below are applicable to nearly any application. To make these concepts more tangible, we’ll use a hypothetical example:
An app that allows users to order a cheap snack and have it delivered by a lil' robot.
We shall call it: Deals on Wheels.
By focusing on this specific scenario, we'll dig in to key system design concepts that you can apply broadly to your own projects or use to prepare for technical interviews.
Pencil and Paper: While software tools exist, sometimes the best way to sketch out system architecture and data flow is with simple pencil and paper. This will help you quickly iterate on designs and capture your initial thoughts.
Diagramming Tool (Optional): If you prefer digital tools, software like Lucidchart, Draw.io, or Excalidraw(!) can help you create more polished system diagrams.
Text Editor or Markdown Tool (Optional): Having a text editor like as VSCode, Sublime Text, or Vim(!) will make it easy to follow along and take notes.
Functional requirements define the core features and actions that a system must support; in this case, answering the crucial question:
What does it mean to be Deals on Wheels?
We will focus on three primary features that are central to the operation of our app: Snack Browsing, Order Placement, and Robot Delivery Management.
This feature will need to handle a high volume of
read
requests as users frequently access and filter snack data.
This feature involves multiple
write
actions, such as updating the cart and saving the order to the database when it's confirmed.
This feature also manages updates related to the robot’s status and location, making this feature both
read
- andwrite
-heavy.
While important to the overall system, the following features are outside the scope of this discussion:
While functional requirements define what a system does, Non-Functional Requirements describe how a system performs. These are critical to ensuring that the system behaves efficiently, securely, and reliably under various conditions.
For Deals on Wheels, non-functional requirements will ensure that the system can scale to meet user demand, perform efficiently during peak usage, and remain secure while processing orders and delivering snacks. Let's outline the non-functional requirements we will focus on:
Availability
Performance
Reliability and Data Consistency
In system design, it’s essential to begin with the simplest unit of the application: a single piece of data.
By designing the database first, we can establish the foundation upon which the entire system will be built. For simplicity, we’ll focus on the database design for Snack Browsing, the most straightforward of our three functional requirements. This will help illustrate core concepts before we move on to more complex features like order placement or delivery management.
Think of the database as shelves in the snack machine we are putting out of business; holding specific items in a systematic, organized way.
This table will store the essential attributes of each item, providing an answer to the key question:
What does it mean to be a Snack?
Table: `Snacks` | Column Name | Data Type | Description | Constraints | |--------------------|------------|-------------------------------------------|--------------------------------------| | `snack_id` | INT | Unique identifier for each snack | Primary Key, Auto-Increment | | `name` | VARCHAR | Name of the snack | Not Null, Unique | | `description` | TEXT | A brief description of the snack | None | | `category` | VARCHAR | Category of the snack (e.g., Sweet, Salty)| Not Null | | `price` | DECIMAL | Price of the snack | Not Null, Positive Constraint | | `availability` | BOOLEAN | Is the snack available for order | Not Null, Default: TRUE | | `dietary_info` | VARCHAR | Dietary options (e.g., Vegan, Gluten-Free)| None | | `created_at` | TIMESTAMP | When the snack option was added | Default: Current Timestamp | | `updated_at` | TIMESTAMP | Last updated time | Auto-update on modification |
If we want to allow more flexibility with categories (instead of hardcoding them), we can separate categories into their own table. This makes it easier to add new categories or modify existing ones without changing the structure of the Snacks
table.
Table: `Categories` | Column Name | Data Type | Description | Constraints | |--------------------|------------|-------------------------------------------|--------------------------------------| | `category_id` | INT | Unique identifier for each category | Primary Key, Auto-Increment | | `name` | VARCHAR | Name of the category (e.g., Sweet, Salty) | Not Null, Unique |
The Snacks
table would then have a foreign key category_id
that references the Categories
table.
Since the snack browsing feature is read-heavy, implementing an index on the name
, category
, and dietary_info
columns can improve the search and filtering performance.
CREATE INDEX idx_snack_search ON Snacks (name, category, dietary_info);
This index will allow faster lookup of snacks when users search by name or filter by category or dietary preferences.
Let’s estimate the storage capacity and plan for constraints:
Average Data Per Row (Snack Entry):
Total Storage per Snack: ~315 bytes per snack entry
If we start with around 1,000 snack options in the system, the total storage required would be roughly:
1,000 snacks * 315 bytes ≈ 315,000 bytes (315 KB)
Even with 10,000 snacks, this would only require around 3 MB of storage, which is easily manageable for most systems.
To ensure the Deals on Wheels system remains reliable, especially under high traffic or hardware failures, it's important to implement database redundancy. Redundancy involves maintaining copies (or replicas) of the database to protect against data loss, improve performance, and ensure availability.
Some common redundancy methods include:
By building redundancy into the database system, we ensure reliability and seamless user experience, even during peak demand or unexpected issues.
While we’re using a relational database design for this tutorial, it’s important to consider that non-relational (NoSQL) databases like MongoDB could also be used, especially if flexibility and scalability are top priorities. In a document-oriented database, each snack could be represented as a JSON-like document. Here's an example:
{ "snack_id": 1, "name": "Chocolate Chip Cookie", "description": "A freshly baked cookie with rich chocolate chips.", "category": "Sweet", "price": 2.99, "availability": true, "dietary_info": ["Vegetarian"], "created_at": "2024-10-10T12:00:00Z", "updated_at": "2024-10-10T12:00:00Z" }
NoSQL databases offer greater flexibility when handling unstructured data or making frequent schema changes, though they may not provide the strong consistency guarantees of relational databases.
To keep things straightforward, we'll continue focusing solely on the Snack Browsing feature for now. This will give us a solid foundation in API design without diving into the complexities of other features like order placement and robot delivery management. Later, you can apply these same principles to expand the system as needed.
An API (Application Programming Interface) serves as the gateway to your system.
You can think of an API as the mechanisms of interacting with the Snack Machine (RIP) from or example earlier. In much the same way a customer can enter a code to retrieve a snack but would need a special key to add additional items to the machine itself; clients interact with APIs by making specific requests under pre-defined rules to access or manipulate resources.
For each feature, start by identifying the actions that users or systems need to take. In our Snack Browsing feature, the core action is retrieving a list of available snacks, with options for filtering based on user preferences.
For simplicity, we’ll focus on just a GET request to fetch snack data.
The API that supports snack browsing must allow users to read from the system efficiently, handling requests to view snacks, filter by categories, and sort by attributes like price or dietary preferences.
Endpoint: /api/snacks
GET
category
(optional): Filter snacks by a specific category (e.g., Sweet, Salty).dietary_info
(optional): Filter by dietary preferences (e.g., Vegan, Gluten-Free).sort
(optional): Sort results by price, name, etc.Example Request:
GET /api/snacks?category=Sweet&dietary_info=Vegan&sort=price
Example Response:
[ { "snack_id": 1, "name": "Vegan Chocolate Chip Cookie", "category": "Sweet", "price": 3.99, "availability": true, "dietary_info": ["Vegan"] } ]
When building the API, you'll need to choose a framework that supports your system's architecture. Popular frameworks like Express (Node.js), Django (Python), Rails (Ruby), or .Net (who knows really) provide tools to scaffold and implement the API, making it easier to define routes, handle requests, and interact with your database.
Now that we've designed the API for the Snack Browsing feature, it's time to think about how the client interacts with our system.
A client is any entity that interacts with your API. This interaction could be through a user interface, such as a web app, or through other systems that need to retrieve and process data. In this case, the client will act as the "user" browsing our snack catalogue and fetching data from the Deals on Wheels API.
Think of the client as a Snack Machine Catalogue—it doesn’t hold the actual products (snacks) but instead provides representations of them, along with instructions on how to get them. The catalogue tells the user what's available (via the API), and the user selects what they want.
The clients of our system can take various forms. Let's define some of the potential entities that would act as clients for the Deals on Wheels service:
Web Browsers + Mobile Apps: The most common form of client for this kind of system. A browser fetches data from the API (e.g., snack availability) and renders the user interface, allowing people to browse and place orders for snacks. The client in this case would be something like a JavaScript front-end (using frameworks like React or Vue) that sends requests to the API.
Robots: Even the delivery robots that handle the snack orders are considered clients of the system. While humans interact with the app to place an order, the robot fetches its instructions from the API as well—getting information about which snack to deliver and the address it should head to.
Automated Systems: In some cases, third-party services or automated systems may act as clients to the API. For instance, a reporting system that tracks snack sales or a system monitoring inventory could automatically request data from the API.
Let’s walk through a simple example of how a browser, acting as a client, interacts with the Deals on Wheels API to display snack data.
User Action: The user opens their browser and navigates to the Deals on Wheels website.
Client Request: The browser (the client) sends a GET
request to the /api/snacks
endpoint, asking for a list of available snacks.
API Response: The API responds with a JSON object containing snack data (e.g., Double Chocolate Chip Cookies, RedBull).
Client Rendering: The browser takes this data and displays it on the screen, allowing the user to browse through the snacks, apply filters, and select their favorites.
This interaction is repeated whenever the user requests new data (for example, if they apply a filter or refresh the page).
When building the client interface for humans, we often rely on frontend frameworks to simplify the development process and provide a polished user experience. These frameworks handle rendering the data fetched from the API, managing user interactions, and ensuring the app is responsive and accessible across different devices.
Here are a few popular frontend frameworks that could be used to build the Deals on Wheels interface:
React: A widely-used JavaScript library developed by Facebook. It allows developers to build user interfaces with reusable components, making it easy to manage dynamic data (like snack lists) and state changes (like adding a snack to a cart).
Vue.js: A progressive framework that is especially good for building interactive user interfaces. It's known for its simplicity and flexibility, making it an excellent choice for small to medium-sized projects like our snack-ordering app.
Angular: A full-fledged framework developed by Google, ideal for building complex, scalable applications. It offers out-of-the-box solutions for routing, form handling, and data management.
These frameworks help manage the user experience efficiently, especially when dealing with frequent updates, such as displaying new snack options or tracking robot deliveries in real-time.
Note: These frontend frameworks are only necessary for human-facing clients. Our robots, which interact with the API programmatically, don’t require a frontend and instead function purely through backend communication.
Here is a diagram that illustrates the overall system architecture for Deals on Wheels, showing how the client, API, and database interact to support snack browsing, order placement, and robot delivery.
As we've seen, functional requirements focus on what the system does—like allowing users to browse snacks or placing an order. But as systems grow, we must also address non-functional requirements, which ensure the system performs well under different conditions. These include availability, performance, and reliability—all critical for a smooth user experience.
To illustrate this, let’s imagine a scenario where our system’s non-functional requirements are tested to their limits.
Our marketing team has announced an exciting promotion: “Get any snack for free if you place your order at exactly noon!” Naturally, users rush to the app at noon to get their free snacks, leading to a sudden surge of traffic and requests.
Here’s what happens next:
read
and write
operations.To handle such a surge in traffic and avoid a system breakdown, we can redesign the system architecture using microservices. In a microservices architecture, each feature (or group of related features) runs as a separate service, communicating through well-defined APIs. This approach offers several advantages, such as isolating failures and scaling specific parts of the system independently.
Here’s how we can break down Deals on Wheels into microservices:
Instead of having a single, monolithic API that handles everything (snack browsing, order placement, and robot management), we can split each feature into its own API:
GET
requests for browsing and filtering available snacks. Since browsing is read-heavy, this service can be optimized for quick data retrieval.By splitting each feature into its own service, we reduce the risk of one feature (e.g., snack browsing) overwhelming the entire system and causing a ripple effect that impacts other features (e.g., robot delivery).
In our marketing promotion scenario, the order system is particularly vulnerable because it must handle both reads (viewing past orders or tracking current ones) and writes (placing new orders). To scale the system even further, we can split the Order Management Service into two parts:
By separating reads from writes, we can independently scale the two services. For example, during the noon rush when everyone is placing orders, we can scale the Order Write Service to handle the influx of new orders, while the Order Read Service remains stable and responsive for users who are just checking the status of their orders.
To manage the sudden spike in traffic, we can implement the following strategies:
Load Balancing: Distribute incoming traffic across multiple servers, ensuring no single server is overwhelmed by the surge in users trying to place orders.
Autoscaling: Automatically increase the number of servers or resources when a surge is detected (e.g., around noon during the promotion), and scale down when traffic returns to normal.
These mechanisms ensure that the system remains available and responsive even under heavy loads, and they allow us to gracefully handle unexpected spikes in traffic without affecting the user experience—or the robots' ability to deliver snacks!
And that's it—mostly! In this tutorial, we’ve explored the core concepts of System Design through the lens of our snack-ordering app, Deals on Wheels. From defining functional and non-functional requirements to crafting efficient APIs, we’ve laid the foundation for a scalable, reliable system that can handle the demands of real-world applications.
Here’s a quick recap of the key points:
Start with the Essentials: We began by defining our system’s functional requirements, focusing on the core features like Snack Browsing, Order Placement, and Robot Delivery Management.
Non-Functional Requirements: Ensuring that the system performs efficiently and remains available even during high-traffic periods, like our marketing promotion, is critical for long-term success. We introduced key non-functional requirements, such as availability, performance, and reliability.
Database Design: We designed a relational database to store snack information and introduced the concept of database redundancy to boost system reliability. Implementing redundancy helps ensure the system can recover from failures and continue to serve users without interruption.
API Design: We created clear, RESTful APIs to support client interactions, focusing on snack browsing for simplicity. The client (whether a human or robot) interacts with the system through these APIs, fetching and processing data as needed.
Scaling with Microservices: To handle the challenges of high traffic (like the promotional surge), we introduced microservices. By separating each feature into its own service, and further splitting the order system into read and write services, we can scale different parts of the system independently, ensuring that one feature’s load doesn’t disrupt the entire system.
System design is about more than just creating an application—it’s about ensuring that the system performs well at scale, can handle unexpected situations, and continues to deliver a smooth user experience.
Remember, Deals on Wheels is just an example; the concepts we've covered are applicable to many other systems. Whether you're preparing for a technical interview or building your next big project, these are the building blocks you’ll need to succeed.