go 48 lines · 8 steps

Cache-aside reads with singleflight in Go

A read-through cache that collapses concurrent misses into a single database fetch to prevent thundering herds.

Explained by highlit
1package cache
2 
3import (
4 "context"
5 "encoding/json"
6 "fmt"
7 "time"
8 
9 "golang.org/x/sync/singleflight"
10)
11 
12type UserService struct {
13 redis RedisClient
14 db *Database
15 group singleflight.Group
16 ttl time.Duration
17}
18 
19func NewUserService(redis RedisClient, db *Database) *UserService {
20 return &UserService{redis: redis, db: db, ttl: 5 * time.Minute}
21}
22 
23func (s *UserService) GetUser(ctx context.Context, id int64) (*User, error) {
24 key := fmt.Sprintf("user:%d", id)
25 
26 if raw, err := s.redis.Get(ctx, key).Bytes(); err == nil {
27 var u User
28 if json.Unmarshal(raw, &u) == nil {
29 return &u, nil
30 }
31 }
32 
33 v, err, _ := s.group.Do(key, func() (interface{}, error) {
34 user, err := s.db.FetchUser(ctx, id)
35 if err != nil {
36 return nil, err
37 }
38 if encoded, err := json.Marshal(user); err == nil {
39 s.redis.Set(ctx, key, encoded, s.ttl)
40 }
41 return user, nil
42 })
43 if err != nil {
44 return nil, err
45 }
46 
47 return v.(*User), nil
48}
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1The cache-aside pattern checks the fast store first and only touches the database on a miss.
  2. 2singleflight collapses duplicate concurrent requests for the same key into one execution, sharing the result.
  3. 3Populating the cache inside the deduplicated fetch ensures one write per stampede rather than many.

Related explainers

Share this explainer

Here's the card — post it anywhere.

Cache-aside reads with singleflight in Go — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code