How do you decide whether to expose a generic `where` input with nested AND/OR/NOT on a GraphQL list field?
answer
- Who absorbs the open-ended work
- Count the consumers and their trust level
- You cannot deprecate a combination
- Introspection advertises what you must serve
- Registered documents make it finite again
basics
~20 sDecide on consumer count and trust. A recursive where input turns the schema into a query language: every combination becomes a supported contract, nobody can enumerate the access paths to index, and static cost scoring loses its grip.
solid answer
~50 sA curated filter publishes the predicates you decided to support; a recursive `where` with `and`/`or`/`not` publishes an open query language, and that is a permanent transfer of work from schema authors to operators. Four costs drive the decision. You cannot deprecate a combination — `@deprecated` attaches to an input field, not to "`or` nested under `not`". Capacity planning loses its object, because the set of access paths to index is no longer finite. Static cost analysis works from selection sets and arguments, and a predicate tree's cost depends on selectivity that is not knowable statically. And introspection starts advertising filters the storage engine serves badly, which is worse than not offering them, because clients build on what they discover. Ship it when consumers are few, internal and trusted, or when registered documents make the effective predicate set finite again; otherwise curate per-field operator inputs with no recursion.
code
graphql · 18 linesinput FloatPredicate { eq: Float, gt: Float, gte: Float, lt: Float, lte: Float }
# open: recursion means the predicate set is unbounded
input ReadingWhere {
and: [ReadingWhere!]
or: [ReadingWhere!]
not: ReadingWhere
temperatureC: FloatPredicate
recordedAt: DateTimePredicate
}
# curated: operators per field, no recursion, still enumerable
input ReadingFilter {
stationId: ID
temperatureC: FloatPredicate
recordedAfter: DateTime
recordedBefore: DateTime
}go deeper
Understand what the shape is first: an input type that contains lists of itself under and/or, which is why the set of expressible filters has no upper bound.
Be able to explain the mechanics of the cost — recursion means the argument value has its own depth, and every field crossed with every operator becomes part of the published contract.
Argue the operational side concretely: which access paths can be indexed when the predicate set is open, how you detect a query shape that defeats every index, and what bound you enforce instead of a cost score.
Own the decision and its reversibility. Weigh consumer count and trust, the storage engine's real capability, and whether registered documents can make the surface finite again — and say what evidence would let you withdraw it later.
## What the choice actually is A curated filter input publishes the predicates you have decided to support. A generic `where` input publishes a **query language**: ```graphql input StringPredicate { eq: String, in: [String!], contains: String } input FloatPredicate { eq: Float, gt: Float, gte: Float, lt: Float, lte: Float } input ReadingWhere { and: [ReadingWhere!] or: [ReadingWhere!] not: ReadingWhere stationName: StringPredicate temperatureC: FloatPredicate recordedAt: DateTimePredicate } ``` That is recursive by construction, and the recursion is the point of the design and the source of every cost below. The decision is not a style preference; it is a decision about who absorbs the open-ended work, and it is close to irreversible. ## The costs, in the order they bite **Every expressible combination becomes a supported contract.** With curated arguments, the set of things a client can ask for is finite and you approved each one. With a `where` tree it is unbounded, and you approved it in aggregate. You cannot deprecate a *combination*: `@deprecated` attaches to an input field, not to "`or` nested three deep under `not`". The only granularity you have is removing a field from the predicate input, which breaks every document that used it in any position. **Capacity planning loses its object.** The question "what access paths must the storage engine serve?" no longer has a finite answer. Nobody can enumerate the queries to index for, so index coverage becomes reactive: you find out which shapes are unserved when one of them is already in production. A dashboard whose `or` branch defeats every index does not fail — it returns an 8,400-row scan in three seconds, every thirty seconds, and looks like a capacity problem rather than a design one. **Static cost analysis loses its grip.** Limiters that score a request before executing it work from the selection set and its arguments. A predicate tree makes the score depend on the *shape and selectivity* of an argument value rather than on the fields selected, and selectivity is not knowable statically. Whatever weights you assign, the honest position is that a `where` tree is much harder to score than a fixed argument list, which is why teams that ship one usually end up enforcing timeouts and row caps instead. **Introspection becomes a promise you may not keep.** This is the subtle one. A curated schema that omits a filter tells the truth: that query is not available. A generic `where` shows every field crossed with every operator, so a client discovers a filter, writes it, ships it, and meets a timeout. Advertising a capability you serve badly is worse than not advertising it, because the client has already built on it. **Input depth is a surface of its own.** Recursion means a caller controls the depth of the *argument value*, not just of the selection set. Depth and complexity limiters are commonly written to walk the selection set, so a deeply nested predicate tree can slip past a limit that looks like it should have caught it. If you ship a `where`, the input-value depth needs an explicit bound of its own. ## When it is nevertheless right When the consumers are few, internal and trusted — an analytics surface, an internal tool, one team's reporting client — the flexibility genuinely saves months of one-off filter fields. When the storage engine really does serve arbitrary predicates at the scale involved. When the schema is generated from storage anyway, and its whole value proposition is that the client can ask for anything. And when you can make the predicate set finite again by other means: registering the documents that are allowed to run turns "unbounded query language" back into "a reviewable list of queries", which is the single most effective way to have both. ## The middle paths worth naming Curated per-field operator inputs with no `and`/`or`/`not` recursion: `temperatureC: FloatPredicate` gives clients the operators they keep asking for while keeping the predicate set flat and enumerable. Or allow one level of `or` and no nesting. Or expose the generic surface on a separate, clearly-scoped field for the analytics consumer, and keep the product-facing list fields curated — the two audiences have different contracts and different failure budgets. ## The principal framing Say plainly that this transfers work from schema authors to operators, permanently, and that the transfer is hard to reverse: once documents exist, you need usage evidence from a registry before you can remove any part of the surface, and there is no evidence at all for the combinations nobody has run yet. Decide it on the number and trust level of the consumers, on whether the storage engine can honour what introspection will advertise, and on whether you have a mechanism — registered documents, a hard row cap, a timeout — that makes the unbounded surface bounded again in practice. A candidate who answers "it depends on the use case" and stops has not answered; the interviewer wants the specific things it depends on.
- Why is a generic `where` input harder for a static cost limiter than a fixed argument list?A limiter scores a request before executing it, from the selection set and its argument values. With fixed arguments the shapes are few and each can carry a weight you calibrated. A predicate tree's real cost depends on selectivity and on which branch defeats an index — neither of which is knowable before execution. Teams that ship one usually fall back to row caps and timeouts, which bound damage without pretending to predict it.
- How do registered documents change the calculus?They turn an unbounded surface back into a finite, reviewable list. If only documents that have been registered ahead of time may execute, the effective predicate set is whatever was registered, so it can be read, indexed for and rejected in review. That is the main way to have a flexible input without an unbounded contract — but it only works when every consumer goes through that pipeline.
- What is the middle path if clients keep asking for operators the curated filter lacks?Give each filterable field its own operator input — `temperatureC: FloatPredicate` with `gt`, `gte`, `lt`, `lte` — and stop there. No `and`/`or`/`not` recursion. The predicate set stays flat and enumerable, so it can still be indexed for and scored, while clients get the ranges and set membership they actually asked for.
- Once a generic `where` has shipped, how do you take it back?Slowly, and only with evidence. Removing a predicate field breaks every document that used it in any position, and there is no evidence at all about combinations nobody has run yet. You need field-usage data from a registry, a deprecation window long enough for the clients you cannot redeploy, and usually a replacement curated field to migrate onto first.
A curated filter is a menu; a generic where is handing the customer the keys to the kitchen. It is a fine arrangement with three regulars and a poor one with a queue out the door.
saying these in an interview costs you the question
- Says a generic where input is strictly more flexible, no cost
- Believes @deprecated can retire a filter combination
- Assumes a selection-set depth limit bounds input nesting
- Cannot name who absorbs the unbounded work afterwards
- Treats removing a predicate field as an additive change
- Answers only it depends without naming what it depends on