GraphQL Deprecation Workflow: Retiring Old Fields Cleanly
AI generated
{ }
type
GraphQL · Schema Lifecycle · Deprecation · Monitoring
GraphQL Deprecation Workflow
retiring old fields cleanly

Simply deleting a field from the schema because the backend team no longer needs it is not deprecation, it's a breaking change. A proper GraphQL deprecation workflow marks fields with the @deprecated directive, measures their real usage, and only removes them once the data shows no consumer is affected anymore.

15 min read @deprecated · Field Usage · Sunset Deadlines GraphQL · Schema Lifecycle · DevOps

1. Why deleting is not deprecation

Simply removing a GraphQL field from the SDL file because the backend team no longer thinks it's needed is the most common cause of broken production frontends. GraphQL lets consumers query exactly the fields they need, which also means every single field is potentially used somewhere in a query the backend team has no visibility into. A proper GraphQL deprecation workflow replaces sudden deletion with a multi-stage process: mark, measure, communicate, only then remove.

The difference between a breaking change and a clean deprecation is not technical, it's temporal. A field marked @deprecated today can keep working for months while usage is measured in parallel. Only once usage has dropped to zero, or a previously communicated deadline has passed, does the field actually get removed. This deprecation workflow is not a nice-to-have, it is the only way to keep a growing schema maintainable long-term without risking customer connections on every cleanup pass.

2. Using the @deprecated directive correctly

GraphQL ships with a built-in @deprecated(reason: String) directive, designed for exactly this purpose. The field stays technically queryable, but tools like GraphiQL, Apollo Studio, or GraphQL Playground display it struck through along with the reason. The most important part of this deprecation workflow is a meaningful reason text: not just "deprecated," but concretely which replacement field to use and by when the old one disappears.

A common mistake: developers mark a field as @deprecated but forget to do the same on enum values and arguments, even though the directive has been allowed there too since the 2021 GraphQL spec revision. An enum value that's no longer supported should be marked the same way as a field, otherwise consumers won't know they need to adjust their filter logic. Consistently applying the directive across all schema elements is the foundation of every working GraphQL deprecation workflow.


type Product {
  id: ID!
  sku: String!
  name: String!

  # Deprecated field — reason always names the replacement and the sunset date
  price: Float @deprecated(reason: "Use `priceV2: Money` instead. Removal planned for 2026-11-01.")

  priceV2: Money!
}

enum SortOrder {
  ASC
  DESC
  # Deprecated enum value — same directive, same convention
  RELEVANCE_DEPRECATED @deprecated(reason: "Use `RELEVANCE` instead. Removal planned for 2026-11-01.")
  RELEVANCE
}

type Query {
  products(
    # Deprecated argument — arguments support @deprecated since GraphQL spec 2021
    legacyFilter: String @deprecated(reason: "Use `filter: ProductFilterInput` instead.")
    filter: ProductFilterInput
  ): ProductConnection!
}

3. Usage tracking: who still queries this field?

The @deprecated directive alone doesn't solve a problem, it only makes it visible. The central building block of any real deprecation workflow is field-level usage tracking: how often was price actually queried in the last 30 days, and by which clients? Without this data, every decision to permanently remove a field remains a guess rather than a substantiated fact. Both Apollo Server and graphql-php offer extension points where the field paths used per request can be extracted before the response goes out.

In practice, a simple middleware pattern is enough: the resolver for a deprecated field writes a counter into a time-series system such as Prometheus, or a simple log line that gets aggregated afterward, on every call. It's important to identify the calling client, for example via an X-Client-Name header or the persisted-query manifest, so the team can approach the responsible frontend team directly instead of having an anonymous number nobody can attribute.


<?php
declare(strict_types=1);

namespace Mironsoft\GraphQlDeprecation\Plugin;

use GraphQL\Type\Definition\ResolveInfo;
use Psr\Log\LoggerInterface;

/**
 * Tracks usage of deprecated GraphQL fields so removal decisions are data-driven,
 * not guesswork based on assumptions about which clients still call them.
 */
final class DeprecatedFieldUsageTracker
{
    private const array TRACKED_FIELDS = ['Product.price', 'Query.legacyFilter'];

    public function __construct(private readonly LoggerInterface $logger)
    {
    }

    /**
     * Logs a structured usage event for deprecated fields, keyed by client identity.
     *
     * @param ResolveInfo $info    Resolver metadata including parent type and field name.
     * @param string      $client  Identifier of the calling client, e.g. from a header.
     * @return void
     */
    public function trackIfDeprecated(ResolveInfo $info, string $client): void
    {
        $fieldPath = $info->parentType->name . '.' . $info->fieldName;
        if (!in_array($fieldPath, self::TRACKED_FIELDS, true)) {
            return;
        }

        $this->logger->info('deprecated_field_usage', [
            'field' => $fieldPath,
            'client' => $client,
            'timestamp' => time(),
        ]);
    }
}

