Skip to content

One of our goals at DoorDash is to surface to consumers a wide range of stores that are quickly deliverable to their given address. This process involves calculating accurate road distances for each store-consumer pair in our real-time search pipeline. Our earlier blog post about recommendations for search primarily focuses on the ranking component of search at DoorDash. This blog post describes how we architected our search system using open source technologies to help determine consumer selection.

Problem and Motivation

Calculating accurate driving distance in real time is critical to the selection that a DoorDash consumer sees. A mere straight line circle-based distance could be inaccurate and would result in very long Dasher drive times, especially when the topology of the region has unevenness due to barriers like mountains, lakes, bridges, parks, etc.

Figure 1 depicts a circle with a nine mile radius centered around an address in Southern California. If a consumer orders from a store on the edge of this circle, it will take at least half an hour (as shown in Figure 2) just for the Dasher to get from the store to the consumer.

Figure 1 (left): Circle with a nine mile radius centered around an address. Figure 2 (right): Time to drive from point on edge of that circle to given address.

Basic Definitions

Before we delve into the system architecture, let us define some terms:

  • Latitude, Longitude: A unique location point on the planet, abbreviated as (lat, lng.)
  • Geohash: A hierarchical encoding system to subdivide space into grid like structure.
  • Isochrone: A curve of equal travel time, represented as a GeoJSONFigure 3 shows an isochrone in San Francisco depicting areas that can be reached in 10 (inner region) and 20 (outer region) minutes by walking. Figure 4 is geojson representation of an isochrone.
Figure 3 (left): Isochrones of 10 and 20 minutes (walking). Figure 4 (right): geojson representation of an isochrone.

Architecture

The following diagram describes the overall architecture to determine if a store is in the consumer’s delivery address to determine its selection.

Figure 5: Structure of architecture to determine stores within a consumer’s delivery address.

Offline component:

The offline component involves an isochrone service responsible for computing isochrones for a given location (lat and lng, which is converted to a level seven geohash) and parameters (eg: travel time).

To compute isochrones, we use our custom fork of Galton, an open source project. Galton is built on top of OSRM, an open source routing engine, and concaveman, a fast implementation of a concave hull algorithm. Galton first generates a grid of coordinates of configurable size and granularity around the input coordinate. OSRM then computes travel times from the input coordinate to each of the grid coordinates. Grid coordinates with travel times greater than the input travel time are filtered out. Finally, the concave hull algorithm generates an outline of the remaining coordinates, producing the appropriate isochrone as shown in Figure 6, which is the nine mile isochrone for the same address in Figure 1.

Figure 6: Nine mile isochrone for address in Figure 1.

The service caches isochrones in DynamoDB, as simple key-value lookups for speedy retrieval. Further, we key by geohash, precision 7, rather than exact coordinate, to reduce the number of entries we need to store. Precision 7 geohashes have an error of ±0.076 km; isochrones for coordinates within these bounds will not vary drastically. We store isochrones in order of millions and with lookup time under ten milliseconds.

For each request, the service queries for DynamoDB: if the isochrone is present then it is returned. If the isochrone is absent then an asynchronous job is launched to generate and store it, returning a null response. On subsequent requests for that address and parameters, the generated isochrone will be returned. When we launch a new market, we bootstrap it by running a script to pre populate isochrone entries for all geohashes in the market, to get the market up to speed for accurate selection upfront.

Online component:

  1. DoorDash clients call the search backend API for the specific (lat, lng)
  2. Search module calls isochrone service with the (lat, lng) and parameters like travel time to fetch the corresponding isochrone. These parameters are district-specific and configurable, so we can run experiments for testing conversion changes based on selection. If the isochrone is absent (as in the case when the isochrone is absent in dynamodb), we fall back to the naive straight line distance computations (with tighter radius). We persist the selection logic (isochrone or straight line along with parameters) at a session level in the backend search module to provide a consistent notion of selection across browsing sessions for the consumer.
  3. The search module on fetching the isochrone for that address is encoded as a polygon geoshape to construct a geoshape query to hit Elasticsearch.
  4. Stores that are indexed into Elasticsearch have the store location encoded in geo-point format. Elasticsearch builds a prefix tree structure at index time to support fast geo queries at runtime. Elasticsearch runs the given ES geoshape query from Step 3 to compute an intersection of the polygon with stores in the index for retrieval.
  5. Store results are deserialized and returned to the client for that address.

Conclusion

Our current implementation accounts for the topology of the region via driving distance addressing the inaccurate selection problem in Figure 1 by isochrone selection as shown in Figure 6. Furthermore, this architecture allows flexibility to configure and control selection logic based on regionality, to dynamically change selection logic based on supply/demand curves, and to run selection experiments.

Some potential areas that we will be working on in the future include getting more accurate real-time traffic and road condition updates into the system.

If you are passionate about solving challenging problems in this space, we are hiring for our Search & Relevance team and our Data Infrastructure team. If you are interested in working on any other areas at DoorDash check out our careers page. Let us know for any comments or suggestions.

As I scurry to Little Star Pizza without breaking a sweat, I ask for Anna’s delivery and pride myself on finally tackling that New Year’s resolution of staying fit. Welcome to my first day at DoorDash, where my first task as a Software Engineering Intern was to learn our technology by delivering food.

I love testing our software by actually hitting the streets and completing deliveries, because it allows me to gain first-hand understanding of the impact of my work. As an engineer on the Dispatch team, we focus on problems related to the underlying logistics of on-demand delivery such as picking the optimal delivery route, reducing restaurant wait times, maximizing Dasher efficiency, building models to predict the number of Dashers on the road, finding the best Dasher-consumer pair, and more. When we successfully reduce wait times, for example, it’s not just numbers we see, but actual Dasher stories to share.

I did not come into my internship being told what to do. Instead, I spent the first weeks deploying four different features in three code bases that the Dispatch team mainly uses. Then, I had a discussion with my mentor, Richard, about my particular interests. I picked the open-ended problem of improving batching — offering a Dasher multiple deliveries in a given time span — which allows her to earn more money.

The first question was where to start. I spent a few days thinking about ways to ensure that my program would be intelligent enough to pick out quality batches. At DoorDash, we strive for a strong balance between quality and quantity. This means that every batch my program creates should not only be on time, but should also provide increased earnings for both Dashers and the company. After some back and forth discussion with the business team, we were able to come up with an algorithm to filter out bad batches. Through this process, I learned the importance of communicating effectively within and across teams at DoorDash.

As the weeks flew by, I realized that my colleagues and co-interns had slowly become some of my close friends. Through activities like hiking, sailing, and white-water rafting, we bonded over climbing rocks, saving a windsurfer, and falling off rafts together. There is probably no better way to get to know someone than to be out in the wild with them — well, unless it’s crying from spicy hot pot soup base with your manager and mentor.

