Uber is committed to creating safer and more reliable transportation solutions for users worldwide. To fulfill this vision, mobile onboarding—an app’s entry point—must be quick, painless, and easy-to-navigate.

Before we released Uber’s new rider app in November 2016, our engineering teams created unique rider experiences tailored to specific markets, and as such, responsibility for mobile onboarding was divided among multiple teams. Although successful for meeting regional demands, these duplicated engineering efforts resulted in a fragmented system that increased overall developer complexity and inhibited rapid feature iteration.

As the rider app redesign and rewrite fast approached, teams sharing responsibility for onboarding new riders collectively acknowledged the need to streamline mobile onboarding to satisfy future product requirements. To accomplish this, we opted to start over completely.

In this article, we discuss how we unified mobile onboarding experiences for the new rider app by redefining our system design, revising our supporting back-end service, and reconstructing our client frameworks to meet growing demand from users around the world.

 

Designing a toolbox of parts

Before we implemented our new solution, we first defined a broad set of challenges affecting the existing product by performing exhaustive user research to acquire an in-depth understanding of the obstacles impeding new and existing riders from getting started on the app. After gathering feedback from several regions, our team supplemented these findings with analysis of mobile analytic events and a comprehensive industry audit of similar onboarding experiences. Equipped with these insights, our team identified several inefficiencies contributing to a sub-optimal UX within our existing product and began simplifying the core design to reduce cognitive overload, provide contextual affordance, promote rapid experimentation, and support dynamic internationalization.

Remove obstacles that create indecision

Between our user research and examination of mobile analytic events, we recognized a subtle yet revealing flaw with our product’s entry point. Before users could begin the onboarding process, our UX required them to consciously choose between Sign In and Register options, depicted in Figure 1, below:

Figure 1: Three iterations of Uber’s mobile onboarding interface within the rider app highlight how we have streamlined its design over the years.

Data inspection revealed that users often bounced between choices by tapping back-and-forth on both repeatedly before attempting either method. To make matters worse, our team also discovered that a sizeable cohort of users frequently chose the wrong option, resulting in higher error rates, lower conversion metrics, elevated churn, and an overall inferior onboarding experience.

Rather than adding clarity to this set of options, we eliminated them altogether by requesting a unique piece of personally identifiable information: a mobile number. By removing options and requesting data upfront, our product could intelligently and discreetly usher users through assorted onboarding scenarios. Once we produced an elegantly simplified and more welcoming design for onboarding users, our team aimed to extend this new UX throughout the entirety of each onboarding process.

Diminish clutter and confusion

After refining our product’s entry point, our team re-evaluated the techniques by which we collected information in subsequent steps to onboard users completely. As Uber’s service evolved over time, existing approaches became increasingly cluttered and lacked the necessary affordances for straightforward and frictionless progression. Our research indicated that existing methods that collected multiple disconnected pieces of information at once, such as those in Figure 2, introduced cognitive overload.

Figure 2: Previously cluttered designs stand in stark contrast to our new information collection interface.

To prevent user confusion, present comprehensible instruction, and provide sufficient guidance during this process, we developed a system design by separating data requests into their individual components, demonstrated in Figure 3, below:

Figure 3: Our rider app rewrite incorporates simplified information collection steps that provide concise instructions for getting started on the app.

By doing so, we not only minimized complexity, but also built a foundation to support product requirements, including continuous validation, swift experimentation, and mutable international behavior.

Verify input frequently

Our research also indicated that users who provided insufficient or invalid information encountered difficulty onboarding as a direct result of infrequent input validation and ambiguous, unactionable feedback. Due to the nature of our existing solution, information was only validated after all required data had been gathered and a network request to submit that information had been issued.

Unfortunately, the existing back end operated as a fail-fast system and returned a single error when recognizing an invalid input. Users submitting multiple invalid data points were subsequently required to individually correct each error. Due to additional network requests and compounding latency, failure to aggregate all errors during initial input validation led to situations where users spent more time correcting mistakes than they did entering input. Furthermore, once users selected either the Sign In or Register options, the existing product was incapable of automatically redirecting them when the information given was intended for the alternative.

By designing a product entry point that requested user information, our team guaranteed input validation would occur as soon as possible. Similarly, by requesting one thing at a time, we could ensure that input validation occurred often. Separating information collection requests into individual components also improved our product’s ability to display errors in a coherent and digestible format. Moreover, product demands to validate input more frequently, aggregate error messaging, and reactively guide users through varying onboarding scenarios necessitated an evolution of the back-end service powering our existing product.