4. Sunset deadlines and a communication plan

Marking a field @deprecated without an end date almost always means it stays in the schema for years, because "eventually" is not a date anyone works toward. A working deprecation workflow sets a concrete sunset date from the start, stated in the reason text and additionally documented in a changelog consumers can subscribe to. Common windows are three to six months for internal APIs and six to twelve months for publicly documented APIs with external partners.

Communication is the part of the deprecation workflow most often neglected. Marking a field and hoping someone reads the SDL file is not enough. Effective channels include an automated release announcement for every new deprecation, a central deprecation overview page listing all currently marked fields with their sunset dates, and for critical fields a direct message to the teams that, according to usage tracking, still actively access it. Only once all three levels are in place is a deprecation workflow complete.

5. Surfacing deprecation in codegen and the IDE

An often overlooked lever in the deprecation workflow is the developer experience right inside the IDE. GraphQL Code Generator automatically marks generated TypeScript types for deprecated fields with the @deprecated JSDoc tag, causing modern editors like VS Code to show the field struck through in autocomplete and display the reason text on hover. That moves the warning from a documentation portal directly into the moment a developer writes the query, much earlier than any after-the-fact check.

ESLint plugins such as eslint-plugin-graphql go a step further and fail the build as soon as a newly written query string references a deprecated field. This prevents a developer from accidentally writing new code against a field already in its sunset window because the deprecation warning was overlooked. This automation matters especially in large codebases with many developers who can't keep every schema change in their head.

6. Enforcing and escalating deprecation at the gateway

In architectures with an API gateway or Apollo Router, the deprecation workflow can additionally be enforced technically: the gateway can attach an extra warning header to responses for requests using a field already past its sunset date, visible to any client-side monitoring tool. As a final escalation stage, the gateway can even respond to such requests with a 410 Gone status for exactly that field, while the rest of the query is processed normally.

This staged escalation, from pure directive marking through active usage tracking to technical blocking at the gateway, gives teams control over the pace at which a field actually disappears. It's important that every escalation stage was communicated in advance, because a sudden 410 response without prior warning is technically clean but practically just as surprising as an immediate deletion without any deprecation at all.


// gateway-deprecation-plugin.js — Apollo Gateway plugin that warns, then blocks
const OVERDUE_FIELDS = { 'Product.price': '2026-11-01' };

module.exports = {
  requestDidStart() {
    return {
      willSendResponse({ request, response }) {
        const usedField = extractUsedDeprecatedField(request.query);
        if (!usedField) return;

        const sunset = OVERDUE_FIELDS[usedField];
        const isPastSunset = sunset && new Date() > new Date(sunset);

        if (isPastSunset) {
          // Escalation stage 2: block with 410, do not silently degrade
          response.http.status = 410;
          response.errors = [{ message: `Field ${usedField} was removed on ${sunset}` }];
        } else {
          // Escalation stage 1: warn but still serve the request
          response.http.headers.set('X-GraphQL-Deprecation-Warning', usedField);
        }
      },
    };
  },
};

7. Deprecation workflow in Magento GraphQL modules

In Magento's own GraphQL modules, the @deprecated directive is maintained directly in the module's schema.graphqls file, exactly as in Magento core itself, where older cart mutation fields were marked with clear reason texts for years before being removed in a major version. For custom extensions, the same approach is recommended: a new field replaces an old one, the old one gets marked, and removal happens no earlier than the next major release of your own module, never in a patch release.

A Magento-specific detail concerns db_schema.xml-backed custom attributes exposed through GraphQL resolvers: if an attribute is removed on the backend but the GraphQL resolver stays in place, the field can still be queried and then consistently returns null. The deprecation workflow must therefore treat both layers, the GraphQL field and the underlying attribute, in sync, otherwise a silent inconsistency arises where a field marked as active is already effectively dead.


{
  "deprecations": [
    {
      "field": "Product.price",
      "reason": "Use priceV2: Money instead",
      "deprecatedSince": "2026-05-01",
      "sunsetDate": "2026-11-01",
      "usageLast30Days": 142,
      "activeClients": ["storefront-web", "partner-app-legacy"]
    },
    {
      "field": "Query.legacyFilter",
      "reason": "Use filter: ProductFilterInput instead",
      "deprecatedSince": "2026-03-15",
      "sunsetDate": "2026-09-15",
      "usageLast30Days": 0,
      "activeClients": []
    }
  ]
}

8. CI automation: finding orphaned deprecations

Without automation, every deprecation workflow degrades into a manual spreadsheet nobody maintains. A CI pipeline can search the SDL file for @deprecated occurrences on every merge, extract the embedded sunset date, and automatically open a ticket once that date has passed. This prevents a field from staying formally marked as deprecated while practically never being removed, because no human actively remembers it anymore.

