Skip to content
Santekno.com | Tech Tutorials and Trends
EN
📖 0%
12 Aug 2025 · 5 min read ·Article 65 / 110
Go

65. Custom gRPC Name Resolver

IH
Ihsan Arif
Writer at Santekno · Backend Engineer

title: 65. Custom gRPC Name Resolver: Optimizing Service Discovery Your Own Way
date: 2024-06-11
tags: [golang, grpc, networking, distributed systems, service discovery]

gRPC has long been a go-to choice for developers building fast, lightweight, and easy-to-maintain microservices. One crucial component that often gets overlooked is the Name Resolver. By default, gRPC ships with several resolvers such as dns:// or passthrough://, but the needs of modern distributed environments — like custom orchestrators, service meshes, or hybrid setups — frequently call for a “custom” touch. The problem is, very few engineers truly understand how powerful and flexible it is to build your own Custom Name Resolver.

In this article, I’ll take a deeper look at the Custom gRPC Name Resolver — covering its architecture, workflow, a simple implementation, and its use cases in real-world systems.


Why Do You Need a Custom Name Resolver?

When working with microservices, the way services discover each other’s endpoints (service discovery) is life and death. In the context of gRPC, a target endpoint like dns:///service.mydomain.com is processed by a name resolver. But what if a service’s source comes from a unique registry, a local file, or even a bespoke internal API? This is exactly where a Custom Name Resolver comes in.

Some common scenarios:

  • Integration with an internal registry (HashiCorp Consul, ETCD, etc.)
  • Dynamic microservices on Kubernetes with custom labels/filters
  • Service discovery through a specialized API Gateway that doesn’t natively support DNS A/AAAA records

How Name Resolution Works in gRPC

Let’s break down a simple diagram of the name resolution process in gRPC.

MERMAID
sequenceDiagram
  participant Client
  participant gRPC Library
  participant NameResolver
  participant LoadBalancer

  Client->>gRPC Library: Dial ("myres://user-service/id-1234")
  gRPC Library->>NameResolver: Build("myres")
  NameResolver->>gRPC Library: UpdateState(Resolved addresses)
  gRPC Library->>LoadBalancer: Pass endpoint list
  LoadBalancer->>Client: Ready to RPC!

Summary:

  • The client initiates a connection with a specific target via proto://target
  • gRPC selects a resolver based on the schema (proto)
  • The Name Resolver fetches and computes the endpoints from the custom source
  • It updates the state in the gRPC core to be passed along to the load balancer and dial the RPC

Anatomy of a Name Resolver in gRPC-Go

A name resolver is an implementation of the following interface (simplified):

go
1type Builder interface {
2    Scheme() string
3    Build(target Target, cc ClientConn, opts BuildOptions) (Resolver, error)
4}
5
6type Resolver interface {
7    ResolveNow(ResolveNowOptions)
8    Close()
9}
  • Builder: The initial registration that defines the resolver’s “scheme”
  • Resolver: The process of fetching/monitoring the backend against the endpoint source

Case Study: A Custom In-Memory Resolver

Let’s say we have a service discovery mechanism based on a local YAML file.

File: services.yaml

yaml
1user-service:
2  - 10.0.0.2:50051
3  - 10.0.0.3:50051
4order-service:
5  - 10.0.0.4:50052

Step 1: Define the Resolver Scheme

go
 1const scheme = "yamlmem"
 2
 3func init() {
 4    resolver.Register(&yamlMemBuilder{})
 5}
 6
 7type yamlMemBuilder struct{}
 8
 9func (b *yamlMemBuilder) Scheme() string { return scheme }
10
11func (b *yamlMemBuilder) Build(target resolver.Target, cc resolver.ClientConn, opts resolver.BuildOptions) (resolver.Resolver, error) {
12    // Fetch endpoints from YAML based on target.Endpoint
13    addrs := resolveFromYaml(target.Endpoint)
14    cc.UpdateState(resolver.State{Addresses: addrs})
15    return &yamlMemResolver{cc: cc}, nil
16}

Step 2: Process the YAML Resolution

go
 1func resolveFromYaml(endpoint string) []resolver.Address {
 2    // 1. Load the YAML file
 3    // 2. Parse the endpoint
 4    // 3. Return []resolver.Address in host:port format
 5    // Here I'm simplifying with hardcoded values
 6    if endpoint == "user-service" {
 7        return []resolver.Address{
 8            {Addr: "10.0.0.2:50051"},
 9            {Addr: "10.0.0.3:50051"},
10        }
11    }
12    return nil
13}
14
15type yamlMemResolver struct{ cc resolver.ClientConn }
16
17func (r *yamlMemResolver) ResolveNow(o resolver.ResolveNowOptions) {}
18func (r *yamlMemResolver) Close() {}

Step 3: Dial Using the Custom Scheme

go
1conn, err := grpc.Dial("yamlmem:///user-service", grpc.WithInsecure())
2if err != nil {
3    log.Fatalf("dial failed: %v", err)
4}

With this pattern, every time we run grpc.Dial with the yamlmem:// scheme, the custom resolver will automatically fetch and refresh the endpoints from the YAML file.


Simulated Results: Resolution Table

Client Dial TargetgRPC Scheme ResolverEndpoints Obtained
yamlmem:///user-serviceyamlmem10.0.0.2:50051, 10.0.0.3:50051
yamlmem:///order-serviceyamlmem10.0.0.4:50052
dns:///api.example.orgdns

Going Further

a. Live Updates

Of course, in the real world the resolver will monitor a source (a file, registry, REST API) and call cc.UpdateState() whenever there’s a change.

go
1go func() {
2    for changes := range watchServiceChange() {
3        cc.UpdateState(resolver.State{Addresses: changes})
4    }
5}()

b. Error Handling

cc.ReportError(err) can be used when the resolve process fails. This sends feedback back to the client/error balancer.

c. Advanced Use Case: A Resolver for Service Mesh Labels

Imagine you only want services labeled blue or canary to be resolvable. A custom resolver can filter the endpoints before updating the state.


When Should You Build a Custom Resolver?

Use CaseNeed Custom?Reason
Ordinary DNSNoThe built-in resolver is sufficient
Loading from your own file/registryYesRequires non-standard access logic
Connecting to an external APIYesThe endpoint data source isn’t DNS-based
Filtering based on business logicYesFiltering/logic lives only on the application side
A single static hostNoYou can use passthrough://

Conclusion

The ability to create a Custom gRPC Name Resolver can be a superpower for engineers who want microservices that are more flexible, scalable, and tailored to modern service distribution needs. Whether it’s an internal registry, a service mesh, or a hybrid environment — with a custom resolver, we can define our own way of doing service discovery, without having to wait for a provider or “hack” DNS.


References & Tools

Danger
Happy experimenting, and explore gRPC even deeper with custom resolvers!
Leave a comment if you have any ideas or questions about unique resolvers or service discovery integrations at your company 👍

Follow me for more articles on distributed systems & scalable microservices!

Related Articles

💬 Comments