This is  the second part of the tutorial series, if you  missed PART I, you might be able to catch up from here.

For the entirety of this part, we will be working on  building the storage layer of our short link generator/forwarder, so the main steps we will be having are :

  1. Setup the Store Service
  2. Storage API design and Implementation
  3. Unit & Integration Testing

Recall, we installed Redis and loaded the needed Go depencies  in the previous chapter, so for this one we will be putting them to use.

Note : In a real world  workload, Redis storage alone can  easily be overwhelmed, so a traditional RDBMS like Postgres or Mysql is  used as a background storage for the least requested data ( cold values )
And Redis is used only to hold hot values that are often used/requested and have to maintain a super fast retrieval time.

Turn up your editor, buckle up  and there we go ! 🚀

II. 1. Store Service Setup 🛠

First thing first, we are gonna have to create the our store folder in the project, So go inside the project directory, create a sub-directory named store and go ahead and create 2 empty Go files :  store_service.go and store_service_test.go (Where we will be writing unit tests for the storage later )

└── store
    ├── store_service.go
    └── store_service_test.go
  • We will start by setting up our wrappers around Redis, the wrappers will be used as interface for persisting and retrieving our application data mapping.
package store

import (
	"context"
	"fmt"
	"github.com/go-redis/redis"
	"time"
)

// Define the struct wrapper around raw Redis client
type StorageService struct {
	redisClient *redis.Client
}

// Top level declarations for the storeService and Redis context
var (
	storeService = &StorageService{}
    ctx = context.Background()
)

// Note that in a real world usage, the cache duration shouldn't have  
// an expiration time, an LRU policy config should be set where the 
// values that are retrieved less often are purged automatically from 
// the cache and stored back in RDBMS whenever the cache is full

const CacheDuration = 6 * time.Hour
Open the store_service.go file and fill in the code above.
  • After defining wrapper structs we can finally be able to initialize the store service, in this case our Redis client.
// Initializing the store service and return a store pointer 
func InitializeStore() *StorageService {
	redisClient := redis.NewClient(&redis.Options{
		Addr:     "localhost:6379",
		Password: "",
		DB:       0,
	})

	pong, err := redisClient.Ping(ctx).Result()
	if err != nil {
		panic(fmt.Sprintf("Error init Redis: %v", err))
	}

	fmt.Printf("\nRedis started successfully: pong message = {%s}", pong)
	storeService.redisClient = redisClient
	return storeService
}
In the store_service.go file just below the top level declarations

II. 2. Storage API design and Implementation ⚙️

Now that we have initialized successfully our data store service, it's time to think about what storage API to expose for our needs in particular for our shortener server.


/* We want to be able to save the mapping between the originalUrl 
and the generated shortUrl url
*/

func SaveUrlMapping(shortUrl string, originalUrl string, userId string){ 


}

/*
We should be able to retrieve the initial long URL once the short 
is provided. This is when users will be calling the shortlink in the 
url, so what we need to do here is to retrieve the long url and
think about redirect.
*/

func RetrieveInitialUrl(shortUrl string) string {



}
In the store_service.go file just below the store initialization code
  • The next step is obviously implementing our storage API, and will all our setup done, it should be fairly straightforward.

func SaveUrlMapping(shortUrl string, originalUrl string, userId string) {
	err := storeService.redisClient.Set(ctx, shortUrl, originalUrl, CacheDuration).Err()
	if err != nil {
		panic(fmt.Sprintf("Failed saving key url | Error: %v - shortUrl: %s - originalUrl: %s\n", err, shortUrl, originalUrl))
	}

}


func RetrieveInitialUrl(shortUrl string) string {
	result, err := storeService.redisClient.Get(ctx, shortUrl).Result()
	if err != nil {
		panic(fmt.Sprintf("Failed RetrieveInitialUrl url | Error: %v - shortUrl: %s\n", err, shortUrl))
	}
	return result
}
In the store_service.go file

II. 3. Unit & Integration Testing 🧪

For preserving the best practices and avoiding unintentional regressions in the future, we are going to have to think about unit and integration tests for our storage layer implementation

Now let's go and install the testing tools we will need to this part.

go get github.com/stretchr/testify

Recall we initially created store_service_test.go file in the store directory, if you haven't done so yet, please go and do it.

Our project directory tree should look something similar to the tree below.

├── go.mod
├── go.sum
├── main.go
└── store
    ├── store_service.go
    └── store_service_test.go
  • We will start by setting up the the  tests shell.
import (
	"github.com/stretchr/testify/assert"
	"testing"
)

var testStoreService = &StorageService{}

func init() {
	testStoreService = InitializeStore()
}
In the store_service_test.go file
  • Now let's go on and unit test the store service initialization.
func TestStoreInit(t *testing.T) {
	assert.True(t, testStoreService.redisClient != nil)
}
In the store_service_test.go file
  • Finally we will go on and add tests for the storage APIs

func TestInsertionAndRetrieval(t *testing.T) {
	initialLink := "https://www.guru3d.com/news-story/spotted-ryzen-threadripper-pro-3995wx-processor-with-8-channel-ddr4,2.html"
	userUUId := "e0dba740-fc4b-4977-872c-d360239e6b1a"
	shortURL := "Jsz4k57oAX"

	// Persist data mapping
	SaveUrlMapping(shortURL, initialLink, userUUId)

	// Retrieve initial URL
	retrievedUrl := RetrieveInitialUrl(shortURL)

	assert.Equal(t, initialLink, retrievedUrl)
}

In the store_service_test.go file

The tests above should all pass if everything was setup well, in case you are having difficulties following up, don't hesitate do DM me on Twitter or email me, I should find some time helping out.

II. 4. Conclusion and Next Steps

In this part we were able to setup our storage service, implemented the APIs and finally added the unit tests that were needed. Hope you enjoyed this part !

As we now have our storage service setup, the next step will be about the short url generation algorithm, trust me that's gonna be too much fun 😄


Ciao ! 👋