Skip to main content

System Design and Behavioral (8 Questions)

Duration: ~20 minutes | Difficulty: Advanced | Prerequisites: Production Operations (6 Questions)


System design questions test synthesis: can you take everything you know about GraphQL and apply it to a problem you have not specifically prepared for? Behavioral questions test judgment and self-awareness: can you reflect on your own decisions honestly and communicate engineering trade-offs to non-technical stakeholders? The questions in this chapter are the senior-level questions that appear in final interviews.


System Design Questions​

Question 1: Design a GraphQL API for a movie streaming service. Walk me through the schema decisions.​

Short answer: Start with the core types: Movie, Episode, Series, User, Profile. Define relationships and access patterns for browsing, playback and search, then design mutations for user actions such as watchlist, rating and playback progress. Add pagination to list fields, define access control boundaries such as subscription tier gates, and consider real-time requirements like now playing and recommendations.

Why this is right / what interviewers want to hear:

System design questions do not have a single correct answer, but interviewers are looking for a structured approach that covers the key concerns and demonstrates trade-off awareness.

Start with the data model: a streaming service has Movie (standalone films), Series (TV shows), Season, and Episode. Decide early: is Movie a subtype of a Watchable interface? Does Series implement the same interface? The case for an interface: a Watchable interface with shared fields (id, title, description, thumbnailUrl, duration) lets the client render a content card whatever the content turns out to be. It also lets the API return mixed lists for "recommendations" and "search results." The case against: movies and episodes have different playback mechanics, and forcing them into one interface may require nullable fields that reduce the non-null guarantees you could otherwise make.

Access patterns drive schema design. For a streaming service the dominant ones are browsing, meaning a list of content for a given genre or editorial category, and playback, meaning the playback URL and metadata for a specific item. The other two are profile, this user's watch history and recommendations, and search. Design the top-level query fields around these patterns: contentByCategory(category: ContentCategory!, first: Int!, after: String): ContentConnection!, movie(id: ID!): Movie, episode(id: ID!): Episode, me: User (returns the current user's profile). Each top-level field is a named access pattern.

Pagination: contentByCategory returns a list that may have thousands of items. Use Relay-style cursor pagination (ContentConnection with edges, node, pageInfo). Avoid offset pagination for dynamic lists, the catalog changes constantly, and offset-paginated browsing will show items twice or skip items as content is added and removed.

Mutations: user actions include addToWatchlist(contentId: ID!): AddToWatchlistResult!, removeFromWatchlist(contentId: ID!): RemoveFromWatchlistResult!, rateContent(contentId: ID!, rating: Int!): RateContentResult!, and updatePlaybackProgress(episodeId: ID!, positionSeconds: Int!): UpdatePlaybackProgressResult!. All return payload types with userErrors for expected failures (duplicate watchlist entry, invalid rating range, invalid position).

Access control: subscription tier gates (some content is only available to premium subscribers) should be expressed in the resolver or service layer using the @PreAuthorize pattern, not in the schema. The schema should not have premiumOnly: Boolean fields that the client has to check; that is client-side access control, which is the wrong layer. The resolver for movie(id: "42") should return the movie if the user has the required subscription level, or an authorization error if not.

Real-time: Subscription.playbackProgressUpdated(contentId: ID!): PlaybackProgress! for shared watch sessions. Emit events when the server receives a updatePlaybackProgress mutation and fan out to all subscribers watching the same content.


Question 2: You have a GraphQL monolith serving 50 teams. How do you migrate to federation?​

Short answer: Start with a strangler fig pattern. Deploy the router in front of the monolith, so the monolith becomes one large subgraph. Then extract domain subgraphs step by step, moving types and resolvers out of the monolith into new subgraphs. Each extraction is deployed independently. The monolith shrinks over time until it is either empty or down to a residual core.

Why this is right / what interviewers want to hear:

This is a migration strategy question. The key point is that you migrate incrementally rather than rebuilding, and the existing monolith stays in production throughout.

The loop on the last phase is the point: every extraction repeats the same step, and the graph keeps serving traffic between them.

