Strangling Legacy

with React Native

Adam Terlson

React Amsterdam—April 12, 2019

Hi, I'm Adam!

 

Minnesota, USA

 

Munich, Germany

 

@adamterlson

❤️

❤️

❤️

Poll:
Native vs Web background?

Mobile App

Architecture

Mobile App

Architecture

🔥 Imposter Syndrome 🔥

Founded in 2007

Global expat social network

3 million users

Communities in 420 cities

~150 employees

Let's build a new app

to replace the old ones.

Legacy
Migration

iOS & Android

Launched in 2015

Objective C/Java + Hybrid

Few iterations
Bad ratings & reviews

Lots of users

Lots O' Features

 

Messaging

Events

Activity Feeds
Payments

Comments

Galleries & Photos

Tracking

Profiles

Groups

Push Notifications

The Mission:

Build a new native app
Be user & data-driven

Migrate all legacy app users

Delete the legacy apps

Don't kill the business

Ship fast,

 

break stuff

DON'T

Risk Feedback amt Migration cost
Instant cut-over High 🔥🔥🔥 High Low
Beta programs Medium Medium Medium
New app
(in parallel)
Low Low High

Ways to Migrate

Microservices

Monolith

"...Gradually create a new system around the edges of the old, letting it grow slowly until the old system is strangled."
—Martin Fowler (2004)

source: https://www.martinfowler.com/bliki/StranglerApplication.html

Strangler Architecture

STEP 1

Create the new service

Registration Microservice

STEP 2

Transparently proxy requests

STEP 3

Direct requests to new service

Monolith

POST /api/users

STEP 1

Create the new service

STEP 2

Transparently proxy requests

STEP 3

Direct requests to new service

Monolith

Registration Microservice

Load Balancer

STEP 1

Create the new service

STEP 2

Transparently proxy requests

STEP 3

Direct requests to new service

Monolith

POST /api/users

Registration Microservice

source: https://docs.microsoft.com/en-us/azure/architecture/patterns/strangler

Early Migration

Late Migration

Complete

How can we apply this to React Native?

Legacy

Legacy

Green Field

Green Field

DOUBLE KILL

3 Steps to Strangle Mobile Legacy with React Native

@adamterlson

Disclaimer:

Concepts > Specifics

Swift Only

Build the new service

STEP 1

"Assets"

  • Profiles
  • Events
  • Messaging
  • Settings
  • Login
  • Registration
  • Payments
  • etc...

Establish the Codebase

Worst Case:

Shove Code

import LegacyFramework

@UIApplicationMain
// Extend from the Legacy's AppDelegate
class AppDelegate: LegacyFramework.AppDelegate {   
  var isDevelopment = false

  // Override methods only when they become relevant
  override func application(_ application: UIApplication, 
    didFinishLaunchingWithOptions launchOptions: 
    [UIApplication.LaunchOptionsKey: Any]?) -> Bool {

    // Check for development mode
    if (!isDevelopment) {
      return super.application(
        application, 
        didFinishLaunchingWithOptions: launchOptions
      )
    }

    // React-native init
  }
}

Transparently proxy requests

STEP 2

Façade

Event

Legacy

GreenField

Event Capture

  • Notification, Events, Delegate, etc
  • Routing/Navigation
  • User Input
  • Push/Local notifications
  • App Lifecycle
/*
 * Legacy/HomeViewController.swift
 */

class HomeViewController: UIViewController {
    @IBAction
    func onViewMessagesPress(_ sender: Any) {
        print("""
        Welcome to Legacy Land where nothing 
        is possible, magic happens, and everything 
        is 200% more expensive.
        """)
    }
}
/*
 * Legacy/HomeViewController.swift
 */

class HomeViewController: UIViewController {
    @IBAction
    func onViewMessagesPress(_ sender: Any) {
        NotificationCenter.default.post(
            name: Notification.Name("ViewMessagesPress"), 
            object: { () -> () in
            
            print("""
            Welcome to Legacy Land where nothing 
            is possible, magic happens, and everything 
            is 200% more expensive.
            """)
        })
    }
}

/*
 * GreenField/Facade.swift
 */

class Facade {
  init() {
    NotificationCenter.default.addObserver(selector: 
        #selector(self.onViewMessagesPress(notification:)), name: 
        Notification.Name("ViewMessagesPress"), object: nil)
  }

  func onViewMessagesPress(notification: Notification) {
    guard let legacyCallback = notification.object as? () -> () else {
      let object = notification.object as Any
      assertionFailure("Invalid callback: \(object)")
      return
    }
    
    legacyCallback()
  }
}

Keep it consistent

TIP

Direct traffic to new service

STEP 3

Façade

Event

Legacy

GreenField

📐

API

/*
 * GreenField/Facade.swift
 * 
 * Direct button press event to appropriate app
 */
