Adapter Pattern in Go

Design patterns – everyone talks about it, yet we barely get to use it. Actually it is not entirely true, we use design patterns daily, most times it’s hidden from us. When you use any web frameworks like Django, Flask or Ruby on Rails, you are using some sort of design pattern. Django, Spring boot and Ruby on Rails makes your application to use the Model View Controller (MVC) pattern. Frameworks like Flask encourages you to use decorator and factory pattern.

In Golang world we are usually encouraged to start small with the standard library to build our APIs without relying too much on frameworks. This is one of the reasons I like working in Go. You get to use different design patterns hands-on as you build your application.

Today we will understand the adapter pattern by building a flight-tracking app. We are going to use adapters to cache live flight data and log our REST API requests. These are the topics that are going to be covered by this post.

  1. What Is Adapter Pattern?
  2. Flight Tracking App
  3. Data Layer
  4. API Layer
  5. Let’s Fly
  6. Caching Flight Data
  7. Middleware Adapter

Before we start, in this post I walk you through a some code that implements functions close to a real app. If you read the post without coding along there might be a cognitive overhead of the code structure itself, so I would suggest you to code along. YMMV.

What Is Adapter Pattern?

Gopher holding VGA adapter explaining adapter pattern terms
Target, Adapter and Adaptee in hardware

The adapter pattern is used to make a target module work with other modules without changing the interface exposed by the target.

It is similar to hardware adapters. If all we have is a VGA output, in order to connect it to an HDMI display, we use an adapter to connect those incompatible interfaces.

Just like hardware adapters we are going to see how to use adapters in our code to add a new feature in the API layer that uses the Data layer without changing its interface.

Flight Tracking App

We are going to build a backend module for our app, Go Flights. We need to get live flight data and then expose it via an API to our frontend.

The project structure looks like this:

goflights
├── api
│   └── rest
│       └── api.go
│       └── middleware.go
├── flightstatus
│   └── aviationstack
│       └── aviationstack.go
└── main.go

Now don’t panic! I have made the code structure deliberately nested so that it looks like a grown project.Generally we don’t start using a design pattern right away while starting a project, it will make your code less flexible and it’s mostly a premature optimization.

Don’t start your project with a design pattern. Start simple, let the project grow, notice problems or code smells, then refactor it using a design pattern.


In this project, we have an API layer – api, a package to wire up the REST API for our frontend and a data layer – flightdata, a package to get flight data from different sources.

Block diagram of Go Flights app containing data and API layer
High level block diagram of GoFlights

If you are planning to code along, create a free API key by signing up to Aviation Stack service. Now let’s code:).

Data Layer

The data layer gets the live data for any given flight number and exposes the data through an interface called Tracker. The interface returns the type LiveData that contains the details of the flight.

package flightdata

type LiveData struct {
	Updated         string  `json:"updated"`
	Latitude        float32 `json:"latitude"`
	Longitude       float32 `json:"longitude"`
	Altitude        float32 `json:"altitude"`
	Direction       float32 `json:"direction"`
	SpeedHorizontal float32 `json:"speed_horizontal"`
	SpeedVertical   float32 `json:"speed_vertical"`
	OnGround        bool    `json:"is_ground"`
}

type Tracker interface {
	GetLiveData(flightNumber string) (LiveData, error)
}

Now let’s add some code that will satisfy the interface and gets live flight data from different sources. One of the sources we are going to use is Aviation Stack.

package aviationstack

import (
	"encoding/json"
	"net/http"
	"net/url"

	"coppermind.io/goflights/flightdata"
	"github.com/pkg/errors"
)

type Flight struct {
	Live flightdata.LiveData `json:"live"`
}

type Response struct {
	Flights []Flight `json:"data"`
}

// AviatonStack implements Tracker interface to get live flight data from Aviation Stack API
type AviatonStack struct {
	baseURL   *url.URL
	client    *http.Client
	accessKey string
}

