The resolver service is tasked with converting a shortCode (short domain and path) into a full size URL and issuing an HTTP 308 Permanent Redirect to the destination URL.

Technical Information

Storage

The service reads and writes from GCP Firestore (NoSQL) as the storage for shortCodes. There is an additional in-memory caching implementation which runs for the lifetime of each GCP Cloud Run invocation. When an instance of a GCP Cloud Run service executes, it typically remains live for up to ten minutes after the initial request to serve subsequent requests. The caching layer is set to keep an item in cache for up to minute which can greatly reduce database lookups when a very popular shortCode is attracting a lot of traffic.

Code

The service is written in Go. The service is managed by my custom service library which is using Fiber for the http framework. The service library wrappers the Fiber handlers so that a bundle of resources can be passed; the bundle includes logging with automatic submission to Grafana Cloud, database connections, a caching implementation, Grafana spans and some other useful bits and pieces.

Fiber is reportedly the fastest web framework available in Go. You can read more about Fiber here.

Endpoints

The service contains only one endpoint, the resolver endpoint. This endpoint handles the lookup and redirect to shortCode destinations.

You can see the code for the endpoint below.

func Handle(bundle serviceComponents.Bundle, c serviceComponents.Request) error {
	logger := bundle.Log()
	if c.Params("shortCode") == "" {
		return c.Redirect("<https://shortify.pro>", http.StatusPermanentRedirect)
	}

	if len(c.Params("shortCode")) > 3 {
		span := bundle.Span("redirecting non-shortcode back to shortify.pro").
		    Attribute("url", c.Params("shortCode"))
		span.Success()
		return c.Redirect("<https://shortify.pro>", http.StatusPermanentRedirect)
	}

	potentialShortCode := fmt.Sprintf("%s/%s", c.Hostname(), c.Params("shortCode"))
	var shortCode entities.ShortCode

	bundle.Attribute("shortCode", potentialShortCode)
	span := bundle.Span("checking if shortCode is cached")
	item := bundle.Cache().Namespace("shortCodes").Get(potentialShortCode)
	span.Success()

  // The in memory cache is searched here to see if the shortCode has been cached
  // The cache lifetime is 1 minute - this helps if a popular URL is getting many requests per second
	var ok bool
	shortCode, ok = item.(entities.ShortCode)
	if ok {
		bundle.Attribute("clicks.remaining", shortCode.Clicks.Remaining)
		bundle.Attribute("clicks.total", shortCode.Clicks.Total)
		bundle.Attribute("expiry", shortCode.Timeline.Expiry.ISO())
		
		// The postProcessing function runs all of it's code in separate goroutines, contributing no
		// extra time to the processing of a request
		postProcessing.Action(bundle, logger, shortCode, false)
		return c.Redirect(shortCode.RawOriginal, http.StatusPermanentRedirect)
	}

	shortCode, err := findShortcode.Action(potentialShortCode, bundle, logger)
	if err != nil {
		return c.Redirect("<https://shortify.pro>", http.StatusPermanentRedirect)
	}

	logger = hydrateLogger.Action(logger, shortCode)
	bundle.Attribute("clicks.remaining", shortCode.Clicks.Remaining)
	bundle.Attribute("clicks.total", shortCode.Clicks.Total)
	bundle.Attribute("expiry", shortCode.Timeline.Expiry.ISO())

	if !shortCode.CanView() {
		deleteExpired.Action(shortCode, bundle, logger)
		return c.Redirect("<https://shortify.pro>", http.StatusPermanentRedirect)
	}

	go func() {
		siteSafetyCheck.IsSiteSafe(shortCode, bundle, logger)
	}()

  // The postProcessing function runs all of it's code in separate goroutines, contributing no
	// extra time to the processing of a request
	postProcessing.Action(bundle, logger, shortCode, true)
	return c.Redirect(shortCode.RawOriginal, http.StatusPermanentRedirect)
}

Example Span

As you can see from this span, a typical resolution of a shortCode into destination URL takes around 50ms. All operations which do not need to be done immediately as part of the response are processed in parallel in separate goroutines. Here is a typical logic flow for a request:

These two actions were sufficient to redirect the user to the destination URL. They make up the entire content of the 50.36ms which the request took to process; the user was redirected in one 20th of a second.

Following these operations, the remaining steps took place via parallel processing in separate goroutines