Recipe

Go singleflight primer

When a cold cache key gets hammered by N concurrent requests, you usually only want ONE backend call. The Go golang.org/x/sync/singleflight package collapses duplicate in-flight calls onto a single execution and broadcasts the result to all waiters. This recipe shows the three patterns we run in production at Meridian.

1. Pick the right key

The first argument to group.Do is the dedupe key. Keys should be specific enough that two requests for different resources never collide, and generic enough that two requests for the same resource always merge. A good rule: use the same key you would use for the downstream cache entry. Avoid request IDs, timestamps, or trace IDs in the key.

2. Use DoChan for cancellable callers

group.Do blocks until the leader returns. If the caller has a tight context deadline you want to abandon the wait without killing the leader. group.DoChan returns a channel you can select on against ctx.Done(). The leader keeps running for other waiters even after this caller walks.

3. Forget on persistent error

If the leader returns an error, every waiter sees it. That is usually fine. But if the error is transient and you want the next wave to retry instead of getting the cached failure, call group.Forget(key) before returning. Otherwise the in-flight slot is freed automatically when the leader returns.

cache/singleflight.go
package cache

import (
	"context"
	"golang.org/x/sync/singleflight"
)

var group singleflight.Group

func GetUser(ctx context.Context, id string) (*User, error) {
	v, err, _ := group.Do("user:"+id, func() (interface{}, error) {
		return fetchUserFromDB(ctx, id)
	})
	if err != nil {
		return nil, err
	}
	return v.(*User), nil
}

func GetUserShared(ctx context.Context, id string) (*User, error) {
	ch := group.DoChan("user:"+id, func() (interface{}, error) {
		return fetchUserFromDB(ctx, id)
	})
	select {
	case res := <-ch:
		if res.Err != nil {
			return nil, res.Err
		}
		return res.Val.(*User), nil
	case <-ctx.Done():
		return nil, ctx.Err()
	}
}