Coordinate experiences dynamically

From the inception of our effort to unite multiple product experiences into a single mobile onboarding framework, our team focused on two critical areas of weakness: experimentation and internationalization. During design explorations, we emphasized devising a system to enhance these drawbacks and mandated superior support from our final product.

Within our existing implementation, experiments assessing hypotheses typically generated complicated feature flag combinations with complex control flows in which race conditions occasionally produced perplexing behavior. When operating within these parameters, adding to, removing from, or reordering this sequence of steps decreased reliability. Moreover, launching in international markets entailed building distinct flows to accommodate cultural differences and regional adoption practices of certain technologies (e.g., credit cards and email). Compiling multiple onboarding experiences specific to regional markets had a detrimental effect on application binary size. Fortunately, by separating data requests into modular components aimed at collecting distinct units of information, as displayed in Figure 3, above, our simplified design empowered our product to add, remove, and sequence information collection steps as experiments or market characteristics demanded.

Learning from past mistakes, we recognized the excessive use of feature flags and repeated creation of independent experiences would not scale well over time. Evaluating our expansive product requirements to combine multiple team’s experiences quickly negated the possibility of an engineering architecture driven solely from the client. Additionally, our modernized system design far exceeded the functionality offered by the back-end service powering our existing product. To satisfy product requirements, our team elected to completely rewrite the back-end service, thereby enabling server-driven orchestration of mobile onboarding experiences.

 

Evolving the back end

Before our mobile onboarding re-design, our product provided sparse input validation throughout the information collection process because the back end only inspected data upon receiving a network request submitted as part of the onboarding experience. Similarly, our product offered inadequate error messaging after submitting invalid data because the back end only supplied one error per response, as opposed to multiple errors per response.

Moreover, new international market launches required back-end hacks to bypass legacy requirements necessitating non-vital information for onboarding (e.g., payment profiles, promotion codes, and profile photos). Since the back end was stateless, it provided no control over business logic and therefore offered no practical way to shift between onboarding scenarios in response to user input. As a result, product experimentation repeatedly required client changes, new build cuts, and application binary releases.

So, we decided to replace the back end entirely using Go and the Cassandra database management system. Defining precise client-server contracts was imperative to successfully fulfilling the ambitious product requirements, simplified designs, and revitalized UX. By crafting a new stateful back-end service capable of server-driven client orchestration, our team had the foundation necessary to prune collection of nonessential data, frequently validate input, expedite experimentation, and optimize dynamic experiences for international markets.

Communicate with a question-answer cycle

To satisfy our product requirements and meet project deadlines, we partnered with a team building advanced web front-end mechanisms for onboarding experiences to define stringent client-server contracts. Our collective effort produced a straightforward communication scheme predicated on a question-answer cycle (Q-A cycle) between back end and client as represented in high-level form, depicted in Figure 4, below:

Figure 4: In our mobile onboarding architecture, answers are submitted in response to questions and errors until a session is received.

To support server-driven client orchestration of information collection steps, the back end provides a question to clients specifying a list of data types required for state machine transition. Once clients submit an answer containing user-gathered data corresponding to these data types, input is then validated by either the back end itself or through a number of services within the company. When answers are considered valid, the back-end state machine transitions according to the client flows defined within its service and the input received in previous answers. If the back end has not accumulated enough information to completely onboard users in a specific market, it issues a new question requesting additional data types, and the cycle repeats.

Instances during which the client submits invalid data results in failed state machine transitions which prompts the back end to respond with a series of errors that clients are required to correct before proceeding. After adequate data has been assembled to transition users into an onboarded state, the back end transmits a session to the client, thereby concluding the cycle.

Style using contextual reference

In addition to requested data types, questions provided by the back end also contain contextual reference to the current onboarding scenario based on previously received input. Since each question incorporates this frame of reference, our product repurposes these information collection steps by styling them appropriately, as displayed in Figure 5, below:

Figure 5: In our redesign, information collection steps may be dynamically styled for differing onboarding scenarios, as when gathering user passwords (depicted above).

