Sharing data across multiple Mobile Squads - with examples
https://docs.google.com/presentation/d/116mNkQAUlhqWODgHluidzjRR4v1dpT0I0UTQm77RNoE/edit?usp=sharing

Sharing data across multiple Mobile Squads - with examples

Earlier I shared an article suggesting a solution to a common problem with teams following the "Spotify Model", which is SDD or Squad Driven Design, that article showcased how multiple squads can own separate modules and interact with each other

but in real life, things are not that perfect, as for every squad should be a cross functional squad owning there Mobile, Frontend, and Backend in isolation of the other squads, and interact with each other with minimum shared data

but still there will be data that is required to be shared across multiple squads, or in the Domain Driven Design language, there will be domain models that are shared between multiple sub-domains, as for each squad is owning a sub-domain, how can they deal with those shared models that have a larger bounded context than the context for each of these squads

back to the mobile world and speaking in plain english, we need to make the squads modules do not depend on each other, but at the same time able to share the common data betwen them smoothly without affecting the decoupled architecture that we have

Let's dive into an example

Lets take food delivery apps as an example, I'll use Talabat as an example here since I'm a frequent user for it, not because I've worked there before, and rest assured that the code and examples shared here are not related to the actual code or squads of that app, also the code here will be simplified to focus on the topic of the article, it is not fully functional

Let's hypothetically assume that we have three squads owning three parts of the application,

The first squad is owning the Home screen, and the other squad is owning the Search Screen, and the third squad is owning the Menu screen, as follows :

Home Screen

Talabat Home Screen as of 2024

Search Screen

Talabat Search Screen with no results as of 2024


Talabat Search Screen with results as of 2024

Menu Screen

Talabat Menu Screen as of 2024

Shared data

so as we can see, both squads have to deal with a restaurant object, but not all squads need the same data from that restaurant object

in a perfect world where a backed can provide this out of the box, as squads could have handled this from there own services, but usually in real world this does not happen, as these features keeps emerging one by one, and squads starts to appear when a feature becomes big enough and at that point the backend is usually not tailored perfectly for those features and there domain models

so in real world, the backend may provide a restaurant DTO through it's APIs, which (in our hypothetical example) looks like this :

@Serializable
data class RestaurantDto(
    @SerialName("id") val id: Long,
    @SerialName("name") val name: String,
    @SerialName("aId") val advertisementId: String?,
    @SerialName("description") val description: String,
    @SerialName("address") val address: AddressDto,
    @SerialName("location") val location: LocationDto,
    @SerialName("branch") val branch: BranchDto,
    @SerialName("contact") val contact: ContactDto,
    @SerialName("website") val website: String?,
    @SerialName("cuisines") val cuisines: List<CuisineDto>,
    @SerialName("ratingStars") val ratingStars: Float,
    @SerialName("ratingUsers") val ratingUsers: Float,
    @SerialName("isFavorite") val isFavorite: Float,
    @SerialName("priceRange") val priceRange: PriceRangeDto,
    @SerialName("isOpen") val isOpen: Boolean,
    @SerialName("openingTime") val openingTime: String,
    @SerialName("closingTime") val closingTime: String,
    @SerialName("openDays") val openDays: List<String>,
    @SerialName("services") val services: ServicesDto,
    @SerialName("imageUrl") val imageUrl: String,
    @SerialName("menuUrl") val menuUrl: String,
    @SerialName("deliveryMinutes") val deliveryMinutes: Int,
    @SerialName("deliveryCost") val deliveryCost: Double,
    @SerialName("categories") val categories: List<CategoryDto>,
    @SerialName("tags") val tags: List<TagDto>,
    @SerialName("paymentMethods") val paymentMethods: List<PaymentMethodDto>,
    @SerialName("status") val status: StatusDto,
    @SerialName("discounts") val discounts: List<String>,
    ...
)
...        

and as you can see it will contain lots of it's own internal DTOs as well, in big projects usually these DTO data classes are generated from API contracts, as they generate a client SDK for each platform, and mobile side adds this SDK as a dependency and have to deal with it ... that way all retrofit interfaces (for android) and networking related data classes (like this one above) are provided out of the box


