autoscale: true
^ My talk is titled
^ I'm originally from Valdivia, Chile
^ For the locals, that's close to Bariloche
^ I started doing iOS Development in 2011
^ Usually working with 1-5 other iOS developers
^ Actually 0-5 other iOS developers
^ Almost 3 years ago I moved to San Francisco to work at Airbnb
^ Share my experience
^ So what I've been used to, for most of my career is an organization chart that looks something like this
^ Teams for functions
^ Part of the iOS team
^ Usually just a couple of people
^ Which sometimes just looked like this
^ Moving to Airbnb
^ Teams per business
^ You can imagine Homes having a Host subteam / or a guest subteam
^ And I work on a sub team of infrastructure, called Native Infra
^ and this is just a part of the iOS team
^ So, when previously I used to interact with this group
^ Now I actually have to interact with these groups
organizations ... are constrained to produce designs which are copies of the communication structures of these organizations -- Conway's law
^ There's a memorable quote by Melvin Conway that says
^ Which basically means that your design will mimic the organization structure
^ The codebase mimics the org structure
Architectural layer
^ At startups my usual first approach to an app was to divide based on "layers"
^ I was using an architecture based on Clean Architecture, so you can see the naming convention here
User Flow
^ The next approach I saw a lot was usually when the codebase started growing
^ "Per flow"
^ As Airbnb grew as a company, the iOS codebase continued growing with it
^ Our first commit was in 2010
^ I love how all data structures were created
^ And now, to date, we have 1 million lines of code
^ with around 100 commits per day
^ So we've been kinda following both patterns at the same time
^ Or, we followed one pattern at a time and now we ended up with both? Who knows!
^ Let's think about this use case
^ As I pointed out before, the code starts resembling the organization
^ So what is Airbnb Booking and how does it look like?
^ We can imagine these 3 dependencies playing together like this
^ But there are more features
^ When we modify Networking, rebuilding the whole app
^ So let's clarify what this means
^ When you make a change to the app, before you can build the app and run in the simulator, you have to wait 50 minutes
^ We reduced this significantly by having faster machines, which got us part of the way there
^ We started doing 2 things:
^ And second, we moved to Buck
^ This was not the only reason why we moved to Buck
^ Buck also has an HTTP cache that makes our local builds much, much faster
^ But that's a talk in itself
^ IMPORTANT: Human readable
^ Buck HTTP cache introduced
^ We went from around 25 mins to 5 mins
^ I was used to pretty small codebases
^ But that still felt bananas
^ There's something inherently wrong here
^ We're depending on a lot of code that we don't need
^ Yeah, I'm not ready to be totally over Clean Architecture ok?
^ Don't judge me
- High-level modules should not depend on low-level modules. Both should depend on abstractions.
- Abstractions should not depend on details. Details (concrete implementations) should depend on abstractions.
^ So we added the concept of interface, lightweigh modules
^ With this we can go back to our original use case
^ No need to present WishList screen
^ But Booking is actually depending on the whole WishList module
^ It only needs access to the Data Source
^ Well that's easy! We just create a WishListDataSource
module and an Interface module!
^ So we go ahead and create the module
^ But there's nothing preventing your from doing
^ Before it worked by just talking to people
^ But when you have 60 iOS Developers...
^ That work in different parts of the organization
^ There are 2 main module types
^ A feature is defined as a screen or a flow in the app ^ Typically a product team will start with a single feature module. As they expand and build more features, they will break it into multiple modules
^ Services manage shared state or resources. They provide a limited interface through which their state or resources can be accessed or observed by features or other services.
^ Now we have these new buckets
^ How do they communicate between each other?
^ And how do we enforce these dependency rules?
^ We create new folders in our repository where these modules live
^ Folder per module type
def service_interface(
name,
deps):
max_visibility = [
"//ios/feature_interfaces/...",
"//ios/features/...",
"//ios/service_interfaces/...",
"//ios/services/...",
]
^ Function defines what a service interface module is
^ Who can depend on my module
^ Makes it visible to these folders
service_interface(
name = "Networking",
deps = [
"//ios/service_interfaces/Logging",
],
)
^ You create a new module by using the function
feature(
name = "Booking",
deps = [
"//ios/service_interfaces/Networking",
"//ios/service_interfaces/WishListService",
],
)
^ We can describe our feature module like this
^ This is what we call the iOS Platform
^ We're creating a lot of modules
^ But since we want to recommend a way
^ Lengthy process
^ That output something like this
^ This is not sane
^ So we did 2 things
^ First, we automated the module creation
[.code-highlight: 1-6, 9, 12, 15-18]
> Provide the type of module you want to create:
1: Non Platform
2: Feature
3: Feature Interface
4: Service
5: Service Interface
4
> New module name:
Swiftable
> Provide a high level description of this module:
This is a module to present at Swiftable
^ And Buck also gave us another major thing
https://github.com/airbnb/BuckSample
^ Human readable dependencies
^ More details in the repo
[.code-highlight: 6]
feature(
name = "Booking",
deps = [
"//ios/service_interfaces/Networking",
"//ios/service_interfaces/WishListService",
"//ios/feature_interfaces/HelpCenter",
],
)
^ One line change
^ The end result
^ We defined a feature as
^ Instead of building the whole app
^ Plug a fake AppDelegate
^ But we need to fulfill these dependencies
^ Can also Mock what we want
^ This is what we call
^ But there's one thing we haven't talked a about
^ Before we started this process...
^ One monolithic module type
libraries/AirbnbBooking
libraries/AirbnbBusinessTravel
libraries/AirbnbHelpCenter
libraries/AirbnbListings
libraries/AirbnbNetworking
libraries/AirbnbWishLists
...
^ Modules with different responsibilities
^ We want to have people contributing to
^ These folders
^ ... One option would be to remove
^ We cannot stop development
^ Other teams need to continue working
^ Their priorities are not the same
^ The iOS Platform has stricter visibility rules
^ Find a way to progressively migrate
^ Let's take the example of migrating
^ This was originally in the libraries module type
^ Let's remember how this looks
^ This is what the end state would look like
^ And as a reminder... this are the iOS Platform visibility rules
^ And as a reminder, these are our dependency rules for the platform
^ But what about libraries?
^ Libraries is not included
^ What can libraries depend on?
^ If we see our end state we can see two types of dependencies
^ There are 2 type of dependencies that we need to consider
^ Inbound dependencies are things that depend on WishList Service
^ If we apply the same strict rules that we have on the iOS platform
^ Not every implementation detail will be visibile
^ Which means..
^ Update everything in Booking and WishLists modules (and everything else
^ We're probably not familiar with the usage
^ The problem, obviously is that we're allowing tech debt
^ So, while we're migrating, we're allowing this
^ Once Booking moves to the platform, they'll need to update their dependencies
^ We allow libraries to depend on the iOS platform module types
^ What about outbound dependencies?
^ But we're familiar with how we use this
^ We control what we depend on
^ So we decided to migrate from the bottom up
^ This way the iOS Platform has better boundaries
^ While still keeping a path that's smooth for migration
^ We do this by expandiing our visibility rules for all on platform modules
[.code-highlight: 1, 11]
def service_interface(
name,
visibility = []):
max_visibility = [
"//ios/feature_interfaces/...",
"//ios/features/...",
"//ios/service_interfaces/...",
"//ios/services/...",
]
add_visibility_for_legacy_module_structure(max_visibility)
[.code-highlight: 2-3]
def add_visibility_for_legacy_module_structure(visibility):
visibility.extend([
"//ios/libraries/...",
])
^ We own a lot of these dependencies
^ Moved infrastructure modules first
^ So we were forced to pilot ourselves
^ And then we piloted with feature teams
^ You're probably wondering
^ As I pointed out at the beggining, each organization will affect the codebase structure
^ Every org is different ^ This works for Airbnb
^ I'd recommend you to innovate and adapt these ideas to your organization
[.build-lists: true]
- Figure out where you're struggling
- Create and document best practices
- Automate best practices where needed