Design Software Systems Like a Senior Engineer – Complete Course

#System Design Interview #Software Architecture #Scalability #Distributed Systems #Database Design #Concurrency #Programming
💬 Chat with this Video
Ask anything about this video…
Learn software system design and pass interviews. This course provides a practical five-step framework for structuring your thinking and tackling vague requirements under pressure. Additionally, the course walks through realworld examples designing systems similar to Spotify, Booking.com, and WhatsApp from scratch. Hike Simon developed this course. >> Most software engineering roles today require system design knowledge. Whether it's for the job itself or for interviews, you are expected to know how to design systems at scale. And especially for senior and staff level positions, it's one of the deciding factors for the company. That's why I put together this complete system design interview guide with real production system examples that show up the most often in system design interviews. First, we'll start with the framework to know how to structure your thinking, what clarifying questions to ask, and how to make sure you cover everything the interviewer is looking for without going off track. And then we'll apply this same framework across three different system design examples so that you can see how the same framework applies to different production systems. This is the most practical framework that I know for passing system design interviews. Early in my career, I failed a lot of these interviews when applying to senior roles. And looking back now, there is one pattern that was the same across all of them, which is system design was almost always part of the conversation. And you can argue that nowadays, this is also a requirement for mid-level positions, especially now when all of the software engineers use AI to write the code implementation. Companies would like to know whether you understand the bigger picture and how components talk to each other, how to design solutions from scratch and discuss the trade-offs. Of course, the depth of knowledge is different depending on whether you apply for mid to senior roles, but still you have to be prepared for these type of interviews, whether you're mid, senior or above. So, this shows up in different ways. Actually the most common one that we know is the whiteboarding style where there is a session where you design a full system or a feature from scratch. But there are actually three more types to this. Another way can be discussion about bottlenecks in the company's existing infrastructure and see if you can suggest solutions to this and you have the basic knowledge of system design. Another type is walking through your past projects. So questions about your past projects to see if you actually understand the architecture of what you built there. And it can also be conceptual questions that test the depth of your knowledge about system design concepts. But even if you learn all the concepts of system design, you still need a framework to navigate a 45 or 60inut discussion and make the most out of that time. That's exactly why we have this framework and guide. And another important thing before we'll get to discuss the framework is that most engineers think they are only being evaluated on the system they design which is just one out of five dimensions that you're being tested on. And just by knowing what these dimensions are will help you way more to prepare for these type of interviews. Before getting to the dimensions, let's first understand why this feels hard compared to other type of interviews like behavioral rounds or live coding. For example, if it's a live coding round, this usually is either a lead code style problem or some real world challenge. But either way, there are clear inputs and outputs, right? Let's say if it is a lead code style challenge, then you have a function which accepts these type of arguments. you know exactly what the type of the argument is. You know exactly what you're expected to return as an output from the function. So it is clear whether your solution works or not because you can copy paste this code and try to run it and you can also double check by running test cases on a whiteboard without even running the code. But system design is actually the opposite of all of that. First of all, the requirements are very vague and it is your job to clarify those requirements and lock in the scope and also the output which is the solution that you will propose. There is no single right answer to it. And this type of interview is testing how you think not just what you know or not whether you can come up with the right solution because there is no single right answer to the question that you will get during system design interview. With that, let's see what are you actually being evaluated on. Most candidates prepare by just memorizing couple of architectures like Spotify, WhatsApp and so on, which is of course a step forward if you know couple of variants of systems and what components go into this. But you need to also understand the other four dimensions that interviewers score you on. So let's start with the first one which is communication and collaboration. This is whether you ask clarifying questions, whether you validate assumptions as you go or you just make assumptions and start building the system. And this kind of testing how you would work in a real world setup as well. Let's say your manager assigns a task to you like a big feature or a big system service you need to build from scratch. What do you do in this kind of situations? If some things are not clear, do you just make assumptions, build a solution and then figure out that it was not what they were expecting? So now you just wasted a lot of time in between and you need to come back and write another solution which is actually what they were looking for or you clarify this in the beginning. So they are trying to also understand whether it's easy to work with you or not. Whether you are good at communication skills apart from technical knowledge. The next one is whether you have structured way of thinking and approaching problems or can you break down the vague problem that you just got into specific outlined actions that you will take or you just jump between random decisions. you go back and forth between different designs phases and so on. So strong candidates here usually show that they can organize their faults even when the problem is not very clear. They at least have an outline of what are the steps we're going to take and that every step is clear what we're working on right now and what's up next. The next one is your technical judgment. So when you pick a solution let's say database a cache a message queue can you explain your decision why like why you chose specifically SQL database why you chose the caching mechanism or the specific type of message queue that you chose. This part shows that if you choose some tools to be used in a system, you choose them based on the requirements not just out of habit or not just because you know what Kafka is but maybe there was another message cue that was a better fit for this type of system. The fourth dimension is your practical depth. This part differentiates between two candidates who might have same theoretical knowledge but one of them also has practice with building these type of systems. So it will show up by seeing whether you think about failure modes about the scale about things that can go wrong in the system because if you just learned at the theory level you wouldn't be able to catch these type of failures in the system. So this part specifically is for interviewers to spot engineers who have actually shipped production systems versus those who just learned from theory. And then lastly, it's the time management as well because these type of interviews are usually either 45 or 60 minutes and here you need to not get stuck on one area and run out of time by doing highle design only. And it's worth asking your interviewer early where they want to focus on or proposing it yourself and getting alignment to make sure that you're managing your time effectively. And along the process of an interview, you need to always know what's the time and how much is left, how much you spent on this section. And if you're spending a lot of time on it, it's time to move on. So you need to also show time management which again all of these five pillars translate to how you will work in a real environment. If you can't manage your time during an interview then you're also not good at managing your time during work. If you don't have practical depth then you also won't be able to ship projects confidently if you only know the theory. Right? And same applies to all of these five pillars. And now that we know these five pillars, let's focus on the time management part and what are the stages you need to take to complete a system design solution. There are five steps for any system design problem. This is a repeatable structure you can apply to any whiteboarding system design interview. And the goal here is to stay organized and show the interviewer how you think through the problems throughout these 45 or 60 minutes. 60 minutes is actually more common because there is a lot to cover in this type of interviews. So assuming it's 60 minutes, let's see what are the steps we will take and how much time we will dedicate to each step. The very first one is clarifying requirements. You don't want to start designing anything before you understand the scope and this is the most common mistake candidates make. So let's see what these five steps are at a high level and then we'll get to discuss each of them in more detail. The very first one is you need to clarify requirements at least for 5 to 10 minutes depending on the length of the interview. You need to make sure you have all the functional and non-functional requirements before moving on to designing anything. After that you can either start by data and API layer and then get into the highle architecture or this part is kind of variable. You can also start from architecture and get into the data and API layer afterwards. whichever feels easier for you. For each of these you would spend 15 to 20 minutes not more and then you would move on to discussing scale and edge cases towards the end once you have the highle architecture the data layer and APIs. So here you might discuss either how you would scale this system or if reliability is more important for that type of system you would discuss other edge cases and how you would handle the failures. And the last part is to wrap up and talk about anything that you could improve on this system but you didn't have time for it. So just briefly mentioning this before wrapping up the interview. Let's start with the first step which usually takes up to 10 minutes or less and this is where you clarify the problem first before getting into the solution. You do not want to jump into designing anything yet before clarifying the functional and non-functional requirements. By functional requirements, we mean what are the main use cases of this system that you're building, what is explicitly out of scope. Because if you're given something to design like Spotify, you can never design it in 60 minutes. This usually takes multiple teams, months of efforts if not years to build and design all of this. Right? So of course you can't do it as a single engineer during 60 minutes. The goal here is to see what are the main use cases we are focusing on. What we leave out of scope and it also helps to clarify who are the users and what type of platform we are building. Then you also focus on nonfunctional requirements which answers what are the constraints for this system. For example, how many daily or monthly active users you will have? What is the readto write ratio? You would clarify the latency requirements and also what is the acceptable downtime which refers to availability of the system before moving on to the next stage. And it's a good idea to always check at the end of each phase whether you're good to move on because if you missed some things during this stage, interviewer can give you hints. For example, you may failed to clarify some of the important functional requirements. This is a good place to ask something like does this scope and these requirements make sense before I move forward to highle design. If they say yes, then you clarified everything. If they give you a hint then usually you missed something in functional and non-functional requirements. So you would clarify that before moving on to the next stage. And next is step two which is either the data and API layer or highle design depending on which one is easier for you. If you think it helps to start with data layers understand how entities connect to each other and then get to the high level design that's the approach to take. If you think imagining it high level first helps you understand the user flow then you start with high level design. I personally start with highle design then get here but sometimes I also start with data and API layer. This part is to model what you're storing and how clients will interact with your system. When it comes to data model we should focus on what are the main entities for this type of system. Let's say if it's something like YouTube, then the main entities are videos and users. And you would also map out the relationship between them. What are the primary keys, foreign keys, key indexes that we will use in the database and how big each record can get. This is also a good place to clarify which data needs strong consistency and where you can afford eventual consistency. Next part of this stage is to also define the API contracts like what are the main endpoints with request and response formats. Here it will also be good if you can talk about pagination, error handling, rate limiting, versioning and also follow the best practices of rest API in case you're using REST type of APIs which is in most cases that's the case and this is also where you will make the database choice whether it's poss MongoDB whatever you pick you should also explain why given the requirements you went with that specific option and also or walk through the tradeoffs of what are other things you considered but you didn't choose. The other important part is the highle architecture which takes another 15 minutes. This is where you spend a big portion of a time design the components and show how data flows through them. In case nothing comes to your mind, you can start with these four components which are almost always present in majority types of systems. These are API servers which will be accepting the requests, a load balancer that distributes traffic between them, database where you persist the data and optionally cache layers that caches some common data that you're fetching from the database. At this step, it usually helps to map out the user journey from front end to back end all the way to the database. So what's the front end? It's some sort of web app or a mobile app or desktop app and then it connects to your API gateway and load balancers which will distribute among API servers. You might have caching layer among these servers. Optionally you may have message cues that decouple the services and you almost always have a database to store the data in the back end. So walk the interviewer through how a request moves through your system from client through load balancer API server into cache and database and back all the read and write operations. If you're using caching make sure to also mention the TTL and what invalidation mechanism you use. And for different parts of your system, you should also walk through your choice between consistency, availability, and partition tolerance, which is coming from the CP theorem, which means some of your components may need different approaches. And explaining that shows that you understand the theorem and also the trade-offs that you're making for this system. The fourth step is discussing bottlenecks, scale and reliability which can be anywhere from 5 to 10 minutes depending on how much time you spend on highle design data and API layer. This is one of the important parts where you show that you can think about what breaks under heavy load not just the happy paths and this is also one of the clearest signals that separates who's a mid-level candidate here who's a senior level candidate. This is where you discuss things like single points of failure. You find them and propose solutions on what we can do to fix this single point of failure. This is where you will talk about fallbacks and failures. For example, whether you have mechanisms to fall back to a slower but functional path in case something goes wrong. Here you can also expect to talk about the redundancy like multi availability zone replicas for high availability application servers to auto scale across availability zones distributed caching with replication and so on. Another point is whether you can spot how you will be monitoring logging and alerting issues for this system. This part is often overlooked but it's a strong signal that you think like a senior engineer. This is where you would cover API latency as well, error rates and thresholds, what alerts you will set up for this type of system and so on. And again, it is always a good idea to check in with the interviewer whether there are specific failure scenarios. For example, if you were discussing failures that they would like you to address and same goes for highle design, whether you covered everything they were looking for and same goes for database and API design. The last part is where you can expect to discuss trade-offs and extensions. So this might be an open-ended question like whether you can spot some things that can be improved in the system. You usually wrap up by summarizing the key decisions you made, what you prioritized for this system, what you sacrificed because it's always about trade-offs in system design. And you should always be explicit about the why behind the sacrifices. And this part is also to see whether you will propose some possible extensions for the future. For example, if you had more time and the load scaled to 10x or more, what would you do to extend this service or system? So this is for you to show that you're thinking beyond the problem that was discussed here. So what if we scale this? What if something goes wrong? And remember that you cannot propose a perfect design in 60 minutes. So of course there are always things that you can improve. The goal here is not to check whether your design was perfect or not because it is not. You just need to show that you could extend this. If you had more time, you had more ideas on how to improve this system. Now that's the five-step system to manage your time. But let's also come back to these five dimensions that you're being evaluated on and see what you should do and what you should avoid doing during the interview. So first thing I recommend you do is ask clarifying questions before designing. This is the first step. So if you proceed jump into a solution before understanding the scope that is already a red flag for the interviewer. So always this is a mandatory step before you get into designing anything. Another common issue I see is engineers get stuck into one of these extremes. They either stay silent and draw diagrams without talking to the interviewer or they talk all the time and try to fill in all the silence without inviting the interviewer for their opinion. What you should do instead is treat the interviewer as your colleague. This is like two software engineers in the same company who are working on a solution together. So after each major decision or each of the phases, it's a good idea to first double check whether this is what they were looking to get out of this part. And if you missed anything, for example, if it was functional requirements that you were clarifying, whether there are some more things that you didn't ask for, but those are still expected to be part of the system. The other thing is always suggest multiple approaches before committing especially when there are multiple ways to go about things. For example, if you chose SQL and even if it's a very obvious choice, for example, it was let's say payment system where consistency is the most important thing. If you chose SQL without explaining why, we just assume you didn't know or the interviewer then has to follow up and ask a question why you chose SQL, but they may also not. So if you don't explain, they would assume you don't know if you chose SQL, still mention why you didn't go with something like NoSQL and what are the tradeoffs between those solutions. And same goes for any other decision that you will make where there are more than one approaches to the problem. So this also ties to the tradeoffs which is you should always explain trade-offs when picking any technology. If you just pick them without justifying the choice then it's again another red flag during the interview. And this last one overlaps with the second point a bit which is you need to check in at each major step. For example, if you did the highle design and you just move on without asking if there are things we forgot to address, then there might be things that you miss during this step. [snorts] So to summarize, use this framework to manage your time and remember to always involve the interviewer into the discussions whenever you're working on any of the stages of this interview. Remember that this is a conversation between two engineers, not a monologue where you talk all the time and also not a take-home assessment where you just work on the solution without communicating with them. And to quickly recap what are these five steps, it's first to clarify the requirements functional and non-functional and understand what's in scope and what's out of scope. Next, you get to the models of data and API layers. Then you come up with the high level design with all the components that are involved and how the data flows for read and write operations. Next you discuss the scale, failure modes and reliability of the system. And last part is usually open-ended which is where you reflect on the system you built. What are the tradeoffs you made and how would you improve this if you had more time? Building a music streaming platform like Spotify is a classic system design problem and that is because it includes audio delivery, metadata management, object storage and everything in between that you need to know to design things from scratch and especially if you're a mid-level developer who struggles to design systems or features from scratch, then this will be very helpful for you. So let's see how we can design this in seven simple steps. First we'll start by understanding the requirements and planning the capacity. Then we'll come up with the highle architecture and the system flow of read and write operations. After which we'll need to design the API and also the storage and database. And then finally we'll move on to scalability and understanding how we can scale this system for let's say 50 million users or above. And at the end, I will also show you the pyramid that separates senior engineers from mid-level developers so that you can understand what are the skills that you need to learn to be able to design such systems from scratch. No matter if it's system design for Spotify or for Google Doc or for any other system, let's start from understanding requirements as a first step because without this we cannot move any further if we don't know what we are building here. So we are looking to build a music streaming platform for about 500,000 users and it will handle over like 30 million songs in this platform. So the core requirements here will be that artists need to be able to upload their songs to the platform. On the other end, users need to be able to search and play the songs that are uploaded by these artists. They need to be able also to create and manage playlists on their accounts. For this we also need to have a profile for users so that they can maintain their own playlists and some basic monitoring and observability like health checks for the servers, error tracking, performance metrics and so on. For audio formats, we will use OG and AAC type of files which are just audio file formats that compress the music without losing the quality and they also take up less space. For example, we can use 64 kilobits per second for mobile data saving and 128 for standard quality. And for premium users, we can deliver 320 kilobits per second. And on average, we also need to know the size of these files like what is the average size of a music file. And let's assume that for normal audio quality, which is the 128 kilobits per second, it is 3 mgabytes on average. But this is also something you need to clarify before moving on to the next stage. Once we have the requirements, the next step is to plan the capacity of this system and especially for song storage, strong metadata and also the user metadata and daily bandwidth that we'll be dealing with. for song storage. Since we assumed that 3 megabytes is the average size of a song and we have 30 million songs that we are working with, this translates to 90 terabytes on average of raw audio data. And this doesn't include the replicas across different regions which might be duplicates of the same song. And it also doesn't include the versioning overhead when artists re-upload the songs for certain reasons. Let's say they want to change something and they just end up re-uploading the same song. So that's why we will also need to estimate this like at least two or three times higher than the amount that we have here which is 90 terabytes. For song meta data let's say each song needs a title, artist references, duration, file urls and so on. So we will need at least like 100 bytes per song to store the metadata on average. And if we assume that it's 100 bytes, then we'll need to multiply that by the 30 million, which will end up being 3 GB. So this is practically like nothing compared to the song storage. As you can see, the main storage that we'll be dealing with in this system is the song storage, which will be an object storage. For user metadata, again, this won't be something huge like let's say we need to obtain the user profiles, preferences, and playlist data. If we assume it's like 1 kilobytes per user profile data that we'll be storing and we have the 500k users which we clarified in the first requirements here. That means that we'll be dealing with 500 * 1 kilobyte which will be just half GB of storage for profiles. And the fourth thing we need to estimate is the daily bandwidth that we'll be using. The average listen time let's say is 3.5 minutes. This is also something that needs to be clarified in the previous step. And if you are using the 128 to 160 kilobits per second, that is roughly 3 to 4 mgabytes per streaming. And let's assume each user streams like 10 to 15 songs daily. This will lead to significant egress costs. Igress costs are the fees that cloud providers charge when data leaves their network to reach to the users. Overall the key insight that we get from the capacity planning is that audio will dominate both storage costs and bandwidth. The metadata is just a small part or portion of this system. So with that we can move on to the highle architecture like a very high level architecture for now. We'll need to have some sort of mobile app or let's say the web interface of the application. This will be the userfacing app that handles the UI, the user searches, the playback controls and playlist management. It will make HTTP calls to the rest APIs. But before that, we will have a load balancer that will distribute these incoming requests across multiple API servers. And these servers will be the restful APIs. As an algorithm for the load balancer, we can use either roundroin or we can use the list connections algorithm. And the load balancer will also make the health checks of the API servers and make sure that they are always available. Next, we have the API layer where we have our restful APIs. Here as a validation, we can use JWT tokens and query the database song for metadata and URLs. We'll use an SQL database for storing the metadata and these servers will be querying it from the SQL database. But for audio files, we'll need some sort of blob storage to store these files since they are the biggest portion of our system. And these will be queried with generated signed URLs, which is a one-time URL that expires after some time. So that we keep the URLs safe and they are not always accessible through the same URL. An example of this blob storage can be like AWS's S3 storage which is a way for storing large files like the OG files that we need to store as part of this Spotify design and these type of blob stoages offer virtually unlimited scalability and it also comes with built-in durability and cost effectiveness for the large files that's why this is a good solution for storing these type of audio files with this highle design. Now we can think about the read and write workflows of the system and let's start from the read workflow which is users playing a song. So the very first step is user hits the play button and they play a song in the mobile app. This will make a request like get to songs/ ID. This is also something we need to design in the API design very soon in the next step. And then after that we'll hit the load balancer which will route the request to the API server. API server as a first step it will validate whether the user is authenticated with the JWT token. And if they are authenticated then we'll get into this block where we will be starting to play the song. So it will first query the song metadata from SQL database where we store the metadata of the song and user profiles. It will return the song details. Once we have the metadata, we will also generate the signed URL here in the API server and then we will return the metadata plus the signed URL of the song to the mobile app. Then mobile app will range requests for audio chunks to the blob storage using the signed URL and blob storage will stream the audio back to the mobile app. Let's say we are also tracking some analytics for each played song. For that reason, we will also make another post request to songs/ ID of the song / SLplay to track the analytics. And here we can send the metadata that we want to store for later. And we will return authentication error in case the JWT is invalid. So all of the steps above won't be handled if the user is not authenticated. Next we have the right workflow of this same system which is artists uploading a media file which is the audio file. So the first point again is the mobile app. It will make a post request to store the song with the meta data and this will again reach to the load balancer first. Then it will forward the upload request to the API server. In the API we will likely have some file size limits. So we will first validate the file format and also the size and if it is valid then we will continue with the next steps. If it is invalid then we will just return the rejected payload with unsupported format or unsupported size of the file. Otherwise as a first step we need to store the audio file in the blob storage. So we'll send it to the blob storage which we are using and then after that we will extract the audio metadata like the duration the bite rate and also the other things which we will get in the post body like the title of the song and so on. And once we have all the metadata like the song type, the artist, the title, we can insert this into the SQL database. Also once it is uploaded we can also move it by hierarchal structure if we want to like if we want to store the file inside of the artists then by artist ID then the album than the album ID we can do that also in double storage. Technically the object stoages don't have hierarchal structures. This is just an illusion that you have it but this is also useful for us to keep track of things like they are in a folder format. Then if you are going to use any sort of CDN, we'll need also some CDN invalidation. CDN here will be used for caching copies of the content like artist pages, album metadata, song details which will be closer to the users for fast delivery and it will also clear the old cached data so users see the latest songs and artist updates right away. And that will also be used in this request to make sure we are keeping everything up to date in the CDNs. And lastly, we will also send the response to the mobile app notifying that the upload has been successful. The next step for us will be to design this API and the database in a bit more detail. So let's start from the API design. For the search and discovery, we'll need a way to search different content types with pagination. Let's say user searches for a specific song. We will need to make a request to / search endpoint and provide the query which will be the exact thing they search for. And here we can pass more details like if we know that the user is searching for a song, we can pass the type as song. And we'll pass the limit which can be like 20 by default and the offset which means that from where we need to start the counter in the songs. Next we will also get the trending songs and optionally this can also be filtered by genre or by some other things. So it will be a get request to slash songs/trending and then we can pass the additional filters if we want to and the limit on how many songs we want to query from the API. [snorts] Users will also need to get all songs by a specific artist. Let's say they are browsing the songs for a specific artist and they want to see the songs only of this artist. For that we'll need to have an endpoint for SL artists/ ID of the artist and then / songs which will return the songs that only belong to this artist and the limit as always to limit the response from the API. Next, we need to also control the content access. The first endpoint here will be to get the song metadata and the URL of the audio file. It will be a get request to / songs/ ID of the song which will return all the metadata and the streaming URL. We also need to have a direct streaming endpoint like / songs/ ID of the song and then slash something like stream which will be an alternative to the signed URLs. And we need to also design for the playlist since the users can maintain playlists and they will be querying it back optionally with also the song list that is included in the playlist. So it can be a get request to/plays id which will just return the playlist details and then if you also include the query like include songs then it will also return the songs that are included in this playlist. Now let's see what are the user actions that we can expect. For example, regular users will also be creating playlists, right? So for that we need some sort of post endpoint to/plays as a metadata. We can expect like something simple like just the name of the playlist and whether it is public or only available to this user. Then once they create it, they will be also adding songs to it which will be a put request to the same playlist with the ID and then / songs and here we can include the ids of the songs like some new songs that they are adding to this playlist and the position where we are adding this. Let's say they already have four songs in the playlist or five songs and then we are adding from position of five or six. So we will be sending the position along with it as well. Similarly, they will also remove a song from the playlist, which will be the same request URL, but at the end, we will also pass the song ID that we want to remove from this playlist. And they can also like or unlike a song so that it is added to their favorites and that will be done with a post request to slash songs/ ID of that song and slike which will be maintaining their favorites in the database. And lastly, we'll need some way to manage the users. For example, we will need to fetch the current users playlists with some request like get to/ users/me/plays. And we can also limit this with some limit if the playlists are too many for the user. Similarly, we need the songs liked by the user. So here they are adding it to their favorites. Here we can use like users/me/ either likes or liked songs. But whatever we are using here, it will be also nice if you use the same plural here as well. So that it's following the restful API principles. And then the limit which can be like 50 or something else and the offset where we are starting to retrieve this from. And they need to also be able to follow artists to be notified from their latest songs which will be done with a post request to the users/me/follow and then we will pass the artist ID to follow the specific artist. That is it for the API basic design. Now we can move on to the data storage. First let's talk about the blob storage we will be using. Let's say it's an S3 in AWS or it can be like Azure's blob storage. We will need to use this for storing the audio files because such large files cannot be stored in SQL or some simple databases. So we need some blob storage or object storage for this. The audio files here are immutable meaning once you upload them you cannot change them. So if you need to make a change to the uploaded file, you need to re-upload it and remove the old one. And we can also organize them in a hierarchical structure as we saw before. So we can store them like artist/ the artist ID/ album/ album ID and then /d this id. This will be easier for us for retrieving the files later. As a database, we will be using some sort of relational database like either PostcarSQL or MySQL. And this is the basic structure we will have in the database. Let's start with users where we will be storing the user ID and some metadata about them like the email, the password, hash created at which will be the creation or like the sign up date for this user when they last logged in, subscription type and country. And this can be many more metadata but this is just the bare minimum that we need to have. And with the user ID we will have a foreign key linked to the owner ID of playlists. So since we will have playlists we also need to have the owner of which playlist belongs to which user and we will also have the playlist ID to for linking this to the playlist items. We need the name and created at for this playlist. Then each playlist will have some songs in it. Right? So here we can use a join table. This can be something like playlist items which will contain first of all the playlist ID and also the song ID and the position like where is this song positioned in the playlist and when is it added to the playlist if we need to also maintain that. This will basically be the table that joins the playlists and the songs together. For the songs again we will have the ID which is the most important thing here and some metadata like the title, duration and so on and another important part here is the file URL which we'll be using to query the audio file to stream it from the blob storage. Then we will also have artists which we can store in another type of table other than the users for keeping this separate. So we can have the artist ID and then the metadata about them like the name, country and so on. And we'll need to connect the artists to the songs. For that we can use another join table like artist songs where we can keep track of most importantly the song ID that they uploaded and the artist ID like which song belongs to which artist. And additionally we can store some more metadata here. But this is the bare minimum that we need to have in this table. And why we choose SQL here? Because we need a lot of joins as you can see for playlists and songs for songs and artists. So this is a natural selection for us to go with SQL type of database. We can do even more complex queries here like find all songs by artists that a user follows or some other complex queries like that. And that is possible with SQL type of databases. And once the database grows, we'll need to also add indexes for the important properties here, which will be the frequently accessed columns. And in our case, we can, for example, index the title in the song table. We can index the name in the artists table. And we can also index the song ids in this join table where we'll be joining the playlists and songs. These indexes will enable us to have fast search capabilities which will also handle the typos and partial matches that user expects when they are searching for a song or for a playlist. And the playlist item index will speed up the queries when fetching songs for a specific playlist or finding which playlists contain a specific song. That's it for the planning of general object storage and also the SQL database. Now let's see how we can scale this. Let's say the user base grows to 50 million users now and the songs grow to 200 million songs. This means our current architecture cannot handle it more. Which means if we calculate the requirements again we will have 100 bytes for 200 million songs. For song metadata that will be 20 GB. For user metadata it will be 1 kilob times the user base which will be 50 GB. But for audio storage, you can see the estimated 3 megabytes per audio file. And if you multiply that by the amount of song, which is now 200 million, let's say, it will be equal to something like 600 terabytes. And this is not even including the replicas, the regional copies or the multifile uploads by artists. So in reality, this will be like two or three times more. So let's see how our scale phase might look like. As a first step, we can add CDN edges for different regions of the world. For example, we can have one for North America, we can have in Europe, we can have in South America. This will speed up the process of retrieving it from CDN. And similar to that, we can have load balancers in different regions. Let's say we have at least two load balancers in two different regions that are still mapping the incoming requests to the API servers that we have. Before getting to API servers for the CDNs, we need to use some sort of eviction policy for cache management. And here it's a good use case to use the LRU cache method which is automatically removing the list accessed content when storage fills up and the cache would fetch from the origin storage on a cashm for the API servers. Since we are not maintaining any type of user session or any storage, these are completely stateless meaning we can scale them as many as needed to handle the incoming traffic. So adding more capacity here is straightforward. We are just replicating the existing API servers. The main thing here is that when designing the API server, we need to design for truly stateless servers so that it's possible for us to scale this easily. Next, we need to also scale the database because the single instance cannot handle that many metadata storage. Which means we need to separate the reads and the rights in the databases. For the rights, we can use a leader replica where we will be writing the new files of the songs or new playlists that users are creating. But when reading it, we can have some slave or follower databases which will be only used for reading. So this will be in sync with the leader database. But whenever we get any read operations like getting the song metadata, getting the playlist metadata, we will be reading them from the follower databases. As a next step, if this is not enough, we can also consider sharding the database. Sharding will split the data across many database instances. Let's say we will have one shard for the US users and one shard for the European users. We can split it by geographically. And then we will partition songs by the artist ID ranges. This will give us horizontal scalability and it will improve the performance by keeping the related data together. And also for the blob storage, we can replicate this to be in different regions. Like for Europe users, they will be uploading to a blob storage that is in Europe region. For North America, they will be uploading songs to the blob storage that is in their own region and so on. For the other geographics in the world and all of them will still be using signed URLs for accessing the audio files and streaming the music chunks. Of course, there are many things that can be improved on this system like improving the reliability or monitoring. But the key, as you can see, is starting simple. We don't want to overengineer for small amount of users and then gradually increasing the scale as we go. And lastly, let's see how we can also be able to design such systems or features from scratch and what's the real difference that separates senior engineers from mid-level developers. If you think the designing part as this pyramid on the bottom which is the main like most important part is the planning which is the blueprinting phase the technical design understanding requirements as you saw estimating capacity choosing the architecture. This is where everyone needs to start when designing such large scale systems and only then we move on to the text selection which is like picking the right database whether it's SQL or NoSQL and if it's SQL then what exact type of SQL database it will be the caching layers eviction policies storage solutions and all of that tech related stock selection and only last stage which is the most smallest part actually at is designing is the coding which is like starting to implement the things but that is actually the thing that requires the least brain power and especially if coding is the only thing that you can do then we have now these cloud or GBT models which can do it faster better and cheaper so what's the point of any company to pay you if the only thing that you can do is coding that is where the most mid-level developers are stuck because they have this completely upside down. They start developing things without even having any plan, without having any text selection. They just start coding and see where it takes them. Whereas the fundamental stickers who learn to think through problems before even touching a keyboard or before even starting to build any code, these are the ones that will never struggle to find roles because all the companies are always in need for such developers who can think through problems from users perspective and also bring rough ideas to reality by understanding the requirements. By the way, if you'd like to learn all these concepts that I'm using for designing these systems from scratch, I have a whole system design guide covering all these concepts that you need for system design interviews on my YouTube channel. You can check out the first link in the description to find the link to my channel and the first pinned video in my channel is that system design guide explaining all these concepts you need for passing these system design interviews. Last design a hotel reservation system similar to booking.com or Airbnb which is a typical system design interview problem. The mistake that most developers make here is when they ask to design such a system they treat it like a typical crowd application with some caching and replication for the availability. But that's not the correct way to think about it. For instance, one of the problems that you might have is a moment when two users are trying to book the last room available at the same time and you try your simple select then insert approach which creates the double booking problem. So you end up charging two credit cards for one room and now you're dealing with refunds with angry customers and the system that cannot be trusted for the next time. That's why hotel reservation systems are hard because they also test the exact skills that separate mid-level developers from seniors. Things like handling concurrency without breaking it under heavy load, keeping the data consistent across multiple systems, and also designing for failure in case something goes wrong in parts of the system. And it's not about memorizing these patterns, but rather understanding the actual problems that might occur. What are the solutions to those? and what are the trade-offs between those solutions. So, we are going to design this system in four simple steps. First, we will gather the design requirements, estimate the storage and queries per second, transactions per second, and anything else that we need for designing the system. Then, we'll get into the basic highle design including the API design, database schema, reservation flows, and so on. And in the first step, we will scale the system to handle 1,00 queries per second and 17 transactions per second, which are the numbers that are close to the realbook.com scale. And then we will handle the failures under heavy load and discuss the system recovery whenever somethings or some parts of the system fail. So if you're tired of system design explanations that skip the hard part and only give you the surface level, then this one is for you. Let's start with the first step which is the design requirements. In such challenges, we usually follow a framework to come up with the solution. And the very first step is always to ask clarifying questions to lock in the scope that we are going to design and then also about functional and non-functional requirements. So let's start with the clarifying questions that we might have. For example, one of the questions can be about the payments which is are we going to allow them to pay at the platform or it is going to be a payment in checking and this will let us know whether we support payments in the platform or not. For this one let's say yes. So we are going to allow them to pay at the platform which means we need to have some sort of payment system inside. Another question can be about booking channels. So is this a website only web app where they are going to book the hotels or we also support the mobile app which might have different rates and different pricings. So let's say we are going to design both like web version and mobile version of this application. Then about cancellations and over booking. So do we support cancellations of the reservations and also over booking because hotels typically allow some sort of over booking taking into account that there might be some cancellations. So if they are getting let's say 10 bookings per month then out of these like two or three are going to be cancelled. So they allow users to also overbook because of the cancellations. Once they get some cancellations the room is free now. So they will allow over booking as well. Then about the dynamic pricing. So are the prices fixed or the price changes based on the demand and the dates. And for this let's say we are going to have dynamic pricing. Whenever the demand is higher so it's let's say Christmas time which is around now. We are going to increase the prices. For example this room where I'm recording from it was triple of the price as in usual because that is the Christmas time. So they are going to increase the numbers and similar to that any other popular dates that people are reserving we are going to increase the prices based on the demand that we are getting. Then we can get to the functional requirements. So one question might be about the hotel management. Are we going to support the admin panel like adding, updating, removing hotels or we are only focused on the user part. Admin panels might be included in this but the core functionality evolves usually around the users which are booking the hotels cancelelling them. Let's say we are going to also support some sort of admin panel in this system. Then we get to the search and discovery questions like what are the core functionalities we are going to support. And a way you can think about this is what are the functionalities that if we remove from booking.com it doesn't become Booking.com anymore. This might be a good approach for identifying what are the core functionalities. For example, if users are not able to search hotels by location, then it becomes some sort of useless application. Right? So of course one of the core functionalities we will support is that users need to be able to search by location, by dates, by guest number, price range and some other filters that we might set. Then about the reservation flow, how usually it happens is users book the rooms. Then they view the reservations, they should be able to cancel the bookings because we also clarified it above. And also the system should prevent double bookings. This is one of the challenges we need to identify here so that we also solve it in the next steps. Then about the payment processing since we clarified that we are going to support payments in the app then we need to integrate some sort of payment processing like stripe, PayPal or something else. And optionally it might also include notifications. So sending confirmations for bookings or cancellations or payments to both user and the admins brokers who are uploading these hotels. Then we get into the non-functional requirements which are more about the consistency, load and latency that we are going to support. Let's start with the consistency. We are going to have strong consistency because two users shouldn't be able to book the same room for the same dates. That is non-negotiable. But whenever they are searching or discovering the rooms there, the eventual consistency is fine and you will see why also when we get to the designing phase. Then about the concurrency. So we need to handle thousands of users trying to book the same hotel simultaneously and without it breaking the system. Also about the latency, we need to provide low latency. So search results should appear under 500 milliseconds and booking confirmations are fine if they are handled between two or 3 seconds. You should also clarify the availability and scale. Let's say for availability we are going to support the free 9 uptime. And when it comes to scale we need to be able to scale this horizontally without redesigning the system from scratch in the future. Once we gather the design requirements, we can move on to the estimation of data and capacity and after that we can start designing the system. Now when it comes to the data estimation, the most important things we are interested here is the queries per second that are going to happen in the system and also transactions per second which are the rights to the database when users are booking hotels. So in this kind of system you can think about it as this funnel here. The users that come uh to the platform are at the top here. So these are the users who are viewing the hotel room. This is the 100% of the traffic somewhere here. And then next we have out of these users probably like only 10% will go into browsing the hotels and searching for some good matches and booking the hotel. So that will be the second layer here. And then third we will have the reservations. So only out of this only the 10% are actually going to book anything because most of the visitors are tire kickers. They are not looking to book anything. They are just browsing. But we still need to design the systems for them because they are also going to increase the queries per second that are happening in the system. So based on this there are two ways for us to calculate the load. One approach is let's say to take a look at the booking.com and you can use some sort of extension like similar web to see the traffic that it's getting per month. For example this month it got 500 million visitors in the website. That means that 500 million users were browsing the system this month. So at the top and then you can start calculating it down. based on this to see also the transactions per second and what are the queries per second based on this load and the other way is the bottom up approach which is to get the number of reservations per year let's say and for booking.com that is 1.1 billion reservations per year and then we can calculate this upwards so starting from here how many visits we get per month or per second to the platform and we will also be able to calculate the transactions per second because we have the number for the year reservation. So each of these numbers is fine. You just need to get one of them from the interviewer and then you can start the calculations and also take into account that these numbers are also rough estimates. So you might get these numbers exactly to calculate or you might get just the visits per year and the bookings per year and you should be able to calculate it after that. The other important metric that we need is the average state. So what is the average stay that users are booking the hotels? Let's just say two or three nights per booking. So let's go with the two nights per booking and then we will be able to calculate the rest. So let's start with the daily reservations. We have the number of bookings per year which is 1.1 billion rooms per night. Let's just start with this approach. Then we can divide this by the nights per booking and that comes down to 550 million bookings per year because this is the room nights in total. This is not the bookings. So we need to divide this by the nights per booking to get the actual number that we are getting booked per year. Then we can divide this by 365 which is the days in the year and we'll get the number of bookings per day which is 1.5 million. And after that we can divide the 1.5 million by the seconds per day which is 86,400. And that means that we are getting 17 bookings per second which is the transactions per second that we are going to support in the database. Then when it comes to the traffic funnel part and the reads as you saw with similar web we are getting something like 500 million visits per month in the booking.com with about eight pages per visit which means that on each visit the users are going to browse around eight pages before they leave the website. Based on this, we can first get the visits per day and we can get that by dividing the monthly traffic with 40 which are the days in the month and we'll get to 17 million visits per day. After that, we can get the visits per second. So, if we divide that by the seconds in the day, we will get uh 200 visits per second to our website. And if we assume that they are going to browse around eight pages per visit before they leave, let's say that it comes down to five backend reads per visit, which can be things like search queries, availability checks. So we need to just increase this number by multiplying it with the five and we'll get around 1,000 read requests per second. And based on these calculations, we can clearly see that this is going to be a strong read heavy system because we have 1,000 QPS queries per second that we need to support and only 17 TPS which are the transactions per second that we will get. So our challenge here will be to optimize the architecture for reads while still keeping the rights consistent to avoid the double booking problem. These are the core numbers we need to start designing the system and then the rest we can look at while designing the API database and if there are any more things that we need it's fine to calculate those there but at least we need to get the main things that we need before starting with the highle design. After the scale and data size calculations we can now design an architecture that fits these numbers. So we can start by using a serviceoriented architecture with clear boundaries but also keep the tightly coupled data in the same database. So as a starting point let's say we have the mobile app or the web app where users are browsing these hotels and making the reservations. We'll use CDN in front here for the static assets like rendering the images, media files, also the JavaScript content and the hotel images. Then we have an API gateway which will be the single entry point to our application. This is where we will handle the authentication, the rate limiting and also routing the requests between all the services that we have. It can also serve as a load balancer across service instances. So if you have multiple instances of hotel service, let's say, this API gateway can serve as a load balancer and route the traffic between the instance one and instance two. When it comes to services, we can separate this with boundaries for hotel service. This is where we will have hotel and room information, mostly static and heavy cachable service. An important service here is a reservation service. This will handle the booking logic, inventory checks, reservation creation and cancellation. And this is also where concurrency will be handled in the application. Next, we have a rate service. This will be for room pricing by date because depending on the date and when you book the hotel, the price will be different. So this will be applying dynamic pricing based on the demand of the rooms. To integrate the payment gateways, we will also need a payment service that will implement let's say Stripe payments, PayPal payments, Apple Pay, Google Pay and Adam providers. And we also have a notification service that talks with the reservation service and with the payment service. This is what sends the emails and push notifications for the data layer. Initially we can start by using some sort of SQL database like possql primarily because we will need the acid guarantees and this will be also useful for the transaction store and we will be using many transactions. So we need the consistency of SQL databases. That's why we go with SQL in this case. We also need some sort of caching like radius for the data that doesn't change that often. But we can also improve this a bit further. So first of all if we think about the scale of the reads that we will have which is around thousand queries per second postsql database won't be able to handle that load. So we need to give some sort of solution to this. For such situations, tools like elastic search are a good solution for handling the massive scale for the search. So we will introduce a search service that will be responsible for retrieving the hotels by the filters that are set on the front end. And elastic search will handle the full textbased search on hotel names and descriptions. Also, it will be able to filter hotels, let's say within 5 kilometers of location or within some range to that location that is set and it will be able to filter by the price, range, ratings and any other filters that we need. The thing with elastic search is that it's way faster than the postgrql database that we will use let's say for listing the hotels and also it is eventually consistent meaning that eventually it gets updated with the database and gets the latest data in the elastic search and indexes everything properly to be able to search quickly based on locations or based on the price range and any other filters. We can also decouple the services a bit. Another approach is to use different databases for each microservices. Like for the payment service, we can have payment database. For the hotel service, we can have hotel database. And we'll discuss what's the pros and cons of this. But to also decouple them, we can introduce a message cube that will be responsible for instance for payments. Whenever a payment is processed in a payment service, we will process that and publish a message in a Kafka rabbit MQ or some other messaging queue. And this will first of all send email push notification to the user or to the owner as well as an email or as an inop notification that the payment was successful and the hotel is reserved. And it will also update the availability meaning the status of the hotel. So it will push that to the search service which will update it in the elastic search and that is again getting eventually consistent. So right after the payment maybe if users search they will still see that hotel available but they won't be able to book it because we are using the hotel database to double check whether it's really available or not. And eventually after some time this will be persisted with the database and it will have the same data as the postgrql database. Next when it comes to choosing whether all these services will be like microservices or a single monolith or they can also be pragmatic microservices. For example, in our case, a single monolith would simplify the transactions, but it will become hard to scale and deploy independently because if many developers, let's say hundreds of developers are working on this single monolith, then it becomes very hard to deploy independently and work on things without stepping on each other. Another approach is full microservices like this where you have rate service, notification service and so on with separate databases which will introduce distributed transactions and consistency problems that don't exist at the monolith level. That is because each service as you can see like hotel service owns its own database, reservation service owns its own database. So another approach is to keep the tightly coupled data together in one database and share it between the services. For example, in our case, the reservations and the hotel databases can be kept together. This will let us to use the local ACIT transactions instead of the distributed transactions. And we will have a service level separation at the application layer but we will still have the shared data for tightly coupled entities. After you agree on the highle design the next step is the API and database design and you can start with API design then database or vice versa whichever way it's easier for you to talk about the flow. So in our case let's start with the API design. We will use restful APIs for all the operations because that is what makes sense in this kind of system. Although as you can see here the front end directly makes calls to hotel API to other APIs as well that we have. Maybe sometimes it will make calls to reservation API. Another approach here a very common one is when you have multiple microservices usually you would use a GraphQL layer in front of the front end. So front end will only make requests to the graphql and it also will know the schema based on the graphql schema and then the graphql layer will be responsible for making calls to all these services that you have behind the scenes. So it kind of extracts away the complexity and the routing between these services. But to keep it simple on the diagram let's just assume that front end makes the calls directly. So we have here admins who are listing the properties. We have users who are browsing the hotels and making reservations. And we have the front end app which is either a mobile app or the web version. The very first step for admins is they need to be able to loged in to be able to use the features for the admins while users can be browsing the hotels without being logged in. But whenever it comes the point for the reservation, they need to also create an account or login. For listing the available hotels, we will make a get request to / hotels with any necessary filters that the user sets and we will get the available hotels and then if they want to browse a specific hotel then we will make a request same get request to / hotel/sp specific ID of the hotel which will return us the hotel that has that specific ID. Now how it will happen behind the scenes is hotel API will fetch the hotel details from the database. So for specific hotel we can also read this from the postcarql database. But in the initial call where we are making a request to / hotels that is where we will be using the elastic search to quickly filter based on the filters that are set on the front end. And at this step for a single hotel we just basically forward the hotel details to the front end and then front end is responsible for parsing and showing the details in the page. For admins who want to add a new hotel, they will need to make a post request to / hotels endpoint. And this is where we will insert it into the SQL first and then once we get a successful insert, we will just forward the 2011 status that the hotel is created and later this will be eventually persisted with the elastic search database. But we will show the confirmation at this step to the admin that the hotel has been created. And similarly, if they want to update the hotel information and they make some edits, we can make a put or patch request to hotels/ ID of that hotel and again update it in the database, get the update result and send the confirmation and show it for the admin. A similar flow for removing hotels. Again a delete request in this case to / hotel/ ID of that hotel. We remove it from the database and we show it to the user and later this will be removed from the elastic search. And within hotels we will also have rooms because one hotel has many available rooms. We will treat them as a sub resource. So under hotels and specific ID of the hotel we will have / rooms first of all which will give you all the rooms that are available under this hotel and to fetch the specific hotel you can make a request to / id and get more details from that hotel which will again fetch from the database and then you will forward it to the client and basically here we are just repeating whatever we have for the hotels but for the rooms. So if you want to add a room you will make a request to post/ id of the hotel/ the rooms. This is again following the general restful principles. We will insert the room in the database and provide with the successful response. Of course you need to also handle the failures whenever something goes wrong showing the error on why something went wrong in the API. But when talking about the happy paths or at least the available endpoints, these are what we will have. We will also have a put endpoint which is for editing the room type again / rooms. In this case, we need to provide an ID because we are not creating a new one but we are editing one. So we need to specify which ID is that room that we are editing. And same goes for deleting the same URL as we have for the put. But in this case the method will be delete which will remove the listing from the hotel. Then for the reservations this is where we will make requests to the reservation service. Let's say a user wants to see their reservation history. We need to provide an endpoint here to fetch the reservations. So a get request to /reservations which will read it from the SQL database and then forward the response to the client. Then they want to see let's say a reservation details and the invoice that they paid. So that is where we will have the get endpoint to /reservations/ ID of a single reservation which will fetch the details from the database. we will get the details back and we will forward it to the front end and we will parse and show it to the client. Next, when creating new reservations, they will fill the reservation form in the front end. Then we will send this form details in a post request to reservations. One important thing here is that at this step we can already prevent double booking for single user. There are two scenarios like one user can try to doubleclick or click the same reservation room twice. So that's one scenario which we can handle here. The other one is that two users can try to book the same room twice. We will handle that in the later step. But at this step for example one solution is we can include a key like a reservation ID and this will be generated on the front end which will be used to prevent double bookings when user clicks submit multiple times. So if we have the hotel ID and the room ID that this user is trying to book, basically we generate a reservation ID once and then if they try to double book it like book it again, we will be able to catch it in the database and see that the reservation ID is the same. So we will block them from making a request or we can even do it on the front end side. Another solution is you can also do it on the back end. For example, when they make a request to book something, you can combine the hotel ID in combination with room ID, the user ID who tries to book this and also the date or some sort of timestamp. Why? Because in the future this user might try to book it again. And if you just use these in combination, then basically you are blocking them to make a future reservation on the same hotel. So you can also use these in combination to generate some sort of hash and whenever the hash match is from the database then you block them from making the reservation. But this is way simpler solution on the front end side to just generate some sort of UU ID uh for the reservation ID. If you are talking only about a single user and preventing them from booking again, then this is a very lightweight solution and way simpler than inserting this hash into the database and checking it every time in the back end. So here we will first check for existing reservation ID. If it's not found, then we will insert the new reservation. So assuming it's not a double booking and we get the reservation confirmation, we will just show them the confirmation and behind the scenes the Kafka message will be published and it will send the notifications to the user and to the owner of the hotel. But if it's a duplicate reservation attempt, then they will make a request and this will contain that reservation ID. So we will check for the existing reservation ID and if a transaction like that already happened and we have the reservation and it already exists then we will return the existing reservation and show it to the user that this is already reserved. Similarly they might want to either cancel the reservation or change the dates of the reservation. So we need to provide endpoints for that as well. In case of cancelling, it will be a delete request to reservations/ ID of that reservation. And in the database, we just need to check whether it's allowed to cancel because if they try to cancel a week before the appointment, then it's allowed or if they try to cancel it last minute, then it's probably not allowed. So, in case it is allowed, we will just send the confirmation. Otherwise, we will send a response that we cannot delete the reservation, meaning we cannot cancel it and show the options to the user. Similar to this, you might also have edit reservation with put request which will update the dates. For example, if plans have changed and you want to update the dates, then we will make a put request to that ID reservation. Let's also see how the search will happen. So another service we will have for this is the search service which will be connected with the elastic search. So let's say user sets the filters like the location where they want this hotel to be located at the dates start and end date and so on and we make a get request to / search. These will include the location, the check-in, checkout time, how many guests you have, how many beds you want, the price range, and any other necessary filters. This is where we will query the available hotels and room types. But an important thing is we will query this from the elastic search. In this case, we won't query it from the database because it's way faster and that's the point of us setting up elastic search so that we can quickly return the search results. And this will return the available hotels within the range of that location and within that price range and the check-in checkout time and front end will be parsing and showing it to the users. Next, let's move on to the data models and the database design. One important thing here is that you book the room type and not the specific room. So you book a specific type of room like is it a double queen bed or something else and not a specific room because the room numbers are assigned at the check-in and not here. But let's start from the hotels because it will make more sense this way. So we will have of course the ID of the hotel which is the indicator of the unique ID for this hotel. It can have a name, address, location, description and so on. Now there might be also hotels that are under the same brand name. In this case you can either have the same hotel name but different hotel ids. So it will be a different hotel LLC in US. Let's say it will be a different hotel LLC in Europe. The name will be the same but the ID will be different. Or you can use a helper table like hotel groups and then group them under a brand name if there are brands that have multiple hotels at different locations. Then the important part is the room type that we need to have. So the ID of the room type, its name, description and then how many people it can serve, the ID of the hotel which connects back to the hotel. So one hotel can have multiple room types and this is a one to many relation. Then each room type is also connected with room type rate because each of these rooms will have a different rate. Let's say we have a room which is the basic package. And as a basic example let's say it costs 100 bucks per day. So this is the basic package. Then you can have a room with a better view. In this case it will be something like 150 per day. You can have a hotel room with breakfast included and that will be something like 200 and so on. So that is the reason why we have this room type rates because for different room type we will have a different rate that will be multiplied by the base uh number that we have here. And this is again one to many relation because one room type can have multiple room type rates. Then this is where we have the actual rooms. So what is the room ID? This is the exact room that we are going to assign at the check-in. Then it will have some more metadata like floor number whether it's available. This is the important part that will be tracking the status. And this is also what we are reading when checking the availability and the room type ID which again refers back to the room type. So one room type can have multiple rooms in it which means you can have a hotel under that hotel you have multiple room types like you have let's say the basic type and in the basic type you might have three rooms available that are all basic rooms. This also connects to the hotel ID so that you can determine which exact hotel this belongs to. You could also join it with the room types and then get the hotel ID like that. But it will be helpful for us to have the hotel ID here. So this will be like which hotel does it belong to the room that we are assigning at the check-in. Next we will have the room type inventory. Inventory is tracked per hotel, per room type and day. So not the physical room. This will enable us to have bulk availability updates also scalable pricing and search and it will be simple over booking logic because we will also support over booking considering the cancellations that we will have in the rooms. So here we will track the room ID type, the hotel ID and this is again one to many relationships between the room type and between the hotel and some more things like the date, the total inventory, total reserved and so on. Another important table here is the reservations because once user selects the hotel room specific room we will need to reserve it. So we will track the reservation ID which we will link later to the transactions. We will have the room type ID. This refers to the room types table. We will have the exact hotel ID where they booked this which refers to the hotels table. The guest ID which is the user who booked it. So for that we will have a guests table. And we can additionally have let's say admins or brokers table which will be the users who are publishing these rooms. So here the most important thing for us is the guest ID the first name last name email and so on. So we will connect it with one to many relationship here because one user one guest can reserve multiple hotels. So that's why it is one to many. Then some more metadata like start date, end date, status, whether it's reserved, whether it's canceled and so on. And another important piece here is that we will separate transactions into a separate table. The reason here is that let's say you reserve the hotel first. So first the status is reserved. Then the user can either change their mind and here they can either cancel the reservation or they might request a change in the date. So they might request to change the start and end date. In the second case, there will be a price difference that they will either have to pay or we have to refund them. So that will be another transaction, right? And that's why one reservation can have multiple transactions. So the first transaction is when they reserve and then the second one is when they decide to change the dates let's say and update the reservation they make another payment here which is a second transaction or if they decide to cancel that is another transaction where the amount in this case is minus so we are refunding the payment but that is still a separate transaction and that's why we will have separate transactions table where we will track the ID of transaction which reservation Does this belong to the type of the transaction like refund, change or the new transaction and the amount which can be both positive and a negative number. With this schema, the room type inventory table has one row per hotel room type and date combination. We will prepopulate this for the next two years and then a daily chrome job can add new dates as the time moves forward. So we can also take a look at some example data here. We'll have the hotel ID where these room types belong to. Then the room type ID. Let's say these three rooms are from the same type like the basic type. uh dates where we are checking these for like December 15, 16, 17 and so on and then we will see the total inventory and also total reserve which will give us an idea on how many rooms we have available. When checking the availability, if you're using the SQL database, let's say for December 23 to 27, we will select the date, total inventory, and total reserved, which is this column here. And then the total inventory, which is the available. We will select it from the room type inventory where the hotel ID and room type ID match. And we will set the date to be between the range that we want to. This will give us the total reserved and total inventory. And by using this we can check that if the total reserve plus rooms to book is still less than equal to the total inventory then we allow the booking. Which means if let's say here we have 100 and here we have 92. So we still have eight rooms available. And if user is trying to book just one room that will be 92 + 1 and we are checking this against 100. So the condition will be true and we will allow the booking and coming back to the initial requirements since we allow let's say 10% over booking or whatever that number is we can include that over booking factor here. Let's say if we allow 10% over booking then we will allow 10% like set the 10% here. Meaning if we have 100 total inventory available, then we will allow them to book up to 110 rooms because we know that the other 10 will probably be cancelled until the date of the reservation. And lastly, let's also discuss why we move with relational database. In this case, we kind of touched on this, but let's get a bit deeper in why we choose this. So first the most important thing we need here is the acid guarantees and specifically the consistency of SQL databases. This is important for us for preventing double pooking issues which we will handle soon. From the calculations we can also see that this is a very read heavy system. So relational databases handle these kind of reads well and we can always combine it with elastic search for scaling it. But with 1,000 reads queries per second versus 17 writes per second, this clearly shows that we need to go with an SQL database here in combination with the consistency factor that we need from the ACIT. And then the relationships are more clear this way like between rooms, hotels, the room types, reservations, transactions and so on. And we'll need to make complex queries to check the availability of the rooms or make multiple joins for inventory with pricing, filter by dates, aggregate totals and so on. So SQL will handle this well as well. We also need to handle the concurrency. We already discussed the first scenario where the same user is trying to book the same room twice and the solution here is just to have that reservation key. So any second request will get duplicate key error and we will return the existing reservation. So that's the simple one that we can solve. But then the second scenario is we have two different users who are trying to book the same room type which is only one spot left for that hotel. And if you know about the SQL isolation layers, by default this will be read committed and concurrency bugs still happen at read committed isolation layer which means that you only see data after other transactions commit. But there is a gap between when you read like check the availability and when you write which is you book the room. And in that gap you might have two different transaction happening like user A reads the total reserved. He sees that one room is available because the total is 100. And user B also reads and sees that there are 99 rooms reserved and 100 available which means that there is one available. So for both of them the check shows successful like the condition is true which means that we allow them to proceed with the booking. So read committed by itself is unsafe for bookings because two transactions can both read valid data pass checks and then override each other and that's exactly how double bookings happen. As a solution to this we have two main approaches in this case. One approach here is that we can use serializable isolation layer which means that when beginning the transaction at the code level we can set the isolation layer to be serializable. By default, it is read committed. This is simple to implement and it will work fine for low TPS meaning transactions per second because these won't be blocking each other if you don't have that much high volume. So it's fine to use serializable in that case. The downsides to this is that transactions will block each other. So if you have high TPS then this won't scale well and also you introduce potential deadlocks if locking multiple resources in different orders. However, you can use let's say log timeouts and set it to some number to prevent indefinite weights and also keep the transactions short. But still there is a better solution in this case which is to use read committed isolation layer which is still the default mode in combination with row locking. This is when you're using select for update and it is used to lock the inventory rows while we are checking and booking them. So that other users have to wait their turn. If you have two transactions who try to log the same row, the second one needs to wait for this transaction to be processed. But if there are other transactions which are not trying to write to this same row, then they are free to move forward and basically they can go in parallel instead of the serializable where the transactions need to go one by one. So the for update here prevents the race condition by serializing access to specific rows in this case. The read committed is still the postgrql's default and works fine here. And serializable is fine to be used if you have low TPS and not that much traffic coming to write to the database every second. The next phase is talking about scaling the system. things like database sharding, whether it's needed, caching strategy, how we are going to optimize the elastic search queries. So, let's start from the database sharding. Since we're going to handle 1,000 queries per second and 17 transactions per second, we need to eventually shard this and that will be obvious by the time it takes for typical queries to run. And there are tables here that we can split. When it comes to sharding, the important decision is what will be the short key and by which field you are going to shart. One example here is to shard it by hotel ID. Why? Because most queries will filter by hotel first to see their location whether it matches with the search query filter and then they are going to fetch the room types available in this hotel. All data for one hotel will still stay on the same shard. So we will not be crosssharding the joints and this will also enable local transactions within the shard. For initial deployment we can use modulo sharding which is the formula for sharding. So the shard number will be equal to this won't be consistent hash initially. So it will be just the hash of the hotel ID in combination with the number of shards that we will have. This will work well when you know your shard count won't change often. So if we decide to let's say split this,000 read queries per second between 16 shards then this will range about 62 reads meaning queries per second and one writes transactions per second per shard. So in that case the modular sharding will work fine and then later that is where we can integrate consistent sharding if needed to minimize the data movement. So the thing with modular sharding is that if we decide to add another database let's say number 17. This will require us to reshuffle almost all the data from 1 to 16. Whereas if we use consistent hashing, this only moves about one of the 17th of the data, which is the data that is going to be stored in the 17th chart. And you can check out the video in the system design course where I talk about consistent hashing. Basically, it uses a hash ring where both servers and keys map to points on a circle and each key goes to the next server clockwise to the ring. So, if you have a ring like this, then you calculated the key which is ending up here and the closest server to that is here. Then this will be stored in that server. So with consistent hashing adding a new server only affects the keys between it and the previous server which is the number 16. Whereas with modulo sharding you need to basically reshuffle all the data. So with small number of shards modulo sharding is the way to go and if you know that you'll be typically adding new shards to the database then consistent hashing is a better solution here. Now let's see the caching strategy that we can use for the hotels. We will need to cache the hot inventory data in radies or memcache for smart invalidation. So this is the cache structure that we can have which is for the key we will store the inventory which will be the combination of hotel ID, room type ID and the date and for the value we will store the inventory available. Let's say in this hotel again 100 are available and the total reserved which can be something like 87 and we will also have in combination with this the over booking rate which will be 10% but of course we don't store this in the cache we have this as a fixed number somewhere and when calculating we will use this in combination with total inventory and total reserved and we will assign the cash a time to leave which can be something like 5 minutes per inventory. Now the cash flow will look something like this. User will first search for hotels and this is where we will check the radius cache first for the hotel inventory. If the cache is hit meaning we stored this data in the cache. If that is true then we will show the hotel inventory to the user immediately from reading from the cache. But in case it is not in the cache we have cache miss. This means that we need to read it from the database then we will also populate it after reading in the radis cache so that next time this will be a cache hit and also if we read it from the cache so it was cash hit but the TTL is expired then we basically repeat the same flow so we read it from the database we store it in the cache and then we show it to the user after we show this to the user let's say the hotel is available user can click on booking. So to book the hotel, this is where we will need to validate the booking against database and update the database with the booking transaction. Again, this is where we will invalidate the affected cache keys. If they decide to book it and the booking goes successful then we need to basically invalidate the cache update the fields and on the next user search we will check the radius again and this will be up to date with what just happens when this user let's say books the hotel. So next time we don't show it as available but we show that this room type is no longer available if it was booked with the previous search. Let's also discuss the search optimization with elastic search and also a bit more in detail on what's the use case for elastic search. So elastic search is a search and analytics engine that is a specialized database for specifically search operations and not transactional workloads like with PostgrSQL or MySQL. So, PostcarSQL or MySQL are still great with give me the exact row where ID equals to 1 2 3 or insert this into the database where the hotel ID equals to something. But on the other hand, Elastic Search is great at finding complex filters. For example, find all documents that kind of match with this text and rank them by relevance. filter by multiple criteria and do it fast across millions of documents. For example, in our case, it will be find all hotel rooms where hotel is located within Georgia. Let's say the price range is between 80 to 180 bucks per day. The stars are above 8 plus and some more stuff. And again, do it quickly. This is why we use elastic search for bookings as well. If you were to just use postgrql for search, this is what we would have to do like select all from hotels where location is like Paris, let's say, where amenities is like Wi-Fi included, where base price is within 200, where the description contains something like luxury because we have this in the search. Basically every time you use the like keyword, this kills the performance of SQL because that is very slow. The problem here is that like and then some text this does full table scan in case of SQL and even indexes won't help us here because in case of like this is basically a full table scan and you can imagine having so many hotels or room types and we basically have to scan the full table just for getting the hotels available in Paris which is probably less than 10% of the full table. Also, it cannot rank by relevance. So, it cannot get the which hotel is most relevant. It cannot handle typos. If a user misses the last S, let's say from Paris, then again, you need to use the like keyword or otherwise if you set it to exactly party, then you won't find any results. It is very bad at geographical like distance calculations and also aggregations are expensive in SQL where we are aggregating data together. Instead, if we use elastic search, we have the full text search with relevance ranking. So when user let's say searches luxury hotels in Paris, we first tokenize the luxury hotel and Paris all the keywords. We find all documents that contain these terms. Then we also rank them by relevance by using an algorithm. One of these algorithms that elastic search uses. It can also detect the typos. So if in the Paris you missed the I here, it can detect that user was searching for Paris and edit the distance of one. And it's also great at hotels with ranking. So it can give you the hotels within 5 kilometers of coordinates close to Paris. So maybe you are fine with booking a hotel near Paris in a smaller town with a cheaper price. Elastic search basically has that indexes optimized for this. But how do we basically store this data in elastic search? Do we duplicate or what do we do? And yes, we kind of duplicate the data. So let's say you have the hotels table in SQL. You have the hotel ID, the name, address, and then you have the room types within this hotel that belong to this hotel specifically. You have the amenities for this hotel and anything else. Basically, elastic search stores this in its own way, which means it will convert this into this JSON like format where it has the hotel ID, the name, address, and then it will nest the location and the room types in it. So here as you can see there are no joints. It basically condenses everything into one document which will also be able to use to search quickly. And then how do we also keep elastic search in sync with postgrql because our single source of truth is still postgrql. This is where we check whether the room is available or not or whether we can do over booking or anything else basically that you check postsql is the single source of truth. That's why when we are doing transactions we are doing transactions by checking the postgrql and not elastic search. But when we are doing searches we use elastic search in that case. So we can use change data capture CDC to keep elastic search eventually consistent with postrSQL. This is how it works. We have postSQL which is again single source of truth. Then it uses write ahead log to send it to the bases connector which reads this write ahead log. It publishes this to Kafka and starts the event stream. Then elastic search consumer will process the events. So we will have some consumer that processes these events and it updates the index in elastic search and makes it eventually consistent with the postgrql database. This is also for crash recovery. So if even the postgrql is crashing then you can read it from the transaction log. You have dbium which is an open-source CDC tool. It connects to PostgrSQL and reads the logs in real time. Then it publishes the changes. Whenever changes are happening, it publishes it to a Kafka topic and each table gets its own topic. So then the consumer reads it from the Kafka events from each topic for each table meaning and it updates it with the elastic search. That is overall the use case of elastic search why we use it and how we keep it up to date with our SQL database. Next, it will also need database replication this system because we are handling thousand queries per second. So, one option is the sharding which is scaling the scans in the tables and it's splitting them based on the geographical locations. But you can also scale it for the reads by replicating it. So you can have the primary write database and basically replicate it to a read replica one in different availability zone and a different replica in another availability zone. So then the user will connect to the closest available database and the search or the query will be faster. In this case, if we imagine the reservations, let's say we are quering the reservations for this user with the specific user ID. We can have a variable which detects whether we should use the primary or not. If it's a recent, write, then we should use the primary because the read replica might not be up to date yet. If that's the case, then we will get the primary connection and read it from this database here. But if it's not the case, if it's not a recent write, then we can basically read from the closest replica, we will get the replica connection and run the query against this replica instead of against the primary database. Next, let's see also the load balancing that we will use here. We can have users from very different various locations, right? We can have users from US, from Europe, from Asia, from other continents of the world. So the algorithm for this of course it makes sense to use geol latency algorithm which is based on their region. Of course there might be exceptions. Maybe a US person is looking to travel to Europe. So that is a different story. But in general the users in Europe are looking to book hotels in Europe. The users in US are looking to book hotels in US. So that's the algorithm that makes the most sense here. And the thing that we can replicate here is based on the locations we can have different database replicas which are read only. We can have the cache for radies again in each location because we will probably cach the US region users hotels in these regions radius and it doesn't make sense to also cach the Europe hotels here. So we will have it in a different replica in a different region and also the elastic search because users again are more likely from US to search for hotels within US. So we have all that data in one region which is quickly accessible and similarly we have it for Europe we have it for Asia and any other continents that we support but the primary database stays one here at least even if this is one of the shards let's say shard 7 this stays as a single database to avoid having conflicts between different regions rewriting to the database and then we have to manage the conflict so we can have one primary database. Since the transactions per second are not a lot as we saw in the calculations, we can have one primary chart and multiple replicas and regions for each of these replicas. This is the most in-depth breakdown of designing a messaging app like WhatsApp from scratch that you will be able to find on the internet. Messaging apps like this show up all the time in senior and staff level interviews and that is because they force you to handle stateful connections. Think about real-time delivery, consistency and scale of the system, failures of pieces of the system or overall disaster recoveries and multi-device sync in case of users using multiple devices like mobile phones and desktop apps. So in a short interview, you need to show that you understand not just the happy path but also all the things that can go wrong in messages when they are flying around between disconnected users and mobile networks. And we are not going to ignore the hard parts and pretend that everything just works without us preventing the failures. So we will start by first building this from scratch. Starting with a simple version that works for a few thousand users. Then we'll scale it up to handle billions of users sending messages across unreliable mobile networks. And after that, we'll identify where it breaks and how to fix it properly. At the end, you will also find the full set of footnotes explaining every technical concept that I mentioned while designing this. So you can revisit anything and you can go deeper on each topic. Before getting to the high level design and sketching diagrams, we need to start with the requirements to understand what we are building here. So the core requirements that we have is that users need to be able to send and receive messages in oneonone setup. So on top of that, we also have group chats with up to 100 participants that we need to support and messages are delivered not only when users are online but also when they are offline. We need to handle that scenario as well. They can attach media files like images, videos or audio clips like voice notes. We need to track the statuses of messages like when they are sent, when they are delivered and when the message is read from the second user. And of course, we need to track the online offline status to show them the last scene date of when the user was online. These are the core requirements. When it comes to non-functional requirements, we need to provide low latency delivery. So under 500 milliseconds when users are online. We also need guaranteed message delivery. So of course messages can't just disappear and not reach to the second user. In the scaling phase, we need to handle billions of users with high throughput. And to keep the storage manageable, we don't store messages on servers longer than they are necessary. So it needs to also stay resilient when individual components fail. Let's say one of the components fails like the server that needs to push the message to the second user. We need to handle these scenarios as well to make sure that the user eventually receives the message when the server is back online. With that we can do some back of the envelope calculations to understand the scale of the system and then we'll start with the highle design. Let's start with user metrics. We have at the scale phase 1 billion registered users and 500 million daily active users. During peak hours it will be handling 50 million concurrent connections and on average we will expect to have 10 to 20 messages per user daily. With this we can start the message estimations. For example, for the daily messages, we need to multiply the active users with the average daily messages that will be sent, which is 20 at max. And that will be 10 billion messages sent every day in the system, which means 10 billion writes to the database every day. That comes down to 115,000 messages per second on average. So 115,000 rightes to the database. And during peak hours, we can expect to get three to five times more of the traffic than on the average, which will be 350 to 500,000 users rights on average per second. Now let's also estimate the storage. So each message with metadata, let's just consider it to be 1 kilobytes. It should include the text content, the server and receiver ids, the timestamped when the message was sent and the status flux. Since we have 10 billion messages per day, it will be 10 billion times the 1 kilobyt. And we will be storing 10 terabytes of storage per day for the messages. And on annual basis, this comes down to 3.6 pabytes per year, which is 1,000 terabytes for each pabyte. And the bandwidth will be 50 million concurrent connections at the peak traffic time. And each connection will average out to 10 kilobytes per second for active messages which comes down to 500 GB per second of bandwidth. One important thing here is that most messages get delivered and cleared from the server within seconds. So we are not going to store this everything forever. users download the messages to their devices and also back up if they want to for example to Google Drive and then we can clean up the server side to not store any unnecessary data. Here in the beginning of requirements we have to set it to be stored for 30-day retention for undelivered messages and maybe let's say 10% of users keeping history on servers. So this allows WhatsApp to run on minimal infrastructure because they are very efficient about not sending the unnecessary data. Now with this row of calculations we can start with the very high level design of what the system will look like. Let's start simple and add complexity only when we will scale to more users. But for now this is the initial setup. We will have the mobile app which is the client like the user's phone. It is responsible to maintain a consistent connection with the chat server, handle the UI and manage the local message storage and also retries when failed operations happen. The next piece is the load balancer which sits between the mobile app and the chat servers. This will be responsible for routing the incoming traffic between multiple chart servers to handle the peak load and traffic. These chat servers will maintain a websocket connection to the client, persistent two-way communication channel that will stay open and they will route messages between users and track who's online and handle messages persistence. Then we'll introduce a message queue which will be for decoupling message writing from delivery. And this is also one of the things that will allow us to handle the failures. So when one of the chat servers receives the message, it will immediately acknowledge and tell the mobile app that the message was sent and then it asynchronously pushes the message to the message queue for storage and delivery. And as an example, it is can be either a Kafka message Q or rabbit MQ that will work here. Now this message storage will consume that message that is coming from the server from the chat server and persist it to the message storage database as a database solution. NoSQL will work better here something like Cassandra or Dynamo DB because we need high write throughput as you saw in the estimations and our query patterns are mostly straightforward. We will be fetching messages by user by conversation or by time stamp. And since we're writing billions of messages per day, we need something that can handle the massive write volume without slowing down. And that's why like something like Cassandra or Dynamo DB is the best fit here. Next, we'll need also some user connection cache, something like radius, which will be tracking which users are online, which chat server they are connected to, and their last activity time stamp. This will help these chart servers to make the routing decisions faster because checking this ready storage takes something like microsconds versus quering it from the database which will take milliseconds and at scale that difference really starts to show up. In the requirements we also have the requirement to store media files like messages with voice notes or with images. For that we will use some sort of globe storage. This can be like the AWS S3 or Google cloud storage and this will be storing the media files and hold the actual files. But then we will also distribute this to a CDN network which will catch the popular files at age locations worldwide. So that downloads are fast regardless of where the users are because one user might be in US and the second user might be in Europe. And in this case you will have the same copy of the file in Europe region as well. And for the Europe user he will download it from the CDN which is closer to his location. Next we need also a notification service. This is mainly for the offline users which need to receive the notifications either through Apple push notification service or Firebase cloud messaging for Android. And when someone messages you while you are offline like let's say user A messages to user B and then user B is offline at the moment they will receive instead the push notification and then later they can read the message when they open the app. And lastly, we need also a dedicated presence service which will be used for managing online and offline statuses of the users. Let's say user A again is online and user B is offline. This presence server will receive the heartbeats from users updates from the radius and also publish the presence changes to the interested subscribers like the user A which is interested whether the user B is online or offline. Now let's choose the communication method between the mobile app and the services the chat servers. We have three options here. Polling, long polling, and websocket communication. Of course, most messaging apps use websocket for communication. But let's understand why is that the case by looking at their pros and cons. Let's start with the polling option. Let's say you have the client on this side and then chat server on that side. with the regular polling option. This is how it works. We will do periodic polling from the front end to back end asking whether they have any new messages or not with some API endpoint. And then let's say we don't have any new messages. So this will be an empty response saying that there are no new messages in the server. And then from sometime again we will also need to query the same thing and ask whether there are any new messages. And then let's say for the second time there are new messages. So they will receive the messages that they received. This as you can see wastes a lot of bandwidth and adds latency because if you're checking every 2 seconds and the message arrives right after you check, you wait 2 seconds to see it while you could get it instantly. And if you also multiply that by the millions of users constantly asking for updates even when there is nothing new in the chat server, you're burning a lot of resources for no reason. That's why one better approach than this is the long polling option. In this case, we wait for the new messages or timeout. So let's say we request the new messages from the server and then the chat server will wait for some time until the timeout. If there are any new messages, it will push them back to the client. And if there are no new messages, then after some time out, it will push the response, which will be again an empty response saying that there are no new messages in the chat. And if there are no new messages, then this needs to again query to the chat server and then basically this process occurs again. This is still problematic because you're constantly opening and closing connections and each message requires a full HTTP handshake. At scale, this is again an overhead because your chat servers spend more time managing connection life cycles than actually delivering messages. Plus there will be some idle HTTP connections like in this one where they are waiting for the response and they will consume the server resources like memory for connection state and thread pool slots without the efficiency benefits of websockets. That's why most of the chat services work with websocket communication. This is one persistent birectional connection between the chat server and between the mobile app. So client and server can both push data at any time. First client will initiate the HTTP connection with the server and then they will perform the web socket handshake and after this point the connection is established and it is persistent throughout the session. This way let's say user A sends a message to user B which is this user server is able to independently push the new messages to the user B which received the message. So push the new messages as soon as they are available and also the client which is the user B is able to send message or acknowledge any time without making a new connection and this connection is persistent and allows real time birectional updates both from the chat server side and from the client side. The trade-offs of websockets is that it requires special load balancers that support them. So, we'll need to use layer 4 load balancing instead of layer 7. They also use more memory per connection on the server side because you're keeping connections open, but it's still worth it because of the real-time messaging and the latency of the message requirements. Next, let's see how we are going to manage the chat servers which are routed through the load balancer. In production and in reality, chat servers come and go. they will crash, get deployed again, scale up and down. And how does the system keep track of what's available? So the problem here is that the load balancer needs some way to know which servers are available like chat server one and two are available at this moment and chat server 3 is unhealthy. Let's say it either throws timeouts or it's throwing errors on requests. So one solution that we can use here is service discovery cluster like a service registry similar to console or AWS cloud map and here is how it will work. Whenever these chat servers start, let's say this server one boots up on some IP address, it will register itself in this service discovery cluster and after that we will do health checks every 10 seconds with this server one to see if the server is up and running. And then similarly we'll do that for server two and server three. And now let's say the load balancer needs the list of available servers. It will first query the healthy servers from the service discovery cluster and then the cluster will send the server list to the load balancer. With this new list, let's say we forwarded only the IP address of the chat server one and two. So the load balancer will be able to update the routing table and then route all the traffic only to server one and to server two and it will know that server 3 is removed from the routing. So we are not making any requests to the server free. That's how we will manage the load balancing and traffic routing. Now let's move on to the data model and designing the data tables that we will have the primary ones. We will first need a users table and this one can be actually stored in a SQL database like PostgresSQL or MySQL. So the user will have an ID which will be the primary key. Then a phone number which needs to be unique for all users. Then a username created at which is a time stamp and last scene which we will use for controlling and showing to the other users when this user was last online. And of course more fields will be here but these are let's say the primary ones that we will need. The next table will be messages which is the table in Cassandra actually where we will be storing messages since this is a high volume database. We will be writing and reading a lot of messages from. We will need the sender ID here which is the connection with the user ID from the users table. We will need the receiver ID which is again a connection to the same field in the users and a message ID which will be the primary key for this table. Apart from that any other things that relate with the message like the content which is the text media URL if we are storing any voice notes or any image video files. The status of the message which will be either sent, delivered or read. the time stamp of when this message was delivered and whether it's deleted or not because they are also able to delete messages on WhatsApp. For managing the groups on WhatsApp, we will also need groups and group members tables and this can be again in the postgrql database. For the groups, we will need ID for each group, which will be the primary key, which we will use for identifying the group, the name of the group, when it was created, and who's the user that created this group. And then this is a one to many connection with the groups and group members where we will be adding new members. And this is the ID of the group where they are added, the role of each member when they joined and the user ID which links to the users table user ID and that's the primary key of users. Apart from this, one more important thing is to have user connections registry where let's say this can be stored in a cache like radius. This will store the user ids and the server ID where they are connected to also the connection ID. if it is open, last active time and device type and we can add this to the radius cache and then update every let's say 300 seconds to refresh it with each heartbeat. Once we have the database ro schema, we can move on to the API design specifically for webocket and rest API flows for the messaging app. The first step from the client will be to authenticate with the server by sending the user ID, the token and device information. If it is successful, the server will respond with off success, sending the session ID, the server time and the onlet count. After that point, the client can start sending and receiving messages. Let's say it sends a message to the receiver. So it will send the information about who's the receiver, the user ID, the content, the media if there are any and the time stamp of the message. And at this point the websocket connection is already established. So there is a birectional communication which means that server is also able to push messages or acknowledgements to the client. So it will acknowledge for example when the message is received. It will also deliver any new messages that there are from other users like the message ID, the sender, the content and the media files. And then client will be able to also acknowledge when they receive the messages from the server. The receiving is one part. Maybe they received but haven't opened the message. Once they open the message, they will also send the read received with message ID and the time stamp when that was read. Whenever let's say the message was read, the server needs to also update the read received with the message ID and when it was read so that the sender who's on the other side also knows when the message was read. Also the client and server need to maintain the presence heartbeat between each other every let's say five or 10 seconds. So client will check whether the server is still available and similarly the server needs to check whether the client is available because maybe you close the app on your phone or you close the browser tab of WhatsApp and in this case the connection needs to be closed to not waste the bandwidth. For uploading the media files we will handle this through a rest API. For example, for uploading a file, we will use a post endpoint something like / API/ media where we will send the file type, the size of the file and also of course the file and conversation where this file is uploaded to. So this will be sent to rest API which will be responsible for handling the media uploads. And then on the other side we have also the media storage where this will be stored. So server will respond with the upload URL the media ID and when this expires and after that the client will be able to send the upload file to the pre-signed URL for the media storage and once upload is successful the media storage will just respond with the upload success and the client will acknowledge it and as a last step let's say it included the media file to be uploaded the server will just acknowledge that the message was delivered or if there was any errors while uploading the file or sending the message, it will send the message delivery failed response with the error message in the body. Now let's cover the multi-device sync. Let's say the user A can have three devices and user B can have two devices. How are we going to sync messages between devices as well? We have only this side user A's phone, user A's tablet and user A's desktop. These are the three devices for user A. And on the other side we have user B's phone and user B's laptop. So first thing let's say first interaction that happens is user A sends a message to the chat server A. And then chat server will acknowledge and display the status as sent for this first message. Since we agreed that we are going to store the statuses of users availability in a radius cache. So the chat server A will then ask where is the user B because let's say this message was sent to the user B and then radius responds that user B has two devices and it is connected on server B and server E each of these devices the other two devices of first user will be connected let's say one of them is connected to chart server C and the second one is to chart server D. So the once the chat server stores the send message, it will also display through the chat server C which is connected to user A's tablet. It will display the sent message and the chat server D will display the sent message for user A's desktop. This is how we will sync the same message between three devices of first user. Now let's see how these messages are sent to the user B and synced with the two devices that the second user has. So user B's phone is connected to chat server B and user B's laptop is connected to chat server E. The chat server B will read the message from database and deliver it to the phone. And this chat server will deliver the same message to the laptop. And let's say user B checks the message from the phone. So it will deliver the acknowledgement to chat server B which sent the message to the phone after which the chat server B will mark this as delivered for the chat server A which then it will be able to send to the first user's mobile device and update the status to delivered and next action that will happen is user B will read the message. So once it reads the message, we will send the read receipt to the chat server B and then this chat server B will again forward this to the chat server A with the message red status in this case and then the chat server A will update the status and now it will display as red and since this is red now we need to also sync that between the user B's devices. So the chat server B will mark it as red also on user B's laptop which is here. So this way the user A's tablet, mobile phone and desktop and also user B's phone and laptop always stay synced with the latest data whether the message was sent whether it was delivered or it was read. And in case there are conflicts between multiple devices, we will use the last right wins technique with version numbers to prevent the conflicts. And of course this is not the ideal solution. First of all, we need to apply optimization here. For example, this can be incremental sync because sending the entire message history to a new device is expensive. Let's say we are syncing with this chat server B and user's phone and also user's laptop with chat server E. We don't need to send the whole history in the device. So instead the device can request messages for last 30 days like most users only need the recent history and older messages are fetched on demand when needed. When they scroll up the user scrolls up and then the old messages will start to load and very old messages will be archived in cold storage and they will be loaded lazily. Adding this multi-device sync comes also with a trade-off because we need to support roughly double of the delivery workload. Each message goes to multiple devices per users. But that is an important feature of chat messaging app like WhatsApp because users demand the same messages the same chats to be synced between their mobile phones and between their desktop apps. Now let's see the core flows on how messages actually move in case of users both being online and sometimes one of the users might be offline. So let's start with both online version. In this case user A let's say sends the message to user B and it's connected with the chat server A. Message goes through web socket and reaches to this chat server. Now this chat server will generate the message ID which is a unique ID timestamp based for this message and then it will send the acknowledgement with status of the message being sent. Then this server will push the message to the message Q for persistence. After that we need to check where user B is and which server it's connected to so that we can send forward this message to user B and we can check that from the radius cache where we have the user connection cache. We will read where the user B is from this cache and then let's say it responds that user B is on server B and once we have that information the server A can route message to server B via Rady's popsup model and on this side we have the server B and user B connected with web soocket. So server B will now deliver message in real time and user B's device will acknowledge the delivery. Next, we need to let the first server know that the message was forwarded and delivered. So, server B will make a request to the server Acknowledging the forward delivery. And then we can update the status with delivered instead of send. And then whenever user B opens the message, the same process will repeat and we will now mark the message as read instead. And optionally we can also incue the message for analytics or archival by using async processing between server A and between the message Q. And meanwhile the message Q will be writing the message to the message database like Cassandra for storing the history. Now let's see this same flow but in case when the user B is offline which makes it a bit harder to manage but still we need to make sure that the message is received and we also notify the user B when they received even when they were offline. So the initial steps are the same for this situation. User A sends the message to user B through chat server A and then we check whether the user B is online from the radius cache and in case the user B is offline which is our situation here we will add the message to user B's inbox with radius and then we will store this in inbox table in the database inbox and after that this chat server will connect with the notification server. service that we have here and send push notifications for user B. Once the notification service receives the message, it also triggers push notification through Apple's or Google's notification system and then we incue the message for further processing to the message queue and after that message Q will persist this into the database and once the user B comes back online let's say they open the app and establish the websocket connection with chat server C we will forward the message for delivery to the chat server C which has the websocket connection with the user B and after that we will deliver the message in real time to the user B's device in case user B was online during this time we will just forward the message for delivery to the chat server where the user B is connected let's say it's the chat server C but if user B was offline first and now they become available this is the flow that we will have. So let's say user B comes online after being offline for a while. We already sent the push notification and they connect to one of the chat servers like chat server C. First thing we will do from this server is we will retrieve the pending messages from the inbox from radius and if there are any messages that are pending we will retrieve them any missed messages from the database inbox and we will deliver the pending messages to user B and similar to the previous flow we will also notify the user A that the message was read. Next, we need to design this for group messages as well and see how we can handle 50 to 100 person groups in WhatsApp. This is how it will look like at a high level. Let's say we have multiple users in the same group chat. We will first of all need a group service which will be managing these groups. And as an example, let's say user A is part of this group and we have another 50 to 100 persons in this group. First flow is when user A sends a message to the group. Let's say for each group we have ID as we described in the data model. This gets sent to the chart server where they are connected. And then the chat server first request it will make to the group service is to check who's in this group which is ID of 789 as an example and it will retrieve the list of users like user ids who are in this group part of this group and after that we can check the status of these 50 users we can use radius to cach the status of users to not waste the database queries for reading this and let's Say we check the status of the 50 users and find out that 30 of them are online and the rest the 20 other users are offline. For each online user, we need to look up which chat server they are on. So we will publish to channel user one, user two to user 30 to the radius popsup model and then this will parallelly deliver to online users. Whoever is online, they will receive their messages in their own chat server where they are connected. Let's say 15 of the users are connected to server B and the other 15 are connected to server C. We will in parallel distribute the message to both servers and then these servers will distribute them via websockets. So it will deliver via websocket to the 15 users from server B and to the other 15 users from server C. For each of these deliveries, they will acknowledge from user's device that the message was delivered. And same for the other 15 who are connected to the other server. And then we will forward these acknowledgements to the server A which originally sent the message so that it can update the status of the message to send or delivered in the user A's device. For the other 20 users who are offline, we need to use the same thing generally that we did for the offline users in oneonone connection. We'll need a radius inbox where we will write the messages that they need to receive which is let's say these other 20 users who were offline. We will write it to user inboxes and we will write to 20 inbox records in the database inbox. And for all of them we need to send the push notification either through Apple's or Google's push notification system to notify them through notification service that the message was sent in the group and then the notification service will push this through either Apple's or Google's notification system. So this way we are just storing message once. So we are doing one write operation from chat server A which received the message from the user to the message Q and then message Q writes this to the database. So eventually it is just one write operation per message and lastly we will also update the status that it was delivered to 30 and it is pending to 20 users for the user a device. Now, so far we haven't covered how we are going to manage the media files, the image assets, the videos. So, let's cover that next. Of course, the wrong way here will be to clients upload the image to the chat server and then chat server store it in database and then recipient downloads from chat server. This is of course not scalable. Your chat servers are now bandwidth limited by file transfers and they are not doing what they are good at which is routing messages quickly and instead they are doing what they are terrible at which is moving large blobs around. So the right approach is using pre-signed URLs and for all these technical terms like pre-signed URLs notification services Apple's notification service these kind of things will be included in the footnotes. If you're not sure what they are, feel free to check it there. So, let's look at the correct approach for uploading the media files. Let's start with the upload flow. Let's say we have user A who's the sender of the file on this site and we have user A's device which is the app and we have user B who will be receiving the file. So user A taps to send a message and this is where the application will request pre-signed upload URL from the chat server which they are connected to. Let's say this is the server A. At this step the chat server will generate pre-signed upload URL to upload this into some blob storage like AWS S3. The blob storage will return the upload URL to the chat server and then we will forward this URL to user ace application. Once they have this upload URL, the user ace application can do a direct upload. It can upload the media using the pre-signed URL we just got from the server and this will upload it directly to the S3 bucket and then S3 will confirm that upload and return the media URL for accessing it later. And in the next step, we can already send that chat message including the media file URL to the chat server A which we are connected to. And this is where we need to notify the user B that they received the message and also include this media file URL. So we will deliver the message with media URL to user B's application. So first user B will receive the notification. Next they will connect to the chat server. Let's say this is the chat server B that they are connected to with via web soockets. So they will receive the messages along with the media file URL. And now then they tap to view the media. This is where we will request the pre-signed download URL. And in this case we will generate the pre-signed download URL from the chat server B with the blob storage that we have on S3. And then S3 will return the download URL to this chat server B which we can now forward to the user P application for it to open and review the media file. And then here we have two approaches for downloading and reading the media file. The more scalable approach is via CDN. So we can replicate the media files across CDNs to be closer to each user. And in this case we will download the media file through CDN. So this is the user application and this is the CDN and they will read it from there and stream the media back to the user application and the simplest approach is to just read it directly from the storage. So the user will request to download the media file directly from the S3 bucket. It will stream back the media. And of course here we have a trade-off between directly reading from the storage or from CDN. We have slightly more complex flow with CDNs because we need to do extra requests to get the pre-signed URL. But the performance and cost benefits here are massive for this scale. So CDN is most likely the best solution and option to go with because without this a single chat server handling 100,000 let's say connections can get crushed by just 100 users simultaneously uploading some small megabyte files into the blob storage. So with this setup, the chat servers like the server A only handles the small text and messages and whenever we need to upload some heavy files like videos, images or voice notes, we will offload that heavy bandwidth to some blob storage or to some CDN and we will use pre-signed URLs which will expire after the TTL that we will set and then the media TTL will align also with the retention policy that we have in the chat servers. So with this setup, chat servers only route small messages with URLs and blob storage and CDNs will handle the heavy bandwidth and our chat servers stay fast and focused on their job which is delivering the messages and media files get the TTL which stands for time to live with automatic expiration of let's say 30 days which will match the message retention and then they will get automatically deleted. Now let's get into how we would scale this because our chat services now can handle thousands of users concurrently. But if you want to scale it even further, we need to think of a way to decouple our servers. And the most common approach here is using some sort of message Q like a Kafka. For our setup, we can have message Q on this side and then we can assign partitions to each instance. Over here we have the message storage service instances. Let's say we scale it up to two, three or even more instances. Since this is like a stateless instance, we can scale it to as much as many as needed. So we can assign partitions to each of these instances. And as an example like this can be from 0 to 9, this can be 10 to 19. This can be 20 to 29 partitions that we will assign to each of these instances here. And the way it will work is we will have messages that arrive to this message queue and then each instance will consume these messages from Kafka. We can even do batch rights for efficiency and then we will handle retries and failures inside of an instance. So first we have the Kafka consumer in each of these instances. We will try to use batch writing to write this to the database cluster. If for any reason this fails, we will have a retry logic inside of each of these clusters and then we will retry to budge right after some time out and write this to the database cluster. And if for any reason this still fails, we make sure that we don't delete the message from the message Q so that we don't end up losing the messages or we can use something which is called dead letter Q. These are cues that you can use separate from the main queue. So whenever some of the messages here fails to be processed from the instance, it will instead be stored in this dead letter Q and then we can have some other mechanisms here for processing it later inside of the same instance and writing it to the database. And then once a database write is successful, we need to also update the indexes. So inside of each instance we will have the index updater which will update the indexing in the database for searching later for querying them effectively. But let's look at a more detailed flow of how the processing will work from message cues. So again on this left side we have the message queue which receives all the messages that arrive from user devices and then we can set some batch size like 500 messages for each batch and we will consume this batch inside of one instance. We will have the Kafka consumer. This consumer can poll for new messages every let's say 100 milliseconds so that it has time to process the previous ones and then it will process the batch in parallel. Let's say we can have thread pool of 20 workers and we can process this in parallel. For each of these single message, we will validate the schema first of all and then do meta data checks to check the size of the file. If they uploaded some media file that exceeds the limit, we will need to respond with an error message. And otherwise, if it passes the criterias, we will validate and enrich the schema. Here is where we can calculate the message hash for dduplication. Extract the mentions like if they mentioned some username right in the WhatsApp you can mention someone with at symbol for example. So if they mention that we need to extract those mentions from the message content. We need to identify the links for preview generation because on WhatsApp you also generate the links if they attached anything to the message. And this is also where we need to generate the indexes for querying them later. And next, instead of writing all these messages one by one, we can cue the validated messages into a batch writer, we can accumulate some batch of messages in the memory like 1,000 messages every 5 seconds. And whenever this buffer hits the 1,000 limit, we will start sorting messages by the target chart. We will group messages by conversation and then we will prepare the botch for inserting into the database. And the reason we are doing this is to save write operations in the database. Of course, if we can budge thousand messages and then try to budge write them into the database. So we will still prepare insert statements for each of those that cannot be inserted in one go. But at least that saves the database sometimes to process the current batch and then after the 5 seconds we will send it the next batch. So the first happy path is that it will be inserted with atomic batch insert into the database cluster. We will also update the search and analytics and indexes via index updater and then we will mark them as processed if everything goes well. But in case this fails, we will have a failure and then we will have the retry and dead letter Q handler. So if these messages or one of these messages, let's say out of this thousand, we have 10 of them which failed to be processed, we will write them into the dead letter Q. And before writing it to dead letter Q, you can have some simple logic that tries to retry. Maybe that was a rate limit we hit or something else simple that we can just retry. If after that point it fails, we write it to the dead letter Q. And then here we will have more mechanisms like step one, step two, two, step something like 10, which are the ways we will try retry to insert this into the database before sending the user a notification that this message was not delivered because of some reason. So the main benefit that we get with dispatching is a single database insert takes 5 milliseconds. If we are inserting something here, it usually takes 5 milliseconds. And all of these thousand individual inserts will be equal to 5 seconds instead. 1 millisecond. 1 second is 1,000 milliseconds. But if we do one batched insert of thousand messages, it will instead end up costing us 50 milliseconds which is actually 100x throughput improvement and it will start to show up when we are starting to write millions of messages concurrently to the database. And if you'd like to see this at the code level as an example, this is how we will write to Cassandra database with the message chart. So this will be a transaction. We will begin the batch. We will insert the messages into the messages table and we will pass the values like the timestamp. the ID of the sender and receiver which are the user ids then the content then anything else like media files and then similarly we will do all the other inserts of other messages into one batch and then this will be like one batch of writing 1,000 messages into the messages table in Cassandra database the next important feature of WhatsApp that you might be asked to design is to add end to end encryption and key management. Typically on WhatsApp, messages are encrypted on the sender's device and only decrypted on the recipient's device. Even WhatsApp servers can't read the content. So if you have user A and user B and you're sending some messages between each other, even the user that is in the middle or the user that is connected with the chat server A and then the user B which is connected to chat server B. Even this A and B servers cannot read the message. So how we are going to design something like that in messaging app? The thing we can use here is a signal protocol. So we can add end to end encryption. And this is how it will work in detail. Whenever user A sends a message to user B, we upload the public identity token into the key server. So each user generates an identity key pair on registration on WhatsApp and then they have the public key uploaded to the server and private key which never leaves the device. So you have private key which is on user A's device and you will have the public key which server has access to and then on the other side we have the user B. So user B will again upload the public identity key to the key server plus they will upload prekeys. These are generated let's say 100 signed prekeys and it uploads them to the server. Server stores them for future initiators. And whenever user A sends a message, we will also fetch the user B's keys which includes their identity key plus one of these prekeys that they stored. Let's say they stored something like 100 keys here. We will fetch one of these for user A. And then once the key server returns the user B's keys to user A, this is where the session will be established. So user A fetches the user B's public identity key and prekey from the server. It generates the key pair which will match for both user A and B and then it will perform the key exchange. After that both user A and B have access to the same keys. So whenever they are sending messages between each other, all of the messages will be encrypted with user A's public key and then once user B receives the message will decrypt the message by using the user A's public key. So each of these messages will be encrypted with some of the algorithms and then we will have the message the session key which is exchanged between these users and even further we can use something like message counter which will increase after each back and forth messages between these user A and B. So this way it's very hard even if the session key is stolen. It will be still very hard to decrypt each of the messages because all for each message we are also adding the counter for encrypting it and then we are decryting it on the other users end. So this is how we will ensure the privacy between user A and B and even the chat servers we won't be able to read the messages in between. Now let's move on to security practices. But in terms of the overall system, let's start from rate limiting and abuse prevention. Without having any rate limits, any hacker can start scraping data or perform DDOS to the system. So we need to build a protection in any kind of system like this. First of all, we have the user actions here which can be messages sent from the user or files uploaded to the media storage. For every action type, we need to have some protection. And these are the per user limits. For example, that we can send whenever users are sending messages or they are creating groups or they are uploading media. So whenever they are sending a message, we can have a limit for sending messages. We need to check first the message token bucket. And here we can have the rules set. For example, these can be 100 messages per minute that a user can sense. And anything above that doesn't seem reasonable. So we can apply rate limit for that situation. And if the rate limit is exceeded, we will block the action. So we will notify the user and show an error message that they exceeded the limit. And this can be something like too many requests in one minute try again from 60 seconds for example. Or we can require some capture to be solved. So these boxes that they need to check and let's say pick which ones are motorcycles to verify that they are human. And then we can also have progressive penalty. So the first one is the warning that we show which is too many requests. If they start abusing the service again like we will have restriction for one hours. This is how you see for example in passwords where you're trying to type your password for an account and you fail multiple times. They will first restrict you from for some time, right? The second time they might restrict you even more like 24 hours. And then the next step after that is we will suspect the account and we will deactivate it for some time for a human to manually review their account and verify whether this was a bot or this was a hackers trying to steal information or it might be still the user who just didn't understand the requirements and they kept abusing the service. So we need to contact them, let them know the issue and then they will reopen the account. Of course with the same rate limits applied and they might even get a tag on their user that this account is suspicious. So if anything happens one more time their account might be permanently deleted from WhatsApp. And now this same process repeats for all the other actions as well. Let's say users can create group as well. Right? So once they create a group, we will check the group crate counter. And this is where we need to set reasonable limits. For example, if they are creating five or more groups per hour, that might seem too much. So we can apply a rate limit for that situation. Or if they upload media and they upload more than let's say 10 or 100 media files per minute, this can be another restriction. Of course, these numbers are just examples and these are some things that will be clarified in the blueprinting phase. But for each of these actions, you need to have some rate limit. And then the process is usually identical for enforcement. So we need to check if the rate limit is exceeded. We will block it. Otherwise, it will work as expected. So we will allow and then we will continue with writing to the database whether it's a media file or it's a message or it's creating a group. This is an example code block like how this can be implemented the rate limiter. So we will initialize with the capacity and with the refill rate which can be different for each action and then we can have something like tokens which show the capacity. So for every request we will check whether they exceeded the tokens or they are still under the limit. So it can be some unit like one and if this is more than one which means they haven't received the rate limit we will return true and this will not really process with other actions like storing the message to the database and then we will deduct one from each step. So if they try this next time this will be zero of for the tokens. So that will end up not getting into this if blocks and then instead they will get into the else block and we will return false because they exceeded the rate limits and this will block them. This is the step here. So we will block them for processing the message or creating the group and we will notify them and alternatively we can also ask them to solve a capture problem and then for that we will set a limit right for let's say 1 hour. So this is where we will have to refill. We will check the time that we have now. We will set the last refill time and then based on this we will calculate when is the next time they can repeat the same action which can be from 1 hour from now. The next action type that we need to limit is connections because we can have connection flow the tax and let's say we get five connection attempts in the single minute for different IP addresses. This is where we need to check their IP connection counter and again apply the rate limit for this specific situation. So if within a minute they have tried to login from five different IP addresses that is something that looks suspicious. So this is again where we need to apply the rate limiting and it will follow the same process as with the other actions. Another example is we can have group invite limits. So if they invite users, they create a group and then the user starts inviting other users to the group. We will check the group invite counter in this case. And similar to all the other actions, it will follow the same process here. Just the counter and the limit is different for each of the actions. So for this to work, we need to also have some centralized store for all servers because each of the users is connected to one server, right? with a websocket connection. So all of them need to use some distributed central store something like radius because this is something we keep up to let's say 24 hours in the memory and that is in the case they get restricted with the most attempts or we can still use database for keeping this data but if it's something we are going to keep up to 24 hours then radius might be a good option here and then our atomic operations will ensure that we have the right accuracy And there is no way to bypass the server hopping by connecting to other server and wishing that it will work and the rate limit isn't applied or that server. Next, we need to scale the database as well. Of course, with this scale now we scaled the servers and applied rate limiting. But the database a single database cannot handle all this traffic. So we need to split data across multiple database instances which is also called sharding. When it comes to sharding, one of the important decisions is on which field we are going to shard by for the databases. So one approach can be to shard it based on user ID. So this way we will make sure that all the rights all the messages for user A end up in one shard in one database. So that whenever we are fetching messages, we are fetching only from one database and we don't have to connect to multiple shards to read the messages for the same user. The catch here is that individual users can become hotspots because some users naturally use it way more often than others. And this way these users will start creating big databases, big tables of messages where the users who use it less often will create smaller charts. So this will create an imbalanced sharding in this case. Another option is to shard it by message ids. This will create even distribution of data. But fetching all messages for a user will require quering multiple shards because there will be in different shards. So your query will hit every chart, aggregate the results and then return them to the server. So the downside in this case is it will become slow and expensive compared to the user ID. And the downside with the user ID is that we will have imbalanced shards. So one shard might be very small compared to the other shard which uses users who are using it very often. So the main goal also for these interviews is not to just choose one of these but just tell them the different approaches that there are and what is the best solution. after all or what is the solution you will go with? So one of the good approaches here it can be sharding still by user ID but applying replication. So let's say user A is sharded to this shard A database. All of the user A's conversations will be stored in the shard A. But if he talks to user B who's connected to shard B, we will also duplicate the rights. So we will write the copy of each user's shard into the shard B as well. And of course this comes with a downside. This doubles the right volume. But on the other hand, it makes the reads blazingly fast. Each user will query only their own shard. User B will query it from shard B and user A will query it from shard A. And we can even replicate each of these shards. So shard B will be replicated into B replica and shard A will be replicated into A replica and user A will now read this history of messages only from the replica A and user B will read it from replica B. they won't affect the shards the primary ones which are responsible for write operations and instead they will be reading here. So it will increase the read performance a lot. The trade-off here is that we are writing each message twice. So that's why it's also important to clarify such things in the beginning in the requirements phase. You need to see this in advance like later we are going to optimize the messages either for write operations or for read operations and which is the more important and if you still failed to clarify that at the beginning this is still a good place to ask instead of trying to guess. So ask them whether reading is the more important in this situation or writing and then based on that you will either replicate or you will go with message ID sharding or you can even go with timestamp ID sharding. There are different approaches here and the main thing here is to understand the tradeoffs between each approach. So typically when it comes to keeping the data available we usually shard for right scalability. So if you're applying sharding which means splitting the database into smaller chunks this is improving the right performance and when we apply replication this is for improving the read scalability. So if you just replicate one of the shards this is usually for the read performance. So once you decide on the sharding strategy and let's say you decided to go with user ID sharding all the client rights will arrive to the shard to the primary node for each of the shards. Let's say user ids from 1 to 1,000 are stored into the shard one. This will be the primary node and then we can basically replicate this and use asynchronous syncing between the primary node and replicas. We can have three replicas and they will be eventually consistent with the primary node by reading and duplicating the data into the replicas. And then whenever we have read operations like opening the messages and seeing the unread messages, this is where we will need to perform read queries. And for read queries, we will only use the replicas. We won't be connecting to the primary node at all. We will connect here only when we have write messages which is someone writing to the user A or user A writing to someone else. Now next for such big systems where we are storing too much media files, too many messages, you need some cleanup way, some way to clean up the garbage data that is never read again. So this can be expired media, old undelivered messages or stale connections. We need to build a cleanup infrastructure to run some chrome job over time and try to clean the expired medias and stuff. So you need to have some job scheduler something like airflow or some chrome job that will be scheduled every either daily basis or weekly basis and this needs to check the job status from monitoring. Then on this side we have a worker pool which is containerized job executors. So this job queue that you see here this is separate from message Q. This is for incuing scheduled tasks and pulling the jobs to the worker pool for processing. Then we will acknowledge that we received the message or if we failed then we will recue it on failure and then we will start executing the tasks in this worker pool which can be cleaning up expired media by checking the date that they were published or the undelivered messages. These can be messages that we failed to write to the database but we still have them somewhere in the memory. So we can remove this or stale connections that we have and any other kind of data that we don't need anymore. This is what the responsibility of this worker pool will be to delete all the data and then submit the job results to the monitoring so that we can see over time how well it is doing and if there are any improvements here as well because this is not something that you will set up once and then forget. This is also something that will be improved over time. So the trade-off here is that these scheduled jobs will add operational complexity because it is another big thing to monitor and debug. But the alternative to this is or the other tradeoff is to let the garbage accumulate until the system becomes so big that you have to do this either way. So regular cleanup like this is essential. And for long-term health, this is important to set up some worker pool that will start cleaning up the junk data that is not used. That's the general flaw of designing such large system like WhatsApp. But you won't usually end there because that is just the happy path of all services work together, everyone is happy, customers are happy, right? But that's never the case in reality. So there can be failures in every part of the system. And one of the major things that you can discuss for example for a messaging app is cache failure. For example, what happens to undelivered messages because radius holds our undelivered messages inboxes and what if the radius cluster goes down itself. Let's start with the problem example. Let's say user A sends a message and they are connected to chat server. So this is where they established the veet communication both ways. Now chat server will first write the undelivered messages to the radius inbox which is the fastest compared to the database and then the second step is we don't only write to the ready store right we also write the backup which is more durable to the database so this is what our permanent storage is in case the radius cluster goes down. So we will first try to fetch the pending messages from radius for the chat server to forward it. But in case this is unavailable let's say radius goes down. We need to fall back to the database instead. So in this case if radius is empty or it's unavailable then we need to fall back and read from the database. So we need to retrieve the undelivered message from the database and then forward it to the server. And either way, we need to at the end be able to deliver the message to user B even if they are offline and then they come back online and fetch the messages from the database in case the radius cache was down. And whenever the radius recovers and comes back online, we will first have a background job that will sync the inbox from database to radius. And then we will query something like select all from messages inbox where the delivered is null which is not set in inbox. And then for each undelivered message we will write to radius inbox and then we will keep the database record as well. So after that we will resume to the normal delivery which is writing to radius and then reading from radius again. So to visualize this at a code level this is for example what the write with fallback will look like. So we will have some function like store undelivered message which will be the user ID of the message and then the message itself. So we will first try reading radius from the cache. So we will try to get the message with this user ID with the message timestamp from radius cache. But in case this goes wrong, we need to log that the radius is unavailable and we will need to start using database again. So we will need to query this from database. This can be either insert operation if the message was written to the other user or this can be a read operation like select operation if the user is trying to fetch the messages and the radius is unavailable. So that is the second scenario. For example, we are trying to get undelivered messages. So we are trying to read it from radius. This is again trying to read from radius because that is the fastest if it's available first. If we see that messages length is greater than zero then we will map parse and then send it to the user. But in case something goes wrong in this try block that means that the radius is unavailable. So again we will log for us for tracing it later in cloudatch or somewhere else and after that we'll need to again read this from the database. So we'll do the select query instead to the database which is again slower but it's better than just failing the whole operation and then once the radius is back online it will sync with the database have all the messages here. So next time we will be reading from this try block. The next topic that you can expect to discuss is about regions and global distribution because WhatsApp is not only in let's say US or it's not only in Europe. It has users from all parts of the world. So you need to have different regions for each of these users. For this we can use the regions. For example, if you are using US, we can use the US East one, US West 2 or these kind of regions that are available to us in the AWS or the cloud provider that we are using. And each region should have the full stack chart servers, radius clusters, database charts so that it can operate independently. Let's say for example, user A opens their app and they are located in Tokyo. So we will first do the DNS lookup to see where they should connect first and then they will connect to the nearest IP address which will be the Singapore region and in this region we will have the full stack which will be the load balancer that they will first connect to. They will make a request here to route them to the chat server. Here we will have more chat servers replicated and for each region this can be different. For example, for US maybe we are or let's say for Ireland we have a lot of users using the app. So here it can be many chart servers and let's say in Tokyo or in Singapore we have less users. So we will have less chart server replicas. Either way they will connect with a chat server with websocket connection and they will have the radius cache for each user. This is for example where we will look up where user B is to be able to send the message to their region and that is how the user B let's say in London will connect. So first again they will do DNS lookup. They will connect to the nearest IP address. They will make a request to the load balancer and they will receive the message that was sent from user A to B through the US West region which is the one that they are connected to. Now another thing that can go wrong here is the Kafka cluster that we have. Kafka itself is very scalable. It can handle thousands of messages per second without any issues. But still we need to plan what happens in case this goes down or something happens with the Kafka cluster. Inside of the cluster we typically have multiple brokers which are called servers in the cluster. So topics are split into partitions and each partition has leader and it has replicas. So producers usually write to the leader similar like write database actions and replicas just provide us with redundancy. So scenario one is that one of the brokers fail inside of the Kafka cluster. So if a broker dies, we need to replace the broker. Let's say this was broker number something like three. we need to replace it and rejoin another broker to the cluster. So, Kafka will rebalance the partitions over time if we add this broker to the cluster. But what if the entire Kafka cluster goes down? This is very rare, but it's still possible. So, this means that chat servers can't publish any messages. All of the chat servers were sending messages to this cluster, right? And now they are no longer able to do that. So message storage service can't consume the messages which means that messages are not persisted and this can be data loss. In such cases we need to have a mitigation of a local buffer. So let's say we create a message buffer before trying to publish them to Kafka in the code. And this is what the publish message would look like as a function. So first we have a try block and this is the happy puff which is we are trying to publish the message to this Kafka cluster but in case it fails it means that this cluster is down. So we need to have some roll back mechanism for that. So we have the catch block for that situation with Kafka error. So first we will persist this error to our logging system to then identify why this message buffer failed. But then we will try to push the message to the local message buffer instead. This is just one approach that we can use. But then this buffer is limited. So we can store up to let's say 100,000 messages in the buffer. In case that also fails, we don't fail the whole operation. So we'll just start dropping the oldest ones from the buffer instead. And then we will need to add some interval for waiting for the Kafka to recover. And once it recovers, we will try to publish it again to the Kafka. And this time needs to be the exact time that takes us to spin up a new Kafka. So if this is entirely like destroyed or deleted, we need to spin up a new Kafka. We need to know exactly how much this takes. Let's say this takes five, like not five even. It needs to take less than five. So let's say 2 seconds to load which is 2,000 milliseconds. So we know that this number is greater than the time we need to spin up a new Kafka. So we are just waiting for that time in the memory in the chat server and then once this is back online with the new Kafka we are publishing this to that one instead of losing the whole memory of messages. So this prevents the memory loss in case the Kafka goes down. But what if this server even like both fail at the same time like we are keeping this in the memory but then Kafka failed and now this server also fails. This is where we can also store these files into the server's local disk. So append only the log file and whenever the chat server also recovers we will replay it from the disk this log file and then we will delete the log after we successfully publish this to the Kafka cluster. So in summary, this is how it will look like. If Kafka is completely destroyed, we are returning the messages from the message storage service and then we request messages since last id. We will query the processed messages from database and then we will check which are the messages that are still not published to the database that we still have in the memory. So this is where we will republish the missing messages into the Kafka once it is back online and once we write this all to the persistent storage like a database this is where we will resume back to the normal consumption. So you need to have such mechanisms to ensure that you are avoiding data loss or at least minimizing the data loss that can happen in case many parts of the system fail concurrently. And lastly, you can expect to also talk about some feature control in such big systems because you don't usually release new features for all users. You usually do gradual rollout. In such cases, we usually use feature flux store. All of the cloud providers come with such solutions to provide you with feature flux. For example, on Azure, you have such system. for AWS you have it and you even have external tools that you can use like launch darkly is one of the popular ones that we used for example in the previous company. So these are just tools that allow you to set key value pairs. It can be like is new theme enabled and it will be true or false and the true or false is also controlled based on the environment. So it can be for example true for staging but it can still be false for production. So this way, let's say developers are testing on staging, they see the new feature, but users on production don't see it. And when you decide to also roll out it for production, you can still set it to true for only 20% of users. So only 20% of users will start this beta feature testing it. And then if it goes well, you don't notice any failure logs. That is when you can enable it for everyone. So in this case the chat server will check the supported features from this feature flag store which can be large dartly it can be the cloud provider's native solution and then we will use gradual enablement per user or per group for example per user will be that 20% users have access to this and then we will return the enabled features from this feature flag store to the chat server and by having this for example the chat server can send messages with new features to the client apps that are above this version and strip the unsupported fields or features for the client apps that are below 2.2 or some version that you set. So that is for example discussing the complete architecture. Now we can also talk about how usually to approach this like a framework. So as you can see we are starting simple. You usually start with the clarifying requirements and then calculating the estimates because this is important for you to estimate what are the storage solutions you are going to use, what are the services are you going to introduce a Kafka broker or maybe it's not needed. So you don't jump straight into radius popsup or database sharding. You usually begin with the high level simple architecture with just thousands of users. Then you build it naturally by talking with the interviewer and then you identify bottlenecks like you could see here where for example the Kafka message can break. We can have rate limiting issues. We can have database that goes down. So you need to start talking about this as well. So it's not something that you can usually memorize. you need to just uh know the steps that are there and then for each system you need to go with the flow and then discuss the trade-offs. So understanding trade-offs is another important piece of this. Every decision has costs. For example, if you are using websockets, it's not all green because for example, they use more memory than HTTP but they provide real-time delivery and for example for messaging app that tradeoff is worth it. For some other situation, the trade-off might not be worth it. Maybe you will go with something like just a polling option instead of just a web socket. So you'll need to also be ready to discuss the back of the envelope calculations before getting to this. So be ready to calculate these numbers. Maybe do some examples and then check whether you calculated it properly or not. And then once you have something stable, then only you will start thinking about failures. You don't need to rush into that because you still didn't decide on the main components and haven't designed the full system. You don't want to get into the failures before that. And don't worry if this was too much in detail. You are not expected to design a perfect system in one or two hours. That's not the goal in the interviews. They will usually want to see how you think, how you discuss tradeoffs, or are you just picking something because you like it. So you need to show your thinking process. Say this out loud whenever you are designing something and that will also help you to get direction if you're going to the wrong prof. The interviewer will correct you and they will give you hints on choosing the right solution for the trade-off that you're thinking about. By the way, if you'd like to learn all these concepts that I'm using for designing these systems from scratch, I have a whole system design guide covering all these concepts that you need for system design interviews on my YouTube channel. You can check out the first link in the description to find the link to my channel. And the first pinned video in my channel is that system design guide explaining all these concepts you need for passing these system design interviews.

Generated algorithmically for Search Engine Indexing.

Summarize Another Video