While everyone at DoorDash has been warm, welcoming and encouraging, I specifically want to thank my mentor, Richard, and my manager, Jeff, for the best summer in college so far. Their genuine desires to understand me as a person — not just as an engineer — enabled me to warm up to DoorDash very quickly. I will never forget Richard’s big smile when we finished our hardware hack together (picture below), how tasty Jeff’s homemade burgers are, and the spontaneous Dispatch Happy Hours that are even happier than they sound. I guess you know you’ve had an amazing time when goodbyes are so tough, so thank you so much for making my internship so memorable.

At DoorDash we recently migrated the codebase of our iOS Consumer and Dasher apps to Swift 3 from Swift 2. We decided to migrate the codebase after XCode 8.3 was released in March, which ended support for Swift 2.3. The newest versions of many third party libraries used by our apps are written in Swift 3, and since Swift 2 and Swift 3 modules cannot import each other, we weren’t able to upgrade to the newest version of many libraries. Additionally, Swift 3 has many improved features and syntactical improvements that we wanted to take advantage of.

Our codebases contain nearly 100,000 lines of Swift code which made the migration a non-trivial and challenging task. We want to share some steps we took to facilitate the migration to Swift 3 and what we learned from the process.

Preparation

Before we began we decided to do a code freeze of feature development so that we would not need to constantly update our migration branch with feature development changes.

We also realized that much of the initial work, such as updating code dependencies and project settings, would have to be done in a sequential fashion. However, we quickly realized that we could speed up the process by dividing the codebase into different subtasks to allow multiple developers to help with the migration effort simultaneously We use JIRA as our task tracking tool, so to aid with dividing up the steps of the migration process we decided to create a JIRA reference ticket along with sub-tasks to make it clear which task each developer was working on and to track the overall migration process.

Those subtasks divided up the codebase in alphabetical order by directory path.

Finally, Swift 3 includes many naming convention improvements over Swift 2. We decided to follow the official Swift API Design Guidelines since it offers suggestions on standard best practices when writing Swift code. For example, when the first argument of a method can form a prepositional phrase with the method name, it should have an argument label.

func configureItemOptionCell(with itemOption: ItemOption)

We also came up with our own internal Swift Style guide that clarified additional style conventions such as spacing (indent with spaces instead of tabs) and guard statement usage.

Initial steps of the migration

Since migration steps are not easily parallelizable, one developer worked sequentially on the first few steps of the migration process. These steps took around two days to complete.

First, the developer updated external third party library dependencies. We use Cocoapods for library dependency management and thankfully all of our Swift dependencies were popular libraries that have Swift 3 versions, enabling us to update each dependency pod to the latest version with a simple change to the Podfile. If we had any dependencies that did not have Swift 3 versions, we would either need to remove the dependency or fork the pod repo and perform the Swift 3 upgrade on the code ourselves.

Next we updated the XCode project file. We decided to forgo using the migration tool since the tool makes a lot of changes that don’t automatically follow the API guidelines, such as having empty argument label for the first parameter of converted functions and access control replacements such as using fileprivate when private is sufficient. We also found that the migration tool made many changes that resulted in unnecessary casting between NSError and Error in our codebase. Another disadvantage of using the migration tool is that it makes changes to the entire project, which makes it hard to create pull requests to peer review code in chunks. However, using the migration tool is a perfectly valid option especially for apps that are divided up into smaller modules.

Since we didn’t use the migration tool, we used regular expressions to find and replace many Apple frameworks changes. For example, CGRectMake(0, 0, 100, 50) can be changed to CGRect(x: 0, y: 0, width:100, height: 50)with a simple regex find and replace.

Lastly, we created a main migration branch for these changes. We used the branch Swift3/MainMigration, which was created for the individual subtasks that covered migrating directories within our app. After the work for each subtask was done, the branch for that subtask was merged into this main branch.

Migrating the codebase

We had five iOS developers work in parallel on the JIRA subtasks that divided up the codebase to apply the remaining migration changes. This process included migrating Swift files for each subtask to support Swift 3 and opening Github pull requests against the main migration branch for changes as they were finished. We also peer reviewed each PR to make sure they conformed with the Swift API guidelines and our style guide.

This entire process took a little over a week to finish.

Compiling, testing and releasing

After the first pass through of all the files, we still had around 100 compile errors. One iOS engineer fixed the remaining issues and made sure the app compiled properly. Many of the issues that remained after the first pass were because SourceKit often will not show errors in files until dependent errors in other files are resolved.

Before releasing, we also did a thorough test of all app functionality since the migration touched so many areas of our codebase. We fixed issues as they were found during testing. One common crash we encountered was due to UIKit button selectors being changed to include WithSender in the selector name.

For example:

[DoordashDriver.BankDetailViewController didTapEditButtonWithSender:]: unrecognized selector sent to instance 0x7fbab0e55150

The fix was simply to add WithSender to the name of the IBAction methods.

Our app next went through another round of testing where we distributed the app to employees internally as well as to beta users. This step was essential to help catch bugs and to reduce the likelihood of regression bugs going out with the release. After our internal testing and beta periods, we felt confident that our release was stable and we released our app through our normal release process.

But of course, a developer’s job has only just begun after an app has been released. Proper crash detection and network request monitoring through tools like New Relic is essential to catch post-release issues and crashes. Thankfully we did not encounter any critical post release bugs in our Swift migration release.

Common changes required

We encountered a number of common issues throughout the migration process. Here are a few of the most common changes that we had to make.

  • Closure parameter labels — Swift 3 removed support for argument labels in function types. This requires the migration process to involve removing parameter labels from closures. This limitation of Swift 3 is discussed in Closure Parameter Names and Labels. In those cases, we simply removed the argument labels in the closures.

  • Optional protocol functions — The function signatures for UIKit protocol delegate methods have changed for many classes like UITableViewController and UICollectionViewController. The impact of the protocol name changes are not obvious since the code will compile fine without updating the functions since the functions are optional protocol functions. For example: func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) has changed to: func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath). Using XCode’s find and replace feature can help with catching and resolving many of these protocol name changes.
  • Variables that are assigned to implicitly unwrapped optionals will now have an optional type — This is related to the change in Abolish ImplicitlyUnwrappedOptional type. Implicitly unwrapped optionals are now only allowed at the top level and as function results so function argument types need to be modified. For example, in the following case we just had to change the implicitly unwrapped optional type to be an optional type instead:

#Swift Lint

We also decided to adopt Swift Lint during our Swift migration process to automate code conformance to our style guide. swiftlint autocorrect will correct many issues (548 files for us!), but many issues such as file length violations can’t be auto-fixed. This can be addressed by using swiftlint:disable comments to temporarily disable the violations and then coming back to fix these issues at a later time.

Final Thoughts and Tips

Ultimately, we’re very happy that we completed this migration. While it took time away from product development and required a lot of experimentation, our codebase is now up to date and more scalable thanks to using Swift 3 across all our apps.

