Skip to main content

Core Concepts (10 Questions)

Duration: ~30 minutes | Difficulty: Intermediate | Prerequisites: Working GraphQL knowledge: you can read SDL and have written queries and mutations against a real API.


The ten questions in this chapter form the bedrock of any GraphQL technical interview. They are not difficult questions, which is precisely why they are dangerous. A candidate who gives a thin answer to a foundational question signals to the interviewer that their knowledge is surface-level across the board, even if they go on to give sharp answers to harder questions. These are the questions where you need to demonstrate not just that you know the answer but that you understand why the answer is the answer.


Question 1: What is GraphQL and how does it differ from REST?​

Short answer: GraphQL is a query language and server-side runtime for APIs, developed by Facebook in 2012. The client specifies the exact shape of the data it needs in one request, and the server returns that shape. REST gives clients fixed endpoints returning fixed shapes. GraphQL gives them one endpoint, and a grammar for describing the shape they want.

Why this is right / what interviewers want to hear:

The question sounds introductory, and it is, but the answer most candidates give is also introductory: "GraphQL has one endpoint and you can ask for what you want." That is correct but thin. The interviewer wants to understand your mental model of the fundamental design difference between the two approaches, not just the surface distinction.

The deeper distinction is about who controls the response shape. In REST, the server decides what a GET /movies request returns. The client takes what it gets. A client wanting only the title and year of each movie still gets the full object, unless the team negotiates a new endpoint. In GraphQL the client writes a selection set, and the server returns that. It changes the collaboration contract between frontend and backend teams. In REST, adding a field to a response is a server concern. In GraphQL, clients read any field they have permission to see, and nobody waits for a backend team to expose it.

The second meaningful difference is around multiple-resource requests. In REST, fetching a movie and its cast and each cast member's other films typically means three roundtrips, or a custom endpoint that denormalizes the data in a way the client might not always need. In GraphQL, the client writes one query that traverses the full relationship graph in a single HTTP request. This was the original pain point that produced GraphQL at Facebook: the News Feed required assembling deeply nested, recursive data from many entities, and round-tripping for each entity was prohibitively slow over mobile networks.

What candidates often get wrong: they say GraphQL "is more efficient" than REST as if that is a general truth. It is not. GraphQL can be more efficient for clients that need variable or deeply nested data. It can also be more expensive for servers that skip DataLoader batching, because the resolver model naturally produces N+1 database queries where a well-designed REST endpoint would produce one. A strong answer names that trade-off, and stops short of presenting GraphQL as strictly better.

What a great answer also mentions: the introspection system. GraphQL's schema is machine-readable via introspection, which enables tooling (type-safe clients, GraphiQL, code generators) that does not have a natural REST equivalent unless you add an OpenAPI spec separately.


Question 2: What is a resolver, and how does the execution model work?​

Short answer: A resolver is a function responsible for returning the value of a single field in a GraphQL schema. When a GraphQL query arrives, the engine traverses the document tree field by field, calling the resolver for each field in turn, and assembles the results into the response shape the client requested.

Why this is right / what interviewers want to hear:

This question sounds more technical than the first one, and it is. The interviewer is checking whether you understand the execution model, not just the vocabulary word "resolver."

The execution model works as follows, and graphql.org's execution page walks the same sequence with a worked example. The client sends a query document. The GraphQL engine parses the document into an abstract syntax tree. It validates the AST against the schema, checking that every field name exists, every argument is the correct type, and the selection set makes sense for the type being selected. Only after validation passes does the engine begin execution.

Execution starts at the root, either Query, Mutation, or Subscription. For a query with several top-level fields, the engine dispatches all of the top-level resolvers in parallel (this is a meaningful implementation detail: Query.movies and Query.viewer in the same document are called concurrently). Each resolver returns either a value or a Promise. When a resolver returns an object type, the engine then calls resolvers for each of the selected fields on that object type. This continues recursively down the tree until every selected field has a scalar or null value.

One call at the root, twenty calls one level down. That fan-out is the execution model, and it is also the N+1 problem waiting to happen.

A critically important detail: most field resolvers are implicit. A Movie type with a title field does not need a resolver for title. The default resolver inspects the parent object, which is what the Movie resolver returned, and reads the property named title. You only need to write a custom resolver when the value cannot be inferred from the parent, or when computing the value requires a database call or other I/O.

Mutations are different: the top-level mutation fields are executed serially, not in parallel. This is because mutations have side effects, and executing two mutations that modify the same record simultaneously would produce race conditions. The GraphQL spec guarantees that mutations in the same document are executed in the order they appear.

