Skip to main content

Introduction

Field resolvers in Cosmo Router enable you to implement custom resolution logic for any field in your GraphQL schema through gRPC integration. This powerful feature allows you to add computed fields, perform complex transformations, and integrate external data sources seamlessly within your federated graph. Unlike traditional GraphQL resolvers that run within your application code, Cosmo’s field resolvers execute as gRPC services, providing better performance, type safety, and language flexibility while maintaining the familiar GraphQL developer experience.

What are Field Resolvers?

Field resolvers are specialized functions that define how specific fields in your GraphQL schema should be resolved. They bridge the gap between your GraphQL API and custom business logic by:
  • Custom Field Logic: Implementing complex calculations, data transformations, or external API calls
  • Argument Handling: Processing field arguments passed from GraphQL operations, either directly or through variables
  • Context Awareness: Accessing parent object data and resolver context to make informed decisions
  • Type Safety: Leveraging protobuf definitions to ensure type-safe communication between the router and your resolver logic
When a GraphQL operation requests a field with a custom resolver, the Cosmo Router delegates the resolution to your gRPC service, which can implement any custom logic required to compute the field’s value.

How Field Resolvers Work in Protobuf

The implementation of field resolvers in a protobuf-based system presents a unique architectural challenge. Since Protocol Buffers don’t natively support field arguments (unlike GraphQL), Cosmo solves this through an innovative approach:

The Challenge

Traditional protobuf message definitions cannot represent GraphQL fields with arguments, as protobuf fields are simple properties without parameter support.

The Solution

To overcome this limitation, Cosmo generates RPC methods for each field resolver instead of simple message fields. This approach:
  1. Converts fields to RPCs: Each field with custom resolution logic becomes a dedicated RPC method in the generated protobuf service
  2. Preserves argument support: Field arguments are mapped to RPC method parameters, maintaining full GraphQL functionality
  3. Enables complex resolution: The RPC method can implement any custom logic needed to resolve the field value

Runtime Execution Flow

When your GraphQL operation requests a field with arguments:
  1. Query Planning: The Cosmo Router analyzes the operation and identifies fields requiring custom resolution
  2. Data Retrieval: The router first fetches any necessary parent data which is needed for the current type to resolve the field value
  3. Field Resolution: For each field resolver, the router calls the corresponding RPC method with the field arguments and parent context
  4. Response Assembly: The resolved field values are integrated back into the GraphQL response structure
This architecture ensures that your field resolvers have access to both the requested arguments and the complete parent object context, enabling sophisticated resolution logic while maintaining GraphQL’s declarative query interface.

Schema Definition and Generated Protobuf

This example GraphQL schema:
type Query {
  foo(id: ID!): Foo!
}

type Foo {
  id: ID!
  bar(baz: String!): String! @connect__fieldResolver(context: "id")
}
will produce the following Protobuf definitions:
// Service definition for ProductService
service ProductService {
  rpc QueryFoo(QueryFooRequest) returns (QueryFooResponse) {}
  rpc ResolveFooBar(ResolveFooBarRequest) returns (ResolveFooBarResponse) {}
}

// Request message for foo operation.
message QueryFooRequest {
  string id = 1;
}
// Response message for foo operation.
message QueryFooResponse {
  Foo foo = 1;
}
message ResolveFooBarArgs {
  string baz = 1;
}

message ResolveFooBarContext {
  string id = 1;
}

message ResolveFooBarRequest {
  // context provides the resolver context for the field bar of type Foo.
  repeated ResolveFooBarContext context = 1;
  // field_args provides the arguments for the resolver field bar of type Foo.
  ResolveFooBarArgs field_args = 2;
}

message ResolveFooBarResult {
  string bar = 1;
}

message ResolveFooBarResponse {
  repeated ResolveFooBarResult result = 1;
}

message Foo {
  string id = 1;
}

Implementation Guide

Step-by-Step Setup

1

Define Schema

Define the schema with field resolvers.
2

Generate Protobuf

Use the wgc grpc-service generate command to generate the protobuf definition for the gRPC service.
3

Implement Resolver Logic

Implement the field resolver logic in the gRPC service.
4

Deploy and Test

Deploy the gRPC service and test the field resolvers.