If you choose to make the update from Swift 2 to Swift 3 be sure to read the API guideline documentation thoroughly to understand the best practices in naming conventions. Also, make sure there is a consensus on the team for Swift style before starting the migration process.

Additionally, if there are compile errors that are difficult to figure out, move on to other errors and go back to the skipped errors later. Many compile errors are due to dependent files having errors. In retrospect, we might have encountered fewer migration issues if we spent more preparation time to order the migration process in a way that migrated files that were dependencies first. If you do encounter errors, the compiler does a relatively good job of suggesting fixes, but pay attention to avoid unnecessary casting and to ensure that function signatures follow the Swift API guidelines.

Remember that proper communication and coordination during the entire effort is critical. We organized a meeting for all the developers involved before starting the migration so that everyone would be up to date on the entire process. We also made sure to sync up regularly during the migration to share what we learned and coordinate working on the remaining tasks.

Finally, try to limit code modifications to changes that are absolutely essential for the Swift 3 migration. Many times we were tempted to refactor code as we worked through the files for the migration changes but we decided that too many code changes at once can lead to hard-to-discover regression bugs. The migration changes are large enough as-is, so we decided it would be better to add tickets to our bug tracking tool in order to address refactoring changes in a future release.

Customers across North America come to DoorDash to discover and order from a vast selection of their favorite stores. Our mission is to surface the best stores for our consumers based on their personal preferences. However, the notion of “best stores” for a consumer varies widely based on their diet, taste, budget, and other preferences.

To achieve this mission, we are building search products that provide a personalized discovery and search experience based on a consumer’s past search and order history with DoorDash. This article details our approach for personalization in our search ecosystem, which has provided a significant lift in conversion from search to checkout.

Search and recommendation challenges

The three-sided nature of DoorDash platform (involving consumers, dashers and merchants) presents a lot of interesting and unique search challenges in addition to the general search and recommendation related problems. Some challenges include:

  • Sparsity: not every consumer can see every store, making this different from a typical e-commerce recommendations problem
  • Cold-start problem: cases when new stores or consumers enter the system
  • Tradeoff between relevance versus diversity
  • Including accurate driving distance in search selection

Search overview at DoorDash

We use Elasticsearch to power the consumer search for our website and apps. Elasticsearch is an open source, distributed, Lucene-based inverted index that provides search engine capabilities without reinventing the wheel.

For our search engine there are two primary components:

The first is the indexing module (offline). This component reads the store object from the database (Postgres in our case) and writes it to Elasticsearch for bootstrapping, as well as for partial asynchronous updates on the database store object.

Second is the search module (online). Web and mobile clients call the backend search API with the specified consumer location. A JSON-based Elasticsearch query is constructed at the Django backend to call Elasticsearch. The query is executed inside Elasticsearch to retrieve relevant results, which are deserialized and returned to the client. The Elasticsearch query is primarily designed to achieve two purposes:

  • Selection: Out of all the available stores, only select those that are orderable from the consumer’s address. This is primarily achieved by the geoshape features of Elasticsearch. How we compute a geoshape to get an accurate driving distance for each address and store pair is a discussion for a separate blog post.
  • Ranking or scoring: Out of the selected subset of stores, we need to rank them according to relevance. Before the personalized ranking we ran a number of sorting experiments including ranking by popularity, price, delivery, estimated time of arrival, ratings, and more. The main learning from the experiments was that there was no global best ranking for every user, but rather the notion of “best” varied across each user, which led us to use personalization.

ML modeling for recommendations

Now let’s talk about the ML model training and testing for personalization. For including personalization in Elasticsearch, we define a knowledge-based recommender system over consumer / store pairs. For every consumer we are evaluating how good the recommendation is for each specific store based on the consumer’s order history and page views.

To help us out, let’s define some basic terms (note that Medium doesn’t handle equations well, so apologies in advance for the janky formatting):

  • c_i: consumer with unique id i
  • s_j: store with unique id j
  • d(c_i): data profile of consumer c_i
  • d(s_j): data profile of store s_j
  • f^k: kth feature in the ML model
  • f^k_ij: value of kth feature for (c_is_j) pair

The data profile of consumer c_i mainly refers to all the data that we need as a signal for the recommendation model. We store and update d(c_i) for each c_iin the database for it to be consumed in the online pipeline.

The data profile of store s_j is stored in Elasticsearch by the indexing pipeline.

f^k is a feature in the machine learning model and f^k_ij is the specific value for the (c_is_j) pair. For example, one feature we include is how much overlap there is between the cuisines the consumer c_i had ordered from in the past and the cuisine of the store s_j. We would include similar features based on viewing store pages, price range, etc. For training, we generate f^k_ij for each ij such that c_is_j are visible to each other from the selection criteria described earlier along with a 0/1 flag, which generates the data in the following format:

[0/1 flag, f^0_ij , f^1_ij , f^2_ij , … f^k_ij …] for each i, j such that s_j falls in selectable range of c_i.

Positive examples (marked as 1 in the data model) are the ones where the consumer c_i ordered from that store and the negatives are the ones where, despite the store being exposed to the consumer, the consumer did not order.

We use this data to compute the probability of consumer c_i ordering from s_jgiven by:

Probability(c_i orders from store s_j) = 1/(1+e^(-1* ( w_k * f^k_ij)) ) where w_k is the weight of kth feature.

We trained the data using the logistic regression model to estimate w_k for our dataset.

Personalization in Elasticsearch

Now let’s discuss how we integrate the personalization piece into the Elasticsearch ecosystem, which serves our app and website in real time. To achieve scoring we have to implement the above mentioned logistic regression scoring function inside Elasticsearch. We accomplished that through the script scoring feature of Elasticsearch, which is used for customized ranking use cases such as ours. This script has access to documents inside Elasticsearch and parameters that can be passed as run time arguments in the Elasticsearch query. The score generated by the script is then used for ranking a script based sorting feature to get the desired ranking.

The following diagram describes the overall architecture depicting offline and online components.

Personalization Search Architecture

Offline components:

  1. The indexing pipeline indexes d(s_j) for all stores in the Elasticsearch index.
  2. ML data pipeline writes d(c_i) for all consumers in the database. The database is updated offline to reflect changes in d(c_i) based on c_iactivity.

Online components:

  1. DoorDash clients call the search backend API for c_i
  2. Search module calls database to fetch d(c_i) for c_i which the offline ML data pipeline has populated
  3. Search Module on fetching d(c_i) generates the Elasticsearch query
  4. Search Module hits Elasticsearch with the generated query where d(c_i) is passed as arguments to the script
  5. Elasticsearch ranking script, which is an implementation of the logistic regression scoring function described in the ML modeling section above, is executed as part of the Elasticsearch JVM process. This script is essentially a function of d(c_i) and d(s_j). The script gets d(c_i) as arguments from step 4 and gets d(s_j) as part of the index data, which was stored from offline step a. The script generates the score and Elasticsearch ranks them by script score.
  6. Personalized results are deserialized and returned to the clients

