Skip to content

CONCEPT Cited by 1 source

Draft schema field

Definition

A draft schema field is a GraphQL field marked — typically by a custom directive — as "not ready for production". The persist step that registers queries into the persisted-queries DB refuses to register any query that references a draft field. The field can be present in the deployed schema, can be queried in development, and can evolve breaking changes freely — but no production-bound UI bundle can embed an ID that touches it.

At Zalando, the directive is spelled @draft on FIELD_DEFINITION (Source: sources/2022-02-16-zalando-graphql-persisted-queries-and-schema-stability).

Mechanism (Zalando)

Directive declaration:

directive @draft on FIELD_DEFINITION

Applied to a field:

type Product {
  fancyNewField: FancyNewType @draft
}

type FancyNewType {
  testField: String
}

Persist-time validation rule (JavaScript, verbatim from the post):

export function draftRule(context) {
  return {
    Field(node) {
      const parentType = context.getParentType();
      const field = parentType.getFields()[node.name.value];
      const isDraft = field.astNode.directives.some(
        (directive) => directive.name.value === "draft"
      );
      if (isDraft) {
        context.reportError(new GraphQLError(`Cannot persist draft field`));
      }
    },
  };
}

The rule is supplied to standard GraphQL validation, which walks the query AST and emits "Cannot persist draft field" on any reference (Source: sources/2022-02-16-zalando-graphql-persisted-queries-and-schema-stability).

The three guarantees

Draft gives a schema change three properties simultaneously that are hard to get otherwise:

  1. Cannot reach production. Persist-time validation refuses to accept queries that use the field.
  2. Breakable at will. Because only persisted queries run in production, and no persisted query references the draft field, its type, name, and semantics can be changed without deprecation ceremony.
  3. Lives on mainline. The schema change can be merged to the main deployment branch and even deployed.

The third point is what distinguishes directive-based lifecycle from branch deployments: the unreleased change is already in production infrastructure, so cross-service coordination (resolvers, backing-service contracts) can be built out on mainline instead of a fork (Source: sources/2022-02-16-zalando-graphql-persisted-queries-and-schema-stability).

Why branch deployments are the rejected alternative

The Zalando post explicitly rejects branch deployments at their topology: "we have microservices and the GraphQL layer is an aggregator from multiple other services. So, maintaining multiple feature branches across 3-5 projects for 1 or 2 product features isn't going to help any developer or team move smoothly. The complexity increases non-linearly as we mix different features that must work together." Draft moves the gate from "which branch is deployed" to "which queries persist," which composes across services better (Source: sources/2022-02-16-zalando-graphql-persisted-queries-and-schema-stability).

Draft vs. deprecation

  • @draft — field is new, not-yet-used, breakable. Usage direction: pre-production. Persist-blocked.
  • @deprecated (standard GraphQL) — field was used, is being removed. Usage direction: post-production. Queries still persist; tooling surfaces a warning.

Both are directive-based schema annotations, but they operate at opposite ends of the field lifecycle.

Lifecycle placement

Draft is the first stage in Zalando's three-stage directive-based field lifecycle (see patterns/directive-based-field-lifecycle):

@draft                   → @allowedFor(components: [...])   → (no directive)
(unpersistable)          (component-scoped, persistable      (stable, unrestricted)
                          within whitelist)

When the field stabilises, both @draft and (if applied) @allowedFor annotations are removed, and the field joins the "non-breaking contract" underwritten by persisted queries.

Seen in

Last updated · 501 distilled / 1,218 read