A pragmatic approach to mobile architecture
An efficient architecture plan for your iOS and Android apps.
What is architecture? Multiple definitions are available, but the one I like is from the IEEE 1471 standard: “The fundamental organization of a system embodied in its components, their relationships to each other, and to the environment, and the principles guiding its design and evolution.”
The idea is to come up with clear boundaries in forming the system’s components to define the relationships between them and with their environment and ensure that certain principles are in place to guide them.
MVC, MVP, MVVM, and VIPER: These architecture patterns have in common an approach that separates the concerns (by leveraging clearly defined components) and aims at making your code decoupled, testable, and reusable. Decoupling allows you to move faster, particularly as a team. Testability makes it easier to test your components, which in turn will make your software more reliable and improve your speed and efficiency since you won’t have to worry about breaking things.
Before moving forward, I want to make a distinction. When we talk about “mobile architecture,” what we mean is “a pattern,” repeated across the application to build our features. We are not necessarily talking about macro-level architecture (similar to how backend architecture is discussed) but rather architecture at the micro level. I’ll come back to this topic later in this article and provide an example in explaining further.
So, what is wrong with the existing architecture patterns out there? Nothing. Depending on your project, team, and requirements, any of them might suit you well. If you have a large team of mobile engineers working on the same app, VIPER, with its clearly defined components, will most likely be your best choice. On the other hand, if you have a simple app with a few screens, MVC will get the job done. Do you have something a little more complicated than that? Then perhaps MVVM might work instead.
Let’s go over some of these architecture patterns.
Model, View, and Controller. For small apps, MVC will save the day. However, with increasing complexity of the app you’re building, your controllers will quickly get out of control, becoming what is known on the street as “massive view controllers.” Moreover, the fact that the application logic and views are tangled together inside the view controllers makes it difficult to test your components.
Model View ViewModel. The first problem with MVVM is the lack of agreement on what the ViewModel should do (terrible naming of it contributes to this problem as well). The question arises as to whether it should be a plain model that can be passed to views or should it be more than that, including network calls and more. In both cases, MVVM puts too much attention on how the data should be transformed for views, and it misses the big picture.
Another issue with MVVM is that it needlessly concerns itself with “bindings,” mainly how the data and events are passed to and from the views. That’s a red flag. Rx and similar concepts should be entirely orthogonal to the architecture patterns. To facilitate communication across different components, some engineers may use Rx, while others may leverage more elemental solutions. Regardless, they must be discussed separately from the architecture patterns.
View, Interactor, Presenter, Entity, and Router. This is one of the best-defined architectures out there that does an excellent job of conforming to the Single Responsibility Principle. However, one of the main problems with VIPER is that it comes with a lot of overhead. Say that you are using MVC and you’re going to create a new screen. You simply spin up a view controller, and you are done. With VIPER, you’ll have to create three additional components: Interactor, Presenter, and Router. In short, the overhead increases significantly.
Of course, the idea here is straightforward: You invest in advance to reap the rewards later. You expect that future benefits (by making it decoupled, testable, and reusable) will justify the overhead. Nevertheless, this might not be so pragmatic for several circumstances. You might want to have something that can scale in the long run but also not slow you down at the initial phases of your development. You want to strike a balance between order and chaos.
The pragmatic approach
So, can we come up with a new architecture pattern that has smaller overhead than some sophisticated architectures (i.e., VIPER), but still employ the essential boundaries that’ll prove to be valuable as the app gets more complicated?
Here’s my proposal: We are going to make some modifications to VIPER. We will make Router, Presenter, and View (yes, View too) optional and add a new component called Builder into the mix: IB (VPR). A group of these will make a Unit. For instance, your Welcome screen might have an Interactor, Builder, and a View. Together, they will make a unit. Your app structure will be a directed graph of units.
Let’s look at these components one by one. I’ll start with the optional components.
Presenter is where you transform your data to make it suitable for your views. A simple example: Your model has name and surname fields, and you want to show the user’s full name inside a label. You concatenate name and surname inside the presenter and pass the full name as a single string to the view. So Presenter, in this case, transformed your model into what your view was expecting.
As someone who has worked on one of the largest mobile repos and consumer apps that are used by millions of people, I’ve seen that the Presentation component is mostly redundant. True, not all transformations are as simple as the example above, but neither are they too complicated to deserve their own boundaries. Unless your units require sophisticated presentation logic, leave this component out and place your logic inside the Interactor.
Router is where you manage the logic for routing your units. For instance, say you are at the Welcome screen and want to present the Login screen once the user taps the login button. The View will let the Interactor know about the button tap, and the Interactor will call the router. The Router will initialize all the related components of the Login screen and present the new view.
Although the Router component is not as redundant as the Presenter component, most of the time, it’s hard to justify the overhead. So if you have a refined routing logic for your unit, keep this; otherwise, place your routing logic inside the Interactor.
View is a view-controller that creates and manages its views. Nothing else.
View is optional, too. The reason for this is to enable having viewless units. This will help you to further divide your logic into multiple interactors that will have clear-cut responsibilities.
Interactor is where the application logic lives; it’s the “brain” of your unit. It should be completely independent of the environment-related dependencies such as UIKit.
Since we said that we are making Presenter and Router optional, Interactor would be the natural place to aggregate those functionalities. I’m well aware that this is not aligned with the Single Responsibility Principle; however, we are aiming for a pragmatic approach, not a purist one.
Builder is where the unit (more specifically, Interactor and other optional pieces) is assembled. It serves as an explicit entry point for the unit. Except for unit tests, you should never initialize any of the components outside of the Builder.
Interestingly, this piece is neglected in pretty much all of the architecture patterns. You might argue that it shouldn’t be part of the core pattern, but I firmly believe it is. I see it as similar to the constructor of a class: Builder is the constructor of the unit. Thus, it should be an essential part of the pattern and serve as a clear entry point for units.
As I described above, for the most part, Presenter and Router can be left out unless you’re expecting your app to contain dense routing and presentation logic. On the other hand, View is also optional, but you’ll probably include it in your unit more often than not. To have something memorable and use fewer letters from the alphabet, I want to name this pragmatic approach IVB: Interactor, View, and Builder.
What about models?
Let’s start by clarifying the terms. Terms are important as we think and communicate with them.
When we say “models,” what we mean is the model layer. This, sadly, is one of those concepts that everybody has an idea about, but nobody knows exactly what it means. Ask ten engineers about the model layer, and you’ll probably get ten different answers.
A model layer should have three components:
- Domain Objects (Model Objects)
Domain Objects (Model Objects)
These are the plain objects that represent an entity (i.e., Person, Book, etc.). These objects should not contain any logic and should be formed from a few attributes. They should be “comparable” to other objects of the same type (e.g., in Swift, they should implement the Equatable protocol, and in Java, it’s the equals method). They should also be encodable and copyable.
In your codebase, ideally, you should never hand-code domain objects. They should be defined via an IDL — perhaps with Thrift or Protobuf — and should be generated across multiple platforms (i.e., iOS, Android, backend services). This is important as hand-coding domain objects is not only time-consuming but also prone to severe mistakes that can lead to hard-to-debug, disastrous bugs.
Client objects are responsible for interacting with the external world, which could be a service in the cloud or your local DB in the application or simply your app’s memory. This is where you put your low-level code that handles all the nitty-gritty details, thus providing a cleaner abstraction. Similar to domain objects, clients can potentially be auto-generated from the IDL definitions.
A service object glues together model objects and clients. Essentially, it makes use of the low-level APIs of the clients and creates, populates, and returns the domain objects. When people say, “Business logic should be part of the models,” it’s the service objects where the business logic goes into — not domain objects, not clients.
Regarding the terms again, there’s a lot of confusion around what exactly is “business logic.” This term tends to be confused with “application logic.” Let me give you a straightforward example to show the difference. You have a registration page, and you are collecting the user’s email address. Once the user taps next, your app retrieves the value from the text field, provides it to a service object for a signup network call, receives the response, and updates the view with the result. In this flow, if you want to add a validation logic for the email address input (i.e., email must contain @mycompany.com suffix), that’s called “business logic,” and it should go into the service object. Your service object will do the validation and can return an error (or throw an exception) if the validation fails. But the rest of the flow (app retrieving the value from the input, passing to the service and sending the response back to the view) is called “application logic.”
What would be the pragmatic approach here? I’d say that if you’re building a stateless app and mainly going to depend on the network calls, you can merge Service and Client objects into one object. Otherwise, say if you want to hit your local cache before making network requests, you may have a client that makes a network call and another one that queries the local cache. That means your service object will depend on both of those clients; it will initially use the cache client to check the query data, and if there are no results, it will call the network client. Obviously, in this case it won’t be wise to merge those two completely different clients into a service.
Avoiding the “kitchen sink” trap
The reason most apps end up having massive view controllers is that we treat view controllers as a “kitchen sink.” We throw everything into them until they become colossal containers. This has been one of the main reasons that we’ve come up with several different architectures so that we can have dedicated components for various roles and we don’t have to put everything into one place.
But still, this doesn’t preclude anybody from creating a new kitchen sink, even in those nicely abstracted architecture patterns such as VIPER. Your interactors can still become massive. If you see this happening, create additional abstractions that are orthogonal to your units so that you can move some functionality out of your interactors and make your interactors depend on them. Don’t forget that “premature optimization is the root of all evil”: use your best judgment and avoid creating a lot of abstractions from the beginning.
App architecture vs. architecture pattern
Let’s come back to the difference between an “architecture” and “architecture pattern.” If someone asks you about your app architecture and you respond by saying “MVC,” that wouldn’t be complete. You’re talking about the pattern that you will use to build the application, not necessarily the architecture of your app.
Let’s assume that we are building the Twitter app and we’ll have the following pages: Welcome, Login, Signup, Profile, Timeline, and Tweet Detail pages. The app architecture would look something like the following:
Here, there’s no discussion of the “architecture pattern.” We can choose VIPER, MVVM, or perhaps IVB to implement the units in the tree. It doesn’t matter.
Also, for the model layer (to clarify the “model layer” discussion above), I create two clients, one for network calls and one for caching. That means the TwitterService will depend on them to fetch the necessary data, and it’ll create and return the domain objects.
The diagram above is quite high-level as it omits communication information between the units and the dependencies of each unit. Of course, it’s always possible to go deeper and have a diagram with higher resolution.
How to get started?
I’ve created an example iOS app that uses the IVB architecture pattern here. You can import the “Common” folder inside the app into your project as well to start using IVB. The app is built around the protocol-oriented programming concept.
I also added template files that you can easily import into your Xcode templates so that you can easily create the unit files without hand-coding all the boilerplate code. Details are on the readme file.