Advantages of this design:

  • Minimal Latency impact: Since search is a latency sensitive product, the personalized version should not contribute to latency. There is only 1 extra database read per search call (which can also be cached). The script ranking function is executed inside Elasticsearch, which is distributed and cache optimized. We have already rolled out the feature to 100% of customers with no impact on Elasticsearch latency.
  • Horizontally scalable: Higher search volume results in more heap usage, which can be addressed by adding more nodes to the Elasticsearch cluster or increasing head size per node.
  • ML model change friendly: The overall architecture works with any ML model. We can experiment with different ML models by implementing the corresponding ranking script and invoking it based on experimentations from backend search modules without changing any other piece.
  • Fault Tolerant: In cases of failure to get d(c_i) in any step we can fall back to the default option and use the baseline non-personalized feed.

Future work

We’ve only scratched the surface with the work we’ve done. Here are some areas that we are working on to make our search engine even better:

  • Machine Learning models: We are testing more sophisticated ML models on top of logistic regression model and experimenting with personalized models for how much variety to include for users.
  • Real time features: We are improving our data pipeline to have real time features and to better incorporate feedback from activity.

If you are passionate about solving challenging problems in this space, we are hiring for the search & relevance team and data infrastructure team. If you are interested in working on other areas at DoorDash check out our careers page. Let us know for any comments or suggestions.

When an engineer at DoorDash opens a GitHub pull request, our goal is to quickly and automatically provide information about code health. GitHub’s status API compliments GitHub webhooks, which allow you to trigger custom routines as events fire in your GitHub account.

When developers push to our largest repo, they see something like this at the bottom of their pull request page:

We initially used a third party CI hosting company to implement our checks. This worked well when the amount of tasks we wanted to trigger was relatively low. However, as the number of checks grew, developers were waiting longer and longer for their CI results. In early 2017, we were waiting more than 20 minutes for an average pull request to complete all checks, despite our use of parallelization features.

Instead of using third parties, we used Jenkins on AWS to build a CI/CD system integrated with GitHub. Our custom solution produces test results within 5 minutes and we’ve also gained an ability to deploy our code continuously — features we will integrate into all new DoorDash microservices.

Jenkins Overview

Jenkins is an open source CI server that’s been around almost as long as WordPress. It’s used by big companies like Netflix and small two person startups alike.

Jenkins has a handful of core concepts:

  • Job — A unit of work such as linting, running a test, or doing an owners check.
  • Pipeline — A sequencing system where one action occurs typically if and only if the previous action successfully took place. Options for parallelizing your work are also available.
  • Queue — Jenkins automatically maintains a queue of jobs that should happen once sufficient capacity is available.
  • Plugin — Like WordPress, Jenkins has many plugins you can install to supplement the default features.
  • Master/Slave — A Jenkins “master” server powers the website and coordinates the work performed in your Jenkins installation. It communicates to “slave” machines responsible for actually performing your work. This means you can scale up/down the number of Jenkins workers you are running in your cluster to control capacity.

Integrating GitHub and Jenkins

To make our GitHub integration work, we created Python scripts that receive 100% of the webhooks from our GitHub account. (There’s an option in the GitHub account settings for where to send webhooks, no matter which specific repository generated the event.) Our Python scripts examine each webhook and conditionally start jobs in Jenkins via the Jenkins API. We refer to this component as our DoorDash GitHub “callback gateway.”

Only certain GitHub events (such as “push”) on a specific list of GitHub repositories (such as our main/monolith repo) actually trigger jobs in Jenkins. For example, when a commit is pushed to our main monolith repository, we immediately begin running tests in Jenkins.

I should note that by default, Jenkins has an ability to poll GitHub repositories and start work when commits are detected to certain branches. Our callback gateway approach allows us to more precisely trigger custom logic against each event rather than polling every 60 seconds. More details on our custom logic in the “Callback Gateway Custom Logic” section below.

Jenkins Pipelines

Rather than starting a handful of Jenkins jobs individually from the callback gateway, the callback gateway instead starts a single Jenkins “Pipeline.

The Jenkins Pipeline for our feature branches has two steps:

  1. Build Docker images.
  2. Kick off many tests, linters, and checks all at the same time which use the Docker images produced in Step 1.

As each test or linter job runs, the first thing it does is send a curl request back to GitHub to notify the developer that the job has started. We emit the local time with a message like, “Javascript Linter started at 9:08am.” This makes it easy to understand how long things have been running while a developer waits.

At the conclusion of a job, we send another curl request to GitHub to update the status check with the results of the job, with a message like, “Linting completed after 30 seconds,” and a GitHub status flag that makes the label either green or red.

Continuous Deployment

When someone pushes code to a feature branch, we trigger a pipeline oriented to testing the code on the branch. However, when someone pushes to the master line, we are interested in starting a pipeline oriented to ultimately deploying the code to production.

DoorDash runs two Jenkins pools: a “general” pool and a “deployment” pool. The general pool runs our tests, docker builds, linters, etc. The deployment pool is reserved for deploying code. The theory is if we need to push an emergency hotfix, it should not be delayed by queueing in the general pool.

When we commit to the “master” line of our GitHub repo, the Deployment Jenkins server notices that the master line received the commit. It will automatically execute instructions found in the Jenkinsfile, located in the master line of the project root. This file uses Jenkins’ pipeline syntax to perform a sequence of events roughly covering these steps:

  1. Build Docker images
  2. Run tests
  3. Build front end artifacts which are uploaded to S3
  4. Confirm with the Pipeline UI someone’s intent to deploy to canary and enable it
  5. Deploy to the canary server
  6. Confirm with the Pipeline UI someone’s intent to disable the canary server
  7. Disable the canary server
  8. Confirm with the Pipeline UI someone’s intent to perform the full deploy
  9. Full production deploy

Jenkinsfile gives you the flexibility to implement a sequence of events that you think is a good idea. For example, you can see that we are gating the deploy sequence at certain points and requiring manual approval before we continue to subsequent steps. You could also easily implement a pipeline which only continues if certain things are true. For example, instead of requiring programmer approval, you might automatically deploy to canary and then automatically check that there is no increase in error levels, and then automatically proceed to deploy to production, etc.

Jenkins has options to depict your pipeline sequences, allowing you to more easily understand what’s going on. The following is an example pipeline I’m currently working on. It is rendered with the Jenkins “Blue Ocean” plugin:

Below is yet another example of how a different, simpler deploy pipeline looks like at DoorDash once it completed:

Callback Gateway Custom Logic

Since our callback gateway is listening to all GitHub events, we have an ability to implement custom features into our GitHub account. For example, sometimes we see a unit test flap and we want to have the tests run again. We have an ability to “fire a blank commit” at the pull request. To do it, you comment the :gun: emoji like this:

it will appear as a normal comment as you would expect in the pull request…

however, after a few seconds, you’ll see the blank commit appear into the branch linage…