For Home Screen Squad, they need to represent the restaurants as follows:

  • Order Again Section

Home Screen Order Again Restaurants Section

  • Popular Today Section

Home Screen Popular Today Restaurants Section

and for this squad domain, a Restaurant can just be represented as these data classes :

// lets assume that in this section, "busy" or "closed" restaurants wont be 
// received from server
data class OrderAgainRestaurant(
    val id: Long,
    val imageUrl: String,
    val name: String,
    val minutesToDeliver: Int
)

// lets assume that in this section, "busy" and "closed" restaurants will be 
// received from server, as we may show "busy" restaurants in this section
data class PopularTodayRestaurant(
    val id: Long,
    val imageUrl: String,
    val name: String,
    val minutesToDeliver: Int,
    val status: Status
) {
    enum class Status {
        OPEN,
        BUSY,
        CLOSED
    }
}        

For Search Screen Squad, they need to represent the restaurant as follows :

  • Featured Restaurants Section

Search Screen Featured Restaurant Section

  • Search Result Restaurants

Search Result Restaurants

and for this squad domain, a Restaurant can just be represented as these data classes :

data class FeaturedRestaurant(
    val id: Long,
    val imageUrl: String,
    val name: String,
    val cuisines: List<String>,
    val ratingStars: Float,
    val ratingCount: Int,
    val deliverWithinMinutes: Int,
    val deliveryFee: Double
)

data class SearchResultRestaurant(
    val id: Long,
    val isAd: Boolean,
    val imageUrl: String,
    val name: String,
    val cuisines: List<String>,
    val ratingStars: Float,
    val ratingCount: Int,
    val deliveryMinutes: Int,
    val deliveryFee: Double,
    val discounts: List<String>
)        

  • in the real UX, results are sorted based on OPEN, BUSY, or CLOSED status, but this is out of the scope of this example ... as mentioned before, this article's code does not represent Talabat or the real app


For Menu Screen Squad, they need to represent the restaurant as follows :

  • Menu Header Restaurant Section

Menu Header Restaurant
data class MenuHeaderRestaurant(
    val id: Long,
    val imageUrl: String,
    val isFavorite: Boolean,
    val name: String,
    val cuisines: List<String>,
    val ratingStars: Float,
    val ratingCount: Int,
    val deliveryMinutes: Int,
    val deliveryFee: Double,
    val deliveryProvider: DeliveryProvider?,
)        

The Common Problems

I'm sure that by now there are multiple problems that you already see in this approach, as every squad is working in there own domain, caring about there own requirements (which is expected), this squad driven approach comes with some common problems:

  1. How will I share the restaurant from one squad scope to the other ?
  2. How will I handle non domain related data, like passing to analytics SDK data that is not in my squad's scope, or passing to the Menu Screen parameters that do not exist in Search Screen data classes ?


Suggested Solutions

First thing we shuold consider is the Uni-Directional Flow of data, which means that we do not pass data between screens (or features) in a horizontal manner, but any data we pass should go to the same flow every time

User Trigger Action in Screen A -> Cache Data In Memory -> Load data in Screen B

That way, instead of Squad A dealing with Squad B, the both are isolated, and squad A just pushes the data into cache, then ask for navigation ... and when squad B screen starts, it loads the cached Data

and this comes with a cost of course, that for every squad they have to follow the same rule, but comparing it to the "Horizontal approach", if squad A did not send data to cache, it is similar to not putting the data into the intent extras bundle ... at the end of the day, the risk factor of missing the data is there in both cases and needs handling

Applying this solution on our screens now

Now we will create an external dependency that all squads can access the same way they do with there data-sources code, and it will be part of the data-sources external modules (which holds access to generated retrofit interfaces, networking utilities, etc..), or can be declared in a separate module on the same level of the data-sources related modules ... and this can be accessed from the data-sources code of the squad