@objc
func onViewMessagesPress(notification: Notification) {
  guard let legacyCallback = notification.object as? () -> () else {
    let object = notification.object as Any
    assertionFailure("Invalid callback: \(object)")
    return
  }
  
  if !self.rnMessagingEnabled {
    legacyCallback()
    return
  }
  
  let props = ["initialRoute": "Messages"]
  launchRN(props: props)
}

Speaker notes: How to go back?

/*
 * GreenField/Facade.swift
 * 
 * Launch GreenField app with props, set it as active
 */
func launchRN(props: Dictionary<String, String>) {
    let jsCodeLocation = 
        RCTBundleURLProvider.sharedSettings()?.jsBundleURL(
        forBundleRoot: "index", fallbackResource: nil)

    let rootViewController = UIViewController()
    rootViewController.view = RCTRootView(
        bundleURL: jsCodeLocation,
        moduleName: "GreenField",
        initialProperties: props
    )

    self.window.rootViewController = rootViewController
    self.window.makeKeyAndVisible()

    self.isGreenFieldActive = true
}

PUSH NOTIFICATION

!

/*
 * Legacy/AppDelegate.swift
 * 
 * Capture push notification receive event
 */

private func application(_ application: UIApplication,
                      didReceive notification: UILocalNotification) {
    NotificationCenter.default.post(
        name: Notification.Name("PushNotification"), object: { () -> () in

        // Original legacy crap :)
    })
}
/*
 * GreenField/MyEventEmitter.swift
 */

@objc(MyEventEmitter)
class MyEventEmitter: RCTEventEmitter {
  @objc open override func supportedEvents() -> [String] {
    return ["PushNotification"]
  }
  
  // Tells RN to initialize this emitter during bridge startup
  @objc open override static func requiresMainQueueSetup() -> Bool {
    return true
  }
}


/*
 * MyEventEmitter.m
 */

@interface RCT_EXTERN_MODULE(MyEventEmitter, NSObject)
  RCT_EXTERN_METHOD(supportedEvents)
@end
/*
 * GreenField/App.js
 * 
 * Receive Event in RN
 */

import {
  NativeModules,
  NativeEventEmitter
} from "react-native";

const { MyEventEmitter } = NativeModules;
const emitter = new NativeEventEmitter(MyEventEmitter);

const subscription = emitter.addListener(
  "PushNotification",
   notification => {
     // Green field alert handler
   }
)

// Don't forget
subscription.remove();
/*
 * GreenField/Facade.swift
 * 
 * Direct push notification events to the appropriate app
 */

@objc
func onPushNotification(notification: Notification) {
  guard let legacyCallback = notification.object as? () -> () else {
    let object = notification.object as Any
    assertionFailure("Invalid callback: \(object)")
    return
  }
  
  if !self.rnMessagingEnabled || 
     !self.isGreenFieldActive || 
     self.myEmitter == nil {

    legacyCallback()
    return
  }
  
  self.myEmitter.sendEvent(withName: "PushNotification", body: [
    "message": "Strangled Push Notification",
  ])
}

Scale, Measure, Iterate

Scale, Measure, Iterate

Scale, Measure, Iterate

Scale, Measure, Iterate

Scale, Measure, Iterate

🎉 100% 🎉

🧹

Repeat steps 1–3

  • Messaging
  • Profiles
  • Events
  • Settings
  • Login
  • Registration
  • Payments
  • etc...

RANDOM $(*%#

(Android App Backgrounded)

FirebaseMessagingService onMessageReceived

Push Event handled by RN

RN fires a tracking call

OAuth token refresh
Token persisted by RN

...some time later...

App Start handled by Legacy

Expired access token

OAuth token refresh fails (expired token)

User gets logged out

🌟 Add more telemetry 🌟

STEP 4

Is this really
a GOOD idea?

Ask the Business

Early user feedback

✅ Be data-driven

✅ Mitigate business risk

✅ Can "stop any time"

❌ More coordination

❌ High analytics demand

Ask the Engineer

❌ Initial setup costs

❌ ObjC/Swift/Java required

❌ More complexity

❌ More bugs

❌ Larger app download size

❌ Hard to reach 100%

✅ Keeps life interesting

✅ Early feedback on bugs/perf

So...
 

has InterNations accomplished its Mission?

✅ Build a new app

✅ Be user & data-driven

🚧 Migrate all legacy app users

🚧 Delete the legacy apps

✅ Don't kill the business

The Mission:

How would this look on Android?
Share your own RN rollout story!

@adamterlson

Thank you
​React Amsterdam!

Special thanks:

Marcus Österberg (pairing)

Pedro Moura (artwork)

Adam Terlson

Strangling Legacy with React Native

By Adam Terlson

Strangling Legacy with React Native

Building an app destined for production is a daunting task, especially to replace existing legacy apps available for download in the App and Play Store. We could have pursued a typical release strategy—instead, we got creative with React Native. We built a strangler-style architecture, a technique commonly reserved for web services, to release our greenfield React Native product user-by-user. The effect was some pretty unique and awesome advantages to how we built and tested our new app—but software is never perfect.

  • 724