Uber developed a new mobile architecture to address issues with their previous architecture and support continued growth. The new architecture uses scoped RIB components with single responsibilities, plugins to enable experimentation and isolation of new features, and a reactive data flow model. These changes resulted in improved stability, velocity of development, and developer happiness.
7. 99.99% availability of core flows
Enable global roll-back of core flows to a
guaranteed working state
Testing as a first-class citizen
Architectural goals
8. 99.99% availability of core flows
Enable global roll-back of core flows to a
guaranteed working state
Testing as a first-class citizen
Support Uber’s growth for years to come
Narrow and decouple functionality as much
as possible
Architectural goals
9. 99.99% availability of core flows
Enable global roll-back of core flows to a
guaranteed working state
Testing as a first-class citizen
Support Uber’s growth for years to come
Narrow and decouple functionality as much
as possible
Provide rails for both design and code
Consistency in engineering, consistency in
UX
Architectural goals
10. 99.99% availability of core flows
Enable global roll-back of core flows to a
guaranteed working state
Testing as a first-class citizen
Support Uber’s growth for years to come
Narrow and decouple functionality as much
as possible
Provide rails for both design and code
Consistency in engineering, consistency in
UX
Monitoring is a first-class citizen
Automatic analytics, logging, debugging, and
tracing
Architectural goals
11. 99.99% availability of core flows
Enable global roll-back of core flows to a
guaranteed working state
Testing as a first-class citizen
Support Uber’s growth for years to come
Narrow and decouple functionality as much
as possible
Provide rails for both design and code
Consistency in engineering, consistency in
UX
Monitoring is a first-class citizen
Automatic analytics, logging, debugging, and
tracing
De-risk experimentation
Application framework with Plugin API
Architectural goals
12. 99.99% availability of core flows
Enable global roll-back of core flows to a
guaranteed working state
Testing as a first-class citizen
Support Uber’s growth for years to come
Narrow and decouple functionality as much
as possible
Provide rails for both design and code
Consistency in engineering, consistency in
UX
Monitoring is a first-class citizen
Automatic analytics, logging, debugging, and
tracing
De-risk experimentation
Application framework with Plugin API
Make magic
Performance second to none, graceful
degradation on low-end devices and networks
Architectural goals
17. Dealing with State
Lots of apps have tricky asynchronous, state issues
At Uber, this compounds quite a bit
Uber’s apps have a lot of asynchronous state, from multiple data sources
150+ contributors
22. Stability and Code Quality Impact
Objects that live longer than
necessary are exposed to more
state they don’t need.
In Uber’s apps in particular, many of
these stateful objects observe
different streams of data. This
means these objects might get
frequent updates even when they
aren’t being used.
class DriverIsOnTheirWayToaster {
private boolean isOnTripAndHasNotBeenNotified;
public DriverIsOnTheirWayToaster(TripStateStream
tripStateStream) {
tripStateStream.subscribe({ (state, driver?) ->
if (state == ON_THEIR_WAY {
isOnTripAndHasNotBeenNotified = true;
showToast(driver!.name);
} else if (state == OFF_TRIP) {
isOnTripAndHasNotBeenNotified = false;
}
})
}
}
23. Input and dependency contracts are diluted
When objects live at app scope,
they cannot have very rigid inputs
and dependencies.
Why does this class take an
optional AuthToken if it’s really
required?
How does this hurt testing?
public class AuthenticatedNetworkRequester {
private AuthToken? authToken;
public void makeRequest() {
networkClient.makeRequest(authToken!)
}
}
24. Other Issues
While most of the classes are very simple, having lots of objects around for the
entire duration of the app’s lifecycle is not super efficient.
Classes can grow to not have a clear purpose.
Although it wasn’t as bad as some of the examples, there was no standard way
to “scope” singletons and objects to different lifecycles of the app.
27. Improvements
Lots of our bugs are state related.
A pattern or framework to encourage creating objects only when relevant would help here.
The view hierarchy doesn’t really line up with business logic.
We should create a hierarchy based on business logic states, which does not necessarily map to
what is on the screen.
There was no easy, consistent way to create your own scopes.
30. Scopes
LoggedIn knows that you have valid
non-null authentication credentials.
Dependencies created and provided
here can take non-null credentials
without having to make any
assumptions.
Root
LoggedIn
31. Moving AuthenticatedNetworkRequester to LoggedIn Scope
public class AuthenticatedNetworkRequester {
private AuthToken? authToken;
public void makeRequest() {
networkClient.makeRequest(authToken!)
}
}
32. Moving AuthenticatedNetworkRequester to LoggedIn Scope
AuthToken is now guaranteed to be
available.
Other objects that depend on
AuthenticatedNetworkRequester
need to be in a logged in state to
use it.
If a dependency requires
AuthenticatedNetworkRequester
outside of the LoggedIn scope, it’s
now a compile error.
public class AuthenticatedNetworkRequester {
private AuthToken authToken;
public void makeRequest() {
networkClient.makeRequest(authToken)
}
}
33. Scopes
OnTrip knows that the user is logged
in and on a trip.
Dependencies created and provided
here can take non-null credentials and
all data related to a trip without having
to make any assumptions.
Root
LoggedIn
OnTrip
34. Moving DriverIsOnTheirWayToaster to OnTrip Scope
class DriverIsOnTheirWayToaster {
private boolean isOnTripAndHasNotBeenNotified;
public DriverIsOnTheirWayToaster(TripStateStream
tripStateStream) {
tripStateStream.subscribe({ (state, driver?) ->
if (state == ON_THEIR_WAY {
isOnTripAndHasNotBeenNotified = true;
showToast(driver!.name);
} else if (state == OFF_TRIP) {
isOnTripAndHasNotBeenNotified = false;
}
})
}
}
35. Moving DrvierIsOnTheirWayToaster to OnTrip Scope
Now DrvierIsOnTheirWayToaster is
exposed to much less state - it’s
easier to understand, and less
prone to bugs.
class DriverIsOnTheirWayToaster {
public DriverIsOnTheirWayToaster(Driver driver) {
showToast(driver.name);
}
}
50. What about other architectures?
MVC
● Massive ViewController
● Locked in view tree and business tree
MVVM
● Massive ViewModel
● Locked in view tree and business tree
VIPER
● Logic separation based on view
● Locked in view tree and business tree
51. What did RIBs give us?
● Rider app broken up into more than 500 RIBs
○ Many are reused with multiple parts of the tree
● All RIBs have less than 300 lines of code per class
● All business logic well unit-tested
53. ● Support Uber’s growth for years to come?
● Provide rails for both design and code
● De-risk experimentation
Architectural goals
Consider three of our architectural goals:
54. Support Uber’s growth for years to come
RIBs give us code isolation
We want more code isolation
60. Support Uber’s growth for years to come
Provide rails for both design and code?
De-risk experimentation
How do Plugins play into Architectural goals?
Consider three of our architectural goals:
71. New plugin points and changes to core get
extra code reviewers added automatically.
72. Support Uber’s growth for years to come
Provide rails for both design and code
De-risk experimentation?
How do Plugins play into Architectural goals?
Consider three of our architectural goals:
73. Derisking Experimentation
Roll out all new features as plugins
Every plugin is initially A/B tested
Every plugin can be disabled from our servers
UI tests ensure the app is still functional when all plugins disabled
75. Engineers want to reuse existing plugin points and now know where the
build new features
We’ve prevented six outages in production by disabling plugins
Statically ensured code isolation between the architecturally significant core
of the app and the app’s features
Results
From using plugins
76. Development Velocity
Able to sustain 500 diffs/week per platform
Diffs landed per week doubled after finishing the four month rewrite
Crash Free Rate
Immediately after releasing the new app, crash rate was better than the old app
iOS crash free free rate of 99.99% and climbing
Android crash free rate of 99.9% and climbing
Performance
App starts 50% faster
Developer Happiness
78.5 developer NPS score improvement
Results
From the new architecture
Welcome
Anyone new here? What Uber mobility is about
Today a special meetup ->
Intro Tuomas, Tony, Yi and Brian
Gonna talk about why and how we came to build a new architecture,
Jump into scopes, what they are in order to understand the architecture
Then we’re talking about RIB’s our new architecture
Finally, we’re looking at how we’re using plugins in our new architecture
4 ½ years ago in Tahoe, the entire team.
Times were different
Today we have almost 400 mobile engineers
4 ½ years ago this is what they build. The team laid the foundation, used MVC and was just hands down shipping feature after feature
Once our team started groing, the architecture came in our way
Testing everyones functionality
Regressions, weird error cases due to shared view controllers
Ownership problems
Main controller was 6000 lines of code
So we started refactoring, with some success ->
Then the UX started breaking down, too
Teams would add horizontal views everywhere
Product slider became super populated.
After careful consideration ->
We put some serious thought into this
With architectural problems AND design wanting to fully redesign the app we started a rewrite
So what do you do when you start from the basics?
You come up with architectural goals
Once we had the goals which obviously applied to both of our platforms, we thought what if we decied to use the same concept on both iOS and Android as closely as possible
Same class names, same functionality, same libraries, wherever possible
Essentially doubling your team when designing
Design once, write twice
Feature teams got a huge productivity boost out of this
So this is what we designed and implemented
Earlier on Tuomas mentioned we had six goals during our architectural development phase. RIBs help with several of these. RIBs don’t get 100% of the way there.
So let’s focus on the first architectural goal. Discuss additional ideas for how to achieve it. And then explain how similar ideas get us the other goal.
Support Uber’s growth for years to come
About code isolation
So large number of engineers can work independently in the same app as we become an even bigger company
So code becomes more reusable
Let’s talk about code isolation between RIBs. Let’s consider the Request Confirmation Steps, for example. After making a pickup request you are shown a series of blocking steps depending on your current context, like whether you’re at an airport.
Each one of these RIBs is farely independent. But what about coupling between parent and child ...
Each one of these RIBs is farely independent. But what about coupling between parent and child ...
Old notes: need to update
One way to address this is with dependency inversion. [Show dependency inversion]. Move the PickupRefinement controller into its own library with the following structure. If you have a mono-repo this isn’t too much work. But it is harder to read code. And it does force the parent of Refinement Steps to know about Refinement Step’s implementation details. Given the Rider app has a 100 different places where orchistration patterns like this occur, it doesn’t make sense to use dependency inversion in all these places
We start thinking about plugin mechanisms. And we settle on one that still allows you to structure your application in a natural way, but with strict seperation between the plugin consumer and the plugin child by using static analysis on Android and framework dependency restrictions on iOS.
The Refinement Steps just consumes Refinement Step interfaces and can’t possibly be coupled to any of the children refinement step classes.
We have this simple plugin idea: A plugin mechanism that can return plugins of some predefined type to a consumer with code isolation.
This gives us more code isolation. But what else does it give us? If all it gave us was code isolation I wouldn’t be spending 20 minutes talking abou it. How can we use it to provide product and code rails?
LocationEditor button plugin used by scheduled rides
Every feed item is a plugin
The location shortcuts are plugins
The map layers are plugins
The button in the top right is a plugin
TODO: show the code that consumes this
TODO: show the code that consumes this
LocationEditor button plugin used by scheduled rides
Every feed item is a plugin
The location shortcuts are plugins
The map layers are plugins
The button in the top right is a plugin
Sometimes plugins don’t need to be routers.
Can be used as a way of building extension points of arbitary types in the app.
Explain how these text entries are actually just represented as RIBs. They could definately be represented as Pair<ClickListener, Text> but we wanted to offer more flexibility. Probably about 75% of all plugin make sense to be represented as RIBs.
TODO: add a GIF on the next screen of clicking on the GiftCard plugin.
Basically only the scaffolding, glue logic and base libraries are inside core
Core: Basically only the scaffolding, glue logic and base libraries are inside core. This coordinates the different plugin points in the app.
Because modifications to “Core” are architecturally significant, this code is reviewed extra thoroughly. To avoid this, engineers want to reuse the existing plugin points that have already been rigorously considered by UX.
Ideas to express:
Not every plugin is a RIB
Basically only the scaffolding, glue logic and base libraries are inside core
Realized that introducing a new plugin was a particularly non-risky way to experiment with adding a new feature. If we wanted to disable the feature we can be 100% confident that doing this won’t systemically break the app because the core of the app doesn’t depend on the existence of any one plugin.
We would never turn off all these plugins of course. Plugins are designed to be isolated enough that we should never get in a situation where we need to disable all of them.
Realized that introducing a new plugin was a particularly non-risky way to experiment with adding a new feature. If we wanted to disable the feature we can be 100% confident that doing this won’t systemically break the app because the core of the app doesn’t depend on the existence of any one plugin.
Joel Spoelsky
Development velocity
Able to sustain 500 diffs/week per platform
Diffs landed per week doubled after finishing the four month rewrite
This number is backed by engineer testimony that say they need to spend less time hunting around the code base to figure out how to write features now
Performance:
Code isolation and consistent patterns naturally lead to