What candidates often miss is that resolvers for nested fields are called once per parent object. If Query.movies returns 20 movies and each movie has a director field with a resolver that hits the database, that resolver will be called 20 times, once per movie in the list. This is the N+1 problem, and the next question asks about it directly. A candidate who explains the execution model and mentions N+1 proactively, before being asked, is demonstrating the kind of associative reasoning that impresses interviewers.


Question 3: What are the three operation types in GraphQL, and when do you use each?​

Short answer: The three operation types are query (read-only data fetching), mutation (any operation with side effects), and subscription (a long-lived connection for receiving server-pushed events). Use queries for reads, mutations for writes or any operation that changes state, and subscriptions for real-time event streams.

Why this is right / what interviewers want to hear:

The distinction between query and mutation is conceptual, not enforced by the runtime. A resolver attached to a mutation root could technically just read data, and a resolver attached to a query root could technically write data. GraphQL does not prevent this. The distinction is a convention the spec relies on for the parallel-vs-serial execution rule: queries execute their root fields in parallel, mutations execute theirs serially. This convention is important, and a candidate who says "mutations are serialized so side effects don't race" is demonstrating understanding of the runtime, not just the naming convention.

The question of when to use a mutation vs. a query is sometimes contested in teams. A common mistake is to make every operation a mutation "because it might change state in the future." This undermines the parallel execution optimization for queries and makes the API's intent harder to read. The correct rule is: if the operation has side effects (writes to a database, enqueues a job, sends an email, increments a counter), it should be a mutation. If it is purely read-only, it should be a query. The spec's guidance is intentional.

Subscriptions deserve more depth than most candidates give them. A subscription is not just a long-running query. The transport is fundamentally different: subscriptions typically run over WebSockets (or Server-Sent Events in some implementations) because they require a persistent, bidirectional connection that the server can push to. The subscription resolver returns an async iterator or Observable, and the GraphQL engine subscribes to it. When the iterator yields a value, the engine runs the selection set against that value and sends the result to the client. This means subscription resolvers have two phases: the subscribe phase (establishing the stream) and the resolve phase (running the selection set on each event).

What candidates often miss: subscriptions do not replace queries for the initial state. A client that subscribes to movieAdded gets notified when new movies are added, but it does not automatically receive the current list of movies at subscription time. The common pattern is to fire a query to get initial state and then subscribe to updates. Not knowing this in an interview suggests you have not actually shipped subscriptions in production.


Question 4: What is the N+1 problem and how do you solve it?​

Short answer: The N+1 problem arises when a resolver for a field on a list type issues one database query per item in the list. If a query returns a list of N movies and each movie has a director field that hits the database, the server executes N+1 queries (one for the list and one per item). The standard solution is DataLoader, which batches the individual loads into a single query.

Why this is right / what interviewers want to hear:

This is the single most common GraphQL interview question, and it is the one where the difference between candidates who have shipped production GraphQL and candidates who have only read about it is most visible.

The mechanism: the GraphQL execution engine calls resolvers recursively. When Query.movies returns a list of 20 movie objects, the engine calls the Movie.director resolver 20 times, once per movie. Each call is independent. The naive implementation of Movie.director calls directorRepository.findById(movie.getDirectorId()). This fires 20 SQL queries. Add the initial list query and you have 21 queries (N+1) to render a single GraphQL response.

With a DataLoader in front of the same resolvers, the twenty calls become one:

The reason the N+1 problem is worse in GraphQL than in a REST API is that the recursive resolver model is fundamental to GraphQL's design. In REST, the endpoint author can join the director data into the movies query manually. In GraphQL, the schema is a graph, and the resolver for director does not know how many siblings it has. Each director resolver only sees its own parent Movie object.

DataLoader solves this by changing the scheduling. A DataLoader batches all the load calls that happen within the same "tick" of the JavaScript event loop (or the equivalent asynchronous unit in Java). When the Movie.director resolver for the first movie calls directorLoader.load(movie.getDirectorId()), DataLoader does not fire immediately. It schedules the load. When the 20th Movie.director resolver calls directorLoader.load(...), the tick completes, and DataLoader fires one batch call with all 20 IDs. The batch function issues one SELECT * FROM directors WHERE id IN (...) and returns an array of directors. DataLoader maps each ID to its result and resolves all 20 promises.

