Upgrade to Pro — share decks privately, control downloads, hide ads and more …

From Service to Platform: A Ranking System in Go

From Service to Platform: A Ranking System in Go

What started out as an experimental service to rank livestreams evolved to a platform powering all types of content recommendations. Go's stringent code philosophy paved the way to a modular pipeline-based system for scatter-gather workflows enabling anyone to add new ranking algorithms.

GopherCon Europe 2022, Berlin
https://www.youtube.com/watch?v=5jSyctW1rPg

GopherCon UK 2022, London
https://www.youtube.com/watch?v=TNyoKBLxfTM

Konrad Reiche

August 18, 2022
Tweet

More Decks by Konrad Reiche

Other Decks in Programming

Transcript

  1. From Service
    To Platform
    A Ranking System in Go
    Konrad Reiche
    r/streetwear
    r/aww
    r/dogecoin
    r/golang
    r/yoga

    View full-size slide

  2. Once upon a time…

    View full-size slide

  3. Once upon a time…
    … or more like last year

    View full-size slide

  4. Once upon a time…
    … or more like last year
    Redis Cluster

    View full-size slide

  5. Once upon a time…
    … or more like last year
    Redis Cluster

    View full-size slide

  6. Once upon a time…
    … or more like last year
    Redis Cluster

    View full-size slide

  7. Redis Monitor
    1655767305.672853 [0 10.8.144.28:61122] "get" "post:3hg9w"
    1655767305.672862 [0 10.8.144.28:61122] "get" "post:5cm6n9"
    1655767305.672869 [0 10.8.144.28:61122] "get" "post:62s7bk"
    1655767305.672876 [0 10.8.144.28:61122] "get" "post:2zqpf"
    1655767305.672880 [0 10.8.144.28:61122] "get" "post:2fn65o"
    ...

    View full-size slide

  8. Redis Monitor
    1655767305.672853 [0 10.8.144.28:61122] "get" "post:3hg9w"
    1655767305.672862 [0 10.8.144.28:61122] "get" "post:5cm6n9"
    1655767305.672869 [0 10.8.144.28:61122] "get" "post:62s7bk"
    1655767305.672876 [0 10.8.144.28:61122] "get" "post:2zqpf"
    1655767305.672880 [0 10.8.144.28:61122] "get" "post:2fn65o"
    ...
    Timestamp Database, Network Address Command and Key

    View full-size slide

  9. Redis Monitor
    1655767305.672853 [0 10.8.144.28:61122] "get" "post:3hg9w"
    1655767305.672862 [0 10.8.144.28:61122] "get" "post:5cm6n9"
    1655767305.672869 [0 10.8.144.28:61122] "get" "post:62s7bk"
    1655767305.672876 [0 10.8.144.28:61122] "get" "post:2zqpf"
    1655767305.672880 [0 10.8.144.28:61122] "get" "post:2fn65o"
    ...

    View full-size slide

  10. Redis Monitor
    1655767305.672853 [0 10.8.144.28:61122] "get" "deleted_posts"
    1655767305.672862 [0 10.8.144.28:61122] "get" "post:5cm6n9"
    1655767305.672869 [0 10.8.144.28:61122] "get" "deleted_posts"
    1655767305.672876 [0 10.8.144.28:61122] "get" "deleted_posts"
    1655767305.672880 [0 10.8.144.28:61122] "get" "post:2fn65o"
    ...

    View full-size slide

  11. func main() {
    flag.Parse()
    b, err := os.ReadFile(path)
    if err != nil {
    log.Fatal(err)
    }
    countByKey := make(map[string]int)
    lines := strings.Split(string(b), "\n")
    for _, line := range lines {
    split := strings.Split(line, " ")
    if len(split) < 5 {
    continue
    }
    key := split[4]
    countByKey[key] += 1
    }
    type keyCount struct {
    key string
    count int
    }
    counts := make([]keyCount, 0, len(countByKey))
    for key, count := range countByKey {
    counts = append(counts, keyCount{key: key, count: count})
    }
    sort.Slice(counts, func(i, j int) bool { return counts[i].count > counts[j].count })
    for i := 0; i < topKeys; i++ {
    fmt.Println(keyCounts[i].count, keyCounts[i].key)
    }
    }

    View full-size slide

  12. func main() {
    flag.Parse()
    b, err := os.ReadFile(path)
    if err != nil {
    log.Fatal(err)
    }
    countByKey := make(map[string]int)
    lines := strings.Split(string(b), "\n")
    for _, line := range lines {
    split := strings.Split(line, " ")
    if len(split) < 5 {
    continue
    }
    key := split[4]
    countByKey[key] += 1
    }
    type keyCount struct {
    key string
    count int
    }
    counts := make([]keyCount, 0, len(countByKey))
    for key, count := range countByKey {
    counts = append(counts, keyCount{key: key, count: count})
    }
    sort.Slice(counts, func(i, j int) bool { return counts[i].count > counts[j].count })
    for i := 0; i < topKeys; i++ {
    fmt.Println(keyCounts[i].count, keyCounts[i].key)
    }
    }
    Parse flags, read file into memory

    View full-size slide

  13. func main() {
    flag.Parse()
    b, err := os.ReadFile(path)
    if err != nil {
    log.Fatal(err)
    }
    countByKey := make(map[string]int)
    lines := strings.Split(string(b), "\n")
    for _, line := range lines {
    split := strings.Split(line, " ")
    if len(split) < 5 {
    continue
    }
    key := split[4]
    countByKey[key] += 1
    }
    type keyCount struct {
    key string
    count int
    }
    counts := make([]keyCount, 0, len(countByKey))
    for key, count := range countByKey {
    counts = append(counts, keyCount{key: key, count: count})
    }
    sort.Slice(counts, func(i, j int) bool { return counts[i].count > counts[j].count })
    for i := 0; i < topKeys; i++ {
    fmt.Println(keyCounts[i].count, keyCounts[i].key)
    }
    }
    Parse flags, read file into memory
    Parse each line, split by
    column and count keys

    View full-size slide

  14. func main() {
    flag.Parse()
    b, err := os.ReadFile(path)
    if err != nil {
    log.Fatal(err)
    }
    countByKey := make(map[string]int)
    lines := strings.Split(string(b), "\n")
    for _, line := range lines {
    split := strings.Split(line, " ")
    if len(split) < 5 {
    continue
    }
    key := split[4]
    countByKey[key] += 1
    }
    type keyCount struct {
    key string
    count int
    }
    counts := make([]keyCount, 0, len(countByKey))
    for key, count := range countByKey {
    counts = append(counts, keyCount{key: key, count: count})
    }
    sort.Slice(counts, func(i, j int) bool { return counts[i].count > counts[j].count })
    for i := 0; i < topKeys; i++ {
    fmt.Println(keyCounts[i].count, keyCounts[i].key)
    }
    }
    Parse flags, read file into memory
    Parse each line, split by
    column and count keys
    Convert to slice of
    tuples, sort and print

    View full-size slide

  15. Redis: Finding Hot Keys
    $ ./find-hotkeys -file monitor.log -n 5

    View full-size slide

  16. Redis: Finding Hot Keys
    $ ./find-hotkeys -file monitor.log -n 5
    8252 "deleted_posts"
    1907 "post:2tk95"
    1756 "post:2xcv7"
    772 "post:3nasz"
    509 "post:2qjpg"

    View full-size slide

  17. $ cat monitor.log | awk '{print $4}' | sort | uniq -c | sort -nr | head -n 5

    View full-size slide

  18. UNIX Pipes
    Output log Print 4th column Count unique lines Sort numerical Print first 5 lines
    The same can be achieved with existing programs and UNIX pipes.
    $ cat monitor.log | awk '{print $4}' | sort | uniq -c | sort -nr | head -n 5

    View full-size slide

  19. UNIX Pipes
    Output log Print 4th column Count unique lines Sort numerical Print first 5 lines
    The same can be achieved with existing programs and UNIX pipes.
    UNIX Toolbox Philosophy
    Write programs that:
    ● Do one thing well
    ● Compose
    ● Easily communicate
    $ cat monitor.log | awk '{print $4}' | sort | uniq -c | sort -nr | head -n 5

    View full-size slide

  20. 20
    Essential Complexity

    View full-size slide

  21. 21
    Essential Complexity
    Accidental Complexity

    View full-size slide

  22. 22
    Essential Complexity
    Accidental Complexity
    No Silver Bullet—Essence and Accident in Software Engineering
    Brooks, Frederick P. (1986)

    View full-size slide

  23. 23
    Go is a language that helps us to
    reduce accidental complexity

    View full-size slide

  24. Konrad Reiche
    Ranking Platform, Reddit

    View full-size slide

  25. Konrad Reiche
    Ranking Platform, Reddit

    View full-size slide

  26. Konrad Reiche
    Ranking Platform, Reddit

    View full-size slide

  27. What is a ranking (recommendation) system?
    A recommendation system helps users to find content they find compelling.

    View full-size slide

  28. What is a ranking (recommendation) system?
    A recommendation system helps users to find content they find compelling.
    1. Candidate Generation
    Start from a potentially huge corpus and generate a much smaller subset of
    candidates.

    View full-size slide

  29. What is a ranking (recommendation) system?
    A recommendation system helps users to find content they find compelling.
    1. Candidate Generation
    Start from a potentially huge corpus and generate a much smaller subset of
    candidates.
    2. Filtering
    Some candidates should be removed, for example content already watched or
    content the user marked as something they do not want to consume.

    View full-size slide

  30. What is a ranking (recommendation) system?
    A recommendation system helps users to find content they find compelling.
    1. Candidate Generation
    Start from a potentially huge corpus and generate a much smaller subset of
    candidates.
    2. Filtering
    Some candidates should be removed, for example content already watched or
    content the user marked as something they do not want to consume.
    3. Scoring
    Assign scores to sort the candidates.

    View full-size slide

  31. 31 From Service to Platform: A Ranking System in Go
    Example: Ranking Service

    View full-size slide

  32. 32 From Service to Platform: A Ranking System in Go
    Popular
    Posts
    Example: Ranking Service

    View full-size slide

  33. 33 From Service to Platform: A Ranking System in Go
    Popular
    Posts
    Fetch posts
    Example: Ranking Service

    View full-size slide

  34. 34 From Service to Platform: A Ranking System in Go
    Popular
    Posts
    Fetch posts Filter posts
    Example: Ranking Service

    View full-size slide

  35. 35 From Service to Platform: A Ranking System in Go
    Popular
    Posts
    Fetch posts Filter posts
    User Post
    Views
    Example: Ranking Service

    View full-size slide

  36. 36 From Service to Platform: A Ranking System in Go
    Example: Ranking Service
    Popular
    Posts
    Fetch posts Filter posts Score posts
    User Post
    Views

    View full-size slide

  37. 37 From Service to Platform: A Ranking System in Go
    Example: Ranking Service
    Popular
    Posts
    Fetch posts Filter posts Score posts
    User Post
    Views
    Model

    View full-size slide

  38. 38 From Service to Platform: A Ranking System in Go
    Example: Ranking Service
    Popular
    Posts
    Fetch posts Filter posts Score posts
    User Post
    Views
    Model

    View full-size slide

  39. 39 From Service to Platform: A Ranking System in Go
    Example: Ranking Service
    Popular
    Posts
    Fetch posts Filter posts Score posts
    Video
    Posts
    User Post
    Views
    Model

    View full-size slide

  40. 40 From Service to Platform: A Ranking System in Go
    Example: Ranking Service
    Popular
    Posts
    Fetch posts Filter posts Score posts
    Video
    Posts
    User Post
    Views
    Model
    remove
    duplicates

    View full-size slide

  41. Example: Ranking Service
    func (s *Service) GetPopularFeed(ctx context.Context, req *pb.FeedRequest) (*pb.PopularFeed, error) {
    posts, err := s.fetchPopularAndVideoPosts(ctx)
    if err != nil {
    return nil, err
    }
    posts = s.filterPosts(posts)
    posts, scores, err := s.model.ScorePosts(ctx, req.UserID, posts)
    if err != nil {
    return nil, err
    }
    posts = s.sortPosts(posts, scores)
    return pb.NewPopularFeed(posts), nil
    }

    View full-size slide

  42. Example: Ranking Service
    func (s *Service) GetPopularFeed(ctx context.Context, req *pb.FeedRequest) (*pb.PopularFeed, error) {
    posts, err := s.fetchPopularAndVideoPosts(ctx)
    if err != nil {
    return nil, err
    }
    posts = s.filterPosts(posts)
    posts, scores, err := s.model.ScorePosts(ctx, req.UserID, posts)
    if err != nil {
    return nil, err
    }
    posts = s.sortPosts(posts, scores)
    return pb.NewPopularFeed(posts), nil
    }

    View full-size slide

  43. Example: Ranking Service
    func (s *Service) GetPopularFeed(ctx context.Context, req *pb.FeedRequest) (*pb.PopularFeed, error) {
    posts, err := s.fetchPopularAndVideoPosts(ctx)
    if err != nil {
    return nil, err
    }
    posts = s.filterPosts(posts)
    posts, scores, err := s.model.ScorePosts(ctx, req.UserID, posts)
    if err != nil {
    return nil, err
    }
    posts = s.sortPosts(posts, scores)
    return pb.NewPopularFeed(posts), nil
    }

    View full-size slide

  44. Example: Ranking Service
    func (s *Service) GetPopularFeed(ctx context.Context, req *pb.FeedRequest) (*pb.PopularFeed, error) {
    posts, err := s.fetchPopularAndVideoPosts(ctx)
    if err != nil {
    return nil, err
    }
    posts = s.filterPosts(posts)
    posts, scores, err := s.model.ScorePosts(ctx, req.UserID, posts)
    if err != nil {
    return nil, err
    }
    posts = s.sortPosts(posts, scores)
    return pb.NewPopularFeed(posts), nil
    }

    View full-size slide

  45. Example: Ranking Service
    func (s *Service) GetPopularFeed(ctx context.Context, req *pb.FeedRequest) (*pb.PopularFeed, error) {
    posts, err := s.fetchPopularAndVideoPosts(ctx)
    if err != nil {
    return nil, err
    }
    posts = s.filterPosts(posts)
    posts, scores, err := s.model.ScorePosts(ctx, req.UserID, posts)
    if err != nil {
    return nil, err
    }
    posts = s.sortPosts(posts, scores)
    return pb.NewPopularFeed(posts), nil
    }

    View full-size slide

  46. Example: Ranking Service
    func (s *Service) GetPopularFeed(ctx context.Context, req *pb.FeedRequest) (*pb.PopularFeed, error) {
    posts, err := s.fetchPopularAndVideoPosts(ctx)
    if err != nil {
    return nil, err
    }
    posts = s.filterPosts(posts)
    posts, scores, err := s.model.ScorePosts(ctx, req.UserID, posts)
    if err != nil {
    return nil, err
    }
    posts = s.sortPosts(posts, scores)
    return pb.NewPopularFeed(posts), nil
    }

    View full-size slide

  47. 47
    We continuously refactor
    to reduce the accidental
    complexity of code

    View full-size slide

  48. Example: Ranking Service
    func (s *Service) GetPopularFeed(ctx context.Context, req *pb.FeedRequest) (*pb.PopularFeed, error) {
    posts, err := s.fetchPopularAndVideoPosts(ctx)
    if err != nil {
    return nil, err
    }
    posts = s.filterPosts(posts)
    posts, scores, err := s.model.ScorePosts(ctx, req.UserID, posts)
    if err != nil {
    return nil, err
    }
    posts = s.sortPosts(posts, scores)
    return pb.NewPopularFeed(posts), nil
    }

    View full-size slide

  49. Example: Ranking Service
    func (s *Service) GetPopularFeed(ctx context.Context, req *pb.FeedRequest) (*pb.PopularFeed, error) {
    posts, err := s.fetchPopularAndVideoPosts(ctx)
    if err != nil {
    return nil, err
    }
    posts = s.filterPosts(posts)
    posts, scores, err := s.model.ScorePosts(ctx, req.UserID, posts)
    if err != nil {
    return nil, err
    }
    posts = s.sortPosts(posts, scores)
    return pb.NewPopularFeed(posts), nil
    }

    View full-size slide

  50. Example: Ranking Service
    func (s *Service) GetPopularFeed(ctx context.Context, req *pb.FeedRequest) (*pb.PopularFeed, error) {
    posts, err := s.fetchPopularAndVideoPosts(ctx)
    if err != nil {
    return nil, err
    }
    posts = s.filterPosts(posts)
    posts, scores, err := s.model.ScorePosts(ctx, req.UserID, posts)
    if err != nil {
    return nil, err
    }
    posts = s.sortPosts(posts, scores)
    return pb.NewPopularFeed(posts), nil
    }

    View full-size slide

  51. Example: Ranking Service
    func (s *Service) GetPopularFeed(ctx context.Context, req *pb.FeedRequest) (*pb.PopularFeed, error) {
    posts, err := s.fetchPopularAndVideoPosts(ctx)
    if err != nil {
    return nil, err
    }
    imagePosts, err := s.cache.FetchImagePosts(ctx)
    if err != nil {
    return nil, err
    }
    posts = s.filterPosts(posts, imagePosts)
    posts, scores, err := s.model.ScorePosts(ctx, req.UserID, posts)
    if err != nil {
    return nil, err
    }
    posts = s.sortPosts(posts, scores)
    return pb.NewPopularFeed(posts), nil
    }

    View full-size slide

  52. 52
    Can refactoring be limited
    through a structural design?

    View full-size slide

  53. 53 From Service to Platform: A Ranking System in Go
    UNIX Toolbox Philosophy
    Candidate
    Generation
    Filter Score

    View full-size slide

  54. 54 From Service to Platform: A Ranking System in Go
    UNIX Toolbox Philosophy
    Stage 1 Stage 2 …

    View full-size slide

  55. 55 From Service to Platform: A Ranking System in Go
    UNIX Toolbox Philosophy
    Stage 1 Stage 2 …
    type Stage interface {
    Rank(ctx context.Context, req *pb.Request)
    }

    View full-size slide

  56. 56 From Service to Platform: A Ranking System in Go
    UNIX Toolbox Philosophy
    Stage 1 Stage 2 …
    type Stage interface {
    Rank(ctx context.Context, req *pb.Request) (*pb.Request, error)
    }

    View full-size slide

  57. 57 From Service to Platform: A Ranking System in Go
    UNIX Toolbox Philosophy
    Stage 1 Stage 2 …
    type Stage interface {
    Rank(ctx context.Context, req *pb.Request) (*pb.Request, error)
    }
    type Request struct {
    Context *Entity
    Candidates []*Entity
    }

    View full-size slide

  58. 58 From Service to Platform: A Ranking System in Go
    UNIX Toolbox Philosophy
    Stage 1 Stage 2 …
    type Stage interface {
    Rank(ctx context.Context, req *pb.Request) (*pb.Request, error)
    }
    type Request struct {
    Context *Entity
    Candidates []*Entity
    }

    View full-size slide

  59. 59 From Service to Platform: A Ranking System in Go
    type Request struct {
    Context *Entity
    Candidates []*Entity
    }

    View full-size slide

  60. 60 From Service to Platform: A Ranking System in Go
    type Request struct {
    Context *Entity
    Candidates []*Entity
    }

    View full-size slide

  61. 61 From Service to Platform: A Ranking System in Go
    type Request struct {
    Context *Entity
    Candidates []*Entity
    }
    type Entity struct {
    ID string
    Features map[string]*Feature
    Score float64
    }

    View full-size slide

  62. 62 From Service to Platform: A Ranking System in Go
    type Request struct {
    Context *Entity
    Candidates []*Entity
    }
    type Entity struct {
    ID string
    Features map[string]*Feature
    Score float64
    }

    View full-size slide

  63. 63 From Service to Platform: A Ranking System in Go
    type Request struct {
    Context *Entity
    Candidates []*Entity
    }
    type Entity struct {
    ID string
    Features map[string]*Feature
    Score float64
    }

    View full-size slide

  64. 64 From Service to Platform: A Ranking System in Go
    type Request struct {
    Context *Entity
    Candidates []*Entity
    }
    type Entity struct {
    ID string
    Features map[string]*Feature
    Score float64
    }

    View full-size slide

  65. 65 From Service to Platform: A Ranking System in Go
    UNIX Toolbox Philosophy
    Stage 1 Stage 2 …
    type Stage interface {
    Rank(ctx context.Context, req *pb.Request) (*pb.Request, error)
    }
    type Request struct {
    Context *Entity
    Candidates []*Entity
    }

    View full-size slide

  66. 66 From Service to Platform: A Ranking System in Go
    UNIX Toolbox Philosophy
    Stage 1 Stage 2 …
    type Stage interface {
    Rank(ctx context.Context, req *pb.Request) (*pb.Request, error)
    }
    type Request struct {
    Context *Entity
    Candidates []*Entity
    }

    View full-size slide

  67. gRPC Protobuf Definition
    syntax = "proto3";
    service Ranking {
    rpc Rank (Request) returns (Request);
    }

    View full-size slide

  68. gRPC Protobuf Definition
    syntax = "proto3";
    service Ranking {
    rpc Rank (Request) returns (Request);
    }
    message Request {
    Entity context = 1;
    repeated Entity candidates = 2;
    RequestOptions options = 3;
    }

    View full-size slide

  69. gRPC Protobuf Definition
    message Entity {
    string id = 1;
    map features = 2;
    double score = 3;
    }
    message Feature {
    oneof value {
    string as_string = 1;
    int64 as_int = 2;
    double as_float = 3;
    bool as_bool = 4;
    // ...
    };
    }

    View full-size slide

  70. gRPC Protobuf Request Example
    context: {
    id: "t2_bd5ts"
    features: {
    key: "geo_city"
    value: { as_string: "SAN_FRANCISCO" }
    }
    features: {
    key: "geo_country"
    value: { as_string: "US" }
    }
    }
    options: {
    method: "rank_popular_feed"
    limit: 20
    }

    View full-size slide

  71. gRPC Service
    type server struct {
    *grpc.Server
    stage stage.Stage
    }
    func (s *server) Rank(ctx context.Context, req *pb.Request) (*pb.Request, error) {
    return s.stage.Rank(ctx, req)
    }

    View full-size slide

  72. 72
    A General-Purpose
    Ranking Service
    From Service to Platform: A Ranking System in Go
    Quickly and flexibly perform complex
    scatter-gather ranking workflows
    at Reddit

    View full-size slide

  73. 73
    A General-Purpose
    Ranking Service
    From Service to Platform: A Ranking System in Go
    Fetch Popular Posts Fetch Video Posts
    Quickly and flexibly perform complex
    scatter-gather ranking workflows
    at Reddit
    Fetch Image Posts

    View full-size slide

  74. 74
    A General-Purpose
    Ranking Service
    From Service to Platform: A Ranking System in Go
    Fetch Popular Posts Fetch Video Posts
    Series
    Series
    Quickly and flexibly perform complex
    scatter-gather ranking workflows
    at Reddit
    Fetch Image Posts
    Series

    View full-size slide

  75. 75
    A General-Purpose
    Ranking Service
    From Service to Platform: A Ranking System in Go
    Fetch Popular Posts Fetch Video Posts
    Series
    Series
    Parallel
    Quickly and flexibly perform complex
    scatter-gather ranking workflows
    at Reddit
    Fetch Image Posts
    Series

    View full-size slide

  76. 76
    A General-Purpose
    Ranking Service
    From Service to Platform: A Ranking System in Go
    Fetch Popular Posts Fetch Video Posts
    Series
    Series
    Parallel
    Merge Candidates
    Quickly and flexibly perform complex
    scatter-gather ranking workflows
    at Reddit
    Fetch Image Posts
    Series

    View full-size slide

  77. 77
    A General-Purpose
    Ranking Service
    From Service to Platform: A Ranking System in Go
    Fetch Popular Posts Fetch Video Posts
    Series
    Series
    Parallel
    Merge Candidates
    Fetch Recently Viewed Posts
    Quickly and flexibly perform complex
    scatter-gather ranking workflows
    at Reddit
    Series
    Fetch Image Posts
    Series
    Filter Recently Viewed Posts

    View full-size slide

  78. 78
    A General-Purpose
    Ranking Service
    From Service to Platform: A Ranking System in Go
    Fetch Popular Posts Fetch Video Posts
    Series
    Series
    Parallel
    Merge Candidates
    Fetch Recently Viewed Posts
    Quickly and flexibly perform complex
    scatter-gather ranking workflows
    at Reddit
    Series
    Fetch Image Posts
    Series
    Score Candidates
    Filter Recently Viewed Posts
    Sort Candidates

    View full-size slide

  79. 79
    A General-Purpose
    Ranking Service
    From Service to Platform: A Ranking System in Go
    Fetch Popular Posts Fetch Video Posts
    Series
    Series
    Parallel
    Merge Candidates
    Fetch Recently Viewed Posts
    Quickly and flexibly perform complex
    scatter-gather ranking workflows
    at Reddit
    Series
    Fetch Image Posts
    Series
    Score Candidates
    Filter Recently Viewed Posts
    Sort Candidates
    Candidates
    Features
    Filtering
    Meta-Stages

    View full-size slide

  80. 80
    A General-Purpose
    Ranking Service
    From Service to Platform: A Ranking System in Go
    Fetch Popular Posts Fetch Video Posts
    Series
    Series
    Parallel
    Merge Candidates
    Fetch Recently Viewed Posts
    Quickly and flexibly perform complex
    scatter-gather ranking workflows
    at Reddit
    Series
    Fetch Image Posts
    Series
    Score Candidates
    Filter Recently Viewed Posts
    Sort Candidates
    Candidates
    Features
    Filtering
    Meta-Stages

    View full-size slide

  81. Stage: Fetch Popular Posts
    type fetchPopularPosts struct {
    cache *store.PostCache
    }
    func FetchPopularPosts(cache *store.PostCache) *fetchPopularPosts {
    return &fetchPopularPosts{cache: cache}
    }
    func (s *fetchPopularPosts) Rank(ctx context.Context, req *pb.Request) (*pb.Request, error) {
    postIDs, err := s.cache.FetchPopularPostIDs(ctx)
    if err != nil {
    return nil, err
    }
    for _, id := range postIDs {
    req.Candidates = append(req.Candidates, pb.NewCandidate(postID))
    }
    return req, nil
    }

    View full-size slide

  82. Stage: Fetch Popular Posts
    type fetchPopularPosts struct {
    cache *store.PostCache
    }
    func FetchPopularPosts(cache *store.PostCache) *fetchPopularPosts {
    return &fetchPopularPosts{cache: cache}
    }
    func (s *fetchPopularPosts) Rank(ctx context.Context, req *pb.Request) (*pb.Request, error) {
    postIDs, err := s.cache.FetchPopularPostIDs(ctx)
    if err != nil {
    return nil, err
    }
    for _, id := range postIDs {
    req.Candidates = append(req.Candidates, pb.NewCandidate(id))
    }
    return req, nil
    }

    View full-size slide

  83. Stage: Fetch Popular Posts
    type fetchPopularPosts struct {
    cache *store.PostCache
    }
    func FetchPopularPosts(cache *store.PostCache) *fetchPopularPosts {
    return &fetchPopularPosts{cache: cache}
    }
    func (s *fetchPopularPosts) Rank(ctx context.Context, req *pb.Request) (*pb.Request, error) {
    postIDs, err := s.cache.FetchPopularPostIDs(ctx)
    if err != nil {
    return nil, err
    }
    for _, id := range postIDs {
    req.Candidates = append(req.Candidates, pb.NewCandidate(id))
    }
    return req, nil
    }

    View full-size slide

  84. Stage: Fetch Popular Posts
    type fetchPopularPosts struct {
    cache *store.PostCache
    }
    func FetchPopularPosts(cache *store.PostCache) *fetchPopularPosts {
    return &fetchPopularPosts{cache: cache}
    }
    func (s *fetchPopularPosts) Rank(ctx context.Context, req *pb.Request) (*pb.Request, error) {
    postIDs, err := s.cache.FetchPopularPostIDs(ctx)
    if err != nil {
    return nil, err
    }
    for _, id := range postIDs {
    req.Candidates = append(req.Candidates, pb.NewCandidate(id))
    }
    return req, nil
    }

    View full-size slide

  85. Stage: Fetch Popular Posts
    type fetchPopularPosts struct {
    cache *store.PostCache
    }
    func FetchPopularPosts(cache *store.PostCache) *fetchPopularPosts {
    return &fetchPopularPosts{cache: cache}
    }
    func (s *fetchPopularPosts) Rank(ctx context.Context, req *pb.Request) (*pb.Request, error) {
    postIDs, err := s.cache.FetchPopularPostIDs(ctx)
    if err != nil {
    return nil, err
    }
    for _, id := range postIDs {
    req.Candidates = append(req.Candidates, pb.NewCandidate(id))
    }
    return req, nil
    }

    View full-size slide

  86. 86
    A General-Purpose
    Ranking Service
    From Service to Platform: A Ranking System in Go
    Fetch Popular Posts Fetch Video Posts
    Series
    Series
    Parallel
    Merge Candidates
    Fetch Recently Viewed Posts
    Quickly and flexibly perform complex
    scatter-gather ranking workflows
    at Reddit
    Series
    Fetch Image Posts
    Series
    Score Candidates
    Filter Recently Viewed Posts
    Sort Candidates
    Candidates
    Features
    Filtering
    Meta-Stages

    View full-size slide

  87. Stage: Filtering Recently Viewed Posts
    func (s *filterRecentlyViewedPosts) Rank(ctx context.Context, req *pb.Request) (*pb.Request, error) {
    seen := req.Context.Features["recently_viewed_post_ids"].GetAsBoolMap()
    var filtered []*pb.Entity
    for _, candidate := range req.Candidates {
    if !seen[candidate.Id] {
    filtered = append(filtered, candidate)
    }
    }
    req.Candidates = filtered
    return req, nil
    }

    View full-size slide

  88. Stage: Filtering Recently Viewed Posts
    func (s *filterRecentlyViewedPosts) Rank(ctx context.Context, req *pb.Request) (*pb.Request, error) {
    seen := req.Context.Features["recently_viewed_post_ids"].GetAsBoolMap()
    var filtered []*pb.Entity
    for _, candidate := range req.Candidates {
    if !seen[candidate.Id] {
    filtered = append(filtered, candidate)
    }
    }
    req.Candidates = filtered
    return req, nil
    }

    View full-size slide

  89. Stage: Filtering Recently Viewed Posts
    func (s *filterRecentlyViewedPosts) Rank(ctx context.Context, req *pb.Request) (*pb.Request, error) {
    seen := req.Context.Features["recently_viewed_post_ids"].GetAsBoolMap()
    n := 0
    for _, candidate := range req.Candidates {
    if !seen[candidate.Id] {
    req.Candidates[n] = candidate
    n++
    }
    }
    req.Candidates = req.Candidates[:n] // in-place filtering
    return req, nil
    }

    View full-size slide

  90. 90
    A General-Purpose
    Ranking Service
    From Service to Platform: A Ranking System in Go
    Fetch Popular Posts Fetch Video Posts
    Series
    Series
    Parallel
    Merge Candidates
    Fetch Recently Viewed Posts
    Quickly and flexibly perform complex
    scatter-gather ranking workflows
    at Reddit
    Series
    Fetch Image Posts
    Series
    Score Candidates
    Filter Recently Viewed Posts
    Sort Candidates
    Candidates
    Features
    Filtering
    Meta-Stages

    View full-size slide

  91. 91
    A General-Purpose
    Ranking Service
    From Service to Platform: A Ranking System in Go
    Fetch Popular Posts Fetch Video Posts
    Series
    Series
    Parallel
    Merge Candidates
    Fetch Recently Viewed Posts
    Quickly and flexibly perform complex
    scatter-gather ranking workflows
    at Reddit
    Series
    Fetch Image Posts
    Series
    Score Candidates
    Filter Recently Viewed Posts
    Sort Candidates
    Candidates
    Features
    Filtering
    Meta-Stages

    View full-size slide

  92. Meta-Stage: Series
    type series struct {
    stages []Stage
    }
    func Series(stages ...Stage) *series {
    return &series{stages: stages}
    }
    func (s *series) Rank(ctx context.Context, req *pb.Request) (*pb.Request, error) {
    var err error
    resp := req
    for _, stage := range s.stages {
    resp, err = stage.Rank(ctx, req)
    if err != nil {
    return nil, err
    }
    req = resp
    }
    return resp, nil
    }

    View full-size slide

  93. type series struct {
    stages []Stage
    }
    func Series(stages ...Stage) *series {
    return &series{stages: stages}
    }
    func (s *series) Rank(ctx context.Context, req *pb.Request) (*pb.Request, error) {
    var err error
    resp := req
    for _, stage := range s.stages {
    resp, err = stage.Rank(ctx, req)
    if err != nil {
    return nil, err
    }
    req = resp
    }
    return resp, nil
    }
    Meta-Stage: Series

    View full-size slide

  94. type series struct {
    stages []Stage
    }
    func Series(stages ...Stage) *series {
    return &series{stages: stages}
    }
    func (s *series) Rank(ctx context.Context, req *pb.Request) (*pb.Request, error) {
    var err error
    resp := req
    for _, stage := range s.stages {
    resp, err = stage.Rank(ctx, req)
    if err != nil {
    return nil, err
    }
    req = resp
    }
    return resp, nil
    }
    Meta-Stage: Series

    View full-size slide

  95. type series struct {
    stages []Stage
    }
    func Series(stages ...Stage) *series {
    return &series{stages: stages}
    }
    func (s *series) Rank(ctx context.Context, req *pb.Request) (*pb.Request, error) {
    var err error
    resp := req
    for _, stage := range s.stages {
    resp, err = stage.Rank(ctx, req)
    if err != nil {
    return nil, err
    }
    req = resp
    }
    return resp, nil
    }
    Meta-Stage: Series

    View full-size slide

  96. type series struct {
    stages []Stage
    }
    func Series(stages ...Stage) *series {
    return &series{stages: stages}
    }
    func (s *series) Rank(ctx context.Context, req *pb.Request) (*pb.Request, error) {
    var err error
    resp := req
    for _, stage := range s.stages {
    resp, err = stage.Rank(ctx, req)
    if err != nil {
    return nil, err
    }
    req = resp
    }
    return resp, nil
    }
    Meta-Stage: Series

    View full-size slide

  97. Meta-Stage: Parallel
    func (s *parallel) Rank(ctx context.Context, req *pb.Request) (*pb.Request, error) {
    resps := make([]*pb.Request, len(s.stages))
    g, groupCtx := errgroup.WithContext(ctx)
    for i := range s.stages {
    i := i
    g.Go(func() error {
    defer log.CapturePanic(groupCtx)
    resp, err := s.stages[i].Rank(groupCtx, pb.Copy(req))
    if err != nil {
    return err
    }
    resps[i] = resp
    return nil
    })
    }
    if err := g.Wait(); err != nil {
    return nil, err
    }
    return s.merge(ctx, req, resps...)
    }

    View full-size slide

  98. Meta-Stage: Parallel
    func (s *parallel) Rank(ctx context.Context, req *pb.Request) (*pb.Request, error) {
    resps := make([]*pb.Request, len(s.stages))
    g, groupCtx := errgroup.WithContext(ctx)
    for i := range s.stages {
    i := i
    g.Go(func() error {
    defer log.CapturePanic(groupCtx)
    resp, err := s.stages[i].Rank(groupCtx, pb.Copy(req))
    if err != nil {
    return err
    }
    resps[i] = resp
    return nil
    })
    }
    if err := g.Wait(); err != nil {
    return nil, err
    }
    return s.merge(ctx, req, resps...)
    }
    golang.org/x/sync/errgroup

    View full-size slide

  99. Meta-Stage: Parallel
    func (s *parallel) Rank(ctx context.Context, req *pb.Request) (*pb.Request, error) {
    resps := make([]*pb.Request, len(s.stages))
    g, groupCtx := errgroup.WithContext(ctx)
    for i := range s.stages {
    i := i
    g.Go(func() error {
    defer log.CapturePanic(groupCtx)
    resp, err := s.stages[i].Rank(groupCtx, pb.Copy(req))
    if err != nil {
    return err
    }
    resps[i] = resp
    return nil
    })
    }
    if err := g.Wait(); err != nil {
    return nil, err
    }
    return s.merge(ctx, req, resps...)
    }
    Calls the function in a goroutine, first non-nil
    error to be returned cancels the group

    View full-size slide

  100. Meta-Stage: Parallel
    func (s *parallel) Rank(ctx context.Context, req *pb.Request) (*pb.Request, error) {
    resps := make([]*pb.Request, len(s.stages))
    g, groupCtx := errgroup.WithContext(ctx)
    for i := range s.stages {
    i := i
    g.Go(func() error {
    defer log.CapturePanic(groupCtx)
    resp, err := s.stages[i].Rank(groupCtx, pb.Copy(req))
    if err != nil {
    return err
    }
    resps[i] = resp
    return nil
    })
    }
    if err := g.Wait(); err != nil {
    return nil, err
    }
    return s.merge(ctx, req, resps...)
    }

    View full-size slide

  101. Meta-Stage: Parallel
    func (s *parallel) Rank(ctx context.Context, req *pb.Request) (*pb.Request, error) {
    resps := make([]*pb.Request, len(s.stages))
    g, groupCtx := errgroup.WithContext(ctx)
    for i := range s.stages {
    i := i
    g.Go(func() error {
    defer log.CapturePanic(groupCtx)
    resp, err := s.stages[i].Rank(groupCtx, pb.Copy(req))
    if err != nil {
    return err
    }
    resps[i] = resp
    return nil
    })
    }
    if err := g.Wait(); err != nil {
    return nil, err
    }
    return s.merge(ctx, req, resps...)
    }

    View full-size slide

  102. Meta-Stage: Parallel
    func (s *parallel) Rank(ctx context.Context, req *pb.Request) (*pb.Request, error) {
    resps := make([]*pb.Request, len(s.stages))
    g, groupCtx := errgroup.WithContext(ctx)
    for i := range s.stages {
    i := i
    g.Go(func() error {
    defer log.CapturePanic(groupCtx)
    resp, err := s.stages[i].Rank(groupCtx, pb.Copy(req))
    if err != nil {
    return err
    }
    resps[i] = resp
    return nil
    })
    }
    if err := g.Wait(); err != nil {
    return nil, err
    }
    return s.merge(ctx, req, resps...)
    }

    View full-size slide

  103. Meta-Stage: If-Else
    type Selector func(context.Context, *pb.Request) bool
    func (s *ifElse) Rank(ctx context.Context, req *pb.Request) (*pb.Request, error) {
    if s.selector(ctx, req) {
    return s.ifStage.Rank(ctx, req)
    }
    return s.elseStage.Rank(ctx, req)
    }

    View full-size slide

  104. 104
    A General-Purpose
    Ranking Service
    From Service to Platform: A Ranking System in Go
    Fetch Popular Posts Fetch Video Posts
    Series
    Series
    Parallel
    Merge Candidates
    Fetch Recently Viewed Posts
    Quickly and flexibly perform complex
    scatter-gather ranking workflows
    at Reddit
    Series
    Fetch Image Posts
    Series
    Score Candidates
    Filter Recently Viewed Posts
    Sort Candidates
    Candidates
    Features
    Filtering
    Meta-Stages

    View full-size slide

  105. 105
    A General-Purpose
    Ranking Service
    From Service to Platform: A Ranking System in Go
    Quickly and flexibly perform complex
    scatter-gather ranking workflows
    at Reddit
    func PopularFeed(d *service.Dependencies) stage.Stage {
    return stage.Series(
    stage.Parallel(merger.MergeCandidates,
    stage.FetchPopularPosts(d.PostCache),
    stage.FetchVideoPosts(d.PostCache),
    stage.FetchImagePosts(d.PostCache),
    ),
    stage.FetchRecentlyViewedPosts(d.UserPostViews),
    stage.FilterRecentlyViewedPosts(),
    stage.ScoreCandidates(d.RankingModel),
    stage.SortCandidates(),
    )
    }

    View full-size slide

  106. From Service to Platform
    When does a service become a platform?

    View full-size slide

  107. From Service to Platform
    When does a service become a platform?
    Service
    Customers are the users / product management

    View full-size slide

  108. From Service to Platform
    When does a service become a platform?
    Service
    Customers are the users / product management
    Platform
    Customers are engineers developing on the service

    View full-size slide

  109. 109 From Service to Platform: A Ranking System in Go
    What is your API?
    Path /api/user/{id}.json
    Description Get a user object
    Method GET

    View full-size slide

  110. package user
    type User struct {
    ID string
    Name string
    permissions []string
    }
    110 From Service to Platform: A Ranking System in Go
    What is your API?

    View full-size slide

  111. package user
    type User struct {
    ID string
    Name string
    permissions []string
    }
    111 From Service to Platform: A Ranking System in Go
    What is your API?

    View full-size slide

  112. package user
    type User struct {
    ID string
    Name string
    permissions []string
    }
    112 From Service to Platform: A Ranking System in Go
    What is your API?

    View full-size slide

  113. package user
    type User struct {
    ID string
    Name string
    permissions []string
    }
    113 From Service to Platform: A Ranking System in Go
    What is your API?

    View full-size slide

  114. package user
    type User struct {
    ID string
    Name string
    permissions []string
    }
    114 From Service to Platform: A Ranking System in Go
    What is your API?

    View full-size slide

  115. package user
    type User struct {
    ID string
    Name string
    permissions []string
    }
    115 From Service to Platform: A Ranking System in Go
    What is your API?

    View full-size slide

  116. package user
    type User struct {
    ID string
    Name string
    permissions []string
    }
    116 From Service to Platform: A Ranking System in Go
    What is your API?
    ● Start with unexported identifiers
    ● Make decisions for exporting identifiers based
    on how you want them to be consumed

    View full-size slide

  117. 117
    A General-Purpose
    Ranking Service
    From Service to Platform: A Ranking System in Go
    Quickly and flexibly perform complex
    scatter-gather ranking workflows
    at Reddit
    func PopularFeed(d *service.Dependencies) stage.Stage {
    return stage.Series(
    stage.Parallel(merger.MergeCandidates,
    stage.FetchPopularPosts(d.PostCache),
    stage.FetchVideoPosts(d.PostCache),
    stage.FetchImagePosts(d.PostCache),
    ),
    stage.FetchRecentlyViewedPosts(d.UserPostViews),
    stage.FilterRecentlyViewedPosts(),
    stage.ScoreCandidates(d.RankingModel),
    stage.SortCandidates(),
    )
    }

    View full-size slide

  118. 118
    Transition to Platform
    From Service to Platform: A Ranking System in Go
    Ranking Service

    View full-size slide

  119. 119
    Transition to Platform
    From Service to Platform: A Ranking System in Go
    Ranking Service
    Livestream Feed

    View full-size slide

  120. 120
    Transition to Platform
    From Service to Platform: A Ranking System in Go
    Ranking Service
    Livestream Feed …

    View full-size slide

  121. 121
    Transition to Platform
    From Service to Platform: A Ranking System in Go
    Ranking Service
    Livestream Feed Popular Feed

    View full-size slide

  122. 122
    Transition to Platform
    From Service to Platform: A Ranking System in Go
    Ranking Service
    Livestream Feed Home Feed
    Popular Feed

    View full-size slide

  123. 123
    Transition to Platform
    From Service to Platform: A Ranking System in Go
    Ranking Service
    Livestream Feed Home Feed
    Popular Feed
    … …

    View full-size slide

  124. 124
    Transition to Platform
    From Service to Platform: A Ranking System in Go
    Ranking Service
    Livestream Feed Home Feed
    Popular Feed

    Ranking Platform

    View full-size slide

  125. package stage
    125
    Maintaining the Platform
    From Service to Platform: A Ranking System in Go
    Series Parallel
    Shuffle Candidates
    Fetch Posts
    Filter By Feature

    If-Else
    Fetch Subscriptions

    View full-size slide

  126. package stage
    126
    Maintaining the Platform
    From Service to Platform: A Ranking System in Go
    Series Parallel
    Shuffle Candidates
    Fetch Posts
    Filter By Feature

    If-Else
    Fetch Subscriptions
    Filter Private Posts
    Filter by Timestamp
    Shuffle Topics

    View full-size slide

  127. Creating A Toolbox
    Four Principles for a Platform API
    127 From Service to Platform: A Ranking System in Go
    Limited Scope Clear Naming Decoupling Strive for Reuse

    View full-size slide

  128. Applying the Four Principles
    const shuffleProbability = 0.2
    type imagePosts struct {
    cache *store.PostCache
    }
    func (s *imagePosts) Rank(ctx context.Context, req *pb.Request) (*pb.Request, error) {
    subredditIDs := req.Context.GetStringArrayFeature("subreddit_ids")
    postIDs, err := s.cache.FetchImagePosts(ctx, subredditIDs)
    if err != nil {
    return nil, err
    }
    s.shufflePostIDs(postIDs, shuffleProbability)
    for _, postID := range postIDs {
    req.Candidates = append(req.Candidates, pb.Candidate(postID))
    }
    return req, nil
    }

    View full-size slide

  129. Applying the Four Principles
    const shuffleProbability = 0.2
    type imagePosts struct {
    cache *store.PostCache
    }
    func (s *imagePosts) Rank(ctx context.Context, req *pb.Request) (*pb.Request, error) {
    subredditIDs := req.Context.GetStringArrayFeature("subreddit_ids")
    postIDs, err := s.cache.FetchImagePosts(ctx, subredditIDs)
    if err != nil {
    return nil, err
    }
    s.shufflePostIDs(postIDs, shuffleProbability)
    for _, postID := range postIDs {
    req.Candidates = append(req.Candidates, pb.Candidate(postID))
    }
    return req, nil
    }

    View full-size slide

  130. Applying the Four Principles
    const shuffleProbability = 0.2
    type imagePosts struct {
    cache *store.PostCache
    }
    func (s *imagePosts) Rank(ctx context.Context, req *pb.Request) (*pb.Request, error) {
    subredditIDs := req.Context.GetStringArrayFeature("subreddit_ids")
    postIDs, err := s.cache.FetchImagePosts(ctx, subredditIDs)
    if err != nil {
    return nil, err
    }
    s.shufflePostIDs(postIDs, shuffleProbability)
    for _, postID := range postIDs {
    req.Candidates = append(req.Candidates, pb.Candidate(postID))
    }
    return req, nil
    }

    View full-size slide

  131. Applying the Four Principles
    const shuffleProbability = 0.2
    type imagePosts struct {
    cache *store.PostCache
    }
    func (s *imagePosts) Rank(ctx context.Context, req *pb.Request) (*pb.Request, error) {
    subredditIDs := req.Context.GetStringArrayFeature("subreddit_ids")
    postIDs, err := s.cache.FetchImagePosts(ctx, subredditIDs)
    if err != nil {
    return nil, err
    }
    s.shufflePostIDs(postIDs, shuffleProbability)
    for _, postID := range postIDs {
    req.Candidates = append(req.Candidates, pb.Candidate(postID))
    }
    return req, nil
    }
    Clear Naming

    View full-size slide

  132. Applying the Four Principles
    const shuffleProbability = 0.2
    type fetchImagePosts struct {
    cache *store.PostCache
    }
    func (s *fetchImagePosts) Rank(ctx context.Context, req *pb.Request) (*pb.Request, error) {
    subredditIDs := req.Context.GetStringArrayFeature("subreddit_ids")
    postIDs, err := s.cache.FetchImagePosts(ctx, subredditIDs)
    if err != nil {
    return nil, err
    }
    s.shufflePostIDs(postIDs, shuffleProbability)
    for _, postID := range postIDs {
    req.Candidates = append(req.Candidates, pb.Candidate(postID))
    }
    return req, nil
    }
    Clear Naming

    View full-size slide

  133. Applying the Four Principles
    const shuffleProbability = 0.2
    type fetchImagePosts struct {
    cache *store.PostCache
    }
    func (s *fetchImagePosts) Rank(ctx context.Context, req *pb.Request) (*pb.Request, error) {
    subredditIDs := req.Context.GetStringArrayFeature("subreddit_ids")
    postIDs, err := s.cache.FetchImagePosts(ctx, subredditIDs)
    if err != nil {
    return nil, err
    }
    s.shufflePostIDs(postIDs, shuffleProbability)
    for _, postID := range postIDs {
    req.Candidates = append(req.Candidates, pb.Candidate(postID))
    }
    return req, nil
    }
    Limited Scope

    View full-size slide

  134. Applying the Four Principles
    type fetchImagePosts struct {
    cache *store.PostCache
    }
    func (s *fetchImagePosts) Rank(ctx context.Context, req *pb.Request) (*pb.Request, error) {
    subredditIDs := req.Context.GetStringArrayFeature("subreddit_ids")
    postIDs, err := s.cache.FetchImagePosts(ctx, subredditIDs)
    if err != nil {
    return nil, err
    }
    for _, postID := range postIDs {
    req.Candidates = append(req.Candidates, pb.Candidate(postID))
    }
    return req, nil
    }
    Limited Scope

    View full-size slide

  135. Applying the Four Principles
    type fetchImagePosts struct {
    cache *store.PostCache
    }
    func (s *fetchImagePosts) Rank(ctx context.Context, req *pb.Request) (*pb.Request, error) {
    subredditIDs := req.Context.GetStringArrayFeature("subreddit_ids")
    postIDs, err := s.cache.FetchImagePosts(ctx, subredditIDs)
    if err != nil {
    return nil, err
    }
    for _, postID := range postIDs {
    req.Candidates = append(req.Candidates, pb.Candidate(postID))
    }
    return req, nil
    }
    Strive for Reuse

    View full-size slide

  136. Applying the Four Principles
    type fetchImagePosts struct {
    cache *store.PostCache
    }
    func (s *fetchImagePosts) Rank(ctx context.Context, req *pb.Request) (*pb.Request, error) {
    subredditIDs := req.Context.GetStringArrayFeature("subreddit_ids")
    postIDs, err := s.cache.FetchImagePosts(ctx, subredditIDs)
    if err != nil {
    return nil, err
    }
    for _, postID := range postIDs {
    req.Candidates = append(req.Candidates, pb.Candidate(postID))
    }
    return req, nil
    }
    Strive for Reuse

    View full-size slide

  137. Applying the Four Principles
    type fetchImagePosts struct {
    cache *store.PostCache
    subredditFeature string
    }
    func (s *fetchImagePosts) Rank(ctx context.Context, req *pb.Request) (*pb.Request, error) {
    subredditIDs := req.Context.GetStringArrayFeature(s.subredditFeature)
    postIDs, err := s.cache.FetchImagePosts(ctx, subredditIDs)
    if err != nil {
    return nil, err
    }
    for _, postID := range postIDs {
    req.Candidates = append(req.Candidates, pb.Candidate(postID))
    }
    return req, nil
    }
    Decoupling

    View full-size slide

  138. Applying the Four Principles
    type fetchImagePosts struct {
    cache *store.PostCache
    subredditFeature string
    }
    func (s *fetchImagePosts) Rank(ctx context.Context, req *pb.Request) (*pb.Request, error) {
    subredditIDs := req.Context.GetStringArrayFeature(s.subredditFeature)
    postIDs, err := s.cache.FetchImagePosts(ctx, subredditIDs...)
    if err != nil {
    return nil, err
    }
    for _, postID := range postIDs {
    req.Candidates = append(req.Candidates, pb.Candidate(postID))
    }
    return req, nil
    }
    Decoupling

    View full-size slide

  139. 139
    Reuse
    Clarity

    View full-size slide

  140. 140
    Single Method
    Interface
    From Service to Platform: A Ranking System in Go

    View full-size slide

  141. “The bigger the interface, the weaker the abstraction” ― Rob Pike

    View full-size slide

  142. Functions Implementing Interfaces
    “The bigger the interface, the weaker the abstraction” ― Rob Pike
    type RankFunc func(context.Context, *pb.Request) (*pb.Request, error)

    View full-size slide

  143. Functions Implementing Interfaces
    “The bigger the interface, the weaker the abstraction” ― Rob Pike
    type RankFunc func(context.Context, *pb.Request) (*pb.Request, error)
    func (f RankFunc) Rank(ctx context.Context, req *pb.Request) (*pb.Request, error) {
    return f(ctx, req)
    }

    View full-size slide

  144. Functions Implementing Interfaces
    “The bigger the interface, the weaker the abstraction” ― Rob Pike
    type RankFunc func(context.Context, *pb.Request) (*pb.Request, error)
    func (f RankFunc) Rank(ctx context.Context, req *pb.Request) (*pb.Request, error) {
    return f(ctx, req)
    }

    View full-size slide

  145. func Pipeline(d *service.Dependencies) stage.Stage {
    return stage.Series(
    stage.FetchSubscriptions(d.SubscriptionService),
    stage.FetchPosts(d.Cache),
    stage.FilterPostsForUK(),
    stage.ShufflePosts(0.2),
    )
    }
    Anonymous Interface Implementations

    View full-size slide

  146. func Pipeline(d *service.Dependencies) stage.Stage {
    return stage.Series(
    stage.FetchSubscriptions(d.SubscriptionService),
    stage.FetchPosts(d.Cache),
    stage.FilterPostsForUK(),
    stage.ShufflePosts(0.2),
    )
    }
    Anonymous Interface Implementations

    View full-size slide

  147. func Pipeline(d *service.Dependencies) stage.Stage {
    return stage.Series(
    stage.FetchSubscriptions(d.SubscriptionService),
    stage.FetchPosts(d.Cache),
    stage.FilterPostsForUK(),
    stage.ShufflePosts(0.2),
    )
    }
    Anonymous Interface Implementations

    View full-size slide

  148. func Pipeline(d *service.Dependencies) stage.Stage {
    return stage.Series(
    stage.FetchSubscriptions(d.SubscriptionService),
    stage.FetchPosts(d.Cache),
    stage.RankFunc(func(context.Context, *pb.Request) (*pb.Request, error) {
    if req.Context.Features["geo_country"] == "uk" {
    // ...
    }
    return req, nil
    }),
    stage.ShufflePosts(0.2),
    )
    }
    Anonymous Interface Implementations

    View full-size slide

  149. Middlewares
    type Middleware func(stage Stage) Stage

    View full-size slide

  150. Middlewares
    type Middleware func(stage Stage) Stage
    func ExampleMiddleware(next stage.Stage) stage.Stage {
    return stage.RankFunc(func(ctx context.Context, req *pb.Request) (*pb.Request, error) {
    // ...
    return next.Rank(ctx, req)
    })
    }

    View full-size slide

  151. func Monitor(next stage.Stage) stage.Stage {
    return stage.RankFunc(func(ctx context.Context, req *pb.Request) (*pb.Request, error) {
    startedAt := time.Now()
    method := req.Options.Method
    defer func() {
    stageLatencySeconds.With(prometheus.Labels{
    methodLabel: method,
    stageLabel: stage.Name(next),
    }).Observe(time.Since(startedAt).Seconds())
    }()
    return next.Rank(ctx, req)
    })
    }
    Middleware: Monitor

    View full-size slide

  152. func Monitor(next stage.Stage) stage.Stage {
    return stage.RankFunc(func(ctx context.Context, req *pb.Request) (*pb.Request, error) {
    startedAt := time.Now()
    method := req.Options.Method
    defer func() {
    stageLatencySeconds.With(prometheus.Labels{
    methodLabel: method,
    stageLabel: stage.Name(next),
    }).Observe(time.Since(startedAt).Seconds())
    }()
    return next.Rank(ctx, req)
    })
    }
    Middleware: Monitor
    Record elapsed time in
    deferred statement
    Delegate to underlying stage

    View full-size slide

  153. Middleware: Monitor makes pprof obsolete

    View full-size slide

  154. Middleware: Monitor makes pprof obsolete

    View full-size slide

  155. Middleware: Log
    func Log(next stage.Stage) stage.Stage {
    return stage.RankFunc(func(ctx context.Context, req *pb.Request) (resp *pb.Request, err error) {
    defer func() {
    if err != nil {
    log.Errorw(
    "stage failed",
    "error", err,
    "request", req.JSON(),
    "response", resp.JSON(),
    "stage", stage.Name(stage),
    )
    }
    }()
    return stage.Rank(ctx, req)
    })
    }

    View full-size slide

  156. Middleware: Log
    func Log(next stage.Stage) stage.Stage {
    return stage.RankFunc(func(ctx context.Context, req *pb.Request) (resp *pb.Request, err error) {
    defer func() {
    if err != nil {
    log.Errorw(
    "stage failed",
    "error", err,
    "request", req.JSON(),
    "response", resp.JSON(),
    "stage", stage.Name(stage),
    )
    }
    }()
    return stage.Rank(ctx, req)
    })
    }
    func (r *Request) JSON() string {
    b, err := protojson.Marshal(r)
    if err != nil {
    log.Error(err)
    return ""
    }
    return string(b)
    }

    View full-size slide

  157. Middleware: Feature Flags for Incident Mitigation
    func FeatureFlag(next stage.Stage) stage.Stage {
    return stage.RankFunc(func(ctx context.Context, req *pb.Request) (*pb.Request, error) {
    key := "feature_flag.stage." + stage.Name(current)
    if !liveconfig.GetBool(key) {
    return req, nil
    }
    return next.Rank(ctx, req)
    })
    }

    View full-size slide

  158. Middleware: Feature Flags for Incident Mitigation
    func FeatureFlag(next stage.Stage) stage.Stage {
    return stage.RankFunc(func(ctx context.Context, req *pb.Request) (*pb.Request, error) {
    key := "feature_flag.stage." + stage.Name(current)
    if !liveconfig.GetBool(key) {
    return req, nil
    }
    return next.Rank(ctx, req)
    })
    }
    Skip stage entirely

    View full-size slide

  159. Accidental Complexity

    View full-size slide

  160. It’s Go time…
    Accidental Complexity

    View full-size slide

  161. A Framework for Refactoring
    ● Being forced to write your business logic into small components helped to limit
    accidental complexity but it did not eliminate the need for refactoring
    ● Our ability to refactor increased due stage interface providing a framework
    ● This framework requires a set of principles for designing those components
    ● Any code contributed to middlewares or meta-stages pays off due to their
    multiplier effect

    View full-size slide

  162. ● Platform-centric thinking starts with the first developers outside of our team
    contributing code—this is not something you can simulate.
    ● Providing an opinionated framework will create friction.
    ● Ways to resolve confusion or disagreement:
    1. Enforce the existing design
    2. Quick-and-dirty workaround
    3. Rethink the existing design
    Platform Building is a Discourse

    View full-size slide

  163. 164
    What’s next?

    View full-size slide

  164. 165
    What’s next?
    Error Semantics

    View full-size slide

  165. Meta-Stage: Parallel
    func (s *parallel) Rank(ctx context.Context, req *pb.Request) (*pb.Request, error) {
    resps := make([]*pb.Request, len(s.stages))
    g, groupCtx := errgroup.WithContext(ctx)
    for i := range s.stages {
    i := i
    g.Go(func() error {
    defer log.CapturePanic(groupCtx)
    resp, err := s.stages[i].Rank(groupCtx, pb.Copy(req))
    if err != nil {
    return err
    }
    resps[i] = resp
    return nil
    })
    }
    if err := g.Wait(); err != nil {
    return nil, err
    }
    return s.merge(ctx, req, resps...)
    }

    View full-size slide

  166. 167
    What’s next?
    Error Semantics

    View full-size slide

  167. 168
    What’s next?
    Error Semantics
    stage.Parallel(merger.MergeCandidates,
    stage.FetchPopularPosts(d.PostCache),
    stage.FetchVideoPosts(d.PostCache),
    stage.FetchImagePosts(d.PostCache),
    )

    View full-size slide

  168. 169
    What’s next?
    Error Semantics
    stage.Parallel(merger.MergeCandidates, strategy.ErrorWhenAllFail,
    stage.FetchPopularPosts(d.PostCache),
    stage.FetchVideoPosts(d.PostCache),
    stage.FetchImagePosts(d.PostCache),
    )

    View full-size slide

  169. 170
    What’s next?
    Latency Budgets
    stage.Series(budget.Latency(100 * time.Millisecond),
    stage.FetchRecentlyViewedPosts(d.UserPostViews),
    stage.FilterRecentlyViewedPosts(),
    )

    View full-size slide

  170. 171
    What’s next?
    No-Code Abstraction

    View full-size slide

  171. 172
    What’s next?
    No-Code Abstraction
    ---
    name: PopularFeed
    stages:
    - name: Series
    stages:
    - name: Parallel
    stages:
    - name: FetchPopularPosts
    - name: FetchVideoPosts
    - name: FetchImagePosts
    - name: FetchRecentlyViewedPosts
    - name: FilterRecentlyViewedPosts
    - name: ScoreCandidares
    - name: SortCandidates

    View full-size slide

  172. 173
    What’s next?
    Pipelines at Runtime
    ---
    name: PopularFeed
    stages:
    - name: Series
    stages:
    - name: Parallel
    stages:
    - name: FetchPopularPosts
    - name: FetchVideoPosts
    - name: FetchImagePosts
    - name: FetchRecentlyViewedPosts
    - name: FilterRecentlyViewedPosts
    - name: ScoreCandidares
    - name: SortCandidates

    View full-size slide

  173. Summary
    ● Essential complexity describes a problem at its core, accidental complexity happens
    as part of solving the problem, creating unnecessary challenges; accidental
    complexity can be reduced
    ● A recommendation system consists of a variety of different ranking flows with the
    goal to generate content that users find compelling
    ● UNIX pipes as an inspiration to build a system of small, reusable components with the
    help of the single-method interface in Go
    ● Reusability and clarity are competing concepts. This is Go: choose clarity.

    View full-size slide

  174. Thank you
    Konrad Reiche
    @konradreiche
    u/konradreiche

    View full-size slide

  175. Questions?
    Konrad Reiche
    @konradreiche
    u/konradreiche

    View full-size slide