androidengineers.Book a session

Network Architecture

GraphQL & Apollo Basics

article20 minMedium

GraphQL vs REST

REST exposes fixed endpoints that return fixed shapes. GraphQL exposes a single endpoint where the client specifies exactly what data it needs.

The over-fetching problem

# REST: GET /users/42 returns everything
{
  "id": 42,
  "name": "Alice",
  "email": "alice@example.com",
  "bio": "...",
  "avatar_url": "...",
  "phone": "...",      // ← not needed in this screen
  "address": {...},   // ← not needed
  "preferences": {...} // ← not needed
}

# GraphQL: ask for exactly what you need
query GetUserName {
  user(id: 42) {
    name
    avatarUrl
  }
}
# Response contains only name and avatarUrl

The under-fetching problem (N+1 requests)

# REST: to show a post list with author names requires:
GET /posts          → 20 posts with author_id fields
GET /users/1        → post 1's author
GET /users/7        → post 2's author
... 18 more requests

# GraphQL: one request for everything
query GetPostsWithAuthors {
  posts(first: 20) {
    title
    createdAt
    author {
      name
      avatarUrl
    }
  }
}

Core GraphQL Concepts

Query — reading data

# Simple query
query GetProduct($id: ID!) {
  product(id: $id) {
    id
    name
    price
    inStock
    category {
      id
      name
    }
  }
}

# Query with multiple fields and aliases
query GetDashboard($userId: ID!) {
  user(id: $userId) {
    name
    email
  }
  recentOrders: orders(userId: $userId, first: 5) {
    id
    total
    status
  }
  notifications(userId: $userId, unreadOnly: true) {
    count
    items {
      id
      message
    }
  }
}

Mutation — writing data

mutation CreateOrder($input: CreateOrderInput!) {
  createOrder(input: $input) {
    id
    status
    total
    estimatedDelivery
  }
}

# Variables passed separately (not inline)
# {
#   "input": {
#     "productIds": ["prod_1", "prod_2"],
#     "shippingAddressId": "addr_42"
#   }
# }

Subscription — real-time updates

subscription OnOrderStatusChanged($orderId: ID!) {
  orderStatusChanged(orderId: $orderId) {
    id
    status
    updatedAt
  }
}

Apollo Android Setup

1. Add dependencies and plugin

// build.gradle.kts (project level)
plugins {
    id("com.apollographql.apollo3") version "3.8.2" apply false
}

// build.gradle.kts (module level)
plugins {
    id("com.apollographql.apollo3")
}

dependencies {
    implementation("com.apollographql.apollo3:apollo-runtime:3.8.2")
    implementation("com.apollographql.apollo3:apollo-normalized-cache:3.8.2")
    // For coroutines support
    implementation("com.apollographql.apollo3:apollo-coroutines-support:3.8.2")
}

apollo {
    service("example") {
        packageName.set("com.example.graphql")
        // schema.json or schema.graphqls must be in src/main/graphql/
    }
}

2. Download schema

# Using Apollo CLI
npx apollo client:download-schema schema.graphqls \
  --endpoint https://api.example.com/graphql \
  --header "Authorization: Bearer $TOKEN"

# Or using Apollo Gradle plugin
./gradlew :app:downloadApolloSchema \
  --endpoint="https://api.example.com/graphql" \
  --schema="src/main/graphql/schema.graphqls"

3. Write .graphql files

Place .graphql files in src/main/graphql/com/example/:

# GetProduct.graphql
query GetProduct($id: ID!) {
  product(id: $id) {
    id
    name
    price
    inStock
  }
}

# CreateOrder.graphql
mutation CreateOrder($input: CreateOrderInput!) {
  createOrder(input: $input) {
    id
    status
    total
  }
}

Apollo generates type-safe Kotlin classes from these files at build time.

ApolloClient Setup

@Module
@InstallIn(SingletonComponent::class)
object GraphQLModule {

    @Provides
    @Singleton
    fun provideApolloClient(
        tokenStore: TokenStore
    ): ApolloClient {
        val memoryCache = MemoryCacheFactory(maxSizeBytes = 10 * 1024 * 1024)

        return ApolloClient.Builder()
            .serverUrl("https://api.example.com/graphql")
            .okHttpClient(
                OkHttpClient.Builder()
                    .addInterceptor { chain ->
                        val request = chain.request().newBuilder()
                            .header("Authorization", "Bearer ${tokenStore.getAccessToken()}")
                            .build()
                        chain.proceed(request)
                    }
                    .build()
            )
            .normalizedCache(memoryCache)
            .build()
    }
}

Executing Queries

