As backend architectures evolve toward microservices, mobile clients face a new challenge: instead of one REST API, they're talking to dozens of services. The Backend-for-Frontend (BFF) pattern and GraphQL are the two dominant solutions.
The Problem with Direct Microservice Calls
Mobile App → User Service (auth)
→ Article Service (content)
→ Comment Service (engagement)
→ Media Service (images/video)
→ Notification Service (badges)
Problems:
- Multiple network round-trips per screen
- Each service has its own API contract, versioning, auth
- Overfetching: Article Service returns full article even for list view
- Hard to optimize for slow mobile networks
Solution 1: Backend-for-Frontend (BFF)
The BFF is a dedicated backend layer, owned by the mobile team, that aggregates multiple microservices into a single, client-optimized API:
Mobile App
└── BFF (owned by mobile team)
├── User Service
├── Article Service
├── Comment Service
└── Media Service
// Client sees ONE API instead of five:
GET /api/mobile/feed?userId=123
→ Returns: articles with authors embedded, thumbnail URLs sized for mobile, unread counts
Advantages:
- Client-optimized payloads (no overfetching)
- One auth token, one endpoint
- Mobile team controls API evolution
- Backend team doesn't need to know about mobile screen layouts
Disadvantages:
- Additional service to maintain
- Can become a bottleneck/single point of failure
- Requires coordination between mobile and backend teams
Solution 2: GraphQL
GraphQL lets the client declare exactly what data it needs in a single query, even if that data spans multiple microservices:
query FeedQuery($userId: ID!) {
feed(userId: $userId) {
articles {
id
title
thumbnail { url width height } # only what mobile needs
author { name avatarUrl }
commentCount # from Comment Service
}
unreadCount # from Notification Service
}
}
On Android with Apollo:
val apolloClient = ApolloClient.Builder()
.serverUrl("https://api.example.com/graphql")
.okHttpClient(okHttpClient)
.build()
viewModelScope.launch {
val response = apolloClient.query(FeedQuery(userId = currentUserId)).execute()
val articles = response.data?.feed?.articles ?: return@launch
_uiState.value = UiState.Success(articles.map { it.toUiModel() })
}
Advantages:
- Client drives query shape — no overfetching or underfetching
- Single endpoint, single auth
- Strong typing from schema
Disadvantages:
- Requires GraphQL server (or gateway)
- N+1 query problem needs server-side DataLoader
- Caching is more complex than REST
Structuring the Data Layer for Microservices
// Repository abstracts whether data comes from one or multiple services
class FeedRepository(
private val articleService: ArticleApi,
private val userService: UserApi,
private val commentService: CommentApi
) {
suspend fun getFeedItems(): List<FeedItem> = coroutineScope {
val articlesDeferred = async { articleService.getArticles() }
val usersDeferred = async { userService.getAuthors() }
val countsDeferred = async { commentService.getCommentCounts() }
// Parallel fetch — all three requests in flight simultaneously
val articles = articlesDeferred.await()
val users = usersDeferred.await().associateBy { it.id }
val counts = countsDeferred.await().associateBy { it.articleId }
articles.map { article ->
FeedItem(
article = article,
author = users[article.authorId],
commentCount = counts[article.id]?.count ?: 0
)
}
}
}
Caching Aggregated Responses
Cache the assembled response, not the individual service responses:
// Room entity matches the aggregated view, not individual service models
@Entity(tableName = "feed_items")
data class FeedItemEntity(
@PrimaryKey val articleId: String,
val title: String,
val authorName: String,
val authorAvatarUrl: String,
val commentCount: Int,
val cachedAt: Long = System.currentTimeMillis()
)
Key Takeaways
| Approach | Best for |
|---|---|
| Direct service calls | Simple apps with ≤ 2 services |
| BFF | Mobile team that controls their own API; screen-specific optimization |
| GraphQL | Apps with complex, varied data needs; teams that already use GraphQL on backend |
| Parallel coroutines | Aggregating multiple endpoints without a BFF |