By styling data-gathering steps for varying onboarding scenarios, our mobile onboarding framework reduces redundancy by offering reusable components and ensures minimal impact on application binary size. Providing context also informs clients when the back end modifies the onboarding scenario in response to previously received answers, i.e., shifting from Sign In to Account Recovery to handle forgotten passwords.

Provide flexible instruction

Alongside reference to the onboarding scenario, questions prepared by the back end include explicit client instructions detailing how to gather specific data types. Under particular circumstances when the back end issues a question soliciting answers to a combination of data types (e.g., first name, last name, email, and mobile number), it may instruct the client to gather those data types by using a single step or through a sequence of steps, the latter of which is depicted in Figure 6, below:

Figure 6: The back end may specify different sets of data gathering steps to collect identical data types.

The capacity to adapt our product’s behavior by adjusting details provided by the back end within the Q-A cycle significantly improves our ability to experiment rapidly and efficiently across international markets.

Adapt system for performance

To provide additional functional flexibility, we also incorporated an optional, alternate set of questions clients can answer in the event users are unable to provide valid data to the original question (e.g., forgotten password or voice mobile verification). Submitting an answer to an alternate question transitions the back-end state machine into a new onboarding scenario and the Q-A cycle proceeds as previously described.

We also incorporate instructions into questions for clients to gather data types using single or multiple information collection steps from which a single answer is generated after collecting all required data. The back-end service’s ability to dynamically adjust the frequency of network requests through batching support vastly enhances both the UX and our product’s capacity to adapt in markets where network connectivity is poor or network latency is high.

 

Re-architecting mobile onboarding

With a fresh UX featuring a revamped design and a new back-end service to support our product requirements, our team began architecting the next iteration of mobile onboarding using our RIB architecture. The following sections describe how we applied this open source architectural pattern to construct the essential onboarding components illustrated in Figure 7. Likewise, the ensuing sections detail the responsibilities that critical components must assume to execute server-driven client orchestration using the client-server contracts.

Figure 7: Uber’s new mobile onboarding framework employs our open source RIB architecture.

Entry

During application startup, our mobile onboarding framework attempts to retrieve the stored session saved on a user’s device. In the event that no session is found (e.g., new app download, a user logged out, etc.), the rider app does not consider users onboarded and therefore proceeds to display our product’s UX to obtain one. By attaching the Entry RIB, the rider app relinquishes control until a session has been received.

The most important function of Entry is to alternate between our product’s inactive and active states based on user interaction. After being attached by the rider app, Entry navigates our product into an inactive state by attaching a Welcome RIB. To accomplish this task, Entry builds an applicable Welcome by performing the operation portrayed in Figure 8, below:

Figure 8: During startup, our system’s Entry RIB attaches a Welcome plugin if applicable (on left); otherwise, it attaches an app default (on right).

As shown on the left-hand side of Figure 8, Entry selects the first applicable Welcome from a list of possibilities created by using a Welcome plugin point, in this case WelcomeA. When no plugins are found to be applicable, as demonstrated on the right-hand side of Figure 8, Entry instead utilizes a default Welcome provided by the rider app. By taking advantage of plugin points, our mobile onboarding framework empowers Entry to display Welcome RIBs that offer an alternative appearance and distinct functionality, improving our ability to experiment and adapt the onboarding experience to regional markets.

After attaching an applicable Welcome, our product remains in an inactive state until user interaction prompts Entry to navigate the mobile onboarding framework into an active state by attaching the Orchestrator RIB. Similarly, when users decide to cancel their onboarding attempt, Entry returns our mobile onboarding framework to an inactive state by navigating to the previously selected applicable Welcome.

Welcome

While Welcome’s most prominent responsibility is displaying a user interface (UI) that compels an engaging and enjoyable user interaction, its fundamental function is forwarding a client-defined question to Entry, as shown in Figure 9, below:

Figure 9: In the rider app’s mobile onboarding framework, Welcome sends Entry a client-defined question Orchestrator uses to initiate the collection of information.

This client-defined question is subsequently relayed to Orchestrator and utilized to initiate a specific onboarding process. To preserve fast startup performance, our mobile onboarding framework refrains from issuing blocking network requests to acquire an initial question from the back end. As a result, communicating a client-defined question is an indispensable assignment for Welcome as Orchestrator has no other means of gathering information to prepare an initial answer for the back end.

