Post on 27-Jul-2020
Dama@damaofficial
Vicnent@vincz_a
Architecture & Automation:
How development processeswork at N26
Contents
1. Isolation on the code level2. Isolation on the project level3. Delivery
Features
Features
● Real bank account
● Real bank account● Opened in under 8 minutes
Features
● Real bank account● Opened in under 8 minutes● Video identification
Features
● Real bank account● Opened in under 8 minutes● Video identification● Mobile first
Features
Features
● Real bank account● Opened in under 8 minutes● Video identification● Mobile first● Realtime (Money transfers, push notifications, card block/unblock)
Features
● Real bank account● Opened in under 8 minutes● Video identification● Mobile first● Realtime (Money transfers, push notifications, card block/unblock)● Financial products (Overdraft, N26 Invest, N26 Credit)
Features
● Real bank account● Opened in under 8 minutes● Video identification● Mobile first● Realtime (Money transfers, push notifications, card block/unblock)● Financial products (Overdraft, N26 Invest, N26 Credit)● ...
N26 - Banking by design
● >100 MB
N26 - Banking by design
● >100 MB● > 250k users across platforms
N26 - Banking by design
● >100 MB● > 250k users across platforms● Tons of features
2014
History of the projectIsolation on the code level
History of the projectIsolation on the code level
History of the projectIsolation on the code level
History of the projectIsolation on the code level
Legacy
Legacy
1. Boostraping and iterating quickly over the product2. Understaffed team3. Developing feature like a hackaton
Isolation on the code level
Dealing with legacyIsolation on the code level
Dealing with legacy
● Isolate as much as possible the legacy and continue with product development
Isolation on the code level
Dealing with legacy
● Isolate as much as possible the legacy and continue with product development
● Refactor as much as you can
Isolation on the code level
● Isolate as much as possible the legacy and continue with product development
● Refactor as much as you can ● Carry on and try to make the best out of it
Dealing with legacyIsolation on the code level
Dealing with legacy
● Isolate as much as possible the legacy and continue with product development
● Refactor as much as you can ● Carry on and try to make the best out of it
Isolation on the code level
Isolate legacy code and continue development
1. Refactor only when needed2. If you don’t touch it you don’t break it!3. Pick up an efficient design pattern to deal with legacy
Isolation on the code level
VIPER
https://www.objc.io/issues/13-architecture/viper/
Wireframe
View Presenter Interactor
Data Store
Entity
Entity
Isolation on the code level
Wireframe
https://www.objc.io/issues/13-architecture/viper/
Wireframe
View Presenter Interactor
Data Store
Entity
Entity
Isolation on the code level
Presenter
https://www.objc.io/issues/13-architecture/viper/
Wireframe
View Presenter Interactor
Data Store
Entity
Entity
Isolation on the code level
View
https://www.objc.io/issues/13-architecture/viper/
Wireframe
View Presenter Interactor
Data Store
Entity
Entity
Isolation on the code level
Interactor
https://www.objc.io/issues/13-architecture/viper/
Wireframe
View Presenter Interactor
Data Store
Entity
Entity
Isolation on the code level
Isolating your views
Wireframe
View Presenter Interactor
Isolation on the code level
Isolating your views
Wireframe
View Presenter Interactor
Black Box
Isolation on the code level
Isolating your views
Wireframe
View
Black Box
Isolation on the code level
Isolating your views
Input something
Output somethingelse
Wireframe
Isolation on the code level
Presenting a view using Wireframe
/**
Present Delete Contact formular
*/
func presentDeleteContact(_ contact: TransferContact, completion: @escaping (_ deleted: Bool) -> Void) { DeleteContactWireframe.present(from: self.navigationController, with: contact, completion: completion) }
DeleteContactWireframe - Dummy implementation
class DeleteContactWireframe { /// Present the delete contact formular /// /// - Parameters: /// - viewController: ViewController from where the view should be displayed /// - contact: The contact to be deleted /// - completion: Completion block, returns true if contact has been deleted static func present(from viewController: UIViewController, with contact: Contact, completion: (_ deleted: Bool) -> Void) { let view = DeleteContactViewController() // Create the view let interactor = DeleteContactInteractor() // Create the Interactor let presenter = DeleteContactPresenter(view: view, interactor: interactor) // Create the presenter view.delegate = presenter // Set view delegate viewController.present(view, animated: true, completion: nil) }}
Isolating features
Wireframe
Black box
Wireframe
Black box
Wireframe
Black box
...
Isolation on the project level
Short-term
ObjC
Isolation on the project level
Long-term
ObjC
Isolation on the project level
Build timeIsolation on the project level
Xcode bugIsolation on the project level
Xcode bugsIsolation on the project level
Problem
https://bugs.swift.org/browse/SR-2461
Isolation on the project level
Problem
https://bugs.swift.org/browse/SR-2461
Xcode 8.3
Isolation on the project level
ProblemIsolation on the project level
Solution
Framework
Isolation on the project level
Module Module
Modules Module
Isolation on the project level
Core
Core
@interface NSString (IBANFormat)
/// Returns a formatted string in groups of 4 characters
separated by a space
- (NSString * _Nonnull)IBANFormattedString;
@end
GraphicsReusable UI components
Networking
public protocol CardService {
/// Fetches cards for the current user
func cards(_ success: @escaping ([Card]) -> Void,
failure: @escaping (Error) -> Void)
}
Data VisualizationCustom drawing
# Specify the private specs repo
source 'https://github.com/owner/Specs.git'
...
# Add private dependencies
pod 'N26Core'
pod 'N26Graphics'
pod 'N26Networking'
pod 'N26DataVisualization'
...
CocoaPods
N26 App
Setup
Data visualization
Core Graphics
Networking
Isolation on the project level
N26 App
Setup
Data visualization
Core_ObjC
Graphics
Networking
Core
Isolation on the project level
Core
Networking
Data visualization
Graphics
Remote hosted modules drawbacksIsolation on the project level
Core
Networking
Data visualization
Graphics
Remote hosted modules drawbacksIsolation on the project level
Core
Networking
Data visualization
Graphics
Remote hosted modules drawbacksIsolation on the project level
Core
Networking
Data visualization
Graphics
Remote hosted modules drawbacksIsolation on the project level
Self hosted modules
Core
Networking
Data visualization
Graphics
Core
Networking
Data visualization
Graphics feature/credit
feature/invest
develop
Isolation on the project level
Current state
● Splitted the app into modules● Modules are now locally hosted● We’re still missing something …
Isolation on the project level
Missing moduleIsolation on the project level
Missing module
We’re missing a module that would
● Handle user sessions● Handle Login and access token● Cache current user data
Isolation on the project level
N26Session
import N26Session
Session.current.start
N26Session, booting up
import N26Session
Session.current.start(with: login, password: password, success: { data in
N26Session, booting up
N26Session, booting up
import N26Session
Session.current.start(with: login, password: password, success: { data in /// User is logged in
}) { error in /// Whatever error happend (Bad credentials, 500 ... )}
N26Session, booting up
import N26Session
Session.current.start(with: login, password: password, success: { data in /// User is logged in
print(Session.current.firstName) // Print the current user name print(Session.current.availableBalance) // Print the current account Balance
}) { error in /// Whatever error happend (Bad credentials, 500 ... )}
Feature-developed modulesIsolation on the project level
Generic Module architecture
Core
Network layer
User session
Graphic library
Tracking tool ...
Feature 1 Feature 2 ... ...
Tier 0
Tier 1
Tier 2
Isolation on the project level
Generic Module architecture
Core
Network layer
User session
Graphic library
Tracking tool ...
Feature 1 Feature 2 ... ...
Tier 0
Tier 1
Tier 2
Isolation on the project level
Generic Module architecture
Core
Network layer
User session
Graphic library
Tracking tool ...
Feature 1 Feature 2 ... ...
Tier 0
Tier 1
Tier 2
Isolation on the project level
Generic Module architecture
Core
Network layer
User session
Graphic library
Tracking tool ...
Feature 1 Feature 2 ... ...
Tier 0
Tier 1
Tier 2
Isolation on the project level
Generic Module architecture
Core
Network layer
User session
Graphic library
Tracking tool ...
Feature 1 Feature 2 ... ...
Tier 0
Tier 1
Tier 2
Isolation on the project level
Generic Module architecture
Core
Network layer
User session
Graphic library
Tracking tool ...
Feature 1 Feature 2 ... ...
Tier 0
Tier 1
Tier 2
Isolation on the project level
Generic Module architecture
Core
Network layer
Graphic library
Tracking tool ...
Feature 1 Feature 2 ... ...
Tier 0
Tier 1
Tier 2
Isolation on the project level
Generic Module architecture
Core
Network layer
User session
Graphic library
Tracking tool ...
Feature 1 Feature 2 ... ...
Tier 0
Tier 1
Tier 2
Isolation on the project level
Our Module architecture
Nucleus
Networking Session Dali Tracker Polyglot
Credit Invest Transactor ...
Tier 0
Tier 1
Tier 2
Isolation on the project level
Our Module architecture
Nucleus
Networking Session Dali Tracker ...
Credit Invest Transactor ...
Tier 0
Tier 1
Tier 2
Isolation on the project level
App Extensions done with modulesIsolation on the project level
Apple Watch
Today Widget
Siri
iMessage
N26 - Podfile
# ...
target 'SiriKit' do
pod 'N26Nucleus', :path => 'N26Modules/N26Nucleus'
pod 'N26Networking', :path => 'N26Modules/N26Networking'
pod 'N26Session', :path => 'N26Modules/N26Session'
end
# ..
func widgetPerformUpdate(completionHandler: @escaping (NCUpdateResult) -> Void) { if Session.current.loggedIn {
// user is already logged in Session.current.syncUserData({ (data, error) in
// Refresh the user data if let _ = error {
// display error completionHandler(.failed) } else {
// Refresh the widget self.refreshDisplayData(completionHandler) } }) } else {
// User is not logged in, use the refresh token Session.current.startUsingStoredData({ (data) in self.refreshDisplayData(completionHandler) }, failure: { (refreshTokenExpired, error) in self.displayNeedToAuthenticate() completionHandler(.failed) }) }
}
Creating new feature
$ pod lib create N26NewSecretFeature
Creating new feature - Demo application
What language do you want to use?? [ Swift / ObjC ]> Swift
Would you like to include a demo application with your library? [ Yes / No ] > Yes
Which testing frameworks will you use? [ Quick / None ] > None
Creating new feature - Demo application
What language do you want to use?? [ Swift / ObjC ]> Swift
Would you like to include a demo application with your library? [ Yes / No ] > Yes
Which testing frameworks will you use? [ Quick / None ] > None
Pod::Spec.new do |s| s.name = 'N26NewSecretFeature' s.version = '0.1.0' s.summary = 'A short description of N26NewSecretFeature.'
s.ios.deployment_target = '9.0'
s.source_files = 'N26NewSecretFeature/Classes/**/*'
N26NewSecretFeature.podspec
Pod::Spec.new do |s| s.name = 'N26NewSecretFeature' s.version = '0.1.0' s.summary = 'A short description of N26NewSecretFeature.'
s.ios.deployment_target = '9.0'
s.source_files = 'N26NewSecretFeature/Classes/**/*'
s.dependency 'N26Nucleus'
N26NewSecretFeature.podspec
Pod::Spec.new do |s| s.name = 'N26NewSecretFeature' s.version = '0.1.0' s.summary = 'A short description of N26NewSecretFeature.'
s.ios.deployment_target = '9.0'
s.source_files = 'N26NewSecretFeature/Classes/**/*'
s.dependency 'N26Nucleus' s.dependency 'N26Session'
N26NewSecretFeature.podspec
Pod::Spec.new do |s| s.name = 'N26NewSecretFeature' s.version = '0.1.0' s.summary = 'A short description of N26NewSecretFeature.'
s.ios.deployment_target = '9.0'
s.source_files = 'N26NewSecretFeature/Classes/**/*'
s.dependency 'N26Nucleus' s.dependency 'N26Session' s.dependency 'N26Dali'
N26NewSecretFeature.podspec
N26NewSecretFeature_Example Podfile
use_frameworks!
target 'N26NewSecretFeature_Example' do pod 'N26NewSecretFeature', :path => '../'
target 'N26NewSecretFeature_Tests' do inherit! :search_paths endend
N26NewSecretFeature_Example Podfile
use_frameworks!
target 'N26NewSecretFeature_Example' do pod 'N26Dali', :path => '../../N26Dali' pod 'N26NewSecretFeature', :path => '../'
target 'N26NewSecretFeature_Tests' do inherit! :search_paths endend
N26NewSecretFeature_Example Podfile
use_frameworks!
target 'N26NewSecretFeature_Example' do pod 'N26Dali', :path => '../../N26Dali' pod 'N26Session', :path => '../../N26Session' pod 'N26NewSecretFeature', :path => '../'
target 'N26NewSecretFeature_Tests' do inherit! :search_paths endend
N26NewSecretFeature_Example Podfile
use_frameworks!
target 'N26NewSecretFeature_Example' do pod 'N26Dali', :path => '../../N26Dali' pod 'N26Session', :path => '../../N26Session' pod 'N26Nucleus', :path => '../../N26Nucleus' pod 'N26N26NewSecretFeature', :path => '../'
target 'N26NewSecretFeature_Tests' do inherit! :search_paths endend
Pod install on example app
~/$ cd Example
~/Example$ pod install
NewFeature - Entry point
// NewFeature entry pointpublic class NewFeature {
// Initialize the NewFeature environment and display it public static func start(on viewController: UIViewController) { //TODO: implement this feature }
}
NewFeature - Entry point
import N26Nucleusimport N26Session
// NewFeature entry pointpublic class NewFeature { // Initialize the NewFeature environment and display it public static func start(on viewController: UIViewController) { // Print user firstName print(Session.current.firstName) // Print users formatted IBAN print(Session.current.iban.IBANFormattedString()) }}
Example Project - ViewController.swift
import N26NewSecretFeature
class ViewController: UIViewController {
override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated)
// Kickoff the new feature NewSecretFeature.start(on: self) }
}
DownsidesIsolation on the project level
Downsides
● Running pod update
Isolation on the project level
Downsides
● Running pod update● Conflicts and clogged pull requests
Isolation on the project level
Downsides
● Running pod update● Conflicts and clogged pull requests● Big repo
Isolation on the project level
Downsides
● Running pod update● Conflicts and clogged pull requests● Big repo
Nucleus
formating library
Isolation on the project level
Downsides
● Running pod update● Conflicts and clogged pull requests● Big repo
Nucleus
formating library
formating library
Nucleus Example
Isolation on the project level
Downsides
● Running pod update● Conflicts and clogged pull requests● Big repo
Nucleus
Credit
formating library
formating library
formating library
Nucleus Example
Credit Example
Isolation on the project level
● Running pod update● Conflicts and clogged pull requests● Big repo
Downsides
Nucleus
Credit
formating library
formating library
formating library
Nucleus Example
Credit Example
Isolation on the project level
● Running pod update● Conflicts and clogged pull requests● Big repo● Multiple compilations of dependencies
when testing
Downsides
Nucleus
Credit
formating library
formating library
formating library
Nucleus Example
Credit Example
Isolation on the project level
● Running pod update● Conflicts and clogged pull requests● Big repo● Multiple compilations of dependencies
when testing
Downsides
Nucleus
Credit
formating library
formating library
formating library
Nucleus Example
Credit Example
Isolation on the project level
● Running pod update● Conflicts and clogged pull requests● Big repo● Multiple compilations of dependencies
when testing
Downsides
Nucleus
Credit
formating library
formating library
formating library
Nucleus Example
Credit Example
Isolation on the project level
Downsides
● Running pod update● Conflicts and clogged pull requests● Big repo● Multiple compilations of dependencies
when testing● Changes break other modules
Isolation on the project level
Internal CI
✚Mac Mini Jenkins
Delivery
Internal CI Downsides
● New version of iOS (Updating Jenkins)
Delivery
Internal CI Downsides
● New version of iOS (Updating Jenkins)● No public IP
Delivery
Internal CI Downsides
● New version of iOS (Updating Jenkins)● No public IP● Not scalable
Delivery
Internal CI Downsides
● New version of iOS● No public IP● Not scalable● …
Delivery
Paid solutions to the rescue
Travis CI
Delivery
Paid solutions to the rescue
✚Travis CI Fastlane
Delivery
Paid solutions to the rescue
✚ ✚Travis CI Fastlane Bob
Delivery
Bob The Builder to the rescue
● Travis CI● Fastlane● Slack + Bob The Builder
Delivery
Bob The Builder - Stack
● Slack● Swift● Vapor (Server swift) ● Communicate to Slack via Sockets
Delivery
Slack Bob Travis
Build staging
Slack Bob Travis
Processing
Slack Bob Travis
Build staging
Slack Bob Travis
Done!
Slack Bob Travis
Bob in action
Bob
Open sourcehttps://github.com/N26-OpenSource/bob
Delivery
build staging
“If your build takes more than pressing a button, you’re doing it wrong”
-Someone
align 3.3 1
sync strings | align 3.3 1 | build staging | build appstore
Making an RCDelivery
https://github.com/N26-OpenSource/bob
BobDelivery
$ curl -sL toolbox.qutheory.io | bash
Getting the vapor toolbox
$ vapor new BobTheBuilder
$ cd BobTheBuilder
Creating a new vapor project
Package.swift
Package.swift
import PackageDescription
let package = Package(
name: "BobTheBuilder",
dependencies: [
.Package(url:
"https://github.com/N26-OpenSource/bob.git", majorVersion: 0)
]
)
$ rm -rf Sources/App/Controllers
$ rm -rf Sources/App/Models
Tidying up
$ vapor xcode
Creating an Xcode project
main.swift
main.swift
import Bob
/// Create the config using a slack token
let config = Bob.Configuration(slackToken: "your-slack-token")
main.swift
import Bob
/// Create the config using a slack token
let config = Bob.Configuration(slackToken: "your-slack-token")
/// Create bob instance
let bob = Bob(config: config)
main.swift
import Bob
/// Create the config using a slack token
let config = Bob.Configuration(slackToken: "your-slack-token")
/// Create bob instance
let bob = Bob(config: config)
/// Start bob up
try bob.start()
Using the TravisScriptCommand/// Create TravisCI config
let travisConfig = TravisCI.Configuration(repoUrl: “repo url”, token: “token”)
Using the TravisScriptCommand/// Create TravisCI config
let travisConfig = TravisCI.Configuration(repoUrl: “repo url”, token: “token”)
/// Specify targets
let buildTargets = [
TravisTarget(name: "staging", script: Script("fastlane ios distribute_staging")),
TravisTarget(name: "appstore", script: Script("fastlane ios distribute_appstore")),
]
Using the TravisScriptCommand/// Create TravisCI config
let travisConfig = TravisCI.Configuration(repoUrl: “repo url”, token: “token”)
/// Specify targets
let buildTargets = [
TravisTarget(name: "staging", script: Script("fastlane ios distribute_staging")),
TravisTarget(name: "appstore", script: Script("fastlane ios distribute_appstore")),
]
/// Create the build command
let buildCommand = TravisScriptCommand(name: "build", config: travisConfig, targets: buildTargets,
defaultBranch: "Develop")
Using the TravisScriptCommand/// Create TravisCI config
let travisConfig = TravisCI.Configuration(repoUrl: “repo url”, token: “token”)
/// Specify targets
let buildTargets = [
TravisTarget(name: "staging", script: Script("fastlane ios distribute_staging")),
TravisTarget(name: "appstore", script: Script("fastlane ios distribute_appstore")),
]
/// Create the build command
let buildCommand = TravisScriptCommand(name: "build", config: travisConfig, targets: buildTargets,
defaultBranch: "Develop")
/// Register the command with bob
try bob.register(buildCommand)
Using the AlignVersionCommand
/// Create GitHub config
let gitHubConfig = GitHub.Configuration(username: "username",
personalAccessToken: "token", repoUrl: "url")
Using the AlignVersionCommand
/// Create GitHub config
let gitHubConfig = GitHub.Configuration(username: "username",
personalAccessToken: "token", repoUrl: "url")
/// Specify .plist file to be changed
let plistPaths: [String] = ["App/Info.plist", "siriKit/Info.plist"]
Using the AlignVersionCommand
/// Create GitHub config
let gitHubConfig = GitHub.Configuration(username: "username",
personalAccessToken: "token", repoUrl: "url")
/// Specify .plist file to be changed
let plistPaths: [String] = ["App/Info.plist", "siriKit/Info.plist"]
/// Create the command
let alignCommand = AlignVersionCommand(config: gitHubConfig,
defaultBranch: "Develop", plistPaths: plistPaths, author: author)
Using the AlignVersionCommand
/// Create GitHub config
let gitHubConfig = GitHub.Configuration(username: "username",
personalAccessToken: "token", repoUrl: "url")
/// Specify .plist file to be changed
let plistPaths: [String] = ["App/Info.plist", "siriKit/Info.plist"]
/// Create the command
let alignCommand = AlignVersionCommand(config: gitHubConfig,
defaultBranch: "Develop", plistPaths: plistPaths, author: author)
/// Register the command with bob
try bob.register(alignCommand)
Creating your commandspublic protocol Command {
}
Creating your commandspublic protocol Command { /// The name used to identify a command (`hello`, `version` etc.). Case insensitive var name: String { get }
}
Creating your commandspublic protocol Command { /// The name used to identify a command (`hello`, `version` etc.). Case insensitive var name: String { get } /// String describing how to use the command. var usage: String { get }
}
Creating your commandspublic protocol Command { /// The name used to identify a command (`hello`, `version` etc.). Case insensitive var name: String { get } /// String describing how to use the command. var usage: String { get } /// Executes the command /// /// - Parameters: /// - parameters: parameters passed to the command /// - sender: object used to send feedback to the user /// - completion: block to be called when the command finishes. In case of an error, pass it in /// - Throws: Throws if something goes wrong while executing the command, usually while parsing the parameters func execute(with parameters: [String], replyingTo sender: MessageSender, completion: @escaping (_ error: Error?) -> Void) throws }
BobDelivery
Lessons learned on scaling
● Split your code into smaller pieces● Continuous integration is essential● Module will save you time● …● Try out Bob!
Appendix
● Architecting iOS Apps with VIPERhttps://www.objc.io/issues/13-architecture/viper/
● Bobhttps://github.com/N26-OpenSource/bob
● Vapor (Web Framework For Swift)https://vapor.codes/
● CocoaPodshttps://cocoapods.org/
● Travis CIhttps://travis-ci.com/
Thanks
Dama@damaofficial
Vicnent@vincz_a
Bobgithub.com/N26-OpenSource/bob