The service implementation

The next layer is implementing the service interfaces as simple Go packages. At this point, each service has its own package:

  • github.com/the-gigi/delinkcious/pkg/link_manager
  • github.com/the-gigi/delinkcious/pkg/user_manager
  • github.com/the-gigi/delinkcious/pkg/social_graph_manager

Note that these are Go package names and not URLs.

Let's examine the social_graph_manager package. It imports the object_model package as om because it needs to implement the om.SocialGraphManager interface. It defines a struct called SocialGraphManager that has a field called store of the om.SocialGraphManager type. So, the interface of the store field is identical to the interface of the manager in this case:

package social_graph_manager

import (
"errors"
om "github.com/the-gigi/delinkcious/pkg/object_model"
)

type SocialGraphManager struct {
store om.SocialGraphManager
}

This may be a little confusing. The idea is that the store field implements the same interface so that the top-level manager can implement some validation logic and delegate the heavy lifting to the store. You will see this in action soon.

In addition, the fact that the store field is an interface allows us to use different stores that implement the same interface. This is very useful. The NewSocialGraphManager() function accepts a store field that must not be nil, and then returns a new instance of SocialGraphManager with the provided store:

func NewSocialGraphManager(store om.SocialGraphManager) (om.SocialGraphManager, error) {
if store == nil {
return nil, errors.New("store can't be nil")
}
return &SocialGraphManager{store: store}, nil
}

The SocialGraphManager struct itself is pretty simple. It performs some validity checks and then delegates the work to its store:

func (m *SocialGraphManager) Follow(followed string, follower string) (err error) {
if followed == "" || follower == "" {
err = errors.New("followed and follower can't be empty")
return
}

return m.store.Follow(followed, follower)
}

func (m *SocialGraphManager) Unfollow(followed string, follower string) (err error) {
if followed == "" || follower == "" {
err = errors.New("followed and follower can't be empty")
return
}

return m.store.Unfollow(followed, follower)
}

func (m *SocialGraphManager) GetFollowing(username string) (map[string]bool, error) {
return m.store.GetFollowing(username)
}

func (m *SocialGraphManager) GetFollowers(username string) (map[string]bool, error) {
return m.store.GetFollowers(username)
}

The social graph manager is a pretty simple library. Let's continue peeling the onion and look at the service itself, which lives under the svc subdirectory: https://github.com/the-gigi/delinkcious/tree/master/svc/social_graph_service.

Let's start with the social_graph_service.go file. We'll go over the main parts that are similar for most services. The file lives in the service package, which is a convention I use. It imports several important packages:

package service

import (
httptransport "github.com/go-kit/kit/transport/http"
"github.com/gorilla/mux"
sgm "github.com/the-gigi/delinkcious/pkg/social_graph_manager"
"log"
"net/http"
)

The Go kit http transport package is necessary for services that use the HTTP transport. The gorilla/mux package provides top-notch routing capabilities. social_graph_manager is the implementation of the service that does all the heavy lifting. The log package is for logging, and the net/http package is for serving HTTP since it's an HTTP service.

There is just one function called Run(). It starts by creating a data store for the social graph manager and then creates the social graph manager itself, passing it the store field. So, the functionality of social_graph_manager is implemented in the package, but the service is responsible for making the policy decisions and passing a configured data store. If anything goes wrong at this point, the service just exits with a log.Fatal() call because there is no way to recover at this early stage:

func Run() {
store, err := sgm.NewDbSocialGraphStore("localhost", 5432, "postgres", "postgres")
if err != nil {
log.Fatal(err)
}
svc, err := sgm.NewSocialGraphManager(store)
if err != nil {
log.Fatal(err)
}

The next part is constructing the handler for each endpoint. This is done by calling the NewServer() function of the HTTP transport for each endpoint. The parameters are the Endpoint factory function, which we will review soon, a request decoder function, and the response encoder function. For HTTP services, it is common for requests and responses to be encoded as JSON:

followHandler := httptransport.NewServer(
makeFollowEndpoint(svc),
decodeFollowRequest,
encodeResponse,
)

unfollowHandler := httptransport.NewServer(
makeUnfollowEndpoint(svc),
decodeUnfollowRequest,
encodeResponse,
)

getFollowingHandler := httptransport.NewServer(
makeGetFollowingEndpoint(svc),
decodeGetFollowingRequest,
encodeResponse,
)

getFollowersHandler := httptransport.NewServer(
makeGetFollowersEndpoint(svc),
decodeGetFollowersRequest,
encodeResponse,
)

At this point, we have SocialGraphManager properly initialized and the handlers for all the endpoints. It's time to expose them to the world via the gorilla router. Each endpoint is associated with a route and a method. In this case, the follow and unfollow operations use the POST method and the following and followers operations use the GET method:

r := mux.NewRouter()
r.Methods("POST").Path("/follow").Handler(followHandler)
r.Methods("POST").Path("/unfollow").Handler(unfollowHandler)
r.Methods("GET").Path("/following/{username}").Handler(getFollowingHandler)
r.Methods("GET").Path("/followers/{username}").Handler(getFollowersHandler)

The last part is just passing the configured router to the ListenAndServe() method of the standard HTTP package. This service is hardcoded to listen on port 9090. Later on in this book, we will see how to configure things in a flexible and more industrial-strength way:

log.Println("Listening on port 9090...")
log.Fatal(http.ListenAndServe(":9090", r))