Phase 0, Assessment: before touching any code, understand the monolith's ownership map. Which types and resolvers are owned by which teams? Where are the natural domain boundaries (Movies domain, Users domain, Billing domain)? Are there cross-cutting dependencies (types that many resolvers reference) that will be difficult to extract? This assessment shapes the extraction order.

Phase 1, Router in front: deploy the Apollo Router (or a compatible federation router) with the monolith registered as a single subgraph. The router schema is initially identical to the monolith schema. Client behavior is unchanged. This step validates the router deployment without any business logic risk.

Phase 2, Add @key directives: identify the entity types (the types that other subgraphs will eventually reference via _entities). Add @key directives to those types in the monolith schema. Add reference resolvers that return the entity by key. The monolith now exposes the _entities query, even though no other subgraph uses it yet. This step prepares the monolith for federation without yet extracting anything.

Phase 3, Incremental extraction: pick the domain with the clearest boundaries and the most willing team as the first extraction candidate. Create a new subgraph service for that domain. Move the type definitions and resolvers from the monolith to the new subgraph. Update the monolith to reference the extracted types via @external and the new subgraph to declare ownership via @key. Register the new subgraph with the router. The router now serves some types from the monolith and some from the new subgraph.

The hard parts: cross-domain queries that need data from both the monolith and the extracted subgraph will now go through the router's _entities mechanism. The latency of those queries may increase slightly (one extra network hop). Test this before extraction, not after. Also, the monolith's JPA entity model may have bidirectional relationships between types that you are trying to separate into different subgraphs, those bidirectional relationships must be broken into unidirectional entity references, which may require service-layer refactoring.

Timeline: plan for 6-18 months for a full extraction, depending on the number of domains and the complexity of the cross-cutting dependencies. Celebrate each extraction as a delivered milestone, not just progress toward a distant goal.


Question 3: Your GraphQL API's p99 latency is 2 seconds. Walk me through your debugging methodology.​

Short answer: Start with the operation breakdown, since a few operations drive the p99 rather than all of them equally. Then take per-resolver timing, to see which resolvers in those operations are slow, and the SQL query log, for how many queries run and which are slow. Then check external dependencies, and finally infrastructure: database connections, JVM GC pauses, and memory pressure.

Why this is right / what interviewers want to hear:

This is a debugging methodology question, and the interviewer wants to hear a systematic process, not guesswork. The key is to narrow the problem space at each step before going deeper.

Step 1: Identify which operations are slow. A 2-second p99 across all operations might be driven entirely by one rarely-called but expensive operation. Use your tracing or metrics system to filter by operation name and compare p99 latency per operation. If SearchMovies is consistently 2 seconds and Movies is consistently 50ms, the problem is scoped to SearchMovies.

Step 2: Per-resolver timing. Within the slow operation, which resolvers are slow? Spring GraphQL with Micrometer gives you per-resolver duration histograms. A Movie.cast resolver that takes 800ms per call (instead of the expected 5ms) is an immediate suspect. If the resolver is an N+1 (calling the database once per movie instead of batching), the total latency is proportional to the number of items in the list.

Step 3: SQL query log. Enable slow query logging on the database (queries taking more than 100ms). For the slow operation, how many queries are issued? If you see 200 queries for one GraphQL request; that is an N+1 that has not been caught by DataLoader. If you see 5 queries but one takes 1.8 seconds; that is a missing index or an unoptimized join.

Step 4: External dependencies. Are any resolvers calling external APIs (search services, recommendation engines, content delivery APIs)? External HTTP calls in a GraphQL resolver chain are a common source of p99 spikes because the external service's p99 adds to yours. Check whether those calls are being made sequentially when they could be made in parallel, or whether they could be cached.

Step 5: Infrastructure. JVM GC pauses are a source of p99 latency spikes because GC can pause the entire JVM for 100-500ms in a stop-the-world collection. Check GC logs for the timing of pauses. Database connection pool exhaustion causes queries to queue, look at the connection pool wait time metric. Memory pressure can cause the OS to swap, which causes disk I/O, which causes latency spikes.


Question 4: How would you implement real-time features (subscriptions) in a production GraphQL API?​