Consequently, by employing a Welcome plugin point within Entry, our product can easily assess the conversion metric impact of utilizing different initial questions, as well as enable us to customize these questions based on specific circumstances. For example, the rider app provides our product with a default Welcome specifying an initial question requesting an answer containing a mobile number. However, Welcome plugins may define alternate questions directing Orchestrator to begin the onboarding process by requesting alternate data types (e.g., email address or social network) before submitting an answer.

Orchestrator

Although Entry and Welcome RIBs provide meaningful separation of concerns, Orchestrator unquestionably performs the most vital tasks within the mobile onboarding framework. The following sections describe how Orchestrator manages communicating with the back end using refined client-server contracts to handle question presentation, oversee error correction, and administer answer preparation.

Answer questions

Once Orchestrator receives a question in either client-defined form as described above or in response to a previous network request, its prime objective is submitting a relevant answer to the back end. Next, we illustrate the chain of events which occur and detail the internal components Orchestrator harnesses to execute this task.

To collect the data types requested, Orchestrator forwards questions received to a component responsible for transforming them into a list of InfoStep RIBs capable of gathering data from users, as depicted in Figure 10, below:

Figure 10: Orchestrator provides Transformer with questions and receives a list of InfoStep RIBs in return.

To accomplish its lone function, Transformer extracts contextual references and client instructions from the questions provided by Orchestrator. Using this data, Transformer proceeds to build InfoStep RIBs by carrying out the actions illustrated in Figure 11. The transformation of provided questions directs Transformer to build InfoStepA, InfoStepB, and InfoStepC RIBs. Transformer initiates this process using an InfoStep plugin point to create a list of plausible InfoStep plugins and prepends those deemed applicable to internal framework defaults. Transformer then compiles a list of InfoStep RIBs by iterating the extracted client instructions, filtering the constructed list by type, and building the first type-matching InfoStep available by providing the extracted contextual references.

Figure 11: Transformer assembles a list of applicable InfoStep RIBs built to satisfy provided questions.

As Figure 11 shows, InfoStepB and InfoStepD plugins are evaluated to be applicable by the InfoStep plugin point and thus prepended to the list of defaults. As a result, the InfoStepB plugin is built in place of the default during the selection process, and because no InfoStepA or InfoStepC plugins are applicable, their default counterparts are built by Transformer. Alternatively, when no plugins are applicable (as displayed on the right-hand side of Figure 11), Transformer compiles a list of InfoStep RIBs built entirely from its default list.

As previously mentioned, providing an InfoStep plugin point to Transformer allows our mobile onboarding framework to more easily and safely test our methods of information collection. By creating an InfoStep plugin to override a default, our team can evaluate new designs, test UXs, and validate hypotheses to improve the mobile onboarding experience without forfeiting reliability or performance.

Once transformation is complete, Orchestrator delivers both the question received and Transformer’s returned list of InfoStep RIBs to a component responsible for maintaining a map between these items, as shown in Figure 12, below:

Figure 12: Orchestrator provides Sequence with a question and a list of InfoStep RIBs for mapping.

By mapping a question to a list of InfoStep RIBs and retaining an index, Sequence provides Orchestrator with context around the InfoStep to present, as well as scheduling its submission of answers to the back end. After receiving input, Sequence appends or replaces items within its mapping based on its index’s location, as depicted in Figure 13, below:

Figure 13: Sequence maps a question to batches of InfoStep RIBs for presentation and answer submission.

In this example, Sequence replaces its empty map with the input received by associating the provided question with the last InfoStep in the batch and adjusts its index to reference the first item in this batch. Once mapping is finished, Orchestrator continues collecting information by attaching the InfoStep referenced by Sequence’s index (in this case, InfoStepA) and moves the UX forward.

When an InfoStep successfully gathers data, user interaction provokes forward navigation of our product and prompts the transfer of this information to Orchestrator, which subsequently proxies to a data storage component, exhibited in Figure 14, below:

Figure 14: InfoStep RIBs transmit gathered data to Orchestrator which uses Info for intelligent storage.

In addition to storing the information Orchestrator provides, Info caches dirty data types to enhance the efficiency of the framework’s network interactions. Incorporating this level of functionality within Info ensures that Orchestrator only attempts to submit answers when new data has been stored for data types required by specific questions.

