Managing a Large GraphQL Schema in Production: What Broke and What Helped
Small GraphQL schemas feel elegant. Large ones do not stay elegant on their own.
As the ticketing platform grew, the schema expanded across events, venues, seating plans, pricing, reservations, orders, payments, scanning, partner integrations, and internal tools. At that point the main question was no longer "is GraphQL a good fit?" It was "how do we stop a large schema from becoming expensive to maintain?"
Why It Grew Quickly
Ticketing platforms accumulate complexity fast.
A simple purchase flow turns into seat maps, hold timers, price categories, discount logic, box office flows, QR validation, white-label requirements, analytics, and different internal views of the same data. GraphQL handles this kind of product surface well, but it does not reduce complexity. It formalizes it.
So the schema grew because the product grew. That part was expected.
What Helped
Code generation
Generating TypeScript types from the schema removed a large class of frontend/backend mismatch problems. Once a field changed, the compiler surfaced the damage immediately.
Schema-first design
Defining the contract before implementation improved cross-team discussions. Ambiguity was harder to hide once it had to be expressed as a concrete type or field.
Those two decisions held up well over time.
Where It Started Breaking
Resolver performance
The main performance problems came from resolver behavior, not the schema definition itself.
Nested queries looked reasonable in GraphQL but expanded into too many database calls under real usage. That is one of the more predictable GraphQL failure modes: clean query ergonomics hiding expensive execution paths.
DataLoader helped, but the broader lesson was that resolver discipline matters more as the schema grows.
Deprecation
Adding fields is cheap. Removing them is not.
Once a field is used by internal tools or partner-facing flows, deleting it becomes operational work, not just refactoring. Without a real deprecation process, old schema surface area accumulates.
Naming consistency
Naming drift becomes visible once enough engineers contribute to the same API over time. Without linting and conventions, different domains start to feel like they were designed by different teams, because they were.
Testing Cost More Than Expected
Unit tests covered isolated logic. The more useful failures appeared in integration tests.
Authorization, middleware effects, nested resolution, feature flag behavior, and composition issues often only showed up when the whole request path executed end to end. That made full GraphQL integration testing more important than expected.
It was slower, but it caught the failures that actually mattered.
What I Would Change Earlier
Modularize sooner
If the schema is likely to grow, split it by domain early. Retrofitting modularity into a large schema is possible, but unnecessarily expensive.
Define conventions before they are needed
Naming rules, mutation patterns, input naming, and deprecation standards all feel optional when the API is small. They do not stay optional.
Think about resolver cost during schema design
Schema discussions tend to focus on type shape and frontend ergonomics. Resolver cost should be part of the same conversation.
A clean SDL file does not imply efficient execution.
Final Takeaway
GraphQL still makes sense for products with complex data relationships and multiple clients. The problem is not that GraphQL stops scaling. The problem is that teams often assume the schema will stay maintainable by default.
It will not.
A large GraphQL API needs conventions, ownership, integration testing, and routine cleanup. Without those, the schema becomes progressively harder to reason about, even if the product itself is successful.
That is the actual tradeoff.