Adam Terlson
Software Engineer
Minnesota, USA
Munich, Germany
@adamterlson
❤️
❤️
❤️
Founded in 2007
Global expat social network
3 million users
Communities in 420 cities
~150 employees
iOS & Android
Launched in 2015
Objective C/Java + Hybrid
Few iterations
Bad ratings & reviews
Lots of users
Messaging
Events
Activity Feeds
Payments
Comments
Galleries & Photos
Tracking
Profiles
Groups
Push Notifications
Risk | Feedback amt | Migration cost | |
---|---|---|---|
Instant cut-over | High 🔥🔥🔥 | High | Low |
Beta programs | Medium | Medium | Medium |
New app (in parallel) |
Low | Low | High |
Microservices
Monolith
source: https://www.martinfowler.com/bliki/StranglerApplication.html
Registration Microservice
Monolith
POST /api/users
Monolith
Registration Microservice
Load Balancer
Monolith
POST /api/users
Registration Microservice
source: https://docs.microsoft.com/en-us/azure/architecture/patterns/strangler
Early Migration
Late Migration
Complete
Legacy
Legacy
Green Field
Green Field
@adamterlson
Concepts > Specifics
Swift Only
STEP 1
"Assets"
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
}
}
STEP 2
Event
Legacy
GreenField
/*
* 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()
}
}
TIP
STEP 3
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
}
/*
* 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",
])
}
(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
STEP 4
✅ Early user feedback
✅ Be data-driven
✅ Mitigate business risk
✅ Can "stop any time"
❌ More coordination
❌ High analytics demand
❌ 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
✅ Build a new app
✅ Be user & data-driven
🚧 Migrate all legacy app users
🚧 Delete the legacy apps
✅ Don't kill the business
@adamterlson
Special thanks:
Marcus Österberg (pairing)
Pedro Moura (artwork)
Adam Terlson
By Adam Terlson
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.