A second automated check compares usage tracking against the deprecation list: fields with zero calls in the last 30 days and an expired sunset deadline get flagged as safe to remove and can be proposed in an automatically generated pull request. Fields with continued active usage despite an expired deadline instead trigger an escalation to the responsible team, rather than being silently left alone or unexpectedly removed.


#!/usr/bin/env bash
# ci-deprecation-check.sh — finds fields past their sunset date and opens tickets
set -euo pipefail

SCHEMA_FILE="schema.graphqls"
TODAY="$(date +%Y-%m-%d)"

# Extract "Removal planned for YYYY-MM-DD" from every @deprecated reason
grep -oP '@deprecated\(reason: "[^"]*Removal planned for \K\d{4}-\d{2}-\d{2}' "$SCHEMA_FILE" \
  | while read -r sunset_date; do
      if [[ "$sunset_date" < "$TODAY" ]]; then
        echo "[OVERDUE] Sunset date $sunset_date has passed — filing ticket"
        gh issue create \
          --title "GraphQL field overdue for removal (sunset: $sunset_date)" \
          --label "graphql,deprecation" \
          --body "Automated deprecation check found a field past its sunset date."
      fi
    done

9. Deprecation strategies compared

There are several common strategies for retiring fields in GraphQL. The table below compares the most important ones and shows what each is best suited for.

Strategy How it works Suited for
Delete immediately Field disappears without warning Never recommended, guarantees a breaking change
@deprecated without a deadline Marking without a concrete end date Initial visibility, but without time pressure the field lingers
Deprecation with sunset date + tracking Deadline, usage measurement, escalation to active clients Recommended standard for production GraphQL APIs
Gateway enforcement (warn header, 410) Technical escalation at the API gateway Last resort for stubborn, unmigrated clients

Combining a sunset date with active usage tracking delivers, in practice, the best balance between cleanup speed and consumer stability. Directive marking alone without a deadline is better than nothing, but experience shows it leads to schemas accumulating years-old deprecated fields, because no concrete point in time ever forces action.

Mironsoft

GraphQL schema lifecycle and API governance

Removing old GraphQL fields without a breaking change?

We set up a complete deprecation workflow for your schema, from the @deprecated directive through usage tracking to automated CI checks for overdue sunset deadlines.

Deprecation audit

Review your existing schema for unmarked legacy fields and missing deadlines

Usage tracking

Measure field usage per client and make removal decisions data-driven

CI automation

Automatic tickets for expired sunset deadlines and remaining active usage

10. Summary

A clean GraphQL deprecation workflow consists of four building blocks that must work together: consistent marking with @deprecated and a meaningful reason text on fields, enum values, and arguments, active usage tracking that delivers real data instead of guesses, a concrete sunset date with clear communication through changelogs and direct messages to affected teams, and CI automation that actively finds overdue deprecations instead of letting them linger.

Anyone who applies this workflow consistently can clean up a growing GraphQL schema over years without ever producing an unannounced breaking change. The effort spent on tracking and automation pays off at the latest once a schema spans dozens of modules and external partner integrations, where nobody on the team remembers off the top of their head who still uses which field.

GraphQL Deprecation Workflow — Key Takeaways

@deprecated directive

Apply on fields, enum values, and arguments, with a replacement field and sunset date in the reason text.

Usage tracking

Measure per field and client instead of assuming a field is unused.

Sunset deadlines

Concrete date, documented in the changelog, communicated to active consumers.

CI automation

Automatically detect overdue deprecations and generate tickets or PRs.

11. FAQ: GraphQL Deprecation Workflow

1Deleting vs. deprecating?
Deleting breaks every query immediately. Deprecation only marks the field first, it keeps working while usage is measured and a deadline is communicated.
2Where to apply @deprecated?
On fields, enum values, and arguments. Mark all three consistently, otherwise parts of the deprecation stay invisible.
3How long should the deadline be?
Three to six months internally, six to twelve months externally. A concrete date matters, not a vague future.
4Measuring field usage?
With usage tracking in the resolver, ideally with client identity via a header or persisted-query manifest.
5Deadline passed, still used?
Escalation to the responsible team, not automatic removal. Only remove after direct contact.
6Codegen and IDE warning?
GraphQL Code Generator carries @deprecated as a JSDoc tag, VS Code shows the field struck through in autocomplete.
7Gateway enforcing deprecation?
Yes, first a warning header, then in the final escalation stage a 410 status for the affected field.
8Deprecated attributes in Magento?
Attribute and resolver must be treated in sync. Missing attribute on the backend means the field consistently returns null.
9Finding overdue deprecations automatically?
CI pipeline searches the SDL for @deprecated, extracts the sunset date, and opens a ticket automatically once it has passed.
10Removal in a patch release?
No. Actual removal is a breaking change and belongs in a major release after the deadline has expired.