Almost 1 year ago at the beginning of summer 2020, my team decided to refactor and modernize a very critical page in our app, the Paywall page. You can understand why breaking the page where people buy the subscription to our service was not an option so we had to be very mindful about how we would proceed.
Why take the risk then? We needed to get faster and more reliable at shipping experiments on this page so that we could optimize our revenues. Remember that a refactoring project always needs a business reason to be accepted by non-technical people. And so we got to work.
With the input of my Engineering Manager Hannes Dorfmann (go check him out he has tons of interesting content) we decided that this time we would do things differently. We decided to design and implement this page in the exact same way on both Android and iOS.
Reading this sentence a question probably popped into your mind.
“Alessandro, are you talking about cross-platform technologies???”
Half of you are already jumping out of the chair from the excitement and the other half are angry at me for even asking the question. You can relax, I’m not talking about using cross-platform technologies, or at least not for now. While the topic could have been interesting to explore it didn’t make too much sense to go for this road. The page was particularly business-critical, and cross-platform technologies are not that common in my company.
So what do I mean by building things in the exact same way?
The scope of the project
Before we start let me specify that this project was mainly carried out on mobile clients (Android and iOS). I won’t talk about the Backend or Web side of things.
The decision we made was simple and yet powerful, we decided to design the page in the same way. Imagine having the same classes, the same methods, the same state representations, and more on both platforms. In other words, we decided to use the same exact architecture.
But we didn’t stop there, we promised that we would have supported each other during the development in order to stay on the right path and verify that the implementation would also stay consistent as much as we could. Side note here, when I talk about architecture I mean everything outside the specifics of the UI implementation. That part will still be handled differently for each platform as the UI frameworks are too different to be implemented in the same way (for now…).
I won’t go too deep into the architecture itself as this is not the point of the article. Leave a comment, if you want to know more details. In very few words we used a modified version of MVI (Model View Intent) relying heavily on creating decoupled components, Reactive Programming, State Management through State Machines, SOLID Principles, and more.
Lots of companies nowadays have cross-functional teams and it’s becoming common practice to start designing a feature with all of the mobile engineers (if your company is not there yet, I’m sorry, hang in there), but that usually doesn’t last for too long.
There is a point where the very high-level refinement is done and everyone understands more or less the high-level components that are required to make a feature work and satisfy the main requirements. Unfortunately, after this moment, engineers split based on their platforms and they go off to decide the middle-level components and implementation details.
While you will reap some rewards from having shared the first refinement sessions, in my opinion, you are just scratching the surface of what can be achieved.
If this is the way you are currently working, I don’t blame you. On the surface it kind of makes sense and it seems to be the easiest path to take. After all, unless you are building an app from scratch a feature has to be placed in a habitat of already existing features, architecture, design pattern constraints, and technical debt.
As we will see in the next section, this “seemingly easy” road hides lots of bumps that will be discovered along the way and will come back and hunt you in the future.
Why using a unified architecture?
In order to understand why we chose this path, first, we need to understand the problems we were trying to solve: consistency and quality.
When building things separately in silos (and yes, if you design together and then implement on your own I would still consider it a silo) you are bound to make different decisions.
Problems are solved in two different ways. Different decisions will compound over time and lead to two very different implementations on the mobile clients. Why is that? Why do we need to solve the same problem twice in two different ways? Personally, I never found a good reason for it except for some very rare platform-specific cases or problems.
During testing (or even worst, live in production) you will discover slight differences in the behavior of the two apps. Differences create technical debt. In the future, nobody will understand why the same feature has these differences between the platforms. People will be tasked to understand which behavior is right and will have to refactor the wrong one on the other platform. Or worse, you will take the “it’s always been like this, let’s leave it like it is” path.
Differences in implementation cause differences in which bugs will come up. While engineers from one platform will be busy fixing bugs the ones from the other platform will continue with their sprint work, creating an imbalance on what the team can achieve in that sprint and reducing the predictability of the team.
Unfortunately, these problems are not confined to single features. Usually, platform innovation is driven by the respective academy (this is just how we call the group of all the engineers from one platform). This group decides all of the guidelines for their platform, usually basing their decision on what is popular in the community. Different academies will create different guidelines which will extend all of the problems we mentioned to the whole app.
Honestly, on a small to medium scale, there is nothing wrong with it, and this is how most companies function anyway. When the stake increases though, you need to find ways to optimize and be more consistent, both in what you ship and in the final quality of the product.
And finally, all the benefits…
How does a unified architecture solve the two main issues of consistency and quality, and all of the side effects described above?
By using a unified architecture you are streamlining the design and development process. Problems are now solved in a single way, which means that with a well-oiled machine you could technically solve a problem dedicating twice as many minds to it (if you are optimizing for quality) or even twice as many problems with the same amount of engineers (if you are optimizing for speed). Only with this simple concept we already solved the two main problems. Quality can be higher because twice as many people are working on the same problem, and you will have consistency because there will be only one solution to a problem.
And just like that, now we are free to explore all of the extra benefits we discovered along the way.
Supercharged cross-platform collaboration in many forms:
- It becomes much easier for engineers to transition from one platform to the other. Kotlin and Swift are very similar languages and also If code is written in the same way (same design patterns, same architecture, same names) it becomes much easier to have engineers that are able to work on both platforms.
- For the same reasons, cross-platform code reviews became a simple habit to pick up. And this is how we kept consistency even in the implementation details.
If the apps are working in a consistent way, the QA (Quality Assurance) process is simplified as well. You can now plan a common testing automation strategy for both apps, from the high-level tests down to the specific unit tests that both apps should have.
Also, we experienced much fewer bugs in the feature we shipped to production (one per platform after more than 6 months from the release). We were able to find and fix most of them before the release which is a great success.
Looking at the market and other tech companies, introducing a unified architecture seems to be the natural next step to increase the quality and speed of the engineering department. A famous example comes from Uber and their RIBs Architecture.
Finally, both platforms will probably start integrating SwiftUI and Jetpack Compose relatively soon. There is a big opportunity here to push for a unified architecture considering these new UI systems are defined using the same paradigm (state-based, declarative UI). From that point on, a unified design can be extended to UIs as well.
Those were a lot of benefits uh?
Using a unified architecture is definitely not a perfect approach (few things in life actually are) but in this article, I tried to explain why it was good for us and how we manage to improve on quality and consistency and even getting more benefits than we could have asked for like cross-platform collaboration and fewer bugs.
I do believe that there is so much more that can be done to improve on consistency and quality but I think a unified architecture is an easy first step that most teams can take, before talking of much bigger initiatives (like cross-platform development, for example).
I hope you can take the learnings with you and make a well-informed decision next time you start a new feature or a new project in your company.
In the next article, we will talk about the downsides of going for a unified architecture, I’m sure you will be surprised by some of them.
So what do you think of this process? Have you ever tried building a feature or an app using the same exact architecture on both Android and iOS? How did it go?
Let me know in the comments down below or tweet at me @_alerecchi