Short answer: Use WebSockets for the transport layer (graphql-transport-ws protocol). Back subscriptions with a message broker (Redis Pub/Sub, Kafka) so that events from any application instance reach all subscribers regardless of which instance they are connected to. Use Spring's reactive Flux<T> return type for subscription resolvers. Implement connection management (heartbeats, timeout, reconnect) at the protocol level.

Why this is right / what interviewers want to hear:

Subscriptions are the part of GraphQL that most candidates have the least production experience with. This question separates candidates who have actually shipped subscriptions from candidates who have read the spec.

Transport: subscriptions require a persistent connection. The standard is WebSockets with the graphql-transport-ws subprotocol (the newer protocol, replacing the older subscriptions-transport-ws). Server-Sent Events (SSE) are an alternative for scenarios where WebSocket support is unavailable, but SSE is unidirectional, sufficient for subscriptions (server pushing to client) but not for full-duplex use cases.

In Spring: the subscription resolver method returns a Flux<Movie> (or Publisher<Movie>). Spring GraphQL wraps this in the WebSocket subscription protocol. Each subscriber gets their own Flux instance. The Flux can be backed by a reactive source: Flux.create(emitter -> subscriptionService.register(emitter)) where subscriptionService distributes events from the message broker to all registered emitters.

Multi-instance deployment: a production GraphQL API runs multiple instances behind a load balancer. A client connecting to instance A cannot receive events that are published by a request handled by instance B unless there is a shared event bus between instances. The standard solution is Redis Pub/Sub: when a mutation creates a movie, it publishes a "movieAdded" event to a Redis channel. Each application instance subscribes to that Redis channel and distributes the event to all WebSocket subscribers on that instance. Kafka is appropriate when event ordering and durability guarantees are required.

Connection management: WebSocket connections can be long-lived but are not reliable. Clients drop network connections, especially mobile clients. The graphql-transport-ws protocol includes a ping/pong mechanism for connection health checks. The server should send pings at a configured interval and close connections that do not respond. Clients should implement reconnect logic with exponential backoff. The application should be stateless, the reconnected client re-subscribes and receives events from the moment of reconnection, not a replay of missed events (unless you have implemented event sourcing or a replay buffer).


Behavioral Questions​

Question 5: Tell me about a time you made a schema design decision you later regretted.​

What interviewers want to hear:

This question is looking for self-awareness, honesty, and the ability to extract a general lesson from a specific mistake. The worst answers are "I can't think of one", which reads as a candidate who has never reflected on their own work, and a story where everything went wrong and you blame the team (suggests poor judgment about what to own).

A strong answer has four parts:

  • the context: what you were building, what the time pressure was, and what you knew then
  • the decision: what you chose, and why it seemed reasonable at the time
  • the consequence: when you found out it was wrong, and what the impact was
  • the lesson: the principle you apply now that prevents the same mistake

Common real patterns worth using if they apply to you:

Making too many fields non-null: shipped a schema where Movie.cast: [Person!]! was non-null. When a downstream API call to fetch cast data failed, GraphQL's null-propagation rules nulled out the entire Movie object instead of just the cast field. The movie listing became empty for every movie. Lesson: nullable by default for any field backed by a dependency that can fail independently.

Not designing for pagination upfront: shipped a Movie.reviews: [Review!]! field that returned all reviews. When a popular movie accumulated 100,000 reviews, the field returned all 100,000 on every query. The lesson is to paginate any one-to-many relationship field from the start, because adding pagination later breaks clients that expect a plain list.

Naming a mutation with REST vocabulary: shipped a postReview mutation because the team was coming from REST. Three months later, the naming was inconsistent with every other mutation (createMovie, deleteMovie). Lesson: establish naming conventions before the first mutation ships and apply them consistently.


Question 6: How do you handle a breaking schema change that your team needs but clients haven't migrated away from?​

What interviewers want to hear:

This question tests communication, process, and prioritization skills as much as technical knowledge. The interviewer wants to hear that you take schema contracts seriously and have a process for managing the tension between engineering velocity and client stability.

A complete answer covers: (1) how you establish the business urgency of the change vs. the cost to clients, (2) the technical mechanism for a compatible migration (add-deprecate-monitor-remove), (3) how you communicate with and support clients during migration, and (4) what you do when a client misses the deadline.

