GraphQL has no packages, so how do you keep type names unique across many teams?
answer
- There are no packages here
- One pool for every named type
- Inputs and outputs draw from it
- Fields and enum values are scoped
- Generic nouns collide first at scale
basics
~20 sEvery named type in a schema shares one flat pool - the specification requires unique type names and offers no namespaces. Uniqueness has to come from a shared vocabulary: qualify types by domain concept and keep generic nouns out of the pool.
solid answer
~50 sGraphQL has one global namespace for named types: objects, interfaces, unions, enums, input objects and custom scalars all draw from it, and the specification requires them to be unique. That is why `type Enrollment` and `input Enrollment` cannot coexist, and why the ecosystem invented the `Input` suffix - a convention, not a rule. Field names are scoped to their type and enum values to their enum, so only type names are global. At scale the problem is organizational: across a 62-subgraph payroll and benefits supergraph with 1,148 named types, three teams will define `Enrollment` and either composition fails or the types merge into one muddled type nobody owns. The fix is a graph-level vocabulary - qualify by concept (`BenefitsEnrollment`), never by team or service, treat generic nouns like `Status` and `Node` as reserved, and check names before publication.
code
graphql · 11 lines# benefits service
type Enrollment {
planId: ID!
effectiveOn: String!
}
# learning service
type Enrollment {
courseId: ID!
seatCount: Int!
}go deeper
Remember the shape of the rule: one schema, one pool of type names, all of them unique, with no packages or namespaces. That is why input types conventionally end in Input - the output type already took the plain name.
Be precise about what is and is not global. Type names are; field names are scoped to their declaring type and enum values to their enum. Then explain why Status and Item are the names that actually collide.
Show you have lived with this across teams: describe both failure modes - composition rejecting a conflict, and near-identical types silently merging into one nobody owns - and the vocabulary and pre-publication checks that prevent them.
Frame it as a coordination problem GraphQL hands to an organization with no coordination mechanism. Own the graph-level naming policy, the reserved generic nouns, and the argument that names are decided before publication because afterwards the only fix is a rename clients can see.
## What "flat" means precisely The specification requires that all types within a GraphQL schema have unique names, and it gives you no construct for scoping them. There are no packages, no modules, no namespaces, no import aliases — one pool of names, drawn on by every object type, interface, union, enum, input object and custom scalar in the schema. Directive names live in their own pool with their own uniqueness rule, and the root operation types are ordinary members of the type pool: `Query`, `Mutation` and `Subscription` are simply the default names, and a schema definition may point the query root at a differently named type. Two things are *not* in that pool, and saying so is what separates a precise answer from a vague one. Field names are scoped to the type that declares them, so `Employee.status` and `PayRun.status` are unrelated and neither constrains the other. Enum values are scoped to their enum, so two enums may both offer `ACTIVE`. Only type names are global. The first consequence bites inside a single service: an object type and an input object type cannot share a name. You cannot write `type Enrollment` alongside `input Enrollment`, which is the entire reason the ecosystem writes `EnrollmentInput` — a convention with no specification backing, adopted because the flat namespace left no alternative. ## The problem is a scale problem With one team and one service, a flat namespace is invisible; you notice a duplicate the moment you write it. It becomes structural when one schema is assembled from many. On a payroll and benefits supergraph composed from 62 subgraphs and carrying 1,148 named types, every team is drawing from the same pool without seeing each other's picks. Three of them will define `Enrollment` — benefits enrollment, pension plan enrollment, training enrollment — and each is right within its own service. There are only two outcomes and both are bad. Either composition fails because the definitions are incompatible, which is loud and blocks whoever composed last rather than whoever chose the name; or the definitions are similar enough to merge and you now have one type whose meaning is a blend of three domains, which nobody owns and which quietly grows fields from all three. The second is worse, because it never produces an error. ## Which names collide first Not domain nouns. The collisions that actually happen are on generic ones: `Status`, `Type`, `Item`, `Filter`, `Metadata`, `Error`, `Result`, `Connection`, `Edge`, `Node`. Enums called `Status` are the single most common case, because every domain has a status and none of them means the same thing. `Node` is worth calling out separately: the Relay server specification's global object identification convention claims that name for the interface implemented by every re-fetchable object, so a domain type called `Node` takes a name a large part of the ecosystem expects to mean something specific. ## Strategies that survive Qualify by concept, not by owner. `BenefitsEnrollment`, `PensionEnrollment` and `CourseEnrollment` describe what the type is, and they stay correct after a re-org. `HrEmployee`, `PayrollEmployee`, `TeamAlphaEmployee` encode who happened to own it this quarter, and they turn every re-org into either a lie or a rename. A schema whose type names are systematically prefixed by team also reads like packages leaked into a contract that has no packages, and it pushes the noise onto every consumer. Keep the bare, generic nouns out of the shared pool entirely. An enum meaning "the state of a benefit enrollment" is `EnrollmentStatus`, never `Status`; the extra word costs nothing and removes the collision permanently. Decide before composition, not during. The realistic control is a check that runs against the pooled set of names before a subgraph is published — mechanising that check is the schema-linting concern next door, but the naming policy it enforces is the thing this leaf is about, and it has to be written down somewhere every team can read. ## Why a collision found late is expensive Type names reach clients. A named or inline fragment carries a type condition written with the type's name, and the meta-field that reports an object's concrete type returns that name as a string, which clients routinely branch on and normalized caches routinely key on. So resolving a collision by renaming a type is not an internal tidy-up; it is a change every consumer sees. That is the real argument for spending time on names at the point a type is created, when the rename costs nothing. ## The senior framing The point to make is that GraphQL pushes a global-uniqueness problem onto an organization that has no global coordination mechanism, and that no amount of local discipline solves it — each of the three teams that wrote `Enrollment` made a defensible local choice. What solves it is a shared vocabulary decided at graph level, generic nouns treated as reserved, and a check that runs before publication rather than after composition.
- Do field names collide across types the same way type names do?No. A field name is scoped to the type that declares it, so `Employee.status` and `PayRun.status` are unrelated, and enum values are scoped to their enum. Only type names are global - directives have their own separate pool with its own uniqueness rule. Getting this distinction right is most of the answer.
- Is prefixing every type with its owning team a workable answer?No, because teams and services are renamed far more often than concepts are. `TeamAlphaEmployee` becomes wrong at the next re-org and the correction is a rename that clients see. Qualify by domain concept instead, and only where the bare noun is genuinely ambiguous - a schema where every type carries a team prefix reads like packages leaked into a contract that has none.
- Which type names collide first in practice?Generic ones: `Status`, `Type`, `Item`, `Filter`, `Metadata`, `Result`, `Error`. Enums named `Status` are the classic case, since every domain has a status and none of them means the same thing. `Node` deserves separate care because the Relay server specification's global object identification convention claims it for the re-fetchable-object interface.
- Why is a type rename more expensive than it looks?Type names travel to clients. Named and inline fragments carry a type condition written with the type's name, and the meta-field reporting an object's concrete type returns that name as a string, which client code branches on and normalized caches key on. So renaming a type is a change every consumer can observe, not an internal tidy-up.
It is a single phone book with no surnames: every entry has to be distinguishable by its first name alone, which works in a village and fails in a city.
saying these in an interview costs you the question
- Thinks GraphQL type names can be scoped by package
- Believes type Plan and input Plan can coexist
- Prefixes every type with the owning team's name
- Assumes a type rename is invisible to clients
- Treats Status as a safe shared enum name
- Thinks field names must be globally unique too