interface SelectedRestaurantGateway {
    suspend fun setSelectedRestaurant(restaurant: ...)
    suspend fun getSelectedRestaurant(): ...?
}        

So that when ever the Home or Search squads want to open a Restaurant Menu, they can then set that selected restaurant through this interface, and then the Menu Screen will load it from here and then use it

But Each Squad designed the Restaurant Data Class as per there requirements, they should not be bothered with other squads

A simple solution for this is to modify the domain models, to have a place holder for the DTO of type Any, since each squad domain data classes are pure kotlin classes with no dependency on any external code, those data classes do not see the data sources, and hence the OrderAgainRestaurant for example does not see the RestaurantDto in it's class path ... we are here assuming that squads are isolating there domain packages and following either an Onion, or Clean, or Hexagonal architecture in there Feature architecture ... just a reminder of how dependencies work in an such architectures when it comes to domain

Onion Architecture Dependencies Direction

So now we can modify our interface to be as follows :

interface SelectedRestaurantGateway {
    suspend fun setSelectedRestaurant(restaurant: RestaurantDto)
    suspend fun getSelectedRestaurant(): RestaurantDto?
}         

And for the OrderAgainRestaurant, we add a placeholder for the DTO :

data class OrderAgainRestaurant(
    val dto: Any, // will hold RestaurantDto
    val id: Long,
    val imageUrl: String,
    val name: String,
    val minutesToDeliver: Int
)        

And to keep the casting operations safe, the only place that will do the casting will be in the Repository Implementation, as this is the class that depends on both the Repository interface (domain) and the squad's internal data-sources module (data-sources), so it will look as follows :

// in squad internal domain code : 

interface HomeRepository {
    ...
    suspend fun getOrderAgainRestaurants(): List<OrderAgainRestaurant>
    suspend fun selectOrderAgainRestaurant(restaurant: OrderAgainRestaurant)
}        
// in squad internal data-sources code : 

class HomeRepositoryImpl @Inject constructor(
    private val restaurantsGateway: RestaurantsGateway,
    private val selectedRestaurantGateway: SelectedRestaurantGateway,
) : HomeRepository {

    ...

    override suspend fun getOrderAgainRestaurants() =
        restaurantsGateway.getDeliveryRestaurants().map { restaurantDto ->
            OrderAgainRestaurant(
                // keep restaurantsDto for future use
                dto = restaurantDto, 
                id = restaurantDto.id,
                ...
            )
        }
    
    override suspend fun selectOrderAgainRestaurant(restaurant: OrderAgainRestaurant) {
        // passing restaurantDto back
        selectedRestaurantGateway.setSelectedRestaurant(restaurant.dto as RestaurantDto)
    }
}        
Notice that all casting operations are done in only one place, which is the mapping logic inside the repository implementers, no where else

And now the Menu Squad can load this while they are initializing there Menu Screen, so they can load it from cache the same way they deal with network calls or data stored in a database, which could be as follows :

// in squad domain code : 

data class MenuHeaderRestaurant(
    val dto: Any, // will hold restaurantDto
    val id: Long,
    ...
)

interface MenuRepository {
    ...
    suspend fun getSelectedRestaurant(): MenuHeaderRestaurant
}
        
// in squad data-srouces code : 

