For Customers:
For Restaurants:
For DoorDash Drivers:
Let's imagine we're serving customers across 10 million area codes, and we expect this number to increase.
On average, there could be 100 restaurants per area code, and each restaurant might offer about 15 different dishes. This results in a total of 15 billion dish records.
With 20 million customers in this system:
If each customer places two orders daily, that totals 40 million orders per day.
Order peaks usually fluctuate throughout the week, with weekends typically busier than weekdays. The busiest times are likely around lunch and dinner in each region.
From a system perspective, searching for menus and restaurants will primarily involve reading data, while placing orders will require more data writing. After an order is delivered and eaten, the likelihood of customers revisiting their past orders is minimal.
Ordering Food:
Managing Profiles:
The selection of a database often hinges on various factors such as the volume of data to be handled, scalability, ease of partitioning, and replication capabilities. Application managers sometimes use a combination of different databases to meet specific requirements. For needs involving ACID principles (Atomicity, Consistency, Isolation, Durability), a relational database is generally preferred over a NoSQL alternative.
Both NoSQL and relational databases offer unique benefits and limitations, and the choice between them should be carefully considered based on the required functionalities.
Given the large volumes of data anticipated, including restaurant details, menu descriptions, user and delivery personnel information, a NoSQL or columnar database like Cassandra might be appropriate. This is especially relevant if the data structure varies significantly across restaurants, which might make it challenging to conform to a fixed relational schema.
Images related to restaurants and menu items are ideally stored in an object storage service like Amazon S3, due to its efficiency in handling large amounts of unstructured data.
Since ordering is a transactional process, it’s best managed using a transaction-capable relational database such as Oracle, MySQL, or Postgres.
The application will be accessible on various devices including mobiles, tablets, and web browsers. Depending on the user's role—whether they are a customer, restaurant staff, a delivery driver (doordasher), or an admin—the interface they interact with will vary. Each version of the interface is tailored to its specific user, connecting them to the appropriate services. For instance, the search functionality is managed by the Restaurant Search Service, while orders are processed by the Ordering Service.
A key feature of our system is the robust search functionality which allows customers to explore menu items, cuisines, and restaurants. This entry point is vital for users who don't have a predetermined choice and rely on our personalized recommendations, which are informed by their past searches and orders. To handle the heavy reading load of this search process, we might integrate established technologies like Elasticsearch or Apache Solr, which excel in fast data retrieval and are based on Apache Lucene.
For updating search data, we'll employ a queue to manage asynchronous updates. When a restaurant's profile or menu is updated in our database, these changes are queued and then processed by a data indexer. This indexer formats the new data correctly and updates the search cluster to reflect changes. The Restaurant Search Service then uses this data to respond to user queries, potentially incorporating geospatial searches to show users nearby dining options.
This service manages the complete ordering process from menu selection and cart management to payment processing, which is handled through an external payment gateway. It also records all transaction details in an Orders database due to the transactional nature of ordering. Additionally, customers can view their complete order history and cancel orders if necessary.
This service ensures the smooth operation from when the restaurant accepts an order to when the order is ready for pickup. It communicates any changes or delays to the customer and notifies the doordasher when the order is ready. Both customers and doordashers can check the status of the orders through this service.
This service manages the creation and updates of profiles for all system users including customers, restaurant staff, doordashers, and admins. Each user role has specific preferences and settings, which are managed here, allowing for personalized interactions with the system according to their role and needs.
This service caters specifically to delivery drivers. It allows doordashers to view available pickup orders, accept them, and review past orders. It also facilitates communication between the doordasher, the customer, and the restaurant in case of issues during the pickup or delivery process.
Responsible for all restaurant-related data, this service allows restaurants to onboard, update their profiles, manage menus, and upload images. It also handles the financial transactions and updates for each restaurant, ensuring they are paid for the orders processed.
This component integrates with well-known payment providers like PayPal, Amazon Payments, ApplePay, and major credit card companies to process payments securely and efficiently at the moment an order is confirmed.
This critical service manages communications across the system, sending tailored notifications to customers, restaurants, and doordashers based on their preferences. Notifications might be delivered via push notifications, emails, texts, or in-app alerts, depending on the user's settings.
This holistic component design ensures that all parts of the service ecosystem work together seamlessly to provide a robust and user-friendly experience.
Let's clarify some terms first:
Here’s a breakdown of the order processing workflow after a customer places an order using either a mobile or web client through the OS:
The system architecture is designed around a microservices model, utilizing the publisher-subscriber pattern with a queuing technology such as Kafka, RabbitMQ, ActiveMQ, Amazon SNS, or Amazon MQ. In this setup, each microservice communicates by publishing messages to and subscribing from queues, channels, or topics. This decouples the services from one another, meaning that microservice A does not need to know about microservice B’s endpoint when it sends out a message. Thus, publishers are unaware of the subscribers, and vice versa. The Pub/Sub system acts as a middleman, facilitating communication between all involved services.
Furthermore, each microservice should operate with its own database, adhering to the database-per-service principle, which prevents services from accessing each other's databases directly. This avoids the traditional monolithic architecture where a single large schema contains all tables. Instead, microservices architecture encourages functional partitioning, assigning specific tables or groups of tables to particular microservices. This method requires thoughtful consideration, especially when integrating both relational and non-relational databases.
As data volumes grow, storing all information in a single database instance becomes impractical. Restaurant data, for instance, can be partitioned based on different criteria such as area code, restaurant ID, or menu items. Each partitioning approach offers unique advantages and potential drawbacks, demanding careful planning to optimize performance and manage any possible negative impacts.
To enhance search response times, frequently ordered or searched items, as well as images of restaurants and dishes, can be stored in a distributed cache. This allows the Restaurant Search Service to quickly access and suggest popular items without continuously querying the main search infrastructure. Technologies like Redis, Hazelcast, and Memcached are commonly used for caching, employing strategies like Least Recently Used (LRU) or Least Frequently Used (LFU) for managing cache eviction.
While a Content Delivery Network (CDN) could be used to distribute content based on geographic location, it may be excessive for our current needs.
To secure data transmission, all communication between web and mobile clients is encrypted using HTTPS/SSL-TLS. For authorization and token management, OAuth 2.0 is implemented. If using Kafka, additional security can be configured with SASL and SSL per topic.
Multiple instances of each service are maintained to handle varying loads, with load balancers placed in front of services to distribute requests efficiently using methods such as the Least Connection or Round Robin. This setup prevents any single instance from being overwhelmed and helps improve overall response times. In environments like Kafka, the queue itself manages load balancing by distributing partitions among consumers within a consumer group, ensuring each consumer handles a fair share of the load.
Ensuring system reliability involves identifying potential points of failure and implementing backup solutions for each critical component. It’s crucial that each service in our architecture is designed to be horizontally scalable. This includes having multiple nodes for our NoSQL databases and search systems, as well as partitioning and replication capabilities within our queuing systems.
Every component should be capable of being scaled independently to manage specific demands. For example, autoscaling should be activated to automatically increase the number of instances during peak load times, ensuring smooth operation under varying loads.
Moreover, should any node or queue partition fail, alternative instances need to be ready to immediately take over their responsibilities. The system should also support self-healing processes where failed nodes can perform cleanup operations and restart without manual intervention, maintaining continuous service availability.