Sharing data across multiple Mobile Squads - with examples
Ahmed Adel Ismail
Engineering Manager @ Yassir | x-SadaPay | x-Swvl | x-Talabat | x-TryCarriage | x-Vodafone | More than a decade of experience in Android development and teams leadership
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
but still there will be data that is required to be shared across multiple squads, or in the Domain Driven Design
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
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
Search Screen
Menu Screen
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:
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 :
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>
)
For Menu Screen Squad, they need to represent the restaurant as follows :
领英推荐
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:
Suggested Solutions
First thing we shuold consider is the Uni-Directional Flow
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
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
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) :