After sending gathered data to Info, Orchestrator queries Sequence to determine whether a question should be answered or an InfoStep should be attached. As depicted in Figure 14, Sequence returns an InfoStepB plugin indicating more information is required before Orchestrator can submit an answer to the back end. This cycle of attaching InfoStep RIBs and storing the information they collect within Info repeats until Sequence’s index references an InfoStep with a mapped question, i.e. InfoStepC in Figure 13.

Assuming Orchestrator receives a question when querying Sequence, it asks Info to formulate an answer by supplying the acquired question, as demonstrated in Figure 15, below:

Figure 15: Orchestrator submits answers to the back end formed by supplying Sequence provided questions to Info.

If Info does not store information or its dirty data types cache does not include any of the requested data types, an answer is not formed. However, when Info successfully produces an answer, Orchestrator submits the returned answer to the back end and awaits a response to continue server-driven client orchestration.

As discussed earlier, answer submissions from Orchestrator may result in additional questions necessitating further information collection, a series of input validation errors requiring user correction, or a session representing successful user onboarding. If Orchestrator receives additional questions from the back end, these events repeat. Otherwise, Orchestrator performs actions described in the following sections.

Handle errors

After Orchestrator acquires a set of errors in response to a previously submitted assortment of answers, its predominant goal is distributing them among the relevant InfoStep RIBs to correct invalid input prior to dispatching new answers to the back end.

Before InfoStep RIBs display pertinent error messaging and request users update invalid input, Orchestrator first establishes which answers are in an error state by delivering the set of errors to Sequence as displayed in Figure 16, below:

Figure 16: Orchestrator provides Sequence with errors to distribute amongst relevant InfoStep RIBs.

Since it maps between received questions and a transformed list of InfoStep RIBs, Sequence is ideally suited for managing error distribution. Upon receiving a set of errors, Sequence’s index references the last InfoStep and question associated within the current batch, as shown in Figure 17, below:

Figure 17: Sequence adjusts its index by distributing errors amongst InfoStep RIBs.

Sequence proceeds to dispense errors by iterating backwards from its index until all have been claimed by the appropriate InfoStep RIBs or a previous batch has been encountered. When an InfoStep claims errors corresponding to its responsible data types, Sequence adjusts its index to reflect this InfoStep instance. By referencing the earliest InfoStep in an error state, Sequence effectively rewinds the information collection process. For instance, despite both InfoStepB plugin and InfoStepC being in an error state, Sequence modifies its index to reference the earliest occurring InfoStep, in this case the InfoStepB plugin.

Once Sequence has completed distributing errors and InfoStep RIBs have refreshed their user interfaces to display error messaging, Orchestrator navigates backwards to the InfoStep referenced by Sequence’s adjusted index and resumes the information collection process.

Procure session

When Orchestrator obtains a session, our mobile onboarding framework accomplishes its principle purpose, thereby terminating further communication with the back end. This section describes Orchestrator’s supervision of events leading up to the conclusion of our product’s execution and the beginning of the rider app’s main experience.

Before Orchestrator shares the session with the app, it first orchestrates a series of applicable ConfigureStep plugins (e.g., payment, promotion code, push notification permissions, etc.) by performing a task similar to those described in the ‘Entry’ and ‘Answer questions’ sections. Unlike InfoStep RIBs, which are built and maintained by our team exclusively, ConfigureStep plugins are predominantly created by other teams so that they can launch crucial early experiences in the user lifecycle. By providing this platform extension, we ensure efforts to streamline the information collection process by removing data requests unnecessary to user onboarding do not cause any repercussions for these teams.

Orchestrator uses a ConfigureStep plugin point to create a list of prospective ConfigureStep plugins which are evaluated for applicability using experimental data, regional market preferences, and details collected during the onboarding process (e.g., onboarding scenario, mobile country code, etc.), as depicted in Figure 18, below:

Figure 18: Orchestrator supplies Sequence data for mapping before sharing the session with the rider app.

As shown on the left-hand side of Figure 18, ConfigureStepA and ConfigureStepC plugins are considered applicable and subsequently prepared for presentation to the user. To display ConfigureStep plugins, Orchestrator exploits Sequence’s mapping functionality by providing the received session and a list of applicable ConfigureStep plugins wherein Sequence adjusts its index for future navigation.

