HandbookFoundationComputed fields

Computed fields

This example demonstrates the core techniques for passing field dependencies between subservices, covering most of the topics discussed in the official computed fields documentation.

Computed fields involve selecting data from various services, and then sending that data as input into another service that internally computes a result upon it. If you’re familiar with the _entities query of the Apollo Federation spec, computed fields work pretty much the same way. Computed fields are fairly complex and therefore not a preferred solution for basic needs. However, they can solve some tricky problems involving the directionality of foreign keys.

In general, computed fields are most appropraite when:

  • A service holds foreign keys without their associated type information (i.e.: a service knows an ID but doesn’t know its type). In these cases, the keys can be sent to a remote service to be resolved into typed objects.
  • A service manages a collection of bespoke GraphQL types that are of no concern to the rest of the service architecture. In such cases, it may make sense to encapsulate the service by sending external types in for data rather than releasing its types out into the greater service architecture.

However, computed fields have several distinct disadvantages:

  • A subservice with computed fields cannot independently resolve its full schema without input from other services. This means computed fields are defunct holes in a subschema unless accessed through the gateway with dependencies satisfied.
  • Computed fields rely on passing complex object keys between services rather than primitive scalar keys. Where a primitive key may be recognized as empty and therefore skip requesting data, complex object keys are always seen as truthy values even if their contents are empty. Thus, the gateway is forced to always request data on their behalf, even for predictably empty results unless manual intervention is taken.

This example demonstrates:

  • Configuring computed fields.
  • Sending complex inputs to subservices.
  • Normalizing subservice deprecations in the gateway.

Related examples:

Sandbox

⬇️ Click ☰ to see the files

You can also see the project on GitHub here.

The following service is available for interactive queries:

  • Stitched gateway: listening on 4000/graphql

For simplicity, all subservices in this example are run locally by the gateway server. You could easily break out any subservice into a standalone remote server following the combining local and remote schemas example.

Summary

Visit the stitched gateway and try running the following query:

query {
  products(upcs: [1, 2, 3, 4]) {
    name
    price
    category {
      name
    }
    metadata {
      __typename
      name
      ... on GeoLocation {
        name
        lat
        lon
      }
      ... on SportsTeam {
        location {
          name
          lat
          lon
        }
      }
      ... on TelevisionSeries {
        season
      }
    }
  }
}

The category and metadata associations come from the Metadata service, and are merged with Products service data using computed fields (this configuration can also be written using schema directives):

merge: {
  Product: {
    fields: {
      category: { selectionSet: '{ categoryId }', computed: true },
      metadata: { selectionSet: '{ metadataIds }', computed: true },
    },
    fieldName: '_products',
    key: ({ categoryId, metadataIds }) => ({ categoryId, metadataIds }),
    argsFromKeys: (keys) => ({ keys }),
  }
}

In this pattern, the category and metadata fields each specify field-level selection sets that will only be collected from other services when their respective fields are requested. The additional computed setting isolates these fields into a standalone schema that assures the fields are always requested directly by the gateway with their dependencies provided. Together, the results of these selection sets are built into an object key and sent off to the Metadata service to be resolved into its version of the Product type.

metadata schema:

type Product {
  category: Category # @computed(selectionSet: "{ categoryId }")
  metadata: [Metadata] # @computed(selectionSet: "{ metadataIds }")
}
 
input ProductKey {
  categoryId: ID
  metadataIds: [ID!]
}
 
type Query {
  _products(keys: [ProductKey!]!): [Product]!
}

Resolving metadata

The metadata association is a pretty good candidate for computed fields.

Looking at the way associations are structured, the Products service holds metadata record IDs without any associated type information. Therefore, the Products service must send these untyped IDs over to the Metadata service to be resolved into type objects (this is inherently a shortcoming of the data model—in a perfect solution, association data would be migrated over to the Metadata service where both ID and type information is available).

Even if the Products service had all association data available though, there’s still some merit in having the Metadata service internalize the Product type rather than externalizing all of its bespoke Metadata types. Encapsulation of concerns is a valid factor to weigh against implementation complexity.

Resolving category

The catagory association is needlessly complex using computed fields. It could just as easily use a basic foreign key pattern in the Products service, which would eliminate the unsightly categoryId field from its schema:

products schema:

type Product {
  ...
  # categoryId: ID << remove this
  category: Category
}
 
type Category {
  id: ID!
}

Deprecating subservice inconsistencies

One of the biggest shortcomings of computed fields is their inconsistency—they cannot be resolved outside of gateway context where their dependencies are satisfied. If you also utilize the subservice as a standalone GraphQL resource, then computed fields appear as defunct holes in its schema.

An imperfect solution to this problem is to deprecate all computed fields in the subservice, and then remove those deprecations in the gateway proxy layer. For example:

metadata schema:

type Product {
  category: Category @deprecated(reason: "gateway access only")
  metadata: [Metadata] @deprecated(reason: "gateway access only")
}

index.js:

import { stitchSchemas } from '@graphql-tools/stitch'
import { RemoveObjectFieldDeprecations } from '@graphql-tools/wrap'
import { metadataSchema } from './services/metadata'
 
stitchSchemas({
  subschemas: [
    {
      schema: metadataSchema,
      transforms: [new RemoveObjectFieldDeprecations('gateway access only')]
    }
  ]
})

The RemoveObjectFieldDeprecations transform will un-deprecate these fields within the gateway schema. The subservice-level deprecations at least offer some insight as to why certain fields don’t work when accessed directly.