Mobile devices seem to be part of the human body now, with billions of people using them to access the internet, make payments, communicate with their friends (I should get some), and so on. Many opportunities have arisen with the fact that people are connected "all the time". With geolocation services and multiple sensors, devices allow many opportunities to provide custom experiences. However, extracting more from these possibilities means developing specific mobile applications, being native or hybrid.
Native mobile development has some specific challenges and we are going to cover two of the main ones here. As good advice, developing a mobile application is much more than just writing a thousand lines of code and publishing it to the Apple Store or Play Store; most importantly, mobile development begins in the back end.
Over the years, many back-end developers have gotten used to working with web clients, accumulating experience that is more than welcome in the mobile world., However, it's not the same, and if you think so, I'm sorry to say you may be making a big mistake.
Challenges
Availability
With Web Clients, we have the possibility to update the application almost simultaneously for all users, making a new version of the application available with no friction. The same doesn't happen with mobile applications; this is because, in addition to doing the entire versioning, build, and deployment process, we must patiently wait for the stores' analysis (especially when dealing with Apple). After the analysis phase, we still have to hope that users update their application, or we can force it, but this is an undesirable and unpleasant experience for users. After all, they are not obligated to do anything.
If this scenario doesn't open your eyes, here's a slightly more drastic perspective: if a bug occurs and the fix is urgent, it could take a couple of hours, or even days, for the users to have that fix in their hands. This implies putting even more emphasis on quality, avoiding forced updates and burning candles praying for the store update acceptance (Help the environment).
Work Duplication
If in web development we have only one way of distribution, which in this case is the webservers with different browsers accessing the contents that will be interpreted/rendered to the users, in mobile development, we have two main vehicles: the Android and iOS systems. These carry with them the need to develop the same functionality for two different platforms, except in cases where hybrid technologies are used (Flutter, React Native...), which are still susceptible to the timing problems mentioned above and some others that we will not deal with in this text.
This duplication introduces the following premise: you often end up paying twice for the same problem. A process of complex integrations, rework, excess logic on the client side, etc., would increase the development costs on one platform alone. Following our premise, this additional cost is often doubled, since two people (iOS and Android) would be in charge of acting in these scenarios.
Okay, now that we understand the complexities of mobile development, it's time to propose some solutions and best practices, starting from the foundation– the backend.
The Mobile Service
The mobile service should be treated as an extension of the client, being highly dedicated to it, even providing abstractions of business rules. Yes, this idea can be applied to all client types, but the emphasis of this article is mobile and I needed a title for this text.
There are a lot of good practices that can be applied to backend development that help to avoid rework and prevent exposing your customers to risk. In this section, we will discuss some practices that can be used to help with services and client integration.
You don't pay for what you don't use
As Bjarne Stroustrup (creator of C++) used to say: "You don't pay for what you don't use" From the perspective of a user, this mantra means that information must be retrieved only when it's requested, and it must contain only the data that will be presented to the user.
For example: in a list, where each of the items has only a title and description, it makes no sense to bring the item details in this first search; the answer should contain only the identifier, title and description. With this, the user doesn't spend mobile data downloading information that they will not use, since most likely only some items will have their details visualized. Another point is that by bringing less data, we can reduce loading time and memory consumption, providing a better experience with greater stability.
If there's a need to subsequently load more details for a particular item, we only need to use the identifier present in the list item to retrieve its details in a later request.
Alignment between the Client, Backend and Design is a crucial point to achieve the premise of "don't pay for what you don't use". It's the best moment to define where certain data is needed and which data are not. Another point is to make it clear to people focused on the backend how and what data will be presented to users, even facilitating later interactions about some specific feature.
Confidence
I'm sorry if you have trust issues, I suggest you work this out with a professional (I'm in this process myself), but we should be able to trust the services we are accessing. If that's not possible then we should build a layer of trust over those toxic services, using the concept of Backend For Frontend, for example. As in human relationships, trust is not bought, but earned, which leads us to the following premises that help the backend gain the client's trust:
Be cohesive: The message content must match the response code. Returning some success code(200..299) with failure content is a bad practice in any system as it creates a huge inconsistency between the HTTP code and the content. If there is an error in the service, explain it clearly with the codes that best represent the problem and the corresponding content.
Do not work with null values: Tony Hoare has already apologized for inventing this disaster of the computing world with the presentation entitled The billion dollar mistake, There's no reason to make the client work with null values, this demands more data handling and increases the potential points of failure in the code, also, null values most of the time brings ambiguity to the code, some field can be null because of a failure or just because there's no value for that field (exceptions exist for every rule 🙂).
Use default values: An alternative to null values are default values, in a text field for example, we can return an empty text instead of simply not returning it and allowing the client to decide whether to present this empty field or hide it.
Cache
Cache is an important ally in improving performance and saving resources and it is interesting to use it whenever it makes sense. If poorly planned, the caching strategy can generate problems with outdated information and break the users' experience, this layer, both in the service and in the client, must be designed with utmost care.
Clients Generate Demand
It's usual to modify the client code to deal with some backend limitations or just because is "simple" to solve some problems on the client, which means that we are hiding the problem under more code instead of solving it… I think that at this point in this article, it's clear how dangerous this can be, the client is the one that generates work demands for the backend since the backend is a service provider to the client and not the opposite.
Creating more code is always easier than designing a solution or finding the real cause of an issue and we can observe this tendency not only in the client-backend relationship. This is what happens when we add logic to work around problems in a certain part of the code instead of solving the issue at the source. That is, we add complexity, but we don't solve the underlying problem.
Specific to General
Sandi Metz in her book Practical Object-Oriented Design (one of the best technical books I've read), establishes that we should depend on abstractions rather than concrete classes. This statement, however, comes along with another premise; code is not born abstract, it moves to abstraction. In other words, you don't create abstractions just to have them as good practice, you design the abstraction when it's needed.
Based on the concrete-to-abstract idea, we can build specific services and move them to more general ones. When creating an API that will initially serve only one client, it doesn't make sense to build it for general purposes. We can start this service with a specific focus and as needed, shape it to a general purpose. This is relatively easy if what makes your API specific is just a layer of the architecture that prepares data for the current client. With this, we just need to extract the lower layers of the system to another service and create a generic communication interface over these extracted layers (obviously, thoughtfully and safely).
General to Specific
There are cases where a company already has a range of general-purpose services out of the box, with many of these services using null values and lacking data response refinement. We may also face scenarios where the services built will serve different types of clients, which in turn have different demands for information and may burden those clients who need fewer data than others (over fetching).
We have two options to improve the relationship between the backend and its clients for these kinds of scenarios. The first is to build a dedicated service, called BFF (Backend for Frontend) and the second is to use GraphQL.
BFF
Backend For Frontend (BFF) is the client’s "Best Friend Forever", being dedicated services and not general purpose. In short, BFFs are experience-oriented– they are in charge of returning exactly what a particular client needs to expose to its users.
The use of these dedicated services makes it possible to extract logic that was previously embedded in clients, such as the validation of information and even business rules. This logic extraction brings the advantage of avoiding work duplication in native mobile development (Android and iOS), as part of the job is done in the BFF.
In addition, abstracting logic to the backend can also help in solving occasional problems in the client, since much of the complexity that was present in the client is now in the service. This capability is extremely advantageous when it comes to mobile clients, as it avoids the need to go through the bureaucratic process of deploying in stores.
Benefits
Performance: The construction of the response can be done in parallel and, as it transmits less data, ends up being faster and less costly to the clients.
Makes use of REST: Given REST is commonly used already, BFF doesn't introduce a new language for those who are already used to working with REST. This is valid for the Client and the API implementation.
Dedicated to the client: As it is a specific service, it is possible to abstract client logic in the API, increasing the ease of integration and reducing the rework between Android and iOS.
Cache: It is possible to improve the caching strategy with the addition of this tool where it was not possible before, either because it is an external API or because it is not interesting to cache data at a lower level of the application.
Disadvantages
More services are being created
Low reusability: Due to its specificity, a mobile BFF will hardly be used by a WEB client or something else.
Code duplication: If you have a service dedicated to each type of client, duplication of certain parts of the code ends up being unavoidable.
Versioning: This problem can arise from the need to maintain compatibility with an older client application. An alternative, in this case, is to do the minimum version control of the application, forcing all users to update their applications in a service contract break.
Client engineers must translate the screen into models that make sense to work with, supplying all the presentation needs and variations. Of course, this requires some Object Oriented modeling skills, which makes the Android and iOS engineers' interactions even more valuable. At the end of this process, the generated models will be translated to the API contract, and that's why client engineers should have ownership of the BFF and work along with the backend team. This knowledge is extremely beneficial and it can greatly speed up feature development, in addition to disseminating more knowledge within the team, making it a little more multidisciplinary.
This proximity of the people from the client to the BFF starts with the language chosen for the construction of the service. For Android and iOS, it's valid to use Kotlin, since it's the default language on Android, can be executed in the JVM and it's supported by Spring, widely used to create backend services. Also, Kotlin syntax is closer to Swift, bringing iOS engineers closer.
More details about BFF can be found in this text written by Sam Newman and also in this article that shows how the Sound Cloud team implemented this dedicated services strategy.
GraphQL
While in Back for Front we have the service defining what data will be sent to the client, in GraphQL we have the client defining what data it needs through queries.
Benefits
Highly reusable: With each customer looking for the data they need, it is not necessary to create a service for each one.
Does not have versioning: Backward compatibility is achieved by creating schemas, which eliminates the complexity of maintaining more than one version of a given service.
No endpoints: As it works through queries, GraphQL only needs an endpoint to carry the response data of the executed searches.
Disadvantages
Performance: The complexity in the graph can greatly burden performance due to the well-known N+1 problem, where a resolver is called for each field searched. These performance issues can be worked on and require a bit more effort and attention to do so, the good news is that there's a lot of performance-related content like this article about N+1.
Complexity: Not a big deal, but the cognitive load becomes higher, since it is necessary to understand more about the framework for its use, maintenance... Boilerplate can also become a problem when new models are added.
Responsible clients: Because they are highly reusable, It's not simple to abstract client-specific logic into Graph services, making those clients more responsible for handling these rules.
No caching strategies: This problem can be solved using some tools like Apollo, however, this new tool, at least on Android clients, brings an increase of about 3MB in the application.
The responsible clients disadvantage can be reduced or solve by having dedicated resolvers for the mobile clients, which means that we don't have a BFF for free when we create a GraphQL Server, we need to specify the resolvers.
GraphQL still generates a certain kind of controversy and concern when it comes to its overwhelm (which can happen with any technology). Some people suggest hybrid models, with GraphQL being used as a BFF, or a dedicated REST BFF consuming from a GraphQL in mobile cases and web clients consuming directly from the Graph.
Here's an awesome article about mobile development with GraphQL. There are tons of content related, keep reading about it :)
Conclusion
If a service doesn't serve its customer easily, then it's not fulfilling its role and we can go back to the old Client / Database applications. As mentioned before, the service is an extension of the client, therefore, people who work on these two fronts (back, client) must interact constantly, avoiding problems and promoting the exchange of knowledge.