iOS App As a Microservice. Build Robust App Architecture

What will you choose: MVVM, MVC, VIPER? Those all are local and problem-specific architectures. But how to structure your app on a larger scale to make it scalable and well-organized?

iOS App As a Microservice. Build Robust App Architecture
Photo by Kaleidico / Unsplash

What will you choose: MVVM, MVC, or VIPER? Those all are local and problem-specific architectures. But how to structure your app on a larger scale to make it scalable and well-organized?

In this post, I will discuss microfeature architecture that is, simply said, amazing when implemented correctly in an iOS app.

Next Episodes

  • Ideas on implementation with SwiftUI
iOS App As a Microservice. Using SwiftUI in Modular App
The modular architecture is excellent. But how to implement it effectively with SwiftUI? From its core, SwiftUI is state-driven, and it can be tricky to modularize an app and define exact responsibility borders.
  • Using tuist to structure microfeature application
iOS App As a Microservice. Modularize Your App With Tuist
This is the second article in a series on modular app architecture. In this post, I will cover implementation details using Tuist

Core Idea

The idea comes from microservice server-side application infrastructure. The whole app is divided into logical components corresponding to different functional areas of the application.

💥
Considering how complex mobile apps can be, why not apply the same architecture to iOS apps?

Briefly, microfeature architecture implies splitting your app into different components that accept other components' interfaces or data as explicit dependencies.

Therefore, your app can be represented as a graph of modules that explicitly interact with each other.

Main Benefits

  • Improved maintainability — each component is small and so is easier to understand and change.
  • Better testability — components explicitly define their public interface. So, they are easier to mock and test.
  • Team organization — different teams can work on different components independently.
  • Scalability, code reuse —when an app is a combination of modules, you can robustly change the app's behaviour by recombining modules. If you decide to create an app extension, watchOS app, or App Clip, just pick the required components and you're all set up.
  • Explicit dependencies — implicit dependencies are one of the worst things that can happen to an app's architecture. This architecture requires defining explicit dependencies for each module.

Details

So, how to structure an iOS app once you decided to use microfeature architecture? The core concept is separation. But you still can use one Xcode project for that and separate features purely by architecture.

💥
You can put each feature into a separate Xcode project. This will push you to a strict separation of components.

I will cover how to do this effectively with tuist in the next episode!

Your codebase will be divided into several blocks:

Features

That's where elements of your app live. Later in this post, I will show by example what this part includes.

Components are logical blocks of your app. Each component explicitly defines an interface to interact with it.

💡
Swift does not have namespaces, but you can use enums to hide internal module logic.

Apps

You can have a WatchOS app, widgets, and the main iOS app. Each app depends on features and builds the final app using features, combining them like bricks.

General apps structure
Photo by Ashkan Forouzani / Unsplash

Tests + Testing Data And Mock

This logic also lies apart from the feature's main parts. It's separate because:

  • We don't want to use mock data accidentally in the app
  • We don't want to include irrelevant data in the final app binary

Feature design

The feature consists of four blocks. Tests and mocks may not be present, but the feature always has an interface and implementation.

One feature structure

Interface

This part defines parts visible for other features. Public interfaces and models or entities of the feature stay here.

Interfaces define ways that are used to interact with the feature.

Models or entities are simple structures with almost no logic that simply define data used to communicate with the feature.

You can include other components in the interface but remember that interface must not expose implementation details

💥
If the feature depends on another feature, then it depends on the other feature's interface.

Features must not depend on other feature's implementation

Implementation

Implementation depends on an interface and provides classes and structures conforming to defined protocols in the interface. Resources, images, and other implementation details also stay here.

💡
Separation Interface/Implementation forces you to write code conforming to the letter D from SOLID.

Dependency inversion happens naturally when other modules know about interfaces and not about implementations.

Knowing this information, we can add details to our app's graph image:

Detailed apps structure

Notice that none of the features depends on the other feature's interface. Each feature interface strictly depends on the other feature's interface.

Now you see that apps take building blocks and combine them to make an app.

Subscribe and don't miss posts!

Case Example

Let's architect a scheduling app. It will have:

  • Schedule view
  • Add event/edit view
  • Schedule WatchOS View

Pretty simple.

Let's split this app into several features:

  • UICommon

Contains common UI elements that can be used to create more complex views

  • Schedule

Contains main schedule views and logic associated with them. The interface defines ways to interact with views or present them.

  • WatchSchedule

Contains watch-specific schedule views and logic associated with them

  • EventModification

Contains event modification logic and views

  • ScheduleData

Data provider. Defines data structures and entities to obtain them.

The interface will contain simple data entities and model protocols defining ways of obtaining these entities.

Implementation defines models conforming to protocols defined in the interface. For example, you may want to define a local storage model or network model. It's up to the final app to decide which option to use.

App Graph

Case app graph

As you see, WatchOS and the main iOS app reuse common components. Also, Each app decides which implementation of modules' interfaces they pick. For example, the WatchOS app can choose different data sources in ScheduleData feature rather than the main iOS app.

In a monolithic app, you would probably need to write almost a second app and copy a lot of code

Next Episodes

In the next posts, I will share my ideas on using microfeature architecture with SwiftUI and tuist to structure code efficiently.

FAQ

When should I create a new feature and when It's better not to?

It purely depends on the case and on what you think the best option is. If you can come up with some use case when your feature will be reused in some other context, then it's a separate feature.

💥
Do not overcomplicate things!
Making a new feature for each class will do more harm than good.

If some block probably will not be reused, but you just feel that it's logically separate functionality, then also go with a new feature as it will help to keep your architecture clean.

What to do with circular references?

Circular references can be a pain and they happen if two features depend on each other's interfaces. If such a situation happens, critically consider if your feature separation is correct. There are two possible options.

  • Two features are actually one feature. Then, you can merge these two features and get rid of circular references.
  • Two features are actually three features. If features depend on each other, then there is some part that's needed by both features. What if this part is an independent feature? If this is the case, extract the third feature and fix dependencies.
Possible circular reference solution

There's a lot said about making dependencies explicit. What's the point?

It's nearly impossible to scale or modify big apps when components are implicitly dependent. Just imagine the mess that is going to happen if you modify some class that is a dependency of all other modules through a singleton.

Your app may start to have unexpected behaviour here and there and you can't even know how your modification will affect the whole app.

It's like sitting on a box of TNT.

🃏
Photo by Mehdi MeSSrro / Unsplash

I encourage you to avoid implicit dependencies whenever possible. Microfeatures architecture will help you with doing that.

References

iOS App As a Microservice. Modularize Your App With Tuist
This is the second article in a series on modular app architecture. In this post, I will cover implementation details using Tuist
µFeatures Architecture | Tuist Documentation
This document describes an approach for architecting a modular Apple OS application to enable scalability, optimize build and test cycles, and ensure good practices.

Subscribe to Alex Dremov

Don’t miss out the latest publications! Only useful posts for developers to bump up your professional skills
bestdeveloper@example.com
Jump In