as a result of the new commit, all of the test jobs implicitly restart:

Jenkins Setup

The easiest way to get started with Jenkins is to run a Jenkins master using Docker. Just type:

docker run -p 8080:8080 jenkins

In just one command, you have a locally running Jenkins master on your computer.

Jenkins doesn’t use a database like MySQL in order to function. Instead, it stores everything in files under /var/jenkins_home. Therefore, you should set a Docker bind mount on the jenkins_home directory. For example:

docker run -v /your/home:/var/jenkins_home -p 8080:8080 jenkins

Additionally, if you host Jenkins in AWS, I recommend that you mount an EBS volume at that host location and set up recurring snapshots of the volume.

Slave Configuration

The Jenkins master server only exists to run the Jenkins core and its website interface. You run as many slaves as you want, though in my experience, you usually do not want to exceed more than 200 slaves per master server.

Jenkins has a concept of “executor” which describes the number of jobs a node will ever run at once. Though you can technically set the number of executors on your master to any number, you should probably set your master to have zero executors and only give executors to your slaves.

Since DoorDash is on AWS, our strategy is to use EC2 reserved instances to run a low baseline number of Jenkins servers that are always running. In the morning, we use EC2 Spot Instances to scale up. If we are outbid, we scale up on demand instances.

Service Discovery

The Jenkins master must have each slave registered in order to be able to dispatch work. When a slave server launches, the slave’s bootstrap script (Amazon’s EC2 “user_data” property) registers a minutely cron job, which upserts the instance’s internal hostname and the current unix timestamp into a t2.micro MySQL RDS database. The master server polls this table each minute for the list of servers that have upserted within the last 2 minutes. Instances failing to upsert are unregistered from the Jenkins master and new ones are idempotently added.

Scaling Jenkins

Each weekday morning, we scale up the number of slave Jenkins servers. Each evening, we initiate a scheduled scale down. If you terminate a Jenkins slave while it’s doing work, the Jenkins jobs it was running will, by default, fail. In order to avoid failing developers’ builds during a scheduled scale-down, we have split all of our slaves into two groups A and B.

At 7:45pm, we mark all slaves in group offline and then we wait 15 minutes. This allows for a graceful drain down of in-flight jobs because Jenkins will not assign new work to slaves marked as offline. At 8pm, we trigger a scheduled AWS scale-down of group A. At 8:15, we mark all remaining slaves in group Aas online. We then repeat this sequenced process for group B, and then finally for our spot instances.

Monitoring Jenkins

We trigger an AWS Lambda function each minute that queries the Jenkins APIs and instruments certain metrics into Wavefront via statsd. The main metric that I watch is what we call “Human wait time” representing the amount of time a real person waited from the moment a pull request was pushed to the moment that all of the CI checks were completed. Wavefront allows us to fire PagerDuty alerts to the infrastructure team if any of the metrics fall to unacceptable levels.

Summary

There are numerous options for setting up CI & CD. Depending on your situation, you may find a 3rd party hosted tools to be perfect for your use case like CircleCI and TravisCI. If you like customizing an open source project and running it yourself, Jenkins might be for you. Still, if you have highly specialized needs or need to customize everything imaginable, you might decide to write something entirely from scratch.

So far, Jenkins has offered us a way to quickly setup CI & CD and scale it using the tools we’re already using like AWS and Terraform.

Amazon has a great white-paper outlining their recommendations and considerations for setting up Jenkins on AWS, found here.

Come back to our blog for more updates on DoorDash’s engineering efforts. If you’d like to help build our our systems which are growing at 250% per year, navigate to our open infrastructure engineering jobs page.

At DoorDash, mobile is an integral part of our end user experience. Consumers, Dashers, and merchants rely on our mobile apps every day for delivery. And our Android team moves fast to ship impactful features that improve the user experience and the efficiency of our platform.

In the past, we verified the functionality of our releases by manually running them through a set of regression tests. In order to scale the development and release process with our growing team, we have tried to automate as much of the testing process as possible. We have taken two major steps to achieve this:

  • Adopt a testable design pattern
  • Automate end to end integration tests to replace manual testing before releases
Adopt a testable design pattern

In order to make code testable, it is important to decouple application logic from Android components. Model View Presenter (MVP) and Model View View Model (MVVM) are the two most popular design patterns used in Android apps. MVP decouples logic from the view and makes it easy to write tests for this logic. MVVM uses data binding to accomplish this. We chose MVP because it was easy to learn and it integrated with the rest of the architecture in our apps very well. We have been able to achieve close to 100% test coverage of view and application logic using this pattern.

Let’s take a look at the login functionality in our app using MVP:

https://gist.github.com/de1b669075d6cdd46fe68890a9948244

This architecture gives us a clear separation of view, business, and application logic, which makes it very easy to write tests. The code is broken down into the following components:

  • LoginContract.java defines the contract between the view and the presenter by defining the two interfaces.
  • LoginPresenter.java handles user interactions and contains logic to update the UI based on data received from the manager.
  • LoginActivity.java displays UI as directed by the presenter.
  • AuthenticationManager.java makes the API call to fetch the auth token and saves it to SharedPreferences. This class gives us the ability to decouple application logic from MVP. Other responsibilities of manager classes that are not covered in this example include caching and transforming model data.

Oftentimes when implementing a new architecture, the tendency is to rewrite the entire app from scratch. In a fast-moving startup such as ours, this approach is not feasible since we need our engineering resources to work on product improvements in our evolving business. Instead, as we make major changes in areas of the codebase, we take the opportunity to refactor to MVP and add unit tests.

Automate end to end integration tests to replace manual testing before releases

MVP with unit tests alone isn’t enough to guarantee stable releases. Integration testing helps us to make sure our apps work with our backend. To accomplish this, we write end to end integration tests for critical flows in our apps. They are black-box tests that verify whether our apps are compatible with our backend APIs. A simple example is the login test. In this test, we launch the LoginActivity, enter the email and password, and verify that the user can login successfully:

https://gist.github.com/ee6182e9db1c94ec8e29e0d441fa15e2

We use Espresso for writing this test. As we are making a real API call, we have to wait for it to finish before moving to the next test operation. Espresso provides IdlingResource to help with this. As defined in the isIdleNow method, LoginIdlingResource tells Espresso to wait until the login call finishes and the app goes to the DashboardActivity.

We took the following steps to build out the infrastructure for running acceptance tests:

  1. Configured a database with sample data and a local web server to service API requests on a machine in our office
  2. Target this web server in our tests. For us, this involved creating a new build variant with a different base url for Retrofit
  3. Set up Continuous Integration with Jenkins to run these tests. We use the same machine to host the Jenkins CI server that runs these tests every time code gets merged into our main development branch
  4. Configured Github webhook for Jenkins to report build status for every pull request