class MenuRepositoryImpl @Inject constructor(
    private val selectedRestaurantGateway: SelectedRestaurantGateway,
    private val menuGateway: MenuGateway,
    ...
) : MenuRepository {

    ...

    override suspend fun getSelectedRestaurant() =
        selectedRestaurantGateway.getSelectedRestaurant()?.let { restaurantDto ->
            MenuHeaderRestaurant(
                // keep restaurantsDto for future use
                dto = restaurantDto,
                id = restaurantDto.id,
                ...
        } ?: throw IllegalStateException("No Restaurant selected")
}        

And then the Menu Screen ViewModel will communicate with this Menu Repository to (through a Use-Case or whatever) and load the restaurant on initialization


What about Analytics or Logging ?

As long as the project is live squads keep being asked to add analytics data or log data to keep track of UX related stuff or help investigate issues on production, and this is totally fine, but in the traditional approach, every time the Marketing team wants to track something, squads keep modifying there domain models to keep hold of this analytics related data, and the more requirements that comes from the marketing team, the more changes that squads do, and these changes means more lines of code to maintain, updating unit tests and production code to compile after those changes, making sure that the test cases are passing after modifying the domain models and the methods parameters that deal with those models

The above approach helps in putting aside most of this hustle as well, assume that in the Search Screen we need track if the user is selecting one of his favorite restaurants or not, and this isFavorite do exist in the RestaurantDto, but it is not of a concern for the Search Squad, as there is nothing in there requirements that mention any thing about handling favorite restaurants

But since we have the original RestaurantDto with us, it is now very easy to access such data, as in our repository implementer we will inject the analytics related class (remember analytics is considered a data source as well), and pass the required data from the DTO directly as follows

class SearchRepositoryImpl @Inject constructor(
    private val selectedRestaurantGateway: SelectedRestaurantGateway,
    private val analytics: SearchResultRestaurantClickAnalytics,
    ...
) : SearchRepository {
   
    ...

    override suspend fun selectSearchResultRestaurant(restaurant: SearchResultRestaurant) {
        val dto = restaurant.dto as RestaurantDto
        selectedRestaurantGateway.setSelectedRestaurant(restaurant.dto)
        analytics.onSearchResultRestaurantClick(
            SearchResultRestaurantClick(
                restaurantId = dto.id,
                isFavorite = dto.isFavorite,
                ...
            )
        )
    }
}        

As I've lived these problems personally in multiple big teams, I hope this can save the day for a team out there

Just a sneak peak about the architecture of this samlpe code, just in case you'd like to know where each of the mentioned code snippet exists (or relates to the other) :

  • in each squad project, the domain code is pure kotlin code that does not have any dependencies

要查看或添加评论,请登录

Ahmed Adel Ismail的更多文章

  • SDD - Squad Driven Design

    SDD - Squad Driven Design

    Working in multiple big teams I've found that we are always trying to apply our known best practices in software, but…

    4 条评论
  • Easier Testing with MVVM, MVI, MVP and Kotlin Multiplatform

    Easier Testing with MVVM, MVI, MVP and Kotlin Multiplatform

    Before we start, this article requires basic knowledge about the following topics : Clean Architecture Unit Testing…

    10 条评论
  • Android - A Cleaner Clean Architecture

    Android - A Cleaner Clean Architecture

    It has been a while now since Clean Architecture was out, and even many of us started embracing hexagonal (ports and…

    10 条评论
  • Beyond Functional Programming

    Beyond Functional Programming

    In the Android industry, lately functional programming was the all new stuff to learn, RxJava, Kotlin, and the whole…

    7 条评论
  • Dependency Injection in Clean Architecture

    Dependency Injection in Clean Architecture

    After Google's Opinionated Guide to Dependency Injection Video, Google made a clear statement that they want developers…

    18 条评论
  • Meta Programming in Android

    Meta Programming in Android

    Year after year we are getting rid of the boilerplate code that we need to write for small and simple tasks in Android,…

    2 条评论
  • MVI Pattern For Android In 4 Steps

    MVI Pattern For Android In 4 Steps

    Lately I wrote an article about MVI pattern, but as we are facing new problems every day and face more use-cases, we…

    7 条评论
  • Agile - Moving Fast

    Agile - Moving Fast

    We always here about Agile, and think about which methodology do we use, what practices do we have, team velocity…

    1 条评论
  • Kotlin Unit Testing with Mockito

    Kotlin Unit Testing with Mockito

    I've always believed that, if the code is designed to be tested, we wont need any testing framework or library ..

    17 条评论
  • MVI - Model View Intent simplified

    MVI - Model View Intent simplified

    I have been searching for a proper resource to explain the MVI pattern, but every time I get hit with Hannes Dorfmann…

    4 条评论

社区洞察

其他会员也浏览了