Example implementation

To provide better insights on how to implement field resolvers, you can take a look at the following Go example. In this example, we want to retrieve the popularity score for a category based on the ID and a given threshold.
type Category {
  id: ID!
  name: String!
  kind: CategoryKind!
  popularityScore(threshold: Int): Int @connect__fieldResolver(context: "id")
}

Data Loading and Batching

Cosmo Connect automatically optimizes field resolver performance through intelligent batching mechanisms that eliminate the N+1 query problem commonly found in GraphQL implementations.

How Batching Works

When your GraphQL operation requests fields across multiple entities, Cosmo Connect:
  1. Aggregates Context: Collects all context elements from the original operation that require field resolution
  2. Batches Requests: Groups multiple field resolution calls into a single gRPC request
  3. Preserves Order: Maintains the original order of context elements to ensure correct response mapping

Implementation Requirements

As a field resolver implementer, you only need to follow one simple rule:
Always return results in the exact same order as the provided context elements. The router relies on positional mapping to correctly associate resolved values with their corresponding entities.
// Good: Maintains positional mapping by returning empty results for skipped elements
func (s *ExampleService) ResolveExample(_ context.Context, req *examplev1.ResolveExampleRequest) (*examplev1.ResolveExampleResponse, error) {
	results := make([]*examplev1.ResolveExampleResult, 0, len(req.GetContext()))
	for _, reqContext := range req.GetContext() {
		if reqContext.GetId() == "1" {
			// Skip resolution for this context element by returning an empty result.
			// This maintains the required 1:1 mapping between context and results.
			results = append(results, &examplev1.ResolveExampleResult{})
			continue
		}

		results = append(results, &examplev1.ResolveExampleResult{
			Example: "example",
		})
	}

	resp := &examplev1.ResolveExampleResponse{
		Result: results,
	}

	return resp, nil
}

// Bad: Breaks positional mapping by skipping result elements
func (s *ExampleService) ResolveExample(_ context.Context, req *examplev1.ResolveExampleRequest) (*examplev1.ResolveExampleResponse, error) {
	results := make([]*examplev1.ResolveExampleResult, 0, len(req.GetContext()))
	for _, reqContext := range req.GetContext() {
		if reqContext.GetId() == "1" {
			// ERROR: Skipping this element without adding a corresponding result
			// will break the positional mapping between context and results.
			continue
		}

		results = append(results, &examplev1.ResolveExampleResult{
			Example: "example",
		})
	}

	resp := &examplev1.ResolveExampleResponse{
		Result: results,
	}

	return resp, nil
}

Entity Resolution

The same batching principles apply to entity lookups in federated scenarios. Whether resolving computed fields or fetching related entities, Cosmo Connect ensures optimal request patterns by aggregating multiple lookups into efficient batch operations.

Performance Considerations

Field resolvers introduce additional execution complexity since they require context-aware resolution across your gRPC services. Understanding the performance implications helps you design efficient resolver implementations.

Execution Flow Impact

When field resolvers are involved in your GraphQL operation:
  1. Context Preparation: The router must first gather all necessary parent data from relevant services
  2. Field Resolution: Additional gRPC calls are made to resolve computed fields with the prepared context
  3. Response Assembly: Resolved field values are merged back into the final GraphQL response

Built-in Optimizations

Cosmo Connect includes several performance optimizations out of the box:
  • Automatic Batching: Multiple field resolutions are automatically grouped into single gRPC calls
  • Parallel Execution: Independent field resolvers can execute concurrently where possible
  • Context Reuse: Shared parent data is fetched once and reused across multiple field resolvers
  • Lazy Loading: Field resolvers are only invoked when their fields are actually requested in the operation

Best Practices

To maximize performance in your field resolver implementations:
  • Minimize External Calls: Reduce dependencies on external services within resolver logic
  • Leverage Batching: Design your resolver to efficiently handle batch requests rather than individual items
  • Cache Strategically: Implement appropriate caching for frequently computed or slowly changing data
  • Monitor Execution: Use Cosmo’s observability features to identify performance bottlenecks

Limitations and Constraints

  • Grouping of multiple field resolvers within a type into a single call is not yet supported.
See also: gRPC Services · Plugins · GraphQL Support