</> MAANG.io
system design Β· 101

Foundations

Master system design interviews with scalable architecture patterns, distributed systems, and real-world design challenges.

0/81 solved 0% complete

GraphQL β€” Part 1: Foundations & System Design Architecture

What is this?

Imagine two ways to order dinner. The first is a prix-fixe menu: you ask for "the chicken dish," and out comes a fixed plate β€” chicken, but also a side of potatoes you didn't want, a garnish you'll ignore, and a sauce you're allergic to. If you wanted the chicken and a salad from the other menu, that's a second trip to a second counter. The second way is Γ  la carte: you hand the kitchen one card listing exactly what you want β€” chicken breast, no sauce, extra greens β€” and they bring back precisely that, on one plate, in one trip.

REST is the prix-fixe menu. Each endpoint returns a fixed shape, and assembling a screen often means several trips to several counters (/user/1, then /user/1/posts, then /posts/9/comments). GraphQL is the Γ  la carte card. The client sends one request describing the exact fields it needs β€” across as many related objects as it likes β€” and the server returns one response shaped to match, no more and no less.

The one-line idea: GraphQL lets the client ask for exactly the data it needs β€” no extra fields, no missing fields β€” through a single endpoint, instead of the server dictating fixed responses across many endpoints.

That single sentence contains the two failures GraphQL was built to kill: over-fetching (the menu gives you food you didn't order) and under-fetching (the meal you want requires several trips). Hold onto that Γ  la carte image β€” we'll come back to it across all four parts.


1. The problem GraphQL was born to solve

GraphQL came out of Facebook in 2012, and the trigger was painfully concrete: the mobile app. On a phone, over a flaky cellular connection, every wasted byte and every extra round-trip hurts β€” it shows up as a spinner the user stares at. The REST APIs powering the app forced the client into exactly the two traps from our menu analogy.

Over-fetching β€” the prix-fixe problem. You call GET /users/42 to show a name and avatar, and the server hands you the user's full record: bio, settings, address, notification preferences, the works. You wanted two fields; you paid to download forty. On a metered mobile connection, that's wasted bandwidth and battery for data you immediately throw away.

Under-fetching β€” the many-trips problem. To render a single profile screen β€” the user, their three latest posts, and the comment counts on each β€” REST makes you choreograph a waterfall:

REST over-fetching: mobile app makes 4+ round-trips to a REST API

Each hop costs a round-trip β€” easily 100ms apiece on a phone β€” and they're often sequential, because you don't know which posts to fetch comments for until the posts come back. Four trips, half a second of latency, and the client still had to write glue code to stitch it all together.

There's a third, slower-burning pain: endpoint sprawl and versioning. Different clients want different shapes β€” web wants one thing, iOS another, the admin dashboard a third. The REST answer is to keep minting endpoints (/users/42/profile-card, /users/42/mobile-summary) and, when a shape must change, to spin up /v2/ and /v3/. The surface area grows without bound, and old versions linger forever because some client still depends on them.


2. GraphQL's answer: one endpoint, client-shaped responses

GraphQL collapses all of that into a single idea. There is one endpoint β€” conventionally POST /graphql β€” and the client sends a query describing the shape it wants. The server walks that shape, fetches each piece, and returns a JSON response that mirrors the query field-for-field.

Here's the profile screen above, as one GraphQL query:

query {
  user(id: "42") {
    name
    avatarUrl
    posts(last: 3) {
      title
      commentCount
    }
  }
}

And the response β€” notice it has exactly the shape of the query, nothing more:

{
  "data": {
    "user": {
      "name": "Alice",
      "avatarUrl": "https://.../a.png",
      "posts": [
        { "title": "Hello world",   "commentCount": 12 },
        { "title": "On databases",  "commentCount": 4  },
        { "title": "Weekend notes", "commentCount": 0  }
      ]
    }
  }
}

One request. One response. No unused fields, no waterfall, no glue code. The four traits that fall out of this design:

  • Single endpoint β€” clients hit /graphql; the query, not the URL, says what you want.
  • Client-driven shape β€” the frontend decides the fields and the nesting; the backend stops guessing what each screen needs.
  • One round-trip for related data β€” the user, their posts, and the comment counts arrive together, even though they may live in three different services.
  • Evolve without versioning β€” add fields freely (old clients ignore them); retire fields with @deprecated instead of minting /v2/.

πŸ’‘ Don't mistake GraphQL for a database or a replacement for your backend. It's a query language plus an execution layer that sits in front of your data sources β€” databases, REST services, gRPC services β€” and shapes their data to the client's request. The data still lives where it always did.


3. The schema: a typed contract for your whole API

Everything in GraphQL hangs off the schema β€” a strongly typed description of every type, field, and operation your API offers. You write it in the Schema Definition Language (SDL), and it doubles as the contract between frontend and backend and as live, queryable documentation.

type User {
  id: ID!
  name: String!
  email: String!
  posts: [Post!]!
}

type Post {
  id: ID!
  title: String!
  author: User!
  comments: [Comment!]!
}

type Query {
  user(id: ID!): User
  posts(last: Int = 10): [Post!]!
}

A few things this small block is telling you:

  • Every field has a type. name is a String, posts is a list of Post. The ! means non-nullable β€” String! will never come back as null. [Post!]! is "a non-null list of non-null Posts."
  • The graph is literal. A User has posts, and each Post has an author that points back to a User. Your data model is a graph, and clients traverse it by nesting fields β€” which is exactly what made the one-query profile screen possible.
  • Query is the entry point. The special Query type lists where you're allowed to start a read. Mutations and subscriptions get their own root types (Part 2).

Because the schema is machine-readable, GraphQL gets a superpower called introspection: a client can ask the server "what types and fields do you have?" and get a full answer. That's what powers tools like GraphiQL (autocomplete-as-you-type) and codegen that produces TypeScript or Kotlin types straight from the schema. The schema isn't just documentation β€” it's executable documentation that can't drift out of date.

GraphQL schema as a typed contract, Query branching to User and Post types


4. How a query actually runs: resolvers

A schema says what data exists. Resolvers say how to fetch it. Every field in the schema can have a resolver β€” a small function whose only job is to produce the value for that one field. When a query arrives, GraphQL walks the query tree and calls the resolver for each field it touches, top-down.

Picture our profile query executing. GraphQL first calls the user resolver to fetch the user. Then, because the query also asked for posts, it calls the posts resolver β€” passing in the user it just resolved as the parent. Then for each post it descends again into whatever fields you requested.

How a GraphQL query runs: resolvers calling the data source in sequence

The mental model: a query is a tree of fields, and execution is a depth-first walk of that tree, calling one resolver per field. A resolver receives four arguments β€” the parent value, the field's arguments (like id: "42"), a context object (shared per-request state such as the logged-in user, DB connections, caches), and info about the query β€” and returns either a plain value or a promise for one. A resolver can read a database, call a REST or gRPC service, or just return a field off the parent object. GraphQL doesn't care where the data comes from; it only cares that the resolver returns the right shape.

This per-field design is GraphQL's great strength and, as we'll see in Part 2, the source of its most famous performance trap β€” the N+1 problem, where that innocent "call posts for each user" turns into one query per user.


5. GraphQL as an API gateway over many services

In a microservices world, GraphQL's single endpoint becomes more than a convenience β€” it becomes an orchestration layer. The client still sends one query. Behind the scenes, the GraphQL server's resolvers fan out to whatever services own each piece of data, then assemble one response.

GraphQL gateway with one /graphql endpoint over many backend services

This is the same job an API gateway does β€” a single front door that hides how many services sit behind it β€” but GraphQL adds the schema and the client-shaped response on top. In this role it's often called a Backend-for-Frontend (BFF): a thin layer whose entire purpose is to give the frontend exactly the data shape it wants, sparing each client from talking to five services directly. We'll build a working version of this in Part 4.

The catch — and it's a real one — is that the gateway can still over-fetch from the backend even when the client doesn't. If your user resolver naively calls GET /users/42 and pulls all forty fields just to return name, you've moved the waste from client→gateway to gateway→service, not eliminated it. Solving that well is what Part 2 is about.


6. GraphQL vs REST vs gRPC: choosing on purpose

GraphQL is not strictly "better" than REST or gRPC β€” it makes a different trade. Knowing the trade is the senior move.

Factor REST GraphQL gRPC
Response shape Fixed per endpoint Client-chosen Fixed (Protobuf)
Round-trips for related data Often many One One per call
HTTP caching Native, easy (GET + URL) Hard (POST, one URL) Not HTTP-native
Versioning /v1, /v2… Evolve + deprecate Regenerate stubs
Wire format JSON (verbose) JSON Binary (compact)
Best fit Public, cacheable, read-heavy Many clients, complex/nested data Service-to-service, low latency

The honest one-liner for each:

  • Reach for GraphQL when you have multiple clients that each want different slices of complex, related data, and you're willing to invest in schema design. The Γ  la carte card shines exactly when the menu is rich and the diners are picky.
  • Reach for REST when your API is simple, public, and read-heavy, and you want to lean on HTTP caching and CDNs. A single well-named endpoint with a GET and a cacheable URL is hard to beat β€” and as we'll see in Part 3, caching is precisely where GraphQL pays its tax.
  • Reach for gRPC for internal, high-throughput, low-latency service-to-service calls where a compact binary protocol and streaming matter more than browser-friendliness.

These aren't mutually exclusive. A very common shape is GraphQL at the edge for your clients, with resolvers that speak gRPC or REST to the services behind it β€” each protocol used where it's strongest.

Choosing GraphQL vs REST vs gRPC by who the client is


7. Key takeaways

  1. GraphQL exists to kill over-fetching (data you didn't want) and under-fetching (too many round-trips) β€” the Γ  la carte card replacing REST's prix-fixe menu.
  2. It uses a single endpoint and a client-driven query: the client describes the exact shape it needs, and the response mirrors that shape field-for-field.
  3. Everything hinges on the schema β€” a strongly typed SDL contract that doubles as introspectable, never-stale documentation.
  4. Resolvers do the actual work: one function per field, executed as a depth-first walk of the query tree, fetching from any data source.
  5. As an API gateway / BFF, GraphQL orchestrates many services behind one query β€” but it trades away REST's easy HTTP caching, a tension we'll unpack in Parts 2 and 3.

Next up, Part 2 opens the execution engine: how a query is parsed, validated, planned, and run β€” and the N+1 problem plus the DataLoader pattern that tames it.

Sign in to MAANG.io

Use your Google or Microsoft account β€” no password to remember.

Continue with Google Continue with Microsoft

Please accept the terms above to continue.