In Spring with @BatchMapping, the framework handles the DataLoader registration automatically. The annotated method receives the entire list of parent objects at once and returns a map from parent to child. The DataLoader machinery is hidden but the batch behavior is the same.

The critical detail that distinguishes great answers: DataLoader must be request-scoped. If a DataLoader instance persists across requests, its cache will return stale data for users who should see updated data, and, far worse, it may serve one user's data to another user's resolver. This is a production security issue, not just a correctness issue. The reason DataLoader is request-scoped is that its cache looks values up by ID alone and does not track the access control context of the requesting user.


Question 5: What is introspection, and when would you disable it?​

Short answer: Introspection is GraphQL's built-in mechanism for querying the schema itself. By sending the __schema or __type query, any client can discover every type, field, argument, and directive in the API. You would disable it in production environments where the schema contains sensitive information (field names, types, descriptions) that you do not want to publish to potential attackers.

Why this is right / what interviewers want to hear:

Introspection is both one of GraphQL's most powerful features and one of its most significant security surface areas. The feature exists so that tooling, GraphiQL, Apollo GraphOS, code generators and type-safe clients, can understand the schema without out-of-band documentation. Every GraphQL endpoint with introspection enabled is, in effect, a fully documented API. That is an enormous benefit for developer experience.

The security concern is that introspection documents the attack surface. An attacker who queries your introspection endpoint knows every field, every argument, every type in your schema, including the internalAuditLog query, the User.socialSecurityNumber field, and the deleteAllContent mutation that you restricted to admins but left documented. They do not need to guess any of this. It is served directly.

The counterargument, which a complete answer should acknowledge, is that disabling introspection is a limited defense. It removes the map, but it does not remove the territory. An attacker who cannot query __schema can still probe the schema by sending queries with invalid field names and reading the "did you mean...?" error responses, which name real fields. They can also scan public GraphQL schemas, GitHub, Shopify, your own previous API responses cached in web archives, to reconstruct the schema over time. Disabling introspection raises the bar for reconnaissance; it does not eliminate the attack surface.

The standard practice is to enable introspection in development and staging environments, where the tooling benefits are valuable, and disable it in production. Spring Boot exposes this as one property, spring.graphql.schema.introspection.enabled=false; the longer route, a RuntimeWiringConfigurer that sets NoIntrospectionGraphqlFieldVisibility.NO_INTROSPECTION_FIELD_VISIBILITY, is what you reach for when the decision has to vary per request. In Quarkus with SmallRye the property is quarkus.smallrye-graphql.field-visibility=no-introspection. Watch for the neighbouring quarkus.smallrye-graphql.schema-available, which serves the schema document over HTTP and leaves runtime introspection alone.

A strong answer adds that some organizations allow introspection in production for authenticated users with specific roles. Internal developers keep the tooling, and anonymous schema discovery stays blocked.


Question 6: What is the difference between an interface and a union in GraphQL?​

Short answer: An interface defines a shared set of fields that implementing types must include, and queries on interface-typed fields can select those shared fields directly without inline fragments. A union groups types without any shared fields, so all field selections on a union-typed field must go inside inline fragments with specific type conditions.

Why this is right / what interviewers want to hear:

This is a schema design question dressed as a definitions question. The interviewer wants to know whether you understand not just the syntax but when to use one vs. the other.

An interface is appropriate when the types being grouped share meaningful fields. The GraphQL GitHub API's Actor interface is a classic example: a User and a Bot can both be actors (entities that take actions on repositories), and they share fields like login and avatarUrl. Queries that want to display "who did this action" can select Actor.login and Actor.avatarUrl without knowing whether the actor is a User or a Bot. Only when the client needs type-specific fields does it need an inline fragment.

A union is appropriate when the types being grouped do not share meaningful fields, or when the grouping is primarily for polymorphic selection sets. A SearchResult that can be a Movie, Person, or Studio is a union instead of an interface, because nothing meaningful is shared between a movie and a person in the context of a search result.

The practical implication: if you find yourself writing an interface with zero fields (just to group types for polymorphic queries), you probably want a union. If you find yourself writing a union where you immediately want to write ... on EveryTypeHere { commonField } for every member type, you probably want an interface.

A detail that trips up candidates: interfaces and unions have different rules for inline fragments. On an interface type, you can write field selections outside of any inline fragment (for the shared fields), and then add type-specific inline fragments for additional fields. On a union type, every field selection must be inside an inline fragment. No "shared" selection exists on a union.

The __typename field is available on both and is often used by client-side code to branch logic based on the concrete type.


