Skip to content

Blog


Eight Things We Learned from Implementing Payments in the DoorDash Android App

October 5, 2021

|

Harsh Alkutkar

Effective implementation of payments in a mobile app requires precise attention to factors such as payment methods, the user experience, and fraud prevention. The critical importance of mobile payments to a business means engineers should take a thoughtful approach, anticipating all eventualities. At DoorDash, we discovered eight essential factors that help create a robust and successful mobile payment system.

For any mobile application, the payment component is key to the customer shopping experience. An easy checkout experience leads to higher conversion and increased sales. In recent years, the use of mobile payments increased significantly across sectors including retail, travel, food delivery, and mobile gaming. 

Infrastructure from credit card companies and mobile operating system makers supports frictionless transactions, but mobile engineers need to do their part as well. DoorDash has processed more than 2 billion orders, so the factors we cover here should be helpful to other companies launching their own mobile payments.

How mobile payments are typically implemented

Before we jump into the lessons we learned, let's first review how payment components are typically implemented in mobile apps. When making an online order, users submit their card information to a payment gateway such as Stripe or PayPal. The gateway encrypts this information and in turn facilitates the transaction with payment processors. The payment processor talks to the issuing bank and requests approval. The approval then bubbles to the backend, which lets the client know if the payment was accepted or declined.

Figure 1: In a typical mobile payment flow, the app sends cart and payment information to a backend, which seeks authorization for the payment from an appropriate financial institution.

In addition to requiring complicated data flow between internal and external systems, mobile payments are complex for the following reasons:

  • Multiple payment methods (Paypal, Google Pay, Stripe, etc.):
    • Payment methods can be a digital wallet on the device such as Google Pay, or through an external payment gateway, such as Stripe or PayPal. We want to offer as many options as possible to the user, but it’s challenging to enable all the payment options available.
    • Each payment method requires its own complicated integration into the app.
    • Each payment method requires a custom testing strategy.
  • User experience: 
    • Mobile payments need to be as frictionless as possible to enable quick checkouts.
    • Not only does the UI need to work with all payment methods, but it also has to work for new and existing users. Multiply the number of payment methods with new and existing user flows and we have a complex matrix of flows to implement and test.
  • Performance:
    • The app must remain performant while still supporting all payment methods.
    • We need to ensure the app has all the necessary information to start processing payments as quickly as it can while still being up to date with the information on our servers.
  • Testing: 
    • Testing cannot be an afterthought. We need to design the backend and the app in a way that allows each payment method and flow to be isolated and tested before a release.
  • Fraud: 
    • Anti-fraud measures need to be implemented in any app that includes a mobile payment component.
  • Location: 
    • We need to account for the user’s region before processing their payment to comply with each country’s laws and regulations.

Every app developer is likely to experience these pain points eventually, so we thought it would be useful to share our experience with payments.

What we learned from implementing payments in our Android app

Figure 2: A typical screen for users to add their preferred method of payment 

Plan and design for future payment methods

Our first versions of the DoorDash Android consumer app only accommodated credit cards and Google Pay, so the database structure and models were all stored as “Cards.” This architecture led to developers trying to combine the result of the cards query in the database along with other payment methods such as Google Pay before sending a consistent result to the view model or presenter. We also were experimenting with PayPal at the time and we had to pivot to accommodate it. 

In an earlier version of the DoorDash app, many decisions were made at higher architectural domain layer objects, such as the manager or the view; they should have been made in the repository layers and also persisted the state in the database. In our tech stack, managers are classes that don't maintain data or state; rather, they interact with the data layer, and then modify and make calculations based on information from it. Because of this, we think of managers as stateless abstractions for encapsulating our business rules. For example, the logic for finding the current payment method was duplicated at the manager layers or higher, when it could have been in a single source of truth at the repository or database layers. To add to the complexity, the concept of credits was introduced later, which we did not initially plan for at the repository or database layers. This caused recalculation of pricing information in several domain layer objects  or view models that could have been avoided by better planning and pushing down the logic into lower infrastructure layers.

This proliferation is why we introduced the notion of payment methods, which can handle payment cards, Google Pay, and PayPal. Broadly, we classify the payment methods into two categories: local payment methods that are part of the device such as Google Pay, and other payment methods that require implementing interactions with a payment gateway such as Stripe.

We should be able to query each of the methods for their associated functionality and use them throughout the app as needed. We included a property that indicates if there are digital wallets on the device such as Google Pay, or if they only exist on the backend. We termed these digital wallets as “local payment methods”.  An example of this is given in figure 3 below where Payment Methods is the abstract class that contains common properties or methods that can be inherited by concrete payment methods.

Figure 3 The Class diagram indicates different types of subclasses (payment methods) that inherit the common functionality and properties of the payment method superclass.

Beware of restrictions and implementation guidelines specific to payment methods

Payment vendors may have specific ways they want to be portrayed in an app. For example, Google Pay’s strict UX guidelines explain how to display its logos and buttons. In addition, Google Pay also requires that it be the primary payment option wherever possible. The app’s assets need to comply with all payment vendor guidelines.

Make the onboarding process easy for first-time users 