// New constructs an AviatonStack object
func New(endPoint, accessKey string, client *http.Client) (*AviatonStack, error) {
	baseURL, err := url.Parse(endPoint)
	if err != nil {
		return nil, errors.Wrap(err, "url parse failed")
	}
	return &AviatonStack{
		baseURL:   baseURL,
		client:    client,
		accessKey: accessKey,
	}, nil
}

// GetLiveData gets the live data form the upstream server
func (a *AviatonStack) GetLiveData(flightNumber string) (flightdata.LiveData, error) {
	rel := &url.URL{Path: "v1/flights"}
	url := a.baseURL.ResolveReference(rel)
	q := url.Query()
	q.Add("access_key", a.accessKey)
	q.Add("flight_iata", flightNumber)
	url.RawQuery = q.Encode()

	req, err := http.NewRequest("GET", url.String(), nil)
	if err != nil {
		panic(err)
	}

	res, err := a.client.Do(req)
	if err != nil {
		panic(err)
	}
	defer res.Body.Close()

	var apiResponse Response
	json.NewDecoder(res.Body).Decode(&apiResponse)

	return apiResponse.Flights[0].Live, nil
}

The struct Flight and Response on line 12 and line 16 respectively are used to represent the JSON response from the upstream server. The logic to handle the upstream’s API goes into the method GetLiveData, there we add API endpoint to the base url and construct the url with query parameters on line 45, 46.

A new GET request is constructed on line 49. Finally, we call the upstream API on line 54, then decode the JSON response and return it.

API Layer

It’s a SOLID practice to separate high-level policies from low-level components. So we have an API layer that depends on the data layer’s interface. It contains the logic for exposing our data through different APIs like REST, GRPC or SOAP. Let’s wire up the REST API for now.

package rest

import (
	"fmt"
	"log"
	"net/http"

	"coppermind.io/goflights/flightdata"
)

type REST struct {
	server *http.Server
	source flightdata.Tracker
}

func NewRESTServer(server *http.Server, source flightdata.Tracker) *REST {
	return &REST{
		server: server,
		source: source,
	}
}

func (s *REST) Start() error {
	s.server.Addr = ":8000"
	mux := http.NewServeMux()
	mux.HandleFunc("/api/flight/live", s.GetFlightStatus)

	s.server.Handler = mux

	log.Printf("Starting the REST server on %s\n", s.server.Addr)
	return s.server.ListenAndServe()
}

func (s *REST) GetFlightStatus(w http.ResponseWriter, r *http.Request) {
	q := r.URL.Query()
	fNo := q.Get("flight_number")
	data, err := s.source.GetLiveData(fNo)
	if err != nil {
		w.WriteHeader(http.StatusInternalServerError)
		fmt.Fprintf(w, "Error getting price")
		return
	}
	fmt.Fprintf(w, "Live data for the flight: %s: %+v", fNo, data)
}

The type REST on line 11 has two fields, server and source – we need http.Server to run the server and our server is going to get data from the source field which is of type flightdata.Tracker.

Our API has only one endpoint /api/flight/live?flight_number=<flight-number>.
GetFlightStatus is a handler that is used to parse the query parameter flight_number. Using flight_number, we call the method GetLiveData from our data layer and return it as a text response1.

Now that we have our both Data layer and API layer, it’s time to wire them up in our main file. We initialize aviationstack object, pass on to our REST server object and then start our server on port 8000.

package main

import (
	"net/http"

	"coppermind.io/goflights/api/rest"
	"coppermind.io/goflights/flightdata/aviationstack"
)

func main() {
	source, err := aviationstack.New("http://api.aviationstack.com", "057cc9cafe3a84e9ddeee7751325cefc", &http.Client{})
	if err != nil {
		panic(err)
	}
	server := rest.NewRESTServer(source)
	server.Start()
}

Make a GET request to our app by opening http://localhost:8000/api/flight/live/flight_number=BA98 in your browser. If our upstream API key is set correctly, we will see the live location, altitude and speed of a British Airways flight.

Now hold on, things are gonna get even more exciting, even if you are not excited, continue below, you’ll learn something about adapter pattern.