Question 7: What does "schema-first" mean, and why might you prefer it to "code-first"?​

Short answer: Schema-first means you write the GraphQL SDL (Schema Definition Language) manually, and the server implementation is derived from it. Code-first means you write annotated server code (classes, methods, decorators) and the schema is generated from the code. Schema-first gives the schema a life independent of any specific server implementation and makes it easier to treat the schema as the source of truth for API contracts.

Why this is right / what interviewers want to hear:

This is a genuinely contested question, and interviewers often ask it precisely because no universally correct answer exists. What they want to hear is an opinion that is grounded in trade-off reasoning, not a recitation of one approach's benefits without acknowledging the other's.

The case for schema-first: the schema becomes a contract artifact that exists independently of the server. Frontend teams can read and contribute to it without understanding the server's implementation language. The schema can be versioned, reviewed, and approved in pull requests before any server code is written. Tools like Apollo GraphOS, GraphQL Inspector (now maintained under GraphQL Hive) and many code generators consume the SDL directly. Schema evolution is visible as SDL diffs, which are legible to anyone who knows GraphQL, not just the server-side team. Our tutorials use a schema-first approach with Spring GraphQL's .graphqls files precisely because it cleanly separates the contract from the implementation.

The case for code-first: the schema and the server types stay in sync automatically, because the schema is derived from the code. In a code-first world, no moment exists where the SDL says one thing and the resolver says another. Schema drift, when the SDL is manually edited and the resolver is not updated, or vice versa, is impossible by construction. Code-first approaches also benefit from the host language's refactoring tools: renaming a method renames the field in the schema automatically.

The strongest practical argument for schema-first in a multi-team environment: it forces API design to happen before implementation. Code-first schemas tend to mirror the implementation's data model instead of the client's access patterns. A Movie type in a code-first schema tends to have exactly the fields that the Movie database table has. A Movie type in a schema-first design has the fields that clients actually need, shaped by deliberate design decisions, because the SDL author had to think about the API before writing any resolvers.


Question 8: What is a DataLoader, and why is it request-scoped?​

Short answer: DataLoader is a utility that batches and caches loads of the same data type within a single request. When multiple resolvers each call load(id) for different IDs, DataLoader defers the loads until the end of the current async tick and then fires a single batch query. It is request-scoped so that its cache, which stores ID-to-value mappings, is not shared across requests from different users.

Why this is right / what interviewers want to hear:

This question is related to Question 4 (N+1) but is specifically about the DataLoader mechanism, so a strong answer goes deeper than the N+1 explanation.

The batching mechanism in more detail: a DataLoader instance is created with a batch function. The batch function takes an array of keys and returns a Promise that resolves to an array of values in the same order as the keys. When you call loader.load(key), DataLoader queues the key and returns a Promise. At the end of the current microtask, DataLoader takes all the queued keys and calls the batch function once with all of them. In modern Node.js that scheduling comes from the JavaScript microtask queue, through Promise.resolve().then(), and other runtimes have their own equivalent. The batch function issues one database query, and DataLoader resolves each individual Promise with its corresponding result.

The caching mechanism: within a single DataLoader instance, the same key is only loaded once. If three resolvers call load(42) for the same movie ID, only one database call is made, and all three Promises resolve to the same result. This is important in GraphQL queries that reference the same entity from multiple paths in the query tree.

Why request-scoped: the cache is a map from key to value. If the DataLoader instance persisted across requests, user A's load(42) might hit the cache that was populated by user B's earlier request. This is not just a data staleness problem (user B might have updated the movie in between), it is potentially a security problem if the loaded data contains access-controlled content. The correct mental model is: one DataLoader instance per request, created fresh at the beginning of each request lifecycle.

In Spring GraphQL, the DataLoaderRegistry is created per-request, and Spring's DataLoaderRegistrar beans are called once per request to populate it. This is the mechanism that ensures request scoping without requiring the application developer to manage the lifecycle manually.


Question 9: What are GraphQL directives? Give three examples of built-in directives.​

Short answer: A directive is a way to annotate a schema element or query operation with additional instructions that the runtime or tooling should act on. Three built-in directives come up most often. @deprecated marks a schema element as deprecated. @skip excludes a field from execution when a boolean variable says so, and @include includes one on the same terms.

Why this is right / what interviewers want to hear:

Directives come in two flavors, schema directives and operation directives, and a complete answer covers both.