class ProductRepository @Inject constructor(
    private val apolloClient: ApolloClient
) {

    suspend fun getProduct(id: String): Result<Product> {
        return try {
            val response = apolloClient
                .query(GetProductQuery(id = id))
                .execute()

            if (response.hasErrors()) {
                val error = response.errors?.firstOrNull()
                Result.failure(Exception(error?.message ?: "GraphQL error"))
            } else {
                val data = response.data?.product
                    ?: return Result.failure(Exception("Product not found"))
                Result.success(data.toProduct())
            }
        } catch (e: ApolloException) {
            Result.failure(e)
        }
    }

    // Cache policy control
    suspend fun getProductFresh(id: String): Result<Product> {
        val response = apolloClient
            .query(GetProductQuery(id = id))
            .fetchPolicy(FetchPolicy.NetworkOnly)  // ignore cache
            .execute()
        // ...
    }

    suspend fun getProductCached(id: String): Result<Product> {
        val response = apolloClient
            .query(GetProductQuery(id = id))
            .fetchPolicy(FetchPolicy.CacheFirst)   // serve cache, fallback to network
            .execute()
        // ...
    }

    // Execute mutation
    suspend fun createOrder(productIds: List<String>, addressId: String): Result<Order> {
        return try {
            val input = CreateOrderInput(
                productIds = productIds,
                shippingAddressId = addressId
            )
            val response = apolloClient
                .mutation(CreateOrderMutation(input = input))
                .execute()

            if (response.hasErrors()) {
                Result.failure(Exception(response.errors?.firstOrNull()?.message))
            } else {
                val order = response.data?.createOrder
                    ?: return Result.failure(Exception("No order in response"))
                Result.success(order.toOrder())
            }
        } catch (e: ApolloException) {
            Result.failure(e)
        }
    }
}

Normalized Cache

The normalized cache stores each object by its ID, not by query. When the same object appears in multiple queries, it is stored once. Updating it in one place automatically updates all queries that reference it.

val memoryCache = MemoryCacheFactory(maxSizeBytes = 10 * 1024 * 1024)
val diskCache = SqlNormalizedCacheFactory("apollo_cache.db")

// Chain: memory cache → disk cache → network
val combinedCache = memoryCache.chain(diskCache)

val apolloClient = ApolloClient.Builder()
    .serverUrl("...")
    .normalizedCache(
        normalizedCacheFactory = combinedCache,
        cacheKeyGenerator = TypePolicyCacheKeyGenerator,  // use __typename + id as key
        cacheResolver = FieldPolicyCacheResolver
    )
    .build()

The cache key is typically "TypeName:id" (e.g., "Product:42"). Configure this in your type policies:

// apollo.config.js (or via Gradle)
// Type policies define the key field per type
// Product.id → cache key "Product:42"

Optimistic Updates

Show the result of a mutation immediately, before the server confirms it:

suspend fun toggleLike(postId: String, currentlyLiked: Boolean) {
    apolloClient.mutation(
        ToggleLikeMutation(postId = postId)
    )
    .optimisticUpdates(
        ToggleLikeMutation.Data(
            toggleLike = ToggleLikeMutation.ToggleLike(
                id = postId,
                liked = !currentlyLiked,
                likeCount = if (currentlyLiked) likeCount - 1 else likeCount + 1
            )
        )
    )
    .execute()
    // If the server returns an error, Apollo automatically rolls back the optimistic update
}

Subscriptions over WebSocket

// Subscription — returns a Flow
fun watchOrderStatus(orderId: String): Flow<OrderStatus> {
    return apolloClient
        .subscription(OnOrderStatusChangedSubscription(orderId = orderId))
        .toFlow()
        .filter { !it.hasErrors() }
        .mapNotNull { it.data?.orderStatusChanged?.toOrderStatus() }
}

// Usage in ViewModel
fun watchOrder(orderId: String) {
    viewModelScope.launch {
        repository.watchOrderStatus(orderId)
            .catch { e ->
                _uiState.update { it.copy(error = e.message) }
            }
            .collect { status ->
                _uiState.update { it.copy(orderStatus = status) }
            }
    }
}

Apollo automatically manages the WebSocket connection lifecycle and reconnects on disconnect.

When GraphQL Is Worth the Complexity

GraphQL WinsREST Wins
Mobile clients need different shapes per screenSimple CRUD with stable, predictable shapes
Multiple teams own different parts of the schemaSmall team, tight schema control
Significant over-fetching bandwidth issuesLow-bandwidth concerns are minimal
Real-time requirements (subscriptions)No real-time needs
Strong type safety between frontend and backendBackend already has well-typed REST contracts
Schema stitching / federation neededSingle monolithic backend

GraphQL adds schema management, codegen tooling, cache complexity, and a learning curve. For simple apps with a few endpoints, REST is usually the pragmatic choice.

Key Takeaways

ConceptKey Point
QueriesRead-only; declare exactly which fields to fetch
MutationsWrite operations; return the modified object
SubscriptionsReal-time updates over WebSocket; returns a Flow
Codegen.graphql files → type-safe Kotlin; build fails on schema mismatch
Normalized cacheObjects stored by ID; one update reflects everywhere
Optimistic updatesShow result instantly; auto-rollback on server error
Fetch policiesCacheFirst, NetworkOnly, CacheAndNetwork for different needs
When to useJustified when over/under-fetching or multi-team schema ownership is a real pain

YOUR LEARNING JOURNEY

0 of 177 available lessons completed

Progress saved in this browser. No account needed.
GraphQL & Apollo Basics | Android System Design | Android Engineers