We have a decent MVP and beta users. Now we find out that our beta users are hitting the rate limit quota of our API plans with the upstream, Aviation Stack. The simplest solution is to cache the data.

We can write a cache module and call it from our API layer but this is not the right way. Because caching is low-level function irrespective of what kind of API we use, the cache policies depend on the data we handle. So we include our caching logic in the data layer, when we do that in order to avoid repetition of code we use a cache wrapper that satisfies Tracker interface.

Caching Using Adapter Pattern

Caching is a complicated topic, when we build a cache that’s a part of the system architecture we need to decide how to populate, store, retrieve and evict the cache data. But that’s for another day. For the scope of this post, let’s build a dead-simple cache by using a hashmap.

Let’s add cache package to our project, it consists of two files inmemory.go which has the logic to cache the data in memory and cache.go which has our adapter code.

package cache

import (
	"errors"

	"coppermind.io/goflights/flightdata"
)

type cache map[string]flightdata.LiveData

func NewInMemoryCache() cache {
	return cache{}
}

// Put inserts the price to the cache
func (c cache) Put(flightNumber string, data flightdata.LiveData) {
	c[flightNumber] = data
}

// Get gets the value from the cache
func (c cache) Get(flightNumber string) (flightdata.LiveData, error) {
	var data flightdata.LiveData
	if data, ok := c[flightNumber]; !ok {
		return data, errors.New("Cache not found")
	}
	return data, nil
}
package cache

import (
	"coppermind.io/goflights/flightdata"
)

type CacheWrapper struct {
	tracker flightdata.Tracker
	cache   cache
}

// New returns new CacheWrapper
func New(tracker flightdata.Tracker) *CacheWrapper {
	c := NewInMemoryCache()
	return &CacheWrapper{
		tracker: tracker,
		cache:   c,
	}
}

// GetLiveData gets flight data
func (c *CacheWrapper) GetLiveData(flightNumber string) (flightdata.LiveData, error) {
	cachedData, err := c.cache.Get(flightNumber)
	// cache miss
	if err != nil {
		flightData, _ := c.tracker.GetLiveData(flightNumber)
		c.cache.Put(flightNumber, flightData)
		return flightData, nil
	}
	// cache hit
	return cachedData, nil
}

The inmemory.go file has a cache type on line 11 which is a plain hashmap. It has Put and Get methods for inserting and retrieving the data.

Target, Adapter and Adaptee in our code
Adding cache functionality to data layer using cacheWrapper as an adapter

Now let’s shift our focus to the most important part of this codebase, cache.go.

The CacheWrapper struct on line 7 in cache.go implements the Tracker interface that we saw at first. The field cache stores the in-memory cache object. The tracker stores the upstream source like aviation stack as we need a way to access the upstream server if there is no data in the cache.

We have the method GetLiveData, there we take flightNumber as a parameter and check whether we have the information in our cache. If it’s a cache miss we call the upstream source on line 26, insert the result into our cache and then return the data. If it’s a cache hit we return the cached data at the end of the method.

This resembles our AviationStack object we saw earlier, but with cache functionality.

Both types AviationStack and CacheWrapper are provisioned by a single interface Tracker. Provisioning of a single interface to entities of different types, sounds familiar?, voila that’s how we do polymorphism in Go.

Sometimes the terms adapters, adaptees and targets could be confusing, the best way to understand is to read it in plain English. “We want to add our adaptee cache to our target AviationStack. AviationStack implements Tracker interface, so we write a CacheWrapper that implements Tracker interface”.

Let’s Fly

Now our app works well. Our users are able to track the flights but we are starting to hit the rate limits of our upstream(aviation stack) API. Fortunately, the requirements say that it’s enough to show near real-time data and it can do hourly updates.

Well, given the problem and also favourable requirements, the simplest solution is caching the data so that we don’t hit the upstream server every time the refreshes the tracking page.

Let’s add our adapter to main.go

package main

import (
	"net/http"

	"coppermind.io/goflights/api/rest"
	"coppermind.io/goflights/flightdata/aviationstack"
	"coppermind.io/goflights/flightdata/cache"
)