We recently rewrote much of our payments stack on Android. When doing this, we realized that most first-time users do not have payment methods set up. The app wouldn’t let them check out and instead showed an error. Users didn’t know what to do when they got that message, so we had to redesign the flow to onboard new users swiftly. One suggestion here is to use the local payment method on the device, such as Google Pay, because it lets users add a card when they initiate the flow. This means the users don’t need to have a card on file with the app to make the payment.

Plan for consumers in different countries or traveling consumers

Payments usually can’t be implemented in a generic way that scales worldwide. Each country typically has its own accounting methods. For example, if a consumer is registered in the U.S. but travels to Canada and orders from a restaurant, there are technical, legal, and accounting implications for the company.

Also, some payment methods might need extra verification or information in other countries. 

In such cases, we have to make sure that the publishable API keys (that are meant solely to identify an account with Stripe) are country-specific and ensure we fetch them based on the consumer's location. Doing this also eliminates any accounting issues or tax issues that might arise as a result of incorrect billing.

Security and fraud

Most businesses aren’t prepared to deal with mobile fraud. Fraudsters try to find bugs or loopholes in each new app that gets launched and can use a variety of methods to game the system. We have an in-house fraud detection system that presents the user with additional verification checks when it suspects an action — such as checkout, login, or profile edit — might be fraudulent. These verification checks are designed to be very difficult for fraudsters to pass but easy for well-intentioned users. Our in-house fraud detection systems consist of machine learning models and rules engines that are constantly being updated to keep up with emerging fraud vectors. 

Storing API keys or secrets on the device is a bad idea and should be avoided. The best way is to get keys from the backend. This method comes with its own challenges, however, because the app would need to cache API keys or secrets and have logic in place to invalidate them and fetch a new key when needed. It’s also important to be careful that the API keys are retrieved at the start of the app and have appropriate retry logic in place so payments on the app don’t fail for a user.

Plan for testing

Most payment providers such as Google Pay or Stripe have test API keys and live API keys. The idea is that the test keys are used to send transactions to a sandbox environment (with no fees incurred) while the live keys are meant to be used on production. Ensure that the backend and client both accept and work on these test keys as well as the live production keys.

The app needs to be in a state where test tokens that represent sensitive card or bank details and test transactions are accepted by backend systems. All major payment processors offer this functionality with some additional setup needed. It’s also crucial to test the build by releasing it through the Google Play Console’s Test Tracks. Signing can affect how Google Pay functions. Google Pay offers a test mode that developers can set up in their apps.

Plan for performance

It’s absolutely critical to keep an app performant while it processes payments. We learned that caching the payment methods and cards hastens the checkout process for a consumer because there’s less waiting for payment information on the cart and checkout screens. This has to be done with care and must account for numerous error cases where the backend and the device may be out of sync. Performance monitoring is also critical; it’s important to know how users interact with payment flows in the app and when those flows break.

For our Android app, we rely on caching of most of our payment information and ensuring that when users start or use the app we have the most up-to-date payment information on file. Payment information is not only required on the final checkout, but also throughout the app flow to calculate benefits at the order cart screen and show items in the cart and their final cost, including credits and benefits.

Add lots of telemetry

Telemetry, or tele-metering, is automated remote measurement and data collection.

Telemetry collects readings and sends useful information that helps monitor the health of features and lets us know if the app is truly serving our business goals. If users are stuck or crash somewhere in the app’s flow, metrics will reflect that information. Telemetry contains events, which are human-readable verbs or actions we want to track. These typically represent some action that the user performed along with some metadata for that action, such as when the user selects a different card or when there's an error as the user tries to pay.

When we rewrote our payments stack on Android, we could see that consumers were running into errors trying to pay. However, we didn’t have enough data on the flows that caused these issues. We added additional telemetry to the user payment flows to detect first-time users, how many payment methods they had, the type of device used, whether Google Pay was set up with an existing card on file, and similar issues. We also added telemetry to record payment failure reasons, such as invalid card, network errors, and fraud check errors.

Payment flows can be tricky to debug if they don’t work properly in the wild. Instead of just sending generic telemetry such as “payment failed,” the system should include as much information as possible, including such things as error codes from providers, device information, or any diagnostics that can help to identify the state of the app when it failed. Be careful not to include any personal identifiable information (PII) or any payment information that could be compromised by an attacker.

Conclusion

Successful mobile payment implementation is critical to business success. If users can’t easily make payments through an app, they will be denied a company’s services and likely will seek out a competitor. Implementing mobile payments is extremely challenging because of such factors as integration issues, security, testing, and performance. But getting mobile payments right from the start can make or break a new business. By sharing our experience, we hope that newer developers will be able to plan ahead for such challenges and ensure that their payments are integrated correctly.

About the Author

  • Harsh Alkutkar

Related Jobs

Location
San Francisco, CA; Mountain View, CA; New York, NY; Seattle, WA
Department
Engineering
Location
San Francisco, CA; Sunnyvale, CA
Department
Engineering
Location
San Francisco, CA; Sunnyvale, CA; Seattle, WA
Department
Engineering
Location
Pune, India
Department
Engineering
Location
San Francisco, CA; Seattle, WA; Sunnyvale, CA
Department
Engineering