Operation directives (@skip and @include) appear in client-written query documents and affect how the client wants to shape the response. They are the client's mechanism for conditional field selection. A mobile client might use @include(if: $includeDetails) to optionally fetch large fields only when the user has expanded a section, saving bandwidth on the common case. These directives are evaluated by the execution engine at runtime, not at schema definition time.

Schema directives (@deprecated) appear in the SDL and carry metadata. @deprecated(reason: "Use movieSlug instead") marks a field for removal without removing it, allowing clients to migrate at their own pace. Tools like GraphiQL display deprecated fields with a strikethrough. Code generators can emit warnings when deprecated fields are used in client queries.

Custom directives are where this gets interesting in production. Teams build custom directives to express cross-cutting concerns at the schema level. @auth(requires: ADMIN) is the classic example, a schema directive that a custom instrumentation layer enforces at execution time. Federation uses directives extensively: @key, @external, @requires, and @provides are all directives on subgraph schemas that the query planner reads to understand how to compose queries across subgraphs.

The important nuance: directives in the schema file are only meaningful if the server implementation knows how to interpret them. Writing @auth(requires: ADMIN) on a field in a .graphqls file is inert unless code in the server reads that directive and enforces the constraint. Spring GraphQL has supported Spring Security's @PreAuthorize since its 1.0 release as a standard way to handle this pattern. Quarkus uses @RolesAllowed on the resolver method. Both approaches use Java annotations on the server side; the SDL directive is a declaration of intent that complements the server-side enforcement instead of replacing it.


Question 10: What is the errors array in a GraphQL response, and how should mutation errors be structured?​

Short answer: The errors array is a top-level field in a GraphQL response that carries field-level error information. A field error does not necessarily mean the entire response is invalid, partial data may still be returned alongside errors. For mutations, common practice keeps domain errors out of the top-level errors array. An error such as "email already taken" goes into a structured userErrors field on the mutation result, which gives clients a typed, predictable contract for expected failures.

Why this is right / what interviewers want to hear:

This question goes to one of GraphQL's most genuinely underspecified areas, and it is asked precisely because there is a consensus pattern that the spec does not mandate but that production usage has converged on.

The errors array in the GraphQL spec carries "request errors" and "field errors", a distinction the response page sets out in full. A request error (such as a parse error or a validation failure) prevents execution entirely. A field error occurs when a resolver throws an exception during execution, the field is set to null, the error is appended to the errors array, and execution continues for other fields if possible. This "partial response" behavior is intentional: GraphQL's philosophy is that some data is better than no data, even if some fields failed.

The problem with using the errors array for mutation failures: it is typed only as a generic Error object with message, locations, and path. No way exists to define a schema contract for what kinds of errors a mutation can produce. Clients cannot write type-safe code against it. The message field is opaque text. The behavior of error.extensions is implementation-defined and not guaranteed to be stable. For a partner-facing API (Shopify's case), this is unacceptable: partners need to know, in the schema, what failures a mutation can produce and how to handle them.

The userErrors pattern (attributed to the Shopify API and documented in their GraphQL design tutorial) moves expected domain errors into the mutation payload itself. A CreateMoviePayload type has both a movie: Movie field (populated on success) and a userErrors: [UserError!]! field (populated on failure, empty on success). UserError has a typed code field (an enum), a message field, and optionally a field field for validation failures. This is a schema contract. Clients can write a switch on code and handle each error type explicitly.

What distinguishes great answers: the userErrors pattern is for expected, domain-level failures that are part of the normal operation of the API, validation errors, constraint violations, business rule failures. It is not for unexpected errors (infrastructure failures, bugs, runtime exceptions). Those should surface through the standard errors array as generic errors, because the client cannot meaningfully handle them programmatically, and trying to enumerate them in a schema type would be both incomplete and misleading. The userErrors pattern rests on one distinction. "This mutation failed because the email address is already in use" is a user error, returned in the mutation result. "This mutation failed because the database is down" is a system error, returned in the errors array.


Further Reading​

The official site is the first place to check any answer in this chapter, and every page below is short enough to read before an interview.

  • Execution on graphql.org walks the resolver traversal this chapter's first diagram draws, including the parallel-for-queries and serial-for-mutations rule.
  • Introspection covers the __schema and __type entry points, which is the vocabulary Question 5 expects.
  • Schema sets out interfaces, unions and the rest of the type system, and is worth re-reading for Question 6.
  • The response format and error handling are the pages behind Question 10, and they are the reference for what the errors array may contain.
  • The specification itself, when an interviewer pushes on an edge case. The September 2025 edition is the current one.