func main() {
	source, err := aviationstack.New("http://api.aviationstack.com", "057cc9cafe3a84e9ddddd6651325cefc", &http.Client{})
	if err != nil {
		panic(err)
	}
	cacheWrapper := cache.New(source)
	server := rest.NewRESTServer(cacheWrapper)
	server.Start()
}

Our main function is almost the same as what we saw earlier except lines 16 and 17 where we wrap the source object returned by aviationstack.New and pass it to the NewRESTServer constructor. The REST server object depends on the type Tracker (interface), so we can add more functionality by wrapping it up with the cache object and then passing it on.

We have cached our data, it increases the speed a bit, our users are happy. We are not hitting the rate limits of the upstream services anymore, we don’t have to pay more, our stakeholders are happy.

Now we are asked to give some metrics about the API traffic we receive. We want all our incoming requests to be logged. We can always include log.Info inside every API handler function, but that is a sub-optimal solution. It is prone to inconsistencies and code repetition, this is where the adapter pattern comes into play, again.

Middleware Adapter

Let’s take a look at the method Start() from api.go from earlier.

package rest

// [...]

func (s *REST) Start() error {
	s.mux.HandleFunc("/api/flight/live", s.GetFlightStatus)
	s.server.Addr = ":8000"
	s.server.Handler = s.mux
	log.Printf("Starting the REST server on %s\n", s.server.Addr)
	return s.server.ListenAndServe()
}

// [...]

On line 6 we are registering our API handler function to the mux object passed to the REST struct. Then we pass on the mux object to the server’s hander so that the http.Server will use it to route the incoming requests.

Checking the type accepted by http.Handler in vscode
Checking the type accepted by http.Handler

Since any API request has to be passed through the multiplexer(mux), it is a perfect place to interpret the requests and log them.

We need to pass our custom mux to server.Handler.

server.Handler accepts handler interface that has a function ServeHTTP. So our custom mux should satisfy this interface.

The middleware.go shown below has a middlewareLogger type, that has handler field, we need this to store the original handler. The ServeHTTP method calls the original handler’s ServeHTTP and logs the incoming request.

package rest

import (
	"log"
	"net/http"
	"time"
)

type middlewareLogger struct {
	handler http.Handler
}

func NewLogger(wrapHandler http.Handler) *middlewareLogger {
	return &middlewareLogger{wrapHandler}
}

func (l *middlewareLogger) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	l.handler.ServeHTTP(w, r)
	log.Printf("%s %s %s", time.Now().UTC().Format(time.StampMilli), r.Method, r.URL.Path)
}

We can wrap up the default mux with our new logger middleware in api.go

package rest

//[...]

func (s *REST) Start() error {
	s.server.Addr = ":8000"
	mux := http.NewServeMux()
	mux.HandleFunc("/api/flight/live", s.GetFlightStatus)

	loggerMux := NewLogger(mux)
	s.server.Handler = loggerMux

	log.Printf("Starting the REST server on %s\n", s.server.Addr)
	return s.server.ListenAndServe()
}

//[...]

We initialize the mux object and wrap it up with logger on lines 7-11. This can be done in a single line:

loggerMux := NewLogger(http.NewServeMux())

This is called Adapter Chaining.

Target, Adapter and Adaptee in our code
Adding logging functionality to API layer using Middleware as an adapter

Pat yourself on the back if you have come this far. That’s a lot of code and fairly big cognitive overhead but you have learnt how to use adapter pattern practically in a project.

We have implemented two adapters, one to cache the data and one to log HTTP requests. Even though adapter pattern is the main take away of the post, we have also seen how to use interfaces to abstract a package and how to write effective HTTP clients.

Remember that the design patterns are just guides to refactor your code and make it more maintainable. It is up to you to use it for good scenario or compromise it if it impedes your development.

The final code can be found in this git repository.

If you found this blog post useful, you can give a shoutout to me on twitter. Let me know your thoughts in the comments below.

  1. Ideally this should be JSON response but this is for the sake of simplicity.

Leave a Comment