We write and maintain acceptance tests for critical flows in our apps. Continuous integration using Jenkins has helped us catch bugs very early in the release cycle. It has also helped us scale our development and testing efforts as we don’t need a lot of manual testing before our releases.

Conclusion

These testing strategies have helped us immensely in making our releases smoother and more reliable. While the automated acceptance testing strategy outlined in this article has some initial setup cost, it scales very well once you build the infrastructure. In addition, MVP makes sure that new features are tested at the unit and functional level. We hope that these steps will help you reduce the amount of manual testing and gain increased confidence in your releases.


As a DoorDash customer, you should always know where your order is in the delivery journey. Whether the Dasher is on the way to the restaurant, waiting for your food, or nearing your location, the DoorDash app keeps you up to date every step of the way.

In the past, we’ve typically used GPS information from a Dasher’s phone, combined with manual prompts for Dashers to check in when they’ve arrived at a restaurant or picked up food, to trigger these updates along the way. However, we’ve been working to make this location information even more accurate with less manual interaction.

We recently rolled out an update to the Dasher and merchant apps that uses bluetooth signals to let us know when a Dasher has arrived at or departed from a restaurant (the technical term for this technology is “beacons”). Now, if a Dasher has bluetooth enabled on their phone, as soon as they are within 20 feet of the merchant app, the bluetooth signals in the two apps will communicate with one another and trigger a notification to DoorDash that the Dasher has arrived at the restaurant. A similar notification will trigger when the Dasher leaves communication range with the tablet, indicating that they have departed from the store.

Bluetooth signals are much more precise than geo-fencing using GPS and this improved location information has a number of benefits across the platform. For example, more accurate information about when a Dasher arrives and leaves a given restaurant helps us improve restaurant wait times, ultimately allowing us to make better delivery offers to Dashers. Similarly, better estimated wait times help us calculate more accurate ETAs for when the customer should expect her delivery. We also hope to use this information for other features in the future, like improving parking information or requiring fewer taps and swipes on the Dasher app.

It’s important to note that we launched this feature with Dasher privacy in mind from the beginning. While we already use location information from the GPS on the Dasher’s phone, the bluetooth signals are only used when Dashers are close to a merchant app and only while on an active delivery. If Dashers prefer not to provide this enhanced location information, they can simply turn off bluetooth on their cell phone.

We’re excited that this new improved location information will improve the experience for Dashers, customers and merchants alike. Order from DoorDash today and let us know if you were able to notice a more accurate ETA.

At DoorDash, most of our backend is currently based in Django and Python. The scrappy Django codebase from our early days has evolved and matured over the years as our business has grown. To continue to scale, we’ve also started to migrate our monolithic app towards a microservices architecture. We’ve learned a lot about what works well and what doesn’t with Django, and hope we can share some useful tips on how to work with this popular web framework.

Be careful about “applications”

Django has this concept of “applications,” which are vaguely described in the documentation as “a Python package that provides some set of features.” While they do make sense as reusable libraries that can be plugged into different projects, their utility as far as organizing your main application code is less clear.

There are some implications from how you define your “apps” that you should be aware of. The biggest one is that Django tracks model migrations separately for each app. If you have ForeignKey’s linking models across different apps, Django’s migration system will try to infer a dependency graph so that migrations are run in the right order. Unfortunately, this calculation isn’t perfect and can lead to some errors or even complex circular dependencies, especially if you have a lot of apps.

We originally organized our code into a bunch of separate “apps” to organize different functionality, but had a lot of cross-app ForeignKey’s. The migrations we had checked in would occasionally wind up in a state where they would run okay in production, but not in development. In the worst case, they would not even play back on top of a blank database. Each system may have a different permutation of migration states for different apps, and running manage.py migrate may not work with all of them. Ultimately, we found that having all these separate apps led to unnecessary complexity and headaches.

We quickly discovered that if we had these ForeignKey’s crossing different apps, then perhaps they weren’t really separate apps to begin with. In fact, we really just had one “app” that could be organized into different packages. To better reflect this, we trashed our migrations and migrated everything to a single “app.” This process wasn’t the easiest task to accomplish (Django also uses app names to populate ContentType’s and in naming database tables — more on that later) but we were happy we did it. It also meant that our migrations all had to be linearized, and while that came with downsides, we found that they were outweighed by the benefit of having a predictable, stable migration system.

To summarize, here are our suggestions for any developers starting a Django project:

  • If you don’t really understand the point of apps, ignore them and stick with a single app for your backend. You can still organize a growing codebase without using separate apps.
  • If you do want to create separate applications, you will want to be very intentional about how you define them. Be very explicit about and minimize any dependencies between different apps. (If you are planning to migrate to microservices down the line, I can imagine that “apps” might be a useful construct to define precursors to a future microservice).

Organize your apps inside a package

While we’re on the topic of apps, let’s talk a little bit about package organization. If you follow Django’s “Getting started” tutorial, the manage.py startapp command will create an “app” at the top-level of the project directory. For instance, an app called foo would be accessible as import foo.models… . We would strongly advise you to actually put your apps (and any of your Python code) into a Python package, namely the package that is created with django-admin startproject.

In Django’s tutorial example, instead of:

mysite/
    mysite/
        __init__.py
polls/
    __init__.py

We’d suggest:

mysite/
    mysite/
        __init__.py
    polls/
        __init__.py

This is a small and subtle change, but it prevents namespace conflicts between your app and third party Python libraries. In Python, top-level modules go into a global namespace and need to be uniquely named. As an example, the Python library for a vendor we used, Segment, is actually named analytics. If we had an analytics app defined as a top-level module, there would be no way to distinguish between the two packages in your code.

Explicitly name your database tables

Your database is more important, more long-lived, and harder to change after the fact than your application. Knowing that, it makes sense that you should be very intentional about how you are designing your database schema, rather than allowing a web framework to make those decisions for you.

While you largely do control the database schema in Django, there are a few things it handles by default that you should know about. For example, Django automatically generates a table name for your models, with the pattern of <app_name>_<model_name_lowercased>. Instead of relying on these auto-generated names, you should consider defining your own naming convention and naming all of your tables manually, using Meta.db_table.

class Foo(Model):
    class Meta:
        db_table = 'foo'

The other thing to watch for is ManyToManyFields. Django makes it easy to generate many-to-many-relationships using this field and will create the join table with automatically-generated table and column names. Instead of doing that, we strongly suggest you always create and name the join tables manually (using the through keyword). It’ll make it a lot easier to access the table directly, and frankly, we found that it’s just annoying to have hidden tables.

These may seem like minor details, but decoupling your database naming from Django implementation details is a good idea because there are going to be other things that touch your data besides the Django ORM, such as data warehouses. This also allows you to rename your model classes later on if you change your mind. Finally, it’ll simplify things like breaking out tables into separate services or transitioning to a different web framework.

Avoid GenericForeignKey

If you can help it, avoid using GenericForeignKey’s. You lose database query features like joins (select_related) and data integrity features like foreign key constraints and cascaded deletes. Using separate tables is usually a better solution, and you can leverage abstract base models if you are looking for code-reuse.

