Upgrade to Pro — share decks privately, control downloads, hide ads and more …

The Evolution of Routing at Airbnb

Amie Kweon
November 29, 2019

The Evolution of Routing at Airbnb

The Airbnb app serves guests and hosts around the world, delivering magical travel experiences to guests and enabling businesses for hosts. In web applications, routing refers to the mechanism of converting a URL into a software component that can handle the specific path and query parameters. In the iOS app context, routing is a feature navigation API in a multiple module project. The presentation goes through the evolution of routing that happened in the last 14 months and dives into different implementations and their trade-offs.

Amie Kweon

November 29, 2019
Tweet

Other Decks in Programming

Transcript

  1. The Evolution of Routing at Airbnb AGENDA Alternatives Routing v2

    Routing v1 Pre Routing Airbnb App Routing on iOS First iteration July 2018 Second iteration March - July 2019 Trade-offs Lessons learned
  2. The Evolution of Routing at Airbnb Alternatives Routing v2 Routing

    v1 Pre Routing Airbnb App Routing on iOS First iteration July 2018 Second iteration March - July 2019 Trade-offs Lessons learned AGENDA
  3. The Evolution of Routing at Airbnb Alternatives Routing v2 Routing

    v1 Pre Routing Airbnb App Routing on iOS First iteration July 2018 Second iteration March - July 2019 Trade-offs Lessons learned AGENDA
  4. The Evolution of Routing at Airbnb Alternatives Routing v2 Routing

    v1 Pre Routing Airbnb App Routing on iOS Airbnb App Routing on iOS Second iteration March - July 2019 Trade-offs Lessons learned AGENDA
  5. The Evolution of Routing at Airbnb Alternatives Routing v2 Routing

    v1 Pre Routing Airbnb App Routing on iOS Airbnb App Routing on iOS Second iteration March - July 2019 Trade-offs Lessons learned AGENDA
  6. • Published in 2010 • Contributed by 400+ engineers •

    Over 1 million lines of code • 10,000+ source files • 300+ modules The Airbnb App STATS
  7. Mechanism of converting a URL into a software component that

    can handle the specific path and query parameters Routing WEB CONTEXT
  8. # Route definition in config/routes.rb Rails.application.routes.draw do resources :products, only:

    [:index, :show] end # Routing in a feature def share ... redirect_to product_url(@product) end Routing RAILS
  9. // Route definition var express = require('express'); var app =

    express(); var products = require('./products'); app.get(‘/products’, products); // Routing in a feature function share(req, res, next) { ... return res.redirect(‘/products’); } Routing NODE.JS
  10. class ProductViewController: UIViewController { func navigate(to hostID: UserID) { let

    viewController = UserViewController(userID: hostID) navigationController?.pushViewController( viewController, animated: true) } } Navigation on iOS PRODUCT TO USER PROFILE
  11. class ProductViewController: UIViewController { func navigate(to hostID: UserID) { let

    viewController = UserViewController(userID: hostID) navigationController?.pushViewController( viewController, animated: true) } } Navigation on iOS PRODUCT TO USER PROFILE
  12. Multi-Module Setup Product User Profile Registration Network Service Account Service

    Logging Service Dependencies Dependencies Dependencies App
  13. Multi-Module Setup Product User Profile Registration Network Service Account Service

    Logging Service Dependencies Dependencies Dependencies App
  14. import UserProfileFeature class ProductViewController: UIViewController { func navigate(to hostID: UserID)

    { let viewController = UserViewController(userID: hostID) navigationController?.pushViewController( viewController, animated: true) } } From Product To UserProfile
  15. import ProductFeature class UserViewController: UIViewController { func navigate(to productID: ProductID)

    { let viewController = ProductViewController( productID: productID) navigationController?.pushViewController( viewController, animated: true) } } From UserProfile To Product
  16. import UserProfileFeature class ProductViewController: UIViewController { func navigate(to hostID: UserID)

    { let viewController = UserViewController(userID: hostID) navigationController?.pushViewController( viewController, animated: true) } } From Product To UserProfile
  17. class ProductViewController: UIViewController { init(userBuilder: (UserID) -> UIViewController) { …

    } func navigate(to hostID: UserID) { let viewController = userBuilder.build(hostID) navigationController?.pushViewController( viewController, animated: true) } } Workaround 1 BUILDER BLOCKS
  18. class ProductViewController: UIViewController { init( userBuilder: (UserID) -> UIViewController, bookingBuilder:

    (ReservationID) -> UIViewController, contactHostBuilder: (ProductID) -> UIViewController, experienceBuilder: (ProductID) -> UIViewController) { … } } Workaround 1 BUILDER BLOCKS
  19. class ProductViewController: UIViewController { func navigate(to hostID: UserID) { let

    command = UserProfileNavigationCommand( userID: hostID) command.execute(from: self) } } Workaround 2 COMMAND PATTERN
  20. • Navigate to another feature without importing its module •

    Establish a pattern for route definition and integration • Reduce code complexity for navigation logic Goals ROUTING V1
  21. Protocol Composition ROUTING V1 public protocol Product.OutboundRoutable { func routeToUser(

    id: String, from viewController: UIViewController) } public protocol UserProfile.InboundRoutable { func routeToUser( id: String, from viewController: UIViewController) } Product UserProfile
  22. // in UserProfileFeature module public final class Router: UserProfile.Routable {

    func routeToUser( id: UserID, from presentingViewController: UIViewController) { // Construct UserProfileViewController // and display in the presenting view controller } } Module Router USER PROFILE ROUTER
  23. extension AppRouter: UserProfile.Routable { func routeToUser( id: UserID, from presentingViewController:

    UIViewController) { userProfileRouter.routeToUser( id: id, from: presentingViewController) } } AppRouter
  24. class ProductViewController: UIViewController { init(router: OutboundRoutable) { self.router = router

    } func navigate(to hostID: UserID) { router.routeToUser(id: hostID, from: self) } } From Product To UserProfile ROUTING V1
  25. 1. Inconsistent implementations 2. Route methods are duplicated across many

    modules 3. All route methods are required 4. It requires too much code in the main target Problems ROUTING V1
  26. • Used in Android, Apple’s SiriKit, and Amazon’s Alexa •

    Routing characteristics: decoupling, composability, pre/post hooks (“interceptors “) Intents and Intent Handlers
  27. • Lightweight module • Contains shared model and behavior definitions

    • Associated with feature modules and service modules • No business logic Interface Module
  28. 1. A route is represented as a model 2. RouterService

    resolves a route and performs navigation 3. A route is registered with a route handler 4. A scope or feature can register route handlers 5. Interceptor defines a precondition Proposal ROUTING V2
  29. 1. A route is represented as a model 2. RouterService

    resolves a route and performs navigation 3. A route is registered with a route handler 4. A scope or feature can register route handlers 5. Interceptor defines a precondition Proposal ROUTING V2
  30. // in UserProfileFeatureInterface import RouterServiceInterface public struct UserProfileRoute: Route {

    public let userID: String public let user: User? } Route Definition #1 ROUTE MODEL
  31. // in ProductFeature module import UserProfileFeatureInterface import RouterServiceInterface class ProductViewController:

    UIViewController { func navigate(to hostID: String) { let route = UserProfileRoute(userID: hostID) routerService.navigate(to: route, from: self) } } Route Consumer #1 ROUTE MODEL
  32. 1. A route is represented as a model 2. RouterService

    resolves a route and performs navigation 3. A route is registered with a route handler 4. A scope or feature can register route handlers 5. Interceptor defines a precondition Proposal ROUTING V2
  33. // in ProductFeature module import UserProfileFeatureInterface import RouterServiceInterface class ProductViewController:

    UIViewController { func navigate(to hostID: String) { let route = UserProfileRoute(userID: hostID) routerService.navigate(to: route, from: self) } } Route Consumer #2 ROUTER SERVICE
  34. 1. A route is represented as a model 2. RouterService

    resolves a route and performs navigation 3. A route is registered with a route handler 4. A scope or feature can register route handlers 5. Interceptor defines a precondition Proposal ROUTING V2
  35. // in RouterServiceInterface module public protocol RouteHandler { func destination(of

    route: RouteType) -> UIViewController func navigate( to route: RouteType, from presentingViewController: UIViewController, presentationStyle: ModalTransitions.Style?) } Route Handler #3 ROUTE HANDLER
  36. 1. A route is represented as a model 2. RouterService

    resolves a route and performs navigation 3. A route is registered with a route handler 4. A scope or feature can register route handlers 5. Interceptor defines a precondition Proposal ROUTING V2
  37. Dynamic Registry #4 REGISTRIES BY FEATURE App Scope Visitor Scope

    (Logged out) Guest Scope (Logged in) Host Scope
  38. Dynamic Registry #4 REGISTRIES BY FEATURE App Scope Visitor Scope

    (Logged out) Guest Scope (Logged in) Host Scope
  39. final class GuestTabBuilder: Builder<GuestTabDependencies> { func build() -> UIViewController {

    let scope = GuestTabScope( dependencies: dependencies) let viewController = GuestTabViewController( dependencies: dependencies) dependencies.routerService.register( scope.routeHandlerRegistries, ownedBy: viewController) return viewController } } Dynamic Registry #4 REGISTRIES BY FEATURE
  40. 1. A route is represented as a model 2. RouterService

    resolves a route and performs navigation 3. A route is registered with a route handler 4. A scope or feature can register route handlers 5. Interceptor defines a precondition Proposal ROUTING V2
  41. public class AuthRequiredInterceptor: RouteLifecycleInterceptor { public func execute( route: Route,

    lifecycle: NavigationLifecycle, from presentingViewController: UIViewController) { guard lifecycle == .started && route is AuthRequiredRoute && !accountManager.isLoggedIn else { return } let registrationRoute = RegistrationRoute() { [weak self] result in guard case .signIn = result else { return } self?.routerService.navigate( to: route, from: presentingViewController) } routerService.navigate( to: registrationRoute, from: presentingViewController) } } #5 INTERCEPTOR AuthRequired Interceptor
  42. public class AuthRequiredInterceptor: RouteLifecycleInterceptor { public func execute( route: Route,

    lifecycle: NavigationLifecycle, from presentingViewController: UIViewController) { guard lifecycle == .started && route is RequiredAuthRoute && !accountManager.isLoggedIn else { return } let registrationRoute = RegistrationRoute() { [weak self] result in guard case .signIn = result else { return } self?.routerService.navigate( to: route, from: presentingViewController) } routerService.navigate( to: registrationRoute, from: presentingViewController) } } #5 INTERCEPTOR AuthRequired Interceptor
  43. public class AuthRequiredInterceptor: RouteLifecycleInterceptor { public func execute( route: Route,

    lifecycle: NavigationLifecycle, from presentingViewController: UIViewController) { guard lifecycle == .started && route is RequiredAuthRoute && !accountManager.isLoggedIn else { return } let registrationRoute = RegistrationRoute() { [weak self] result in guard case .signIn = result else { return } self?.routerService.navigate( to: route, from: presentingViewController) } routerService.navigate( to: registrationRoute, from: presentingViewController) } } #5 INTERCEPTOR AuthRequired Interceptor
  44. 1. A route is represented as a model 2. RouterService

    resolves a route and performs navigation 3. A route is registered with a route handler 4. A scope or feature can register route handlers 5. Interceptor defines a precondition Proposal ROUTING V2
  45. • Translates well from web • Easy to implement •

    Difficult to represent complex routing parameter • Parsing strings to models can be error prone URL-based Routing let productURL = URL( string: “airbnb://d/product?id=\(productID)”) urlRouter.handle(productURL)
  46. • Easy to discover route (e.g. Routes.userProfile) • Route precondition

    is abstracted as a plugin • Initiating a route requires a route identifier and route parameters. • Static route registry Central Routing Service routerService.routeTo( Routes.product.detail, parameters: ProductDetailNavigationParameter( presentingViewController: self, productID: id))
  47. • Route models are extensible • Single source of truth

    • API is simple and consistent across features • Interceptors provide advanced customization • Adapts to user context Intents and Intent handlers
  48. Code evolves You can’t make everyone happy Don’t forget developer

    productivity There’s no right solution Lessons Learned