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.
- What Is Adapter Pattern?
- Flight Tracking App
- Data Layer
- API Layer
- Let’s Fly
- Caching Flight Data
- 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?

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.

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.

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.

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.

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.