GraphQL and Flexible Query APIs Questions
Designing schema-driven query APIs where clients specify exactly the data they need. Covers types/queries/mutations/subscriptions, resolvers, the N+1 problem and batching/dataloaders, over-fetching vs. under-fetching trade-offs against REST, and schema federation. Includes when a flexible query layer is warranted versus a fixed-endpoint API.
Design a GraphQL schema for an e-commerce product listing that supports filtering (category, price range), sorting, cursor-based pagination, and nested reviews. Explain how you'd prevent N+1 queries on nested fields and how you'd balance GraphQL flexibility with server-side performance constraints.
Sample Answer
Requirements (brief): product listing with filters (category, price range), sorting, cursor-based pagination, nested reviews.
Schema (SDL):
type Query {
products(filter: ProductFilter, sort: ProductSort, first: Int = 20, after: String): ProductConnection!
}
input ProductFilter {
categoryIds: [ID!]
minPrice: Float
maxPrice: Float
search: String
}
input ProductSort {
field: ProductSortField = RELEVANCE
direction: SortDirection = DESC
}
enum ProductSortField { RELEVANCE NAME PRICE CREATED_AT }
enum SortDirection { ASC DESC }
type ProductConnection {
edges: [ProductEdge!]!
pageInfo: PageInfo!
totalCount: Int
}
type ProductEdge {
cursor: String!
node: Product!
}
type PageInfo {
hasNextPage: Boolean!
endCursor: String
}
type Product {
id: ID!
name: String!
price: Float!
category: Category!
reviews(first: Int = 5, after: String): ReviewConnection!
}
type Category { id: ID! name: String! }
type ReviewConnection { edges: [ReviewEdge!]! pageInfo: PageInfo! totalCount: Int }
type ReviewEdge { cursor: String! node: Review! }
type Review { id: ID! author: String! rating: Int comment: String createdAt: String }
Why cursor pagination: stable ordering and efficient continuation for large datasets.
Preventing N+1:
- Use DataLoader-style batching at resolver layer: batch load categories and reviews by product IDs in single DB queries.
- For reviews pagination, push cursor/limit into DB query (SELECT ... WHERE product_id IN (...) AND cursor > X ORDER BY ... LIMIT N) to fetch all needed rows in one request per page.
- When resolving nested lists for many products, fetch reviews grouped by product_id and attach results; avoid calling DB per product.
Balancing GraphQL flexibility vs server performance:
- Enforce query complexity and depth limits; set reasonable max first (e.g., 100).
- Require/encourage server-side defaults and hard caps on 'first' and search complexity.
- Use persisted queries or whitelisting for expensive clients.
- Add analytics and rate limiting; cache common filter+sort results at CDN/DB layer.
- Deny or rewrite queries that request large nested pages; provide bulk endpoints for admin/bulk consumers.
- Document recommended queries and provide server-side profiles to guide clients.
Trade-offs: strict limits restrict client freedom but protect DB; batching and DB-side pagination keep latency low.
Architect a gateway that accepts GraphQL queries and composes data by calling multiple backend REST services. Explain how to analyze the GraphQL AST to group field fetches for batching, apply caching at resolver granularity, implement fallbacks or partial responses when backends fail, and how to surface errors according to GraphQL error semantics.
Sample Answer
Requirements & constraints:
- Accept GraphQL queries, compose from multiple REST backends, support batching, per-resolver caching, partial responses on backend failure, and proper GraphQL error semantics (data + errors) with low latency and high throughput.
High-level architecture:
- Ingress GraphQL Gateway → Parser/Validator → AST Analyzer & Planner → Executor (batcher + cache layer + fallback handler) → REST Clients + Circuit Breakers → Response Assembler → Client.
AST analysis & grouping for batching:
- Parse query into AST. Walk selections grouped by parent type and field: collect resolver calls by identical REST endpoint + identical parameterization (e.g., same userId). For list fields, detect N+1 patterns: when field resolves over a list of parent IDs, emit a single batched REST request (e.g., /users?ids=1,2,3).
- Build execution plan: nodes with inputs, batching keys, dependency edges (parallelize independent branches).
Resolver-granularity caching:
- Use cache keyed by (resolver-name, args-hash, selection-set-hash). Selection-set hashing ensures different sub-selections don’t mix. Support TTLs and cache invalidation hooks (via events from backends) and stale-while-revalidate to reduce latency.
- For batched requests, cache at individual item granularity when possible (e.g., per-id), and assemble from cache before making network calls.
Fallbacks & partial responses:
- Wrap each resolver call with circuit breaker and timeout. On failure:
- If non-critical field: return null for that field and add an error entry.
- If critical (configured): return default/fallback value or trigger alternative backend.
- Support partial responses by continuing execution for other fields; assemble data where available.
Error surfacing per GraphQL spec:
- Return "data" with whatever succeeded; include "errors" array with entries containing message, path (field path from root), locations (from original query), and optional extensions {code, backend, retryable}.
- Map HTTP failures to codes (e.g., 5xx → INTERNAL, 4xx → BAD_USER_INPUT) and include safe details in extensions. Ensure sensitive backend internals are not leaked.
Scalability & trade-offs:
- Use per-request execution plan caching to avoid repeated AST walks.
- Trade-offs: aggressive batching reduces requests but may increase payload sizes and complexity; caching reduces latency but needs strong invalidation strategy.
Observability & testing:
- Trace each resolver with request-id, metrics for latency/hits/misses/errors, and add end-to-end tests for partial-failure scenarios.
This design yields efficient batching, safe caching, graceful degradation on failures, and GraphQL-compliant error reporting.
You're migrating a large monolithic REST API to GraphQL for internal teams while keeping external REST clients stable. Propose a migration strategy that minimizes disruption, allows iterative rollout, and preserves developer velocity. Discuss using BFFs, GraphQL gateway/facade, schema stitching, and how to keep documentation and SDKs coherent during the transition.
Sample Answer
Requirements & constraints:
- Expose GraphQL to internal teams while keeping external REST contracts unchanged.
- Minimize disruption, enable iterative rollout, keep developer velocity.
- Support observability, testing, and coherent docs/SDKs during transition.
High-level strategy (phased, incremental):
- Discover & design: inventory REST endpoints, payloads, SLA, auth, and consumer needs. Design a canonical GraphQL schema aligned to domain capabilities (not 1:1 REST mapping). Define deprecation policy and SLAs.
- Introduce a GraphQL Gateway/Facade: deploy a gateway that serves internal GraphQL requests and proxies (or federates) to existing backends. Initially the gateway implements resolvers that call the monolith’s REST endpoints (translation layer). This keeps external REST stable.
- Use BFFs per team or product area: for teams with complex UX needs, introduce lightweight BFFs that sit behind the gateway. BFFs expose the GraphQL schema tailored to a team or compose from gateway. BFFs enable independent deployments and performance tuning without touching the monolith.
- Iterative backend migration with schema stitching/federation:
- As backend services are extracted from the monolith, migrate those capabilities to new microservices.
- Use schema stitching or Apollo Federation to merge service-specific GraphQL schemas into the gateway with clear ownership.
- Switch resolvers for a given field from the monolith-proxy to the new service gradually (feature flags / rollout).
- Dual-writing and shadowing: for high-risk areas, implement shadow writes/reads to the new services and compare results. Use Canary releases and percentage rollouts.
- Maintain external REST stability: keep the monolith’s REST endpoints available; if you need to change behavior, do it behind a versioned facade. Optionally expose a REST-to-GraphQL translator for external clients that want to adopt GraphQL later.
Key components & responsibilities:
- GraphQL Gateway/Facade: central schema, auth, rate-limit, tracing, gateway-level caching, query whitelisting, and batching.
- BFFs: per-team adapters, composition logic, specialized caching, response shaping.
- Monolith REST Layer: unchanged initially, consumed by gateway resolvers.
- New Microservices: own business capabilities, expose GraphQL (federated) or REST.
- Schema Registry & CI: schema linting, contract tests, breaking-change checks.
- Observability: request tracing (distributed), metrics per resolver, error budgets.
Data flow (example):
Client (internal) -> GraphQL Gateway -> route:
- For field X still in monolith: gateway resolver -> monolith REST -> gateway -> client
- For field Y migrated: gateway resolver -> microservice GraphQL (federation) -> gateway -> client
Developer velocity, docs & SDK coherence:
- Single source of truth: use the gateway’s GraphQL schema/schema registry as canonical. Publish introspection-enabled schemas.
- Auto-generate SDKs: use codegen (graphql-code-generator, Apollo TypeScript/Java clients) from schema; publish versioned SDK packages to internal registries. Keep generator config in repo so SDKs stay in sync CI triggers on schema changes to regenerate SDKs and run integration tests.
- Documentation: generate API docs from schema + examples (GraphQL Playground, GraphiQL, or Apollo Studio). Publish migration guide mapping REST endpoints -> GraphQL queries, field-by-field deprecation notes, and sample code for both REST and GraphQL clients.
- Deprecation strategy: mark fields deprecated in schema, provide timeline and migration steps. Use auto-generated changelogs and release notes.
- Onboarding: provide example BFF templates, shared client libs (auth, dataloader), and clear SLAs.
Operational concerns & best practices:
- Protect against N+1: include DataLoader/batching at gateway and BFFs.
- Caching: use persisted queries, response caching for idempotent queries, and CDN where applicable.
- Security: unify auth (JWT/OAuth) at gateway; propagate tokens to services. Validate RBAC at gateway and enforce service-level checks.
- Performance & quotas: add per-field timeouts and circuit breakers when calling monolith to avoid cascading failures.
- Testing: create contract tests between gateway and monolith, end-to-end tests for key queries, and schema compatibility tests in CI.
- Monitoring: per-resolver latency/error metrics, compare results during shadowing, and alert on divergence.
Trade-offs:
- Initial latency may increase due to translation layer; mitigated by caching and BFF optimizations.
- Gateway centralizes responsibility (simpler client surface) but can be a bottleneck—scale horizontally and cache aggressively.
- Schema design effort up-front pays off in long term developer velocity; resist 1:1 REST translation.
Concrete rollout steps (short checklist):
- Inventory & schema design.
- Deploy GraphQL gateway that proxies to monolith. Expose only to internal teams.
- Start with low-risk read-only fields; add logging/observability.
- Offer internal SDKs and docs generated from gateway schema.
- Migrate backend capability-by-capability: extract service -> federate schema -> switch resolvers behind feature flag -> validate via shadowing.
- Deprecate monolith endpoints only after consumers migrate; continue supporting external REST unchanged.
- Remove gateway-to-monolith resolver when fully migrated and safe.
This approach minimizes disruption by keeping external REST stable, allows iterative migration via gateway/BFF and schema federation, preserves developer velocity through codegen'd SDKs and clear docs, and reduces risk with feature flags, shadowing, and strong observability.
You are asked to design a GraphQL API. Explain the N+1 query problem and how data loader patterns and batching solve it. Describe additional mitigations such as persisted queries, query complexity analysis, depth limiting, and caching best-practices for GraphQL endpoints.
Sample Answer
The N+1 problem: when a GraphQL resolver fetches a list (N items) and for each item triggers another DB/API call, you do 1 query for the parent + N queries for children → O(N) extra calls, high latency and DB load. Example: query posts { id, author { id, name } } where author resolver queries DB per post.
Solution — DataLoader / batching:
- Batch requests within a single tick or request lifecycle.
- Cache results per-request to avoid duplicate loads.
Example (JavaScript using facebook/dataloader):
const DataLoader = require('dataloader')
// batch load function receives array of keys and returns Promise of values in same order
const userLoader = new DataLoader(async (ids) => {
const users = await db('users').whereIn('id', ids) // single DB call
const usersById = new Map(users.map(u => [u.id, u]))
return ids.map(id => usersById.get(id) || null)
})
// in resolvers, use loader from context
const resolvers = {
Post: {
author(post, args, ctx) {
return ctx.loaders.userLoader.load(post.authorId)
}
}
}
Why it works: converts N independent loads into one batched query and caches within request.
Additional mitigations:
- Persisted queries: store allowed query hashes on server so clients send short hashes, reducing parsing overhead and preventing arbitrary heavyweight queries.
- Query complexity analysis: compute a cost metric (fields × multipliers for expensive fields) and reject queries exceeding threshold to prevent abuse.
- Depth limiting: cap AST depth to stop extremely nested queries that multiply work.
- Caching best-practices:
- Use per-field caching where safe (idempotent fields).
- Use HTTP caching for persisted queries and GET requests; add Cache-Control, ETag.
- Use CDN for public data; use short-lived caches for user-specific data and invalidate on writes.
- Combine DataLoader (per-request cache) with a shared read-through cache (Redis) for expensive resolved entities.
Trade-offs: aggressive caching and complexity rules can block legitimate queries; tune thresholds and provide monitoring/metrics (resolver call counts, DB query counts, latency).
Design a federated GraphQL architecture where multiple microservices contribute types and resolvers. Describe schema composition (Apollo Federation or schema stitching), query planning across services, cross-service joins, latency control, tracing, and strategies to prevent overly expensive federated queries.
Sample Answer
Requirements & constraints:
- Multiple autonomous microservices own types & resolvers; gateway composes a single graph for clients.
- Low-latency, safe cross-service joins, observability, and guarding against expensive/DoS queries.
High-level choice:
- Use Apollo Federation (v2) for explicit ownership, entity/federation directives, and a composable supergraph. Schema stitching is an alternative (good for heterogeneous schemas), but federation better supports ownership and query planning.
Schema composition:
- Each service exposes SDL with @key/@external/@requires/@provides to declare entities and fields it owns.
- Central composer (managed supergraph or composition service) builds the supergraph schema and validates conflicts at build time.
- Example: Order service extends Product @key(fields: "id") { id: ID! reviews: [Review] @requires(fields: "id") }
Query planning & execution:
- Gateway performs query planning: breaks incoming query into a sequence/graph of fetches (subqueries) against subgraphs based on required entities and selection sets.
- Plan includes batching and parallelization where possible; sequential fetches occur only when subsequent subqueries need values from previous responses.
- Use per-plan execution with DataLoader-style batching at subgraph clients to collapse duplicate fetches and reduce N+1.
Cross-service joins:
- Implement joins by passing entity keys (ids) between services in the planned fetches. For heavy joins, prefer:
- Push-down filters: let owning service do filtering/aggregation.
- Denormalized/edge data: maintain small lookup caches or materialized views for hot joins.
- Use async enrichment: respond with partial data quickly, enrich via background jobs or on-demand edges.
Latency control & resilience:
- Timeouts per subgraph call, global query deadline propagated via tracing context.
- Circuit breakers and retries with exponential backoff for transient errors.
- Fallbacks: return partial data with errors array when non-critical subgraph fails.
- Caching: response-level caching at gateway (cache key by query + variables), and per-field or REST/cache layers in subservices. Use stale-while-revalidate for UX improvements.
Tracing & observability:
- Instrument gateway and subgraphs with OpenTelemetry (trace id propagated in headers). Capture span per subgraph call, resolver durations, and plan stages.
- Centralized logs/metrics (Prometheus/Grafana). Use distributed tracing in traces to detect slow hops and hotspots.
- Collect query-level metrics: depth, complexity, execution time, subgraph latencies.
Preventing expensive queries:
- Persisted queries / query whitelisting for public APIs.
- Query complexity analysis: cost functions per field (cost multipliers for lists / expensive resolvers) and reject if over threshold.
- Depth limiting and limiting total returned items (max-limit on list sizes) and pagination enforced.
- Rate limits per client and per-operation quotas.
- Allow schema-level annotations for expensive fields (e.g., @cost(multiplier: 10)) and require special permission / throttling.
- Use automatic sampling to detect slow/expensive queries and add them to denylist or force persisted variants.
Operational considerations:
- CI to validate composition and run integration tests against composed supergraph.
- Versioning: non-breaking additions only; deprecate and migrate ownership carefully.
- Security: validate inputs server-side, sanitize arguments, and apply authentication/authorization at gateway or subgraph depending on ownership.
Trade-offs:
- Federation adds runtime complexity in planning and debugging but offers clear ownership and safer evolution vs stitching.
- Heavy cross-service joins can increase latency; prefer data locality or materialized joins for high-throughput paths.
This design gives teams ownership, predictable query planning, observability, and multiple defenses against runaway queries while allowing incremental evolution of the graph.
Unlock Full Question Bank
Get access to all 6 GraphQL and Flexible Query APIs interview questions and detailed answers.
Sign in to ContinueJoin thousands of developers preparing for their dream job.