Because ConfigureStep plugins are self-contained components provided by external frameworks, they furnish no information for storage, issue their own network requests as necessary, and simply notify Orchestrator when their assignments have ended so the onboarding experience can advance. When the final ConfigureStep plugin completes its associated tasks, Sequence returns the received session to Orchestrator much like it returns a question when reaching the end of an InfoStep batch. Upon receiving the session from Sequence (or the back end when no ConfigureStep plugins are considered applicable, as demonstrated in Figure 18), Orchestrator immediately shares the session with the app. Once this session is received, the product experience built within our team’s mobile onboarding framework is detached and deallocated as the app launches users into its main experience.

 

Building an app-agnostic framework

After months of collaborating with groups across the company, our team fulfilled our ambitious and meticulous product requirements within the new rider app’s tight deadline. Our results were fourfold:

  • We simplified the core design of the information collection process, leading to reduced cognitive overload and improved overall UX.
  • We crafted a new back-end service to provide server-driven client orchestration, thereby enhancing experimentation and enabling dynamic international flows.
  • We architected new client frameworks taking advantage of convenient client-server contracts to unify mobile onboarding experiences; as a result, engineering efforts were not duplicated but instead immediately shared across regions.
  • We built a platform extending our mobile onboarding framework to provide external teams with an integration point for their own logged-out experiences.

Observing our product’s performance and framework’s reliability after launch, our team contemplated whether we could do more to unify mobile onboarding experiences across Uber’s other products. After witnessing the success of our client frameworks within the rider app, we were excited by the challenge of refining them to provide an agnostic integration for mobile apps at Uber. In theory, creating an app-agnostic mobile onboarding framework would further reduce redundant engineering efforts and enable other groups to reallocate resources. Once the idea gained traction and interest grew among product teams, we put substantial effort into growing the initiative. 

To accomplish this feat, our team started compiling a list of rider app-specific dependencies referenced throughout our mobile onboarding framework. Because our implementation relied on concrete instances unavailable in other mobile apps, it was imperative that we instrument their inclusion regardless of the consuming integration. Through the use of dependency inversion and a reliance on protocol-conforming implementations rather than concrete classes, our team removed these dependencies and provided a process for apps to supply the components necessary for integration. 

Since our product requires a default Welcome RIB and allows for the provisioning of optional, alternate Welcome, InfoStep, and ConfigureStep plugin points, our team built a broad set of customization tools for prospective consumers. By necessitating a default Welcome, Entry can display a unique inactive UX, thereby guaranteeing that Orchestrator begins the data gathering process as an integration’s product requirements demand.

To tailor the look and feel of default InfoStep RIBs so they match consuming apps, we allow integrations to provide a theme comprised of preferences (e.g., colors, fonts, sizes, etc.) that alter the treatment of information collection steps. Moreover, while InfoStep RIBs offer default appearance and functionality, consuming integrations can override UIs or information collection behavior completely by supplying an optional InfoStep plugin point. Similarly, if consuming apps intend to display additional experiences after Orchestrator receives a session from the back end, they can do so by providing an optional ConfigureStep plugin point.

Upon importing our mobile onboarding framework and injecting protocol-observant implementations, consuming integrations simply attach Entry after encountering a logged-out state. With flows defined in the back end describing their respective use cases, our mobile onboarding framework is empowered to handle the heavy lifting needed to onboard new users across all Uber products.

With all dependencies specific to the rider app removed and a bevy of customization techniques added, teams across the company began integrating our platform into their apps.

 

Planning for the future

After producing client frameworks to unify mobile onboarding experiences at Uber, we have already begun considering prospective improvements to our server-driven platform. While our current implementation is stable and reliable, we continue to iterate on our solution to make it more performant, and although we have made great strides in the areas of experimentation and internationalization, more can be done to further reduce client modification.

Between extending the back end to generate additional dynamic contextual references (e.g., copy, images, etc.) and supply client instructions, including preferred transformation-rendering engines (e.g., native, web, dynamic native forms, etc.), the future of unified mobile onboarding experiences at Uber is arriving soon.

Kyle Gabriel is a software engineer on Uber’s Rider Growth team. He is one of the technical leads responsible for architecting the cross-platform mobile onboarding framework and helped define client-server contracts for the new back-end service. In his spare time, he enjoys weightlifting, competing in Spartan Race, and attending concerts.

If helping Uber engineering create more seamless user experiences interests you, consider applying for a role on our team.

 

Comments
Kyle Gabriel

0 Comments