On the technical side: if the change can be expressed as add-new-field + deprecate-old-field (for renames) or add-new-type + deprecate-old-type (for type changes), the migration is non-breaking in practice and the client can migrate at their own pace. The key is that the old and new names coexist for a migration window.

If the change is genuinely breaking with no compatible migration path (for example, removing a field that lacks a replacement because the underlying data no longer exists), the answer is a sequence:

  1. Establish the firm removal date.
  2. Notify all clients.
  3. Give the maximum possible migration window.
  4. Follow up with clients still using the field as the deadline approaches.
  5. After the deadline, weigh delaying removal if a significant client has not migrated, against that client's importance and the cost of the delay.

What you should never do: remove a field without notice, remove a field before the announced deadline, or declare a field "deprecated" and remove it a week later. Schema trust takes years to build and one incident to lose.


Question 7: A junior engineer on your team wants to return SQL error messages directly to GraphQL clients. How do you respond?​

What interviewers want to hear:

This is a code review and mentorship scenario. The interviewer wants to see whether you can explain the security and product reasoning clearly without being dismissive, and whether you can propose a concrete alternative.

The security problem: SQL error messages contain implementation details (table names, column names, constraint names, index names) that an attacker can use to construct more targeted attacks. A message like ERROR: duplicate key value violates unique constraint "users_email_key" tells an attacker that the users table has an email column with a unique constraint, more information than you want to give.

The product problem: SQL error messages are not user-facing text. "ERROR: duplicate key value violates unique constraint..." is not a message a user can act on. A user who receives this message cannot fix the problem because they don't know what a constraint is.

The correct alternative: catch the database exception in the service layer, translate it into a domain-level error (EMAIL_ALREADY_IN_USE), and return it as a UserError in the mutation payload. The UserError has a code: UserErrorCode! (the enum value) and a message: String! (a user-facing English string). This gives the client enough information to display a useful error message ("That email address is already registered") without exposing implementation details.

How to frame the feedback: "I see what you're trying to do, surface the error to the client so they know what went wrong. That's the right goal. The concern is that the raw SQL message isn't meant for users and contains internal details we don't want to expose. Let's translate it in the service layer: catch this specific exception, return a UserError with a code the client can act on, and write a user-friendly message." This frames the feedback as a direction toward the right solution, not just a "no."


Question 8: How do you keep up with the evolving GraphQL ecosystem?​

What interviewers want to hear:

This is a culture and learning question. The interviewer wants to see intellectual curiosity, specific resources, and evidence that you apply what you learn instead of just passively consuming it.

Specific resources that demonstrate genuine engagement with the ecosystem:

The GraphQL spec: reading the spec directly is the most rigorous source of truth. Most engineers never read it; the ones who have can answer edge-case questions with precision that others cannot. The spec is at spec.graphql.org, and it is published as dated editions. The September 2025 edition is the one to know: it added the @oneOf input object, schema coordinates, and deprecation on input fields and arguments. Naming it, and naming what it changed, answers "do you keep up?" in one sentence.

GraphQL Foundation working groups: the GraphQL specification is developed in public working groups. Following the discussion on GitHub and in working group notes gives you early access to upcoming features (the @defer and @stream directives, the incremental delivery specification) and the reasoning behind design decisions.

Engineering blogs: Apollo, Shopify, GitHub, and GitLab all publish detailed technical posts about their GraphQL evolution. These are primary sources, the people who built and operate these systems describing what they learned.

Community: the GraphQL Weekly newsletter, the graphql.org blog, and the #graphql channel in the local tech community. Following the maintainers of graphql-java, Spring for GraphQL, and the Apollo ecosystem on social media or GitHub.

The learning loop: the most credible answer is not "I read blogs" but "I read a post about X, tried it in a project, and it either worked or I found a limitation." Demonstrating that you apply what you read converts passive consumption into genuine expertise.

What not to say: "I follow the ecosystem closely" without any specific examples. Vague claims about staying current are not credible. Specific resources, specific recent developments (Netflix DGS adopting Spring for GraphQL as its underlying engine, the incremental-delivery spec progress, Quarkus SmallRye GraphQL's improvements) demonstrate genuine engagement.


Further Reading​