That said, there are situations where it can still be helpful to have a table that can point to different tables. If so, you would be better off doing your own implementation and it isn’t that hard (you just need two columns, one for the object ID, the other to define the type). One think we dislike about GenericForeignKey is that they have a dependency on Django’s ContentTypes framework, which stores identifiers for tables in a mapping table named django_contenttypes.

That table is not a lot of fun to deal with. For starters, it uses the name of your app (app_label) and the Python model class (model) as columns to map a Django model to an integer id, which is then stored inside the table with the GFK. If you ever move models between apps or rename your apps, you’re going to have to do some manual patching on this table. More importantly, having a common table hold these GFK mapping will greatly complicate things if you ever want to move your tables into separate services and databases. Similar to the earlier section about explicitly naming your tables — you should own and define your own table identifiers as well. Whether you want to use an integer, string, or something else to do this, any of these is better than relying on an arbitrary ID defined in some random table.

Keep migrations safe

If you are using Django 1.7 or later and are using a relational database, you are probably using Django’s migration system to manage and migrate your schema. As you start running at scale, there are some important nuances to consider about using Django migrations.

First of all, you will need to make sure that your migrations are safe, meaning they will not cause downtime when they are applied. Let’s suppose that your deployment process involves calling manage.py migrate automatically before deploying the latest code on your application servers. An operation like adding a new column will be safe. But it shouldn’t be too surprising that deleting a column will break things, as existing code would still be referencing the nonexistent column. Even if there are no lines of code that reference the deleted field, when Django fetches an object (e.g. Model.objects.get(..), under the hood it performs a SELECT on every column that is defined in the model. As a result, pretty much any Django ORM access to that table will raise an exception.

You could avoid this issue by being sure to run the migrations after the code is deployed, but it does mean deployments have to be a bit more manual. It can get tricky if developers have committed multiple migrations ahead of a deploy. Another workaround is to make these and other dangerous migrations into “no-op” migrations, by making migrations purely “state” operations. You will then need to perform the DROP operations after the deploy.

class Migration(migrations.Migration):
    state_operations = [ORIGINAL_MIGRATIONS]
    operations = migrations.SeparateDatabaseAndState(
        state_operations=state_operations
    )

Of course, dropping columns and tables aren’t the only operation you will want to watch out for. If you have a large production database, there are many unsafe operations that may lock up your database or tables and lead to downtime. The specific types of operations will depend on what variant of SQL you are using. For example, with PostgreSQL, adding columns with an index or that are non-nullable to a large table can be dangerous. Here’s a pretty good article from BrainTree summarizing some of the dangerous migrations on PostgreSQL.

Squash your migrations

As your project evolves and accumulates more and more migrations, they will take longer and longer to run. By design, Django needs to incrementally play back every single migration starting from the first one in order to construct its internal state of the database schema. Not only will this slow down production deploys, developers will also have to wait when they initially set up their local development database. If you have multiple databases, this process will take even longer, because Django will play all migrations on every single database, regardless of whether the migration affects that database.

Short of avoiding Django migrations entirely, the best solution we’ve come up with is to just do some periodic spring cleaning and to “squash” your migrations. One option is to try Django’s built-in squashing feature. Another option, which has worked well for us, is to just do this manually. Drop everything in the django_migrations table, delete existing migration files, and run manage.py makemigrations to create fresh, consolidated migrations.

Reduce migration friction

If many dozens of developers are working on the same Django codebase, you may frequently run into race conditions with merging in database migrations. For example, consider a scenario where the current migration history on master looks like:

0001_a
0002_b

Now suppose engineer A generates migration 0003_c on his local branch, but before he is able to merge it, engineer B gets there first and checks in migration 0003_d. If engineer A now merges his branch, anyone that tries to run migrations after pulling the latest code will run into the error “Conflicting migrations detected; multiple leaf nodes in the migration graph: (0003_c, 0003_d).”

At a minimum, this results in the migrations having to be manually linearized or creating a merge migration, causing friction in the team’s development process. The Zenefits engineering team discusses this problem in more detail in a blog post, from which we derived inspiration to improve upon this.

In less than a dozen lines of code, we were able to solve a more general form of this problem in the case where we have multiple Django applications. We did this by overriding the handle() method of our makemigrations command to generate a multiple-application migration manifest:

https://gist.github.com/084ef47c4f68922c587bf1252e2d6d89

Applying this to the above example, the manifest file would have one entry doordash: 0002_b for our app. If we generate a new migration file 0003_c off HEAD, the diff on the manifest file will apply cleanly and can be merged as is:

- doordash: 0002_b
+ doordash: 0003_c

However, if the migrations are outdated, such as if an engineer only has 0001_a locally and generates a new migration 0002_d, the manifest file diff will not apply cleanly and thus Github would declare that there are merge conflicts:

- doordash: 0001_a
+ doordash: 0002_d

The engineer would then be responsible for resolving the conflict before Github will allow the pull request to be merged. If you have integration tests on which code merges are gated (which any company of that size should), this is also another motivation to keep the test suite fast!

Avoid Fat Models

Django’s promotes a “fat model” pattern where you put the bulk of your business logic inside model methods. While this is what we used initially, and it can even be pretty convenient, we realized that it does not scale very well. Over time, model classes become bloated with methods and get extremely long and difficult to read. Mixins are one way to mitigate the complexity a bit, but do not feel like an ideal solution.

This pattern can be kind of awkward if you have some logic that doesn’t really need to operate on a full model instance fetched from the database, but rather just needs the primary key or a simplified representation stored in the cache. Additionally, if you ever wish to move off the Django ORM, coupling your logic to models is going to complicate that effort.

That said, the real intention behind this pattern is to keep the API/view/controller lightweight and free of excessive logic, which is something we would strongly advocate. Having logic inside model methods is a lesser evil, but you may want to consider keeping models lightweight and focused on the data layer. To make this work, you will need to figure out a new pattern and put your business logic in some layer that is in between the data layer and the API/presentational layer.

Be careful with signals

Django’s signals framework can be useful to decouple events from actions, but one use case that can be troublesome are pre/post_save signals. They can be useful for small things (e.g. checking when to invalidate a cache) but putting too much logic in signals can make program flow difficult to trace and read. Passing custom arguments or information through a signal is not really possible. It is also very difficult, without the use of some hacks, to disable a signal from firing on certain conditions (e.g. if you want to bulk update some models without triggering expensive signals).

Our advice would be to limit your use of these signals, and if you do use them, avoid putting other than simple and cheap logic inside them. You should also keep these signals organized in a predictable and consistent place (e.g. close to where the models are defined), to make your code easy to read.

Avoid using the ORM as the main interface to your data

If you are directly creating and updating database objects from many parts of your codebase with calls to the Django ORM interface (Model.objects.create() or Model.save()), you may want to revisit this approach. We found that using the ORM as the primary interface to modify data has some drawbacks.

The main problem is that there isn’t a clean way to perform common actions when a model is created or updated. Suppose that every time ModelA is created, you really want to also create an instance of ModelB. Or you want to detect when a certain field has changed from its previous value. Apart from signals, your only workaround is to overload a lot of logic into Model.save(), which can get very unwieldy and awkward.

One solution for this is to establish a pattern in which you route all important database operations (create/update/delete) through some kind of simple interface that wraps the ORM layer. This gives you clean entry points to add additional logic before or after database events. Additionally, decoupling your application code a bit from the model interface will give you the flexibility to move off the Django ORM in the future.

Don’t cache Django models

If you are working on scaling your application, you are probably taking advantage of a caching solution like Memcached or Redis to reduce database queries. While it can tempting to cache Django model instances, or even the results of entire Querysets, there are some caveats you should be aware of.

If you migrate your schema (add/change/delete fields from your model), Django actually does not handle this very gracefully when dealing with cached instances. If Django tries to read a model instance that was written to the cache from an earlier version of the schema, it will pretty much die. Under the hood, it’s deserializing a pickled object from the cache backend, but that object will be incompatible with the latest code. This is more of an unfortunate Django implementation detail than anything else.

You could just accept that you’ll have some exceptions after a deploy with a model migration, and limit the damage by setting reasonably short cache TTL’s. Better yet, avoid caching model’s altogether as a rule. Instead, only cache the primary keys, and look up the objects from the database. (Typically, primary key lookups are pretty cheap. It’s the SELECT queries to find those IDs that are expensive).

Taking this a step further to avoid database hits entirely, you can still cache Django models safely if you only maintain one cached copy of a model instance. Then, it is pretty trivial to invalidate that cache upon changes to the model schema. Our solution was to just create a unique hash of the known fields and adding that to our cache key (e.g. Foo:96f8148eb2b7:123). Whenever a field is added, renamed or deleted, the hash changes effectively invalidate the cache.

Conclusion

Django is definitely a powerful and feature-filled framework for getting started on your backend service, but there are subtleties to watch out for that can save you headaches down the road. Defining Django apps carefully and implementing good code organization up front will help you avoid unnecessary refactoring work later. Meanwhile, by taking full control over your database schema and being deliberate about how you use Django features like GenericForeignKey’s and the ORM, you can ensure that you aren’t too coupled to the framework and and migrate to other technologies or architectures in the future.

By thinking about these things, you can maintain the flexibility to evolve and scale your backend in the future. We hope that some of the things we’ve learned about using Django will help you out in building your own apps!

All types of Dashers use DoorDash on a daily basis to complete deliveries. Some people drive cars, while others use bikes, motorcycles, scooters, or even walk. Each form of transportation has its own unique set of skills. Scooters can go through traffic easier, but are limited in the size of food they can carry. Bikers don’t have to worry about parking, but hills can be tough. Cars can pick up big catering orders but sometimes have to circle the block looking for a parking spot. Knowing that each type of Dasher has its own tradeoffs, today we’re adding another Dashing method into the mix, with it’s own unique set of skills and abilities: robots.

Today, we’re announcing a pilot partnership with Starship Technologies to test integrating their delivery robots into DoorDash’s operations. The partnership will begin by trialing robot deliveries in Redwood City, CA, in the heart of one of our most popular markets. As part of the test, we’ll look to see if robots can provide the speed and service customers come to expect from DoorDash, while also exploring how customers interact with the robots and learning how the robot works with the restaurant’s operations. People can expect to see Starship robots carrying a DoorDash order in Redwood City in the coming weeks.

We see robot deliveries as a unique complement to the existing Dasher community. Since, Starship’s robots have a smaller carrying capacity and drive on sidewalks, they are better suited for carrying a small meal down the street, rather than a few pizzas. We expect to use robots to deliver these smaller, short-distance orders that Dashers often avoid, thereby freeing up Dashers to fulfill the bigger and more complex deliveries that often result in more money for them. We also plan to explore using robots to bring food from a restaurant to a local hub: with this approach, Dashers would no longer need to park outside a restaurant or wait for the food, but could simply meet a robot at a parking lot to pick up the food and take it directly to the customer. Ultimately, we think we can use robots to improve the Dasher experience and make the deliveries they do even easier and more efficient.

DoorDash and Starship share a vision of transforming last-mile delivery. While some companies have pursued autonomous technologies by flouting local laws, Starship has already received approval to test their robots in Redwood City. And while other companies are exploring robotic vehicles that lack the infrastructure for delivery, we’re excited to be using our existing logistics network, meaning you’ll see robots fulfilling deliveries in weeks not years.

DoorDash is first and foremost a technology company. When we started the company, we knew that the only way to solve the complex logistics problems of a three-sided marketplace was through software. Over the past few years we’ve built technologies for placing orders, receiving orders, dispatch, support, and more, while pioneering new innovations like DoorDash Drive’sfulfillment platform and the Delight Score for ranking restaurants. We’re excited to continue pursuing innovation and experimentation with this newest announcement and hope to better learn how autonomous robots can improve the DoorDash experience for customers, merchants and Dashers.

At DoorDash our engineering teams are constantly building out new code to improve our user interface (UI) on iOS. Recently, we developed our own unique testing system that combines a framework called Keep it Functional with Quick, a behavior-driven development framework. We have seen that our KIF-Quick system provides the very best testing results and are proud to announce that that it’s an open source project, so we encourage others to take a look and give it a try.

To dive deeper, we chose to use Keep it Functional, or KIF for testing for many reasons, but one stands apart — reliability. We need to be able to trust that a test is 100% accurate, the test either passes or fails, everytime. KIF also tests quickly and with ease, which was a major draw to the framework. It’s key that a test does not take up too much time to run, while still providing reliable results.

At the same time, we also develop and write our unit tests in Swift and use the Quick and Nimble frameworks to help. For more context, Quick is a behavior-driven development (BDD) framework for Swift inspired by RSpec. We chose to use Quick because it is easier to read due to its organization, which helps making testing code a lot easier.

Using Quick for our iOS UI testing gives engineers more readable specifications with nested contexts. Plus, our UI tests benefit even more from BDD style when unit tests in our domain specific language are shared beyond the development. Which is why at DoorDash we have chosen to use the two in conjunction. KIF-Quick gives us hands down the best experience and results for UI testing because of its ease and reliable outcomes — because after all, they say “your code is only as good as your test”

Examples

Compare two examples of tests for app login below. The first example is using KIF-Quick and the other shows a regular XCTest.

KIF Spec using Quick BDD syntax in Swift

KIF Test in Objective-C

As you can see from above two examples, the first (using KIF-Quick) offers more readable syntax, which is especially useful in organizing multiple contexts.

If you want to learn more, check out KIF-Quick here, we hope you find it just as beneficial for testing as we have.