Architecting enterprise mobile applications –
Venturing into creating a singularity in multi-feature application
I’ve recently been working on a new initiative to figure out how to achieve development excellence and agility with regards to mobile application development, to support for a unified developer experience across multiple teams in multiple business units through the outcome of this initiative.
A singularity depicts nothing more than being singular. Putting in the perspective of mobile development, it would mean singular application, singular delivery pipeline, singular experience.
The key benefit of singularity is consistency.
Consistency in approach to develop a part of a bigger application ecosystem. Very often in a company with multiple business units and engineering teams we see very different approaches and development choices even if they are on the same technology, due to different preferences of the development team itself. For example, to have these teams to work together on a single application, we may end up seeing pull requests in very different coding approaches and styles — what follows is the team would start falling into the trap of distraction from the actual important work, and spend most of the time to achieve a consensus in cases like pull requests, but ultimately the teams will still need to come to an agreement to move forward as one team.
The nightmare is when a fresh team joins forces, it becomes at least linearly as hard to scale.
So what do we want to achieve?
In my case, it was relatively simpler as most teams have yet to establish a standard mobile development workflow.
The major area to target in the first part of this initiative is:
- Streamlined mobile development workflow and development standards
- Elastic app architecture
- High agility CI/CD and recovery pipelines
Divide and Conquer
While in traditional enterprises, teams are often formed through people that are responsible for different business verticals, or smaller branches of the same business vertical.
In these teaming structure it is often difficult to have all their development teams to work on a single application at a time, due to differences aforementioned, we started asking ourselves:
What if we divide the problem among the teams themselves?
We could practically keep the essence of the existing formed teams, e.g. working style and team chemistry. We can utilise this for teams to work in isolation, based on pre-defined development rules, so the teams could focus on what’s important (for their business needs).
To strike this balance between teaming and singularity — we ideated with a basal architectural concept, which is to start having an imperative shell, and functional cores.
Let’s start from the inside. As a developer for a single functional team, we would want to focus on the feature that the team is responsible for. It would be almost necessary to have physical boundaries around logical partitions or cores for solely feature team’s responsibility when it comes to taking care of bugs and feature enhancements.
Each feature is responsible for three major parts to form a functional component in an application, namely: Domain logic, UI, data exchange. They should be written for only what they are responsible for — nothing more. Each feature core should be able to be fully unit tested.
Factor the commons
As all feature cores are going to carry out some common functions at some point, for example:
- Calling APIs
- Storing objects into databases
- Navigating between user interfaces
We can start seeing that there is a need for a thin layer, to heavy-lift these common process executions.
Laying the foundations
The base layer would be able to provide the following:
- Development paradigm on the interface level
- An in-between point for dependency injection between application and feature cores, to provide common operations (e.g. interacting with remote API, keychain, local database)
- Type convenient extensions
Setting the development paradigm
Interfaces on the base layer could be optionally set up as such that it utilises the development paradigm of choice, for example, reactive programming, by exposing reactive interfaces through the base layer to the feature cores.
This remains as one of the key parts of the base layer as it serves as the brain for the rest of the feature cores to work.
A simple example — A feature core needs a data persistence to the application database. The base layer would have database persistence mechanism written generically based on the local database APIs, while the actual database object would be injected to the feature cores through the base layer while feature core resolves the database interactor against the interface provided through the base layer.
Same could be done to factor out common functionalities of feature cores, e.g. remote API execution, logging, analytics tracking, UI navigation, through the same approach.
This way feature cores would be able to focus more on writing domain logic and creating user interfaces and care a little less about building common operations and reinventing the cycle.
What about common features? e.g. authentication, payment?
We can also utilise the same way to factor common business-critical logic like authentication and payment by introducing interfaces for such dependency to be injected from the application directly, hence feature cores would not require direct dependency from these business logics instead their lifecycles will be fully managed from the application level.
Connecting the dots
Summarising points above, the base layer would have looked something like this:
where it essentially is a thin layer providing access interfaces for various common functions and business logic, and to be reused through the dynamic resolution of dependencies.
At 10,000 feet view, each team will be working on feature cores that their business team are responsible for, through the programming paradigm that has already been set in place in the base layer.
As all feature cores depend on the base layer and built the almost same way in the interface level and able to encapsulate all internal logics completely, this allows the team to have the flexibility to create internal service however the team likes to, but still remain consistency on the interface level.
This kind of isolation also allows isolated testing without the influence of other feature cores at the logical level, which means every feature core unit should be able to carry out tests for the features on their own before integration test is carried out.
This gives us a basic architecture to start putting things into places, however, I do not think this is a golden architecture yet, as there are still a lot of gaps to be filled until this architecture is production-ready.
Some immediate questions:
- What is the development workflow like for a feature core developer from developing, testing, and deploying the feature to test?
- How are the dependencies are to be managed in terms of version controlling the base layer as well as the feature cores?
- What are the other shortcomings? Can